Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
math_tutor_dev
public_math_tutor
Commits
c9c6a73e
Commit
c9c6a73e
authored
Mar 05, 2026
by
Kantz
Browse files
Quellen wieder zugreifbar
parent
8a194678
Changes
3
Hide whitespace changes
Inline
Side-by-side
math-tutor/backend/app/deterministic_services/referenz_decoder.py
View file @
c9c6a73e
from
__future__
import
annotations
from
__future__
import
annotations
from
typing
import
Dict
,
Iterable
,
Tuple
from
typing
import
Dict
,
Iterable
,
Protocol
,
Set
,
Tuple
import
re
import
re
from
urllib.parse
import
quote
from
urllib.parse
import
quote
from
app.deterministic_services
import
Source
,
SourceID
_BRACKET_RE
=
re
.
compile
(
r
"\[(?P<ref>[^\[\]]+)\]"
)
_BRACKET_RE
=
re
.
compile
(
r
"\[(?P<ref>[^\[\]]+)\]"
)
def
_source_id_key
(
source_id
:
SourceID
)
->
str
:
class
_SourceIDLike
(
Protocol
):
chapter_title
:
str
|
None
section_title
:
str
|
None
subsection_title
:
str
|
None
title
:
str
|
None
doc_type
:
str
|
None
def
to_string
(
self
)
->
str
:
...
class
_SourceLike
(
Protocol
):
source_id
:
_SourceIDLike
def
_source_id_key
(
source_id
:
_SourceIDLike
)
->
str
:
parts
=
[
parts
=
[
source_id
.
chapter_title
or
""
,
source_id
.
chapter_title
or
""
,
source_id
.
section_title
or
""
,
source_id
.
section_title
or
""
,
...
@@ -30,9 +42,11 @@ def _normalize_ref_token(token: str) -> str:
...
@@ -30,9 +42,11 @@ def _normalize_ref_token(token: str) -> str:
return
t
.
casefold
()
return
t
.
casefold
()
def
_build_source_index
(
sources
:
Iterable
[
Source
])
->
Dict
[
str
,
str
]:
def
_build_source_index
(
sources
:
Iterable
[
_
Source
Like
])
->
Tuple
[
Dict
[
str
,
str
]
,
Set
[
str
]]
:
index
:
Dict
[
str
,
str
]
=
{}
index
:
Dict
[
str
,
str
]
=
{}
doc_types
:
Set
[
str
]
=
set
()
for
source
in
sources
:
for
source
in
sources
:
doc_types
.
add
(
_normalize_ref_token
(
source
.
source_id
.
doc_type
or
""
))
key
=
_source_id_key
(
source
.
source_id
)
key
=
_source_id_key
(
source
.
source_id
)
normalized_key
=
_normalize_ref_token
(
key
)
normalized_key
=
_normalize_ref_token
(
key
)
index
.
setdefault
(
normalized_key
,
key
)
index
.
setdefault
(
normalized_key
,
key
)
...
@@ -46,22 +60,46 @@ def _build_source_index(sources: Iterable[Source]) -> Dict[str, str]:
...
@@ -46,22 +60,46 @@ def _build_source_index(sources: Iterable[Source]) -> Dict[str, str]:
source
.
source_id
.
subsection_title
,
source
.
source_id
.
subsection_title
,
source
.
source_id
.
title
,
source
.
source_id
.
title
,
]
]
filtered
=
[
p
for
p
in
parts
if
p
and
p
.
lower
()
!=
"none"
]
for
start
in
range
(
len
(
parts
)):
if
filtered
:
filtered
=
[
minimal
=
"|"
.
join
(
filtered
+
[
source
.
source_id
.
doc_type
])
p
for
p
in
parts
[
start
:]
index
.
setdefault
(
_normalize_ref_token
(
minimal
),
key
)
if
p
and
p
.
lower
()
!=
"none"
]
if
filtered
:
minimal
=
"|"
.
join
(
filtered
+
[
source
.
source_id
.
doc_type
])
index
.
setdefault
(
_normalize_ref_token
(
minimal
),
key
)
if
source
.
source_id
.
title
:
if
source
.
source_id
.
title
:
compact
=
f
"
{
source
.
source_id
.
title
}
|
{
source
.
source_id
.
doc_type
}
"
compact
=
f
"
{
source
.
source_id
.
title
}
|
{
source
.
source_id
.
doc_type
}
"
index
.
setdefault
(
_normalize_ref_token
(
compact
),
key
)
index
.
setdefault
(
_normalize_ref_token
(
compact
),
key
)
return
index
return
index
,
{
dtype
for
dtype
in
doc_types
if
dtype
}
def
_candidate_tokens
(
token
:
str
,
doc_types
:
Set
[
str
])
->
Iterable
[
str
]:
normalized
=
_normalize_ref_token
(
token
)
yield
normalized
parts
=
[
part
for
part
in
normalized
.
split
(
"|"
)
if
part
]
if
not
parts
:
return
for
idx
,
part
in
enumerate
(
parts
):
if
part
in
doc_types
:
yield
"|"
.
join
(
parts
[:
idx
+
1
])
if
len
(
parts
)
>=
2
:
yield
"|"
.
join
(
parts
[
-
2
:])
if
len
(
parts
)
>=
3
:
yield
"|"
.
join
(
parts
[
-
3
:])
if
len
(
parts
)
>=
4
:
yield
"|"
.
join
(
parts
[
-
4
:])
def
decode_references
(
text
:
str
,
sources
:
Iterable
[
Source
])
->
Tuple
[
str
,
int
]:
def
decode_references
(
text
:
str
,
sources
:
Iterable
[
_
Source
Like
])
->
Tuple
[
str
,
int
]:
if
not
text
:
if
not
text
:
return
text
,
0
return
text
,
0
index
=
_build_source_index
(
sources
)
index
,
doc_types
=
_build_source_index
(
sources
)
if
not
index
:
if
not
index
:
return
text
,
0
return
text
,
0
...
@@ -75,8 +113,11 @@ def decode_references(text: str, sources: Iterable[Source]) -> Tuple[str, int]:
...
@@ -75,8 +113,11 @@ def decode_references(text: str, sources: Iterable[Source]) -> Tuple[str, int]:
if
end
<
len
(
text
)
and
text
[
end
:
end
+
1
]
==
"("
:
if
end
<
len
(
text
)
and
text
[
end
:
end
+
1
]
==
"("
:
return
match
.
group
(
0
)
return
match
.
group
(
0
)
normalized
=
_normalize_ref_token
(
ref
)
key
=
None
key
=
index
.
get
(
normalized
)
for
candidate
in
_candidate_tokens
(
ref
,
doc_types
):
key
=
index
.
get
(
candidate
)
if
key
:
break
if
not
key
:
if
not
key
:
return
match
.
group
(
0
)
return
match
.
group
(
0
)
...
...
math-tutor/backend/test/referenz_decoder_unit_test.py
0 → 100644
View file @
c9c6a73e
from
__future__
import
annotations
from
dataclasses
import
dataclass
from
pathlib
import
Path
import
importlib.util
import
unittest
_MODULE_PATH
=
(
Path
(
__file__
).
resolve
().
parents
[
1
]
/
"app"
/
"deterministic_services"
/
"referenz_decoder.py"
)
_SPEC
=
importlib
.
util
.
spec_from_file_location
(
"referenz_decoder"
,
_MODULE_PATH
)
assert
_SPEC
is
not
None
and
_SPEC
.
loader
is
not
None
_MODULE
=
importlib
.
util
.
module_from_spec
(
_SPEC
)
_SPEC
.
loader
.
exec_module
(
_MODULE
)
decode_references
=
_MODULE
.
decode_references
@
dataclass
class
SourceIDStub
:
chapter_title
:
str
|
None
section_title
:
str
|
None
subsection_title
:
str
|
None
title
:
str
doc_type
:
str
def
to_string
(
self
)
->
str
:
parts
=
[
self
.
title
]
if
self
.
subsection_title
:
parts
.
insert
(
0
,
self
.
subsection_title
)
if
self
.
section_title
:
parts
.
insert
(
0
,
self
.
section_title
)
if
self
.
chapter_title
:
parts
.
insert
(
0
,
self
.
chapter_title
)
return
f
"[
{
'|'
.
join
(
parts
)
}
|
{
self
.
doc_type
}
]"
@
dataclass
class
SourceStub
:
source_id
:
SourceIDStub
def
_mk_source
()
->
SourceStub
:
return
SourceStub
(
source_id
=
SourceIDStub
(
chapter_title
=
"Kapitel 1"
,
section_title
=
"Lineare Funktionen"
,
subsection_title
=
"Steigung"
,
title
=
"Definition"
,
doc_type
=
"subsection"
,
)
)
class
ReferenzDecoderUnitTest
(
unittest
.
TestCase
):
def
test_decodes_reference_without_chapter
(
self
)
->
None
:
source
=
_mk_source
()
text
=
"Siehe [Lineare Funktionen|Steigung|Definition|subsection]."
decoded
,
count
=
decode_references
(
text
,
[
source
])
self
.
assertEqual
(
count
,
1
)
self
.
assertIn
(
"(doc://Kapitel%201%7CLineare%20Funktionen%7CSteigung%7CDefinition%7Csubsection)"
,
decoded
,
)
def
test_decodes_reference_with_extra_tail_segment
(
self
)
->
None
:
source
=
_mk_source
()
text
=
"Siehe [Lineare Funktionen/Steigung/Definition | subsection | ...]."
decoded
,
count
=
decode_references
(
text
,
[
source
])
self
.
assertEqual
(
count
,
1
)
self
.
assertIn
(
"(doc://Kapitel%201%7CLineare%20Funktionen%7CSteigung%7CDefinition%7Csubsection)"
,
decoded
,
)
def
test_skips_existing_markdown_links
(
self
)
->
None
:
source
=
_mk_source
()
text
=
"Bereits verlinkt: [Definition](doc://foo)."
decoded
,
count
=
decode_references
(
text
,
[
source
])
self
.
assertEqual
(
count
,
0
)
self
.
assertEqual
(
decoded
,
text
)
if
__name__
==
"__main__"
:
unittest
.
main
()
math-tutor/frontend/src/components/Chat/MessageBubble.tsx
View file @
c9c6a73e
import
{
useEffect
,
useRef
}
from
"
react
"
;
import
{
useEffect
,
useRef
}
from
"
react
"
;
import
ReactMarkdown
from
"
react-markdown
"
;
import
ReactMarkdown
,
{
defaultUrlTransform
}
from
"
react-markdown
"
;
import
type
{
RetrievedDoc
}
from
"
../Retrieval/DocPanel
"
;
import
type
{
RetrievedDoc
}
from
"
../Retrieval/DocPanel
"
;
import
{
t
}
from
"
../../i18n
"
;
import
{
t
}
from
"
../../i18n
"
;
...
@@ -54,6 +54,12 @@ export default function MessageBubble({
...
@@ -54,6 +54,12 @@ export default function MessageBubble({
</
div
>
</
div
>
<
div
className
=
"message-text"
>
<
div
className
=
"message-text"
>
<
ReactMarkdown
<
ReactMarkdown
urlTransform
=
{
(
url
)
=>
{
if
(
url
.
startsWith
(
"
doc://
"
))
{
return
url
;
}
return
defaultUrlTransform
(
url
);
}
}
components
=
{
{
components
=
{
{
a
:
({
href
,
children
,
...
props
})
=>
{
a
:
({
href
,
children
,
...
props
})
=>
{
const
target
=
href
||
""
;
const
target
=
href
||
""
;
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment