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
c4ee9dda
Commit
c4ee9dda
authored
May 29, 2026
by
Kantz
Browse files
anpassung des Sokrates auf neue Struktur.
parent
e31b3ff9
Changes
11
Hide whitespace changes
Inline
Side-by-side
math-tutor/README.md
View file @
c4ee9dda
...
@@ -127,7 +127,7 @@ VITE_PROXY_TARGET="http://<BACKEND_HOST>:8000"
...
@@ -127,7 +127,7 @@ VITE_PROXY_TARGET="http://<BACKEND_HOST>:8000"
Template files:
Template files:
-
`math-tutor/docker/docker-compose.yml`
-
`math-tutor/docker/docker-compose.y
a
ml`
-
`math-tutor/docker/nginx.conf`
-
`math-tutor/docker/nginx.conf`
-
`math-tutor/backend/Dockerfile`
-
`math-tutor/backend/Dockerfile`
-
`math-tutor/frontend/Dockerfile`
-
`math-tutor/frontend/Dockerfile`
...
@@ -190,7 +190,7 @@ python -m scripts.retrieval_cli query --q "Was ist eine Teilmenge?" --k 8 --expa
...
@@ -190,7 +190,7 @@ python -m scripts.retrieval_cli query --q "Was ist eine Teilmenge?" --k 8 --expa
```
powershell
```
powershell
cd
math-tutor/backend
cd
math-tutor/backend
.
\.venv\Scripts\Activate.ps1
.
\.venv\Scripts\Activate.ps1
python
-m
scripts.generate_socratic_chats
--source-root
sources/lecture_script
--output
sources/inital_socratic_prompt/initial_prompts.yml
python
-m
scripts.generate_socratic_chats
--source-root
sources/lecture_script
--output
sources/inital_socratic_prompt/initial_prompts.y
a
ml
```
```
## Configuration
## Configuration
...
...
math-tutor/backend/app/LLM_services/socratic_LLM.py
View file @
c4ee9dda
...
@@ -5,12 +5,16 @@ from typing import Iterable
...
@@ -5,12 +5,16 @@ from typing import Iterable
from
app.deterministic_services
import
llm_client
from
app.deterministic_services
import
llm_client
HINT_SYSTEM_PROMPT
=
"""
ParentRef
=
tuple
[
int
,
int
,
int
,
int
]
SubsectionRef
=
tuple
[
int
,
int
,
int
]
HINT_SYSTEM_PROMPT
=
r
"""
Du bist ein didaktischer Mathe-Tutor.
Du bist ein didaktischer Mathe-Tutor.
Antworte auf Deutsch, kurz und präzise.
Antworte auf Deutsch, kurz und präzise.
Nutze ausschließlich den bereitgestellten Kontext.
Nutze ausschließlich den bereitgestellten Kontext.
Wenn der Student eine Frage stellt die nichts mit dem Thema zu tun hat gehe nicht darauf ein.
Wenn der Student eine Frage stellt die nichts mit dem Thema zu tun hat gehe nicht darauf ein.
Stelle offene Verständnisfragen und leite den Studeten sokratisch.
Stelle offene Verständnisfragen und leite den Studeten sokratisch.
Nutze keine Lückentexte oder Frage auf die man mit einem Wort antworten kann.
Nutze keine Lückentexte oder Frage auf die man mit einem Wort antworten kann.
Fordere den Nutzer auf mit einem ganzen Satz oder einer Formel zu antworten, falls er wiederholt nur 1 Wort eingaben macht.
Fordere den Nutzer auf mit einem ganzen Satz oder einer Formel zu antworten, falls er wiederholt nur 1 Wort eingaben macht.
...
@@ -25,21 +29,33 @@ minf steht für minus unendlich.
...
@@ -25,21 +29,33 @@ minf steht für minus unendlich.
"""
"""
def
_format_subsection_refs
(
subsection_refs
:
Iterable
[
tuple
[
int
,
int
,
int
]]
|
None
)
->
str
:
def
_format_parent_refs
(
parent_refs
:
Iterable
[
ParentRef
]
|
None
)
->
str
:
if
not
subsection_refs
:
if
not
parent_refs
:
return
"Keine Subsection-Referenzen übergeben."
return
"Keine Parent-Referenzen übergeben."
formatted
=
[
f
"
{
chap
}
:
{
sec
}
:
{
sub
}
"
for
chap
,
sec
,
sub
in
subsection_refs
]
formatted
:
list
[
str
]
=
[]
for
chap
,
sec
,
sub
,
subsub
in
parent_refs
:
if
sub
<=
0
:
formatted
.
append
(
f
"
{
chap
}
:
{
sec
}
"
)
elif
subsub
<=
0
:
formatted
.
append
(
f
"
{
chap
}
:
{
sec
}
:
{
sub
}
"
)
else
:
formatted
.
append
(
f
"
{
chap
}
:
{
sec
}
:
{
sub
}
:
{
subsub
}
"
)
return
", "
.
join
(
formatted
)
return
", "
.
join
(
formatted
)
def
generate_dialog
(
def
generate_dialog
(
query
:
str
|
None
,
query
:
str
|
None
,
subsection_refs
:
list
[
tuple
[
int
,
int
,
int
]]
|
None
=
None
,
subsection_refs
:
list
[
SubsectionRef
]
|
None
=
None
,
parent_refs
:
list
[
ParentRef
]
|
None
=
None
,
history
:
list
[
dict
]
|
None
=
None
,
history
:
list
[
dict
]
|
None
=
None
,
sources
:
str
|
None
=
None
,
sources
:
str
|
None
=
None
,
)
->
str
:
)
->
str
:
effective_parent_refs
=
parent_refs
if
effective_parent_refs
is
None
and
subsection_refs
is
not
None
:
effective_parent_refs
=
[(
chap
,
sec
,
sub
,
0
)
for
chap
,
sec
,
sub
in
subsection_refs
]
context_parts
=
[
context_parts
=
[
f
"
Subsection
-Referenzen:
\n
{
_format_
subsection_refs
(
subsection
_refs
)
}
"
,
f
"
Parent
-Referenzen:
\n
{
_format_
parent_refs
(
effective_parent
_refs
)
}
"
,
]
]
if
sources
:
if
sources
:
context_parts
.
append
(
f
"Kontext/Sources:
\n
{
sources
}
"
)
context_parts
.
append
(
f
"Kontext/Sources:
\n
{
sources
}
"
)
...
...
math-tutor/backend/app/api/chat.py
View file @
c4ee9dda
...
@@ -195,10 +195,10 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe
...
@@ -195,10 +195,10 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe
if
not
task_catalog
.
select_subsection_by_key
(
sheet
,
request
.
subsection_key
):
if
not
task_catalog
.
select_subsection_by_key
(
sheet
,
request
.
subsection_key
):
raise
HTTPException
(
status_code
=
404
,
detail
=
"subsection not found"
)
raise
HTTPException
(
status_code
=
404
,
detail
=
"subsection not found"
)
refs
=
task_catalog
.
get_selected_subsection_refs
(
sheet
)
refs
=
task_catalog
.
get_selected_subsection_
parent_
refs
(
sheet
)
sources
=
retrieval_store
.
retrieve_for_
subsection
s
(
sources
=
retrieval_store
.
retrieve_for_
parent_ref
s
(
pg_url
=
config
.
get_postgres_url
(),
pg_url
=
config
.
get_postgres_url
(),
subsection
_refs
=
refs
,
parent
_refs
=
refs
,
)
)
context_store
.
set_sources
(
sheet
,
sources
)
context_store
.
set_sources
(
sheet
,
sources
)
context_store
.
set_initialized
(
sheet
,
True
)
context_store
.
set_initialized
(
sheet
,
True
)
...
...
math-tutor/backend/app/api/tasks.py
View file @
c4ee9dda
...
@@ -36,6 +36,7 @@ class TopicEntry(BaseModel):
...
@@ -36,6 +36,7 @@ class TopicEntry(BaseModel):
class
SubsectionEntry
(
BaseModel
):
class
SubsectionEntry
(
BaseModel
):
subsection_key
:
str
subsection_key
:
str
label
:
str
label
:
str
level
:
str
=
""
refs
:
List
[
List
[
int
]]
refs
:
List
[
List
[
int
]]
summary
:
str
=
""
summary
:
str
=
""
...
...
math-tutor/backend/app/deterministic_services/orchestrators/orchestrator_socratic.py
View file @
c4ee9dda
...
@@ -21,28 +21,27 @@ def _apply_selected_subsection(
...
@@ -21,28 +21,27 @@ def _apply_selected_subsection(
def
_retrieve_context_for_subsection
(
state
:
base
.
ChatState
,
query_text
:
str
)
->
int
:
def
_retrieve_context_for_subsection
(
state
:
base
.
ChatState
,
query_text
:
str
)
->
int
:
# Deprecated: socratic retrieval still uses subsection refs until it is migrated to the new parent model.
refs
=
task_catalog
.
get_selected_subsection_parent_refs
(
state
.
sheet
)
refs
=
task_catalog
.
get_selected_subsection_refs
(
state
.
sheet
)
if
not
refs
:
if
not
refs
:
return
0
return
0
def
_retrieve
()
->
dict
:
def
_retrieve
()
->
dict
:
sources
=
retrieval_store
.
retrieve_for_
subsection
s
(
sources
=
retrieval_store
.
retrieve_for_
parent_ref
s
(
pg_url
=
config
.
get_postgres_url
(),
pg_url
=
config
.
get_postgres_url
(),
subsection
_refs
=
refs
,
parent
_refs
=
refs
,
)
)
context_store
.
update_retrieval_context
(
state
.
sheet
,
sources
)
context_store
.
update_retrieval_context
(
state
.
sheet
,
sources
)
return
{
return
{
"
subsection
_refs"
:
refs
,
"
parent
_refs"
:
refs
,
"source_count"
:
len
(
sources
),
"source_count"
:
len
(
sources
),
}
}
result
=
base
.
log_timed_call
(
result
=
base
.
log_timed_call
(
state
.
tool_log
,
state
.
tool_log
,
"retrieve_context_with_socratic_
subsection
s"
,
"retrieve_context_with_socratic_
topic
s"
,
{
{
"query"
:
query_text
,
"query"
:
query_text
,
"
subsection
_refs"
:
refs
,
"
parent
_refs"
:
refs
,
},
},
_retrieve
,
_retrieve
,
)
)
...
@@ -54,16 +53,16 @@ def _on_bootstrap(state: base.ChatState, query_text: str) -> None:
...
@@ -54,16 +53,16 @@ def _on_bootstrap(state: base.ChatState, query_text: str) -> None:
def
_on_turn_logic
(
state
:
base
.
ChatState
)
->
None
:
def
_on_turn_logic
(
state
:
base
.
ChatState
)
->
None
:
if
not
task_catalog
.
get_selected_subsection_refs
(
state
.
sheet
):
if
not
task_catalog
.
get_selected_subsection_
parent_
refs
(
state
.
sheet
):
return
return
def
_on_build_reply
(
state
:
base
.
ChatState
)
->
str
|
None
:
def
_on_build_reply
(
state
:
base
.
ChatState
)
->
str
|
None
:
history_turns
=
context_store
.
get_history_turns
(
state
.
sheet
)
history_turns
=
context_store
.
get_history_turns
(
state
.
sheet
)
subsection
_refs
=
task_catalog
.
get_selected_subsection_refs
(
state
.
sheet
)
parent
_refs
=
task_catalog
.
get_selected_subsection_
parent_
refs
(
state
.
sheet
)
args
=
{
args
=
{
"query"
:
state
.
last_user
,
"query"
:
state
.
last_user
,
"
subsection_refs"
:
subsection
_refs
,
"
parent_refs"
:
parent
_refs
,
"history"
:
history_turns
,
"history"
:
history_turns
,
"sources"
:
"
\n
"
.
join
([
source
.
to_string
()
for
source
in
context_store
.
get_retrieval
(
state
.
sheet
)]),
"sources"
:
"
\n
"
.
join
([
source
.
to_string
()
for
source
in
context_store
.
get_retrieval
(
state
.
sheet
)]),
}
}
...
...
math-tutor/backend/app/deterministic_services/socratic_oranisator.py
View file @
c4ee9dda
...
@@ -9,7 +9,7 @@ import yaml
...
@@ -9,7 +9,7 @@ import yaml
from
app.deterministic_services
import
task_catalog
from
app.deterministic_services
import
task_catalog
PROMPTS_PATH
=
Path
(
__file__
).
resolve
().
parents
[
2
]
/
"sources"
/
"inital_socratic_prompt"
/
"initial_prompts.yml"
PROMPTS_PATH
=
Path
(
__file__
).
resolve
().
parents
[
2
]
/
"sources"
/
"inital_socratic_prompt"
/
"initial_prompts.y
a
ml"
@
lru_cache
(
maxsize
=
1
)
@
lru_cache
(
maxsize
=
1
)
...
@@ -28,7 +28,7 @@ def load_initial_prompt_items(path: Path = PROMPTS_PATH) -> dict[str, dict[str,
...
@@ -28,7 +28,7 @@ def load_initial_prompt_items(path: Path = PROMPTS_PATH) -> dict[str, dict[str,
items
:
dict
[
str
,
dict
[
str
,
Any
]]
=
{}
items
:
dict
[
str
,
dict
[
str
,
Any
]]
=
{}
for
raw_key
,
raw_value
in
raw_items
.
items
():
for
raw_key
,
raw_value
in
raw_items
.
items
():
key
=
task_catalog
.
_normalize_
subsection
_key
(
str
(
raw_key
))
key
=
task_catalog
.
_normalize_
parent_ref
_key
(
str
(
raw_key
))
if
not
key
or
not
isinstance
(
raw_value
,
dict
):
if
not
key
or
not
isinstance
(
raw_value
,
dict
):
continue
continue
items
[
key
]
=
raw_value
items
[
key
]
=
raw_value
...
@@ -36,7 +36,7 @@ def load_initial_prompt_items(path: Path = PROMPTS_PATH) -> dict[str, dict[str,
...
@@ -36,7 +36,7 @@ def load_initial_prompt_items(path: Path = PROMPTS_PATH) -> dict[str, dict[str,
def
get_initial_message
(
subsection_key
:
str
,
path
:
Path
=
PROMPTS_PATH
)
->
str
:
def
get_initial_message
(
subsection_key
:
str
,
path
:
Path
=
PROMPTS_PATH
)
->
str
:
normalized_key
=
task_catalog
.
_normalize_
subsection
_key
(
subsection_key
)
normalized_key
=
task_catalog
.
_normalize_
parent_ref
_key
(
subsection_key
)
if
not
normalized_key
:
if
not
normalized_key
:
raise
ValueError
(
"invalid subsection key"
)
raise
ValueError
(
"invalid subsection key"
)
...
@@ -53,31 +53,41 @@ def get_initial_message(subsection_key: str, path: Path = PROMPTS_PATH) -> str:
...
@@ -53,31 +53,41 @@ def get_initial_message(subsection_key: str, path: Path = PROMPTS_PATH) -> str:
def
build_subsection_catalog
(
path
:
Path
=
PROMPTS_PATH
)
->
list
[
dict
[
str
,
Any
]]:
def
build_subsection_catalog
(
path
:
Path
=
PROMPTS_PATH
)
->
list
[
dict
[
str
,
Any
]]:
items
=
load_initial_prompt_items
(
path
)
items
=
load_initial_prompt_items
(
path
)
subsection
_summaries
=
task_catalog
.
load_
subsection
_summaries
()
topic
_summaries
=
task_catalog
.
load_
topic
_summaries
()
response
:
list
[
dict
[
str
,
Any
]]
=
[]
response
:
list
[
dict
[
str
,
Any
]]
=
[]
for
raw_key
,
item
in
items
.
items
():
for
raw_key
,
item
in
items
.
items
():
subsection_key
=
task_catalog
.
_normalize_
subsection
_key
(
str
(
raw_key
))
subsection_key
=
task_catalog
.
_normalize_
parent_ref
_key
(
str
(
raw_key
))
if
not
subsection_key
:
if
not
subsection_key
:
continue
continue
label_source
=
str
(
item
.
get
(
"subsection"
)
or
subsection_key
)
label_source
=
str
(
item
.
get
(
"label"
)
or
item
.
get
(
"subsection"
)
or
subsection_key
)
label
=
task_catalog
.
_format_subsection_label
(
label_source
)
label
=
task_catalog
.
_format_subsection_label
(
label_source
)
ref
=
task_catalog
.
_parse_subsection_ref
(
str
(
item
.
get
(
"index"
)
or
""
))
refs
:
list
[
list
[
int
]]
=
[]
refs
=
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
])]]
if
ref
is
not
None
else
[]
refs_raw
=
item
.
get
(
"refs"
,
[])
if
isinstance
(
refs_raw
,
list
):
for
raw_ref
in
refs_raw
:
parsed
=
task_catalog
.
_parse_parent_ref
(
raw_ref
)
if
parsed
is
not
None
:
refs
.
append
(
task_catalog
.
_parent_ref_to_list
(
parsed
))
if
not
refs
:
parsed_index
=
task_catalog
.
_parse_parent_ref
(
str
(
item
.
get
(
"index"
)
or
""
))
if
parsed_index
is
not
None
:
refs
.
append
(
task_catalog
.
_parent_ref_to_list
(
parsed_index
))
response
.
append
(
response
.
append
(
{
{
"subsection_key"
:
subsection_key
,
"subsection_key"
:
subsection_key
,
"label"
:
label
or
task_catalog
.
_format_subsection_label
(
subsection_key
),
"label"
:
label
or
task_catalog
.
_format_subsection_label
(
subsection_key
),
"level"
:
str
(
item
.
get
(
"level"
)
or
(
task_catalog
.
_parent_ref_level
(
tuple
(
refs
[
0
]))
if
refs
else
""
)).
strip
(),
"refs"
:
refs
,
"refs"
:
refs
,
"summary"
:
subsection
_summaries
.
get
(
subsection_key
,
""
),
"summary"
:
topic
_summaries
.
get
(
subsection_key
,
""
),
}
}
)
)
response
.
sort
(
response
.
sort
(
key
=
lambda
item
:
(
key
=
lambda
item
:
(
item
[
"refs"
][
0
]
if
item
[
"refs"
]
else
(
9999
,
9999
,
9999
),
item
[
"refs"
][
0
]
if
item
[
"refs"
]
else
(
9999
,
9999
,
9999
,
9999
),
item
[
"subsection_key"
],
item
[
"subsection_key"
],
)
)
)
)
...
...
math-tutor/backend/app/deterministic_services/task_catalog.py
View file @
c4ee9dda
...
@@ -172,6 +172,12 @@ def _extract_subsection_summary(text: str) -> str:
...
@@ -172,6 +172,12 @@ def _extract_subsection_summary(text: str) -> str:
def
load_topic_summaries
(
base_dir
:
Path
=
SOURCES_DIR
)
->
dict
[
str
,
str
]:
def
load_topic_summaries
(
base_dir
:
Path
=
SOURCES_DIR
)
->
dict
[
str
,
str
]:
summaries
:
dict
[
str
,
str
]
=
{}
summaries
:
dict
[
str
,
str
]
=
{}
folders
=
(
folders
=
(
base_dir
/
"lecture_script"
/
"sections"
,
base_dir
/
"lecture_script"
/
"subsections"
,
base_dir
/
"lecture_script"
/
"subsubsections"
,
base_dir
/
"stochastik_context"
/
"sections"
,
base_dir
/
"stochastik_context"
/
"subsections"
,
base_dir
/
"stochastik_context"
/
"subsubsections"
,
base_dir
/
"lecture_script_new"
/
"sections"
,
base_dir
/
"lecture_script_new"
/
"sections"
,
base_dir
/
"lecture_script_new"
/
"subsections"
,
base_dir
/
"lecture_script_new"
/
"subsections"
,
base_dir
/
"lecture_script_new"
/
"subsubsections"
,
base_dir
/
"lecture_script_new"
/
"subsubsections"
,
...
@@ -343,14 +349,15 @@ def build_subsection_catalog(path: Path = SUBSECTION_MAP_PATH) -> list[dict[str,
...
@@ -343,14 +349,15 @@ def build_subsection_catalog(path: Path = SUBSECTION_MAP_PATH) -> list[dict[str,
response
:
list
[
dict
[
str
,
Any
]]
=
[]
response
:
list
[
dict
[
str
,
Any
]]
=
[]
for
item
in
build_topic_catalog
(
path
):
for
item
in
build_topic_catalog
(
path
):
refs
=
item
.
get
(
"refs"
,
[])
refs
=
item
.
get
(
"refs"
,
[])
if
item
.
get
(
"level"
)
!=
"subsection"
or
not
refs
:
if
not
refs
:
continue
continue
ref
=
refs
[
0
]
ref
=
refs
[
0
]
response
.
append
(
response
.
append
(
{
{
"subsection_key"
:
item
[
"topic_key"
],
"subsection_key"
:
item
[
"topic_key"
],
"label"
:
item
[
"label"
],
"label"
:
item
[
"label"
],
"refs"
:
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
])]],
"level"
:
item
[
"level"
],
"refs"
:
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
]),
int
(
ref
[
3
])]],
"summary"
:
item
[
"summary"
],
"summary"
:
item
[
"summary"
],
}
}
)
)
...
@@ -371,14 +378,15 @@ def _resolve_task_subsection_options(
...
@@ -371,14 +378,15 @@ def _resolve_task_subsection_options(
response
:
list
[
dict
[
str
,
Any
]]
=
[]
response
:
list
[
dict
[
str
,
Any
]]
=
[]
for
option
in
options
:
for
option
in
options
:
refs
=
option
.
get
(
"refs"
,
[])
refs
=
option
.
get
(
"refs"
,
[])
if
option
.
get
(
"level"
)
!=
"subsection"
or
not
refs
:
if
not
refs
:
continue
continue
ref
=
refs
[
0
]
ref
=
refs
[
0
]
response
.
append
(
response
.
append
(
{
{
"subsection_key"
:
option
[
"topic_key"
],
"subsection_key"
:
option
[
"topic_key"
],
"label"
:
option
[
"label"
],
"label"
:
option
[
"label"
],
"refs"
:
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
])]],
"level"
:
option
[
"level"
],
"refs"
:
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
]),
int
(
ref
[
3
])]],
"summary"
:
option
.
get
(
"summary"
,
""
),
"summary"
:
option
.
get
(
"summary"
,
""
),
}
}
)
)
...
@@ -591,6 +599,7 @@ def set_selected_task(
...
@@ -591,6 +599,7 @@ def set_selected_task(
sheet
.
pop
(
"task_subsection_refs"
,
None
)
sheet
.
pop
(
"task_subsection_refs"
,
None
)
sheet
.
pop
(
"selected_subsection_key"
,
None
)
sheet
.
pop
(
"selected_subsection_key"
,
None
)
sheet
.
pop
(
"selected_subsection_label"
,
None
)
sheet
.
pop
(
"selected_subsection_label"
,
None
)
sheet
.
pop
(
"selected_subsection_parent_refs"
,
None
)
sheet
.
pop
(
"selected_subsection_refs"
,
None
)
sheet
.
pop
(
"selected_subsection_refs"
,
None
)
...
@@ -600,21 +609,27 @@ def set_selected_subsection(
...
@@ -600,21 +609,27 @@ def set_selected_subsection(
subsection_option
:
dict
[
str
,
Any
],
subsection_option
:
dict
[
str
,
Any
],
)
->
None
:
)
->
None
:
refs_raw
=
subsection_option
.
get
(
"refs"
,
[])
refs_raw
=
subsection_option
.
get
(
"refs"
,
[])
refs
:
list
[
tuple
[
int
,
int
,
int
]
]
=
[]
refs
:
list
[
ParentRef
]
=
[]
if
isinstance
(
refs_raw
,
list
):
if
isinstance
(
refs_raw
,
list
):
for
item
in
refs_raw
:
for
item
in
refs_raw
:
if
isinstance
(
item
,
(
list
,
tuple
))
and
len
(
item
)
>=
3
:
parsed
=
_parse_parent_ref
(
item
)
try
:
if
parsed
is
not
None
:
refs
.
append
((
int
(
item
[
0
]),
int
(
item
[
1
]),
int
(
item
[
2
])))
refs
.
append
(
parsed
)
except
Exception
:
continue
if
not
refs
:
if
not
refs
:
return
return
sheet
[
"selected_subsection_key"
]
=
str
(
subsection_option
.
get
(
"subsection_key"
,
""
)).
strip
()
key
=
str
(
subsection_option
.
get
(
"subsection_key"
)
or
subsection_option
.
get
(
"topic_key"
)
or
""
).
strip
()
sheet
[
"selected_subsection_key"
]
=
key
sheet
[
"selected_subsection_label"
]
=
str
(
subsection_option
.
get
(
"label"
,
""
)).
strip
()
sheet
[
"selected_subsection_label"
]
=
str
(
subsection_option
.
get
(
"label"
,
""
)).
strip
()
sheet
[
"selected_subsection_refs"
]
=
[[
chap
,
sec
,
sub
]
for
chap
,
sec
,
sub
in
sorted
({
*
refs
})]
parent_refs
=
sorted
({
_normalize_parent_ref
(
ref
)
for
ref
in
refs
})
sheet
[
"selected_subsection_parent_refs"
]
=
[
_parent_ref_to_list
(
ref
)
for
ref
in
parent_refs
]
sheet
[
"selected_subsection_refs"
]
=
[
[
chap
,
sec
,
sub
]
for
chap
,
sec
,
sub
,
subsub
in
parent_refs
if
sub
>
0
and
subsub
<=
0
]
sheet
.
pop
(
"task_file_id"
,
None
)
sheet
.
pop
(
"task_id"
,
None
)
sheet
.
pop
(
"task_id"
,
None
)
sheet
.
pop
(
"task_parent_refs"
,
None
)
sheet
.
pop
(
"task_parent_refs"
,
None
)
sheet
.
pop
(
"task_subsection_refs"
,
None
)
sheet
.
pop
(
"task_subsection_refs"
,
None
)
...
@@ -651,8 +666,7 @@ def select_subsection_by_ids(
...
@@ -651,8 +666,7 @@ def select_subsection_by_ids(
if
not
task_file
:
if
not
task_file
:
return
False
return
False
subsection_map
=
load_subsection_map
()
options
=
_resolve_task_subsection_options
(
task_file
)
options
=
_resolve_task_subsection_options
(
task_file
,
subsection_map
=
subsection_map
)
normalized_key
=
_normalize_subsection_key
(
subsection_key
)
normalized_key
=
_normalize_subsection_key
(
subsection_key
)
if
not
normalized_key
:
if
not
normalized_key
:
return
False
return
False
...
@@ -669,7 +683,11 @@ def select_subsection_by_key(
...
@@ -669,7 +683,11 @@ def select_subsection_by_key(
subsection_key
:
str
,
subsection_key
:
str
,
subsection_map
:
dict
[
str
,
tuple
[
int
,
int
,
int
]]
|
None
=
None
,
subsection_map
:
dict
[
str
,
tuple
[
int
,
int
,
int
]]
|
None
=
None
,
)
->
bool
:
)
->
bool
:
mapping
=
subsection_map
if
subsection_map
is
not
None
else
load_subsection_map
()
mapping
:
dict
[
str
,
ParentRef
]
if
subsection_map
is
None
:
mapping
=
load_topic_map
()
else
:
mapping
=
{
key
:
(
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
]),
0
)
for
key
,
ref
in
subsection_map
.
items
()}
normalized_key
=
_normalize_subsection_key
(
subsection_key
)
normalized_key
=
_normalize_subsection_key
(
subsection_key
)
if
not
normalized_key
:
if
not
normalized_key
:
return
False
return
False
...
@@ -679,7 +697,13 @@ def select_subsection_by_key(
...
@@ -679,7 +697,13 @@ def select_subsection_by_key(
sheet
[
"selected_subsection_key"
]
=
normalized_key
sheet
[
"selected_subsection_key"
]
=
normalized_key
sheet
[
"selected_subsection_label"
]
=
_format_subsection_label
(
normalized_key
)
sheet
[
"selected_subsection_label"
]
=
_format_subsection_label
(
normalized_key
)
sheet
[
"selected_subsection_refs"
]
=
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
])]]
normalized_ref
=
_normalize_parent_ref
(
ref
)
sheet
[
"selected_subsection_parent_refs"
]
=
[
_parent_ref_to_list
(
normalized_ref
)]
chap
,
sec
,
sub
,
subsub
=
normalized_ref
if
sub
>
0
and
subsub
<=
0
:
sheet
[
"selected_subsection_refs"
]
=
[[
chap
,
sec
,
sub
]]
else
:
sheet
[
"selected_subsection_refs"
]
=
[]
sheet
.
pop
(
"task_file_id"
,
None
)
sheet
.
pop
(
"task_file_id"
,
None
)
sheet
.
pop
(
"task_id"
,
None
)
sheet
.
pop
(
"task_id"
,
None
)
sheet
.
pop
(
"task_parent_refs"
,
None
)
sheet
.
pop
(
"task_parent_refs"
,
None
)
...
@@ -702,17 +726,24 @@ def get_selected_subsection_ids(sheet: dict[str, Any]) -> tuple[str | None, str
...
@@ -702,17 +726,24 @@ def get_selected_subsection_ids(sheet: dict[str, Any]) -> tuple[str | None, str
def
get_selected_subsection_refs
(
sheet
:
dict
[
str
,
Any
])
->
list
[
tuple
[
int
,
int
,
int
]]:
def
get_selected_subsection_refs
(
sheet
:
dict
[
str
,
Any
])
->
list
[
tuple
[
int
,
int
,
int
]]:
refs_raw
=
sheet
.
get
(
"selected_subsection_refs"
,
[])
refs
:
list
[
tuple
[
int
,
int
,
int
]]
=
[]
for
chap
,
sec
,
sub
,
subsub
in
get_selected_subsection_parent_refs
(
sheet
):
if
sub
<=
0
or
subsub
>
0
:
continue
refs
.
append
((
chap
,
sec
,
sub
))
return
sorted
(
set
(
refs
))
def
get_selected_subsection_parent_refs
(
sheet
:
dict
[
str
,
Any
])
->
list
[
ParentRef
]:
refs_raw
=
sheet
.
get
(
"selected_subsection_parent_refs"
)
if
not
isinstance
(
refs_raw
,
list
):
if
not
isinstance
(
refs_raw
,
list
):
re
turn
[]
re
fs_raw
=
sheet
.
get
(
"selected_subsection_refs"
,
[]
)
refs
:
set
[
tuple
[
int
,
int
,
int
]
]
=
set
()
refs
:
set
[
ParentRef
]
=
set
()
for
item
in
refs_raw
:
for
item
in
refs_raw
:
if
isinstance
(
item
,
(
list
,
tuple
))
and
len
(
item
)
>=
3
:
parsed
=
_parse_parent_ref
(
item
)
try
:
if
parsed
is
not
None
:
refs
.
add
((
int
(
item
[
0
]),
int
(
item
[
1
]),
int
(
item
[
2
])))
refs
.
add
(
_normalize_parent_ref
(
parsed
))
except
Exception
:
continue
return
sorted
(
refs
)
return
sorted
(
refs
)
...
...
math-tutor/backend/scripts/generate_socratic_chats.py
View file @
c4ee9dda
...
@@ -14,17 +14,17 @@ from app.deterministic_services.vector_store import parse_markdown_with_frontmat
...
@@ -14,17 +14,17 @@ from app.deterministic_services.vector_store import parse_markdown_with_frontmat
BACKEND_ROOT
=
Path
(
__file__
).
resolve
().
parents
[
2
]
BACKEND_ROOT
=
Path
(
__file__
).
resolve
().
parents
[
2
]
DEFAULT_SOURCE_ROOT
=
BACKEND_ROOT
/
"sources"
/
"
with_chapters
"
DEFAULT_SOURCE_ROOT
=
BACKEND_ROOT
/
"sources"
/
"
lecture_script
"
DEFAULT_OUTPUT
=
BACKEND_ROOT
/
"sources"
/
"inital_socratic_prompt"
/
"initial_prompts.yml"
DEFAULT_OUTPUT
=
BACKEND_ROOT
/
"sources"
/
"inital_socratic_prompt"
/
"initial_prompts.y
a
ml"
DEFAULT_TEMPLATE
=
BACKEND_ROOT
/
"sources"
/
"inital_socratic_prompt"
/
"template.yml"
DEFAULT_TEMPLATE
=
BACKEND_ROOT
/
"sources"
/
"inital_socratic_prompt"
/
"template.y
a
ml"
DEFAULT_PROMPT
=
(
DEFAULT_PROMPT
=
(
"Was sind die Themen dieses Abschnitts? Numeriere sie durch. Frage mich, mit welchem ich mich "
"Was sind die Themen dieses Abschnitts? Numeriere sie durch. Frage mich, mit welchem ich mich "
"zuerst beschäftigen möchte."
"zuerst beschäftigen möchte."
)
)
DEFAULT_TEMPLATE_FIELDS
=
[
"
subsection
"
,
"index"
,
"inital_message"
,
"sources"
]
DEFAULT_TEMPLATE_FIELDS
=
[
"
label"
,
"level
"
,
"index"
,
"refs"
,
"inital_message"
,
"sources"
]
Subsection
Ref
=
tuple
[
int
,
int
,
int
]
Parent
Ref
=
tuple
[
int
,
int
,
int
,
int
]
@
dataclass
(
frozen
=
True
)
@
dataclass
(
frozen
=
True
)
...
@@ -33,8 +33,9 @@ class MarkdownDoc:
...
@@ -33,8 +33,9 @@ class MarkdownDoc:
relative_path
:
str
relative_path
:
str
meta
:
dict
[
str
,
Any
]
meta
:
dict
[
str
,
Any
]
body
:
str
body
:
str
ref
:
Subsection
Ref
|
None
ref
:
Parent
Ref
|
None
title
:
str
title
:
str
level
:
str
def
_meta_int
(
meta
:
dict
[
str
,
Any
],
key
:
str
)
->
int
|
None
:
def
_meta_int
(
meta
:
dict
[
str
,
Any
],
key
:
str
)
->
int
|
None
:
...
@@ -47,87 +48,119 @@ def _meta_int(meta: dict[str, Any], key: str) -> int | None:
...
@@ -47,87 +48,119 @@ def _meta_int(meta: dict[str, Any], key: str) -> int | None:
return
None
return
None
def
_detect_doc_level
(
path
:
Path
)
->
str
:
folder
=
path
.
parent
.
name
.
lower
()
if
folder
==
"sections"
:
return
"section"
if
folder
==
"subsections"
:
return
"subsection"
if
folder
==
"subsubsections"
:
return
"subsubsection"
return
"child"
def
_resolve_parent_ref
(
meta
:
dict
[
str
,
Any
],
title
:
str
,
topic_map
:
dict
[
str
,
ParentRef
])
->
ParentRef
|
None
:
chapter_index
=
_meta_int
(
meta
,
"chapter_index"
)
section_index
=
_meta_int
(
meta
,
"section_index"
)
if
chapter_index
is
not
None
and
section_index
is
not
None
:
subsection_index
=
_meta_int
(
meta
,
"subsection_index"
)
or
0
subsubsection_index
=
_meta_int
(
meta
,
"subsubsection_index"
)
or
0
return
task_catalog
.
_normalize_parent_ref
(
(
chapter_index
,
section_index
,
subsection_index
,
subsubsection_index
)
)
key_candidates
=
[
title
,
str
(
meta
.
get
(
"subsubsection_title"
)
or
""
),
str
(
meta
.
get
(
"subsection_title"
)
or
""
),
str
(
meta
.
get
(
"section_title"
)
or
""
),
]
for
candidate
in
key_candidates
:
key
=
task_catalog
.
_normalize_parent_ref_key
(
candidate
)
if
key
and
key
in
topic_map
:
return
topic_map
[
key
]
return
None
def
_read_markdown_doc
(
def
_read_markdown_doc
(
path
:
Path
,
path
:
Path
,
source_root
:
Path
,
source_root
:
Path
,
subsection
_map
:
dict
[
str
,
Subsection
Ref
],
topic
_map
:
dict
[
str
,
Parent
Ref
],
)
->
MarkdownDoc
:
)
->
MarkdownDoc
:
text
=
path
.
read_text
(
encoding
=
"utf-8"
)
text
=
path
.
read_text
(
encoding
=
"utf-8"
)
meta
,
body
=
parse_markdown_with_frontmatter
(
text
)
meta
,
body
=
parse_markdown_with_frontmatter
(
text
)
title
=
str
(
title
=
str
(
meta
.
get
(
"title"
)
meta
.
get
(
"title"
)
or
meta
.
get
(
"subsubsection_title"
)
or
meta
.
get
(
"subsection_title"
)
or
meta
.
get
(
"subsection_title"
)
or
meta
.
get
(
"section_title"
)
or
meta
.
get
(
"section_title"
)
or
path
.
stem
or
path
.
stem
).
strip
()
).
strip
()
ref
=
_resolve_subsection_ref
(
meta
,
title
,
subsection_map
)
return
MarkdownDoc
(
return
MarkdownDoc
(
path
=
path
,
path
=
path
,
relative_path
=
path
.
relative_to
(
source_root
).
as_posix
(),
relative_path
=
path
.
relative_to
(
source_root
).
as_posix
(),
meta
=
meta
,
meta
=
meta
,
body
=
body
.
strip
(),
body
=
body
.
strip
(),
ref
=
ref
,
ref
=
_resolve_parent_ref
(
meta
,
title
,
topic_map
)
,
title
=
title
,
title
=
title
,
level
=
_detect_doc_level
(
path
),
)
)
def
_resolve_subsection_ref
(
meta
:
dict
[
str
,
Any
],
title
:
str
,
subsection_map
:
dict
[
str
,
SubsectionRef
],
)
->
SubsectionRef
|
None
:
chapter_index
=
_meta_int
(
meta
,
"chapter_index"
)
section_index
=
_meta_int
(
meta
,
"section_index"
)
subsection_index
=
_meta_int
(
meta
,
"subsection_index"
)
if
chapter_index
is
not
None
and
section_index
is
not
None
and
subsection_index
is
not
None
:
return
(
chapter_index
,
section_index
,
subsection_index
)
key_candidates
=
[
title
,
str
(
meta
.
get
(
"subsection_title"
)
or
""
),
str
(
meta
.
get
(
"title"
)
or
""
),
]
for
candidate
in
key_candidates
:
key
=
task_catalog
.
_normalize_subsection_key
(
candidate
)
if
key
and
key
in
subsection_map
:
return
subsection_map
[
key
]
if
section_index
is
not
None
and
subsection_index
is
not
None
:
matches
=
[
ref
for
ref
in
subsection_map
.
values
()
if
int
(
ref
[
1
])
==
section_index
and
int
(
ref
[
2
])
==
subsection_index
]
unique_matches
=
sorted
(
set
(
matches
))
if
len
(
unique_matches
)
==
1
:
return
unique_matches
[
0
]
return
None
def
_load_markdown_docs
(
def
_load_markdown_docs
(
source_root
:
Path
,
source_root
:
Path
,
folder_name
:
str
,
folder_name
:
str
,
subsection
_map
:
dict
[
str
,
Subsection
Ref
],
topic
_map
:
dict
[
str
,
Parent
Ref
],
)
->
list
[
MarkdownDoc
]:
)
->
list
[
MarkdownDoc
]:
folder
=
source_root
/
folder_name
folder
=
source_root
/
folder_name
if
not
folder
.
exists
():
if
not
folder
.
exists
():
return
[]
return
[]
return
[
return
[
_read_markdown_doc
(
path
,
source_root
,
subsection
_map
)
_read_markdown_doc
(
path
,
source_root
,
topic
_map
)
for
path
in
sorted
(
folder
.
glob
(
"*.md"
))
for
path
in
sorted
(
folder
.
glob
(
"*.md"
))
]
]
def
load_subsections_and_children
(
def
_is_descendant_ref
(
candidate
:
ParentRef
,
ancestor
:
ParentRef
)
->
bool
:
cand
=
task_catalog
.
_normalize_parent_ref
(
candidate
)
anc
=
task_catalog
.
_normalize_parent_ref
(
ancestor
)
if
cand
==
anc
:
return
False
if
anc
[
2
]
<=
0
:
return
cand
[
0
]
==
anc
[
0
]
and
cand
[
1
]
==
anc
[
1
]
if
anc
[
3
]
<=
0
:
return
cand
[
0
]
==
anc
[
0
]
and
cand
[
1
]
==
anc
[
1
]
and
cand
[
2
]
==
anc
[
2
]
return
False
def
_leaf_topic_map
(
topic_map
:
dict
[
str
,
ParentRef
])
->
dict
[
str
,
ParentRef
]:
refs
=
list
(
topic_map
.
values
())
leaves
:
dict
[
str
,
ParentRef
]
=
{}
for
key
,
ref
in
topic_map
.
items
():
if
any
(
_is_descendant_ref
(
other
,
ref
)
for
other
in
refs
):
continue
leaves
[
key
]
=
task_catalog
.
_normalize_parent_ref
(
ref
)
return
leaves
def
load_topics_and_children
(
source_root
:
Path
,
source_root
:
Path
,
subsection_map
:
dict
[
str
,
SubsectionRef
]
|
None
=
None
,
topic_map
:
dict
[
str
,
ParentRef
]
|
None
=
None
,
)
->
tuple
[
list
[
MarkdownDoc
],
dict
[
SubsectionRef
,
list
[
MarkdownDoc
]]]:
)
->
tuple
[
dict
[
ParentRef
,
MarkdownDoc
],
dict
[
ParentRef
,
list
[
MarkdownDoc
]]]:
mapping
=
subsection_map
if
subsection_map
is
not
None
else
task_catalog
.
load_subsection_map
()
mapping
=
topic_map
if
topic_map
is
not
None
else
task_catalog
.
load_topic_map
()
subsections
=
_load_markdown_docs
(
source_root
,
"subsections"
,
mapping
)
parents
:
dict
[
ParentRef
,
MarkdownDoc
]
=
{}
children
=
_load_markdown_docs
(
source_root
,
"childs"
,
mapping
)
parent_docs
=
[
*
_load_markdown_docs
(
source_root
,
"sections"
,
mapping
),
*
_load_markdown_docs
(
source_root
,
"subsections"
,
mapping
),
*
_load_markdown_docs
(
source_root
,
"subsubsections"
,
mapping
),
]
for
doc
in
parent_docs
:
if
doc
.
ref
is
None
:
continue
parents
.
setdefault
(
doc
.
ref
,
doc
)
children_by_ref
:
dict
[
SubsectionRef
,
list
[
MarkdownDoc
]]
=
{}
children
=
_load_markdown_docs
(
source_root
,
"childs"
,
mapping
)
children_by_ref
:
dict
[
ParentRef
,
list
[
MarkdownDoc
]]
=
{}
for
child
in
children
:
for
child
in
children
:
if
child
.
ref
is
None
:
if
child
.
ref
is
None
:
continue
continue
...
@@ -136,25 +169,27 @@ def load_subsections_and_children(
...
@@ -136,25 +169,27 @@ def load_subsections_and_children(
for
ref_children
in
children_by_ref
.
values
():
for
ref_children
in
children_by_ref
.
values
():
ref_children
.
sort
(
key
=
lambda
item
:
(
_meta_int
(
item
.
meta
,
"child_index"
)
or
0
,
item
.
relative_path
))
ref_children
.
sort
(
key
=
lambda
item
:
(
_meta_int
(
item
.
meta
,
"child_index"
)
or
0
,
item
.
relative_path
))
return
subsections
,
children_by_ref
return
parents
,
children_by_ref
def
_subsection_key
(
subsection
:
MarkdownDoc
)
->
str
:
return
task_catalog
.
_normalize_subsection_key
(
subsection
.
title
or
subsection
.
path
.
stem
)
def
_format_ref
(
ref
:
ParentRef
)
->
str
:
chap
,
sec
,
sub
,
subsub
=
task_catalog
.
_normalize_parent_ref
(
ref
)
if
sub
<=
0
:
return
f
"
{
chap
}
-
{
sec
}
"
if
subsub
<=
0
:
return
f
"
{
chap
}
-
{
sec
}
-
{
sub
}
"
return
f
"
{
chap
}
-
{
sec
}
-
{
sub
}
-
{
subsub
}
"
def
_format_ref
(
ref
:
SubsectionRef
)
->
str
:
return
f
"
{
ref
[
0
]
}
-
{
ref
[
1
]
}
-
{
ref
[
2
]
}
"
def
build_llm_sources_text
(
topic
:
MarkdownDoc
,
children
:
list
[
MarkdownDoc
])
->
str
:
def
build_llm_sources_text
(
subsection
:
MarkdownDoc
,
children
:
list
[
MarkdownDoc
])
->
str
:
ref_text
=
_format_ref
(
topic
.
ref
)
if
topic
.
ref
else
""
ref_text
=
_format_ref
(
subsection
.
ref
)
if
subsection
.
ref
else
""
parts
=
[
parts
=
[
f
"Abschnitt:
{
subsection
.
title
}
"
,
f
"Thema:
{
topic
.
title
}
"
,
f
"Level:
{
topic
.
level
}
"
,
f
"Index:
{
ref_text
}
"
,
f
"Index:
{
ref_text
}
"
,
f
"Datei:
{
subsection
.
relative_path
}
"
,
f
"Datei:
{
topic
.
relative_path
}
"
,
"
Abschnitts
inhalt:"
,
"
Themen
inhalt:"
,
subsection
.
body
,
topic
.
body
,
]
]
for
child
in
children
:
for
child
in
children
:
...
@@ -171,12 +206,12 @@ def build_llm_sources_text(subsection: MarkdownDoc, children: list[MarkdownDoc])
...
@@ -171,12 +206,12 @@ def build_llm_sources_text(subsection: MarkdownDoc, children: list[MarkdownDoc])
return
"
\n
"
.
join
(
parts
).
strip
()
return
"
\n
"
.
join
(
parts
).
strip
()
def
generate_initial_message
(
subsection
:
MarkdownDoc
,
children
:
list
[
MarkdownDoc
])
->
str
:
def
generate_initial_message
(
topic
:
MarkdownDoc
,
children
:
list
[
MarkdownDoc
])
->
str
:
sources_text
=
build_llm_sources_text
(
subsection
,
children
)
sources_text
=
build_llm_sources_text
(
topic
,
children
)
refs
=
[
subsection
.
ref
]
if
subsection
.
ref
else
None
refs
=
[
topic
.
ref
]
if
topic
.
ref
else
None
return
socratic_LLM
.
generate_dialog
(
return
socratic_LLM
.
generate_dialog
(
query
=
DEFAULT_PROMPT
,
query
=
DEFAULT_PROMPT
,
subsection
_refs
=
refs
,
parent
_refs
=
refs
,
history
=
None
,
history
=
None
,
sources
=
sources_text
,
sources
=
sources_text
,
).
strip
()
).
strip
()
...
@@ -240,43 +275,46 @@ def build_initial_prompt_index(
...
@@ -240,43 +275,46 @@ def build_initial_prompt_index(
output_path
=
output_path
.
resolve
()
output_path
=
output_path
.
resolve
()
template_fields
=
_load_template_fields
(
template_path
)
template_fields
=
_load_template_fields
(
template_path
)
existing_items
=
_load_existing_items
(
output_path
)
existing_items
=
_load_existing_items
(
output_path
)
subsections
,
children_by_ref
=
load_subsections_and_children
(
source_root
)
topic_map
=
task_catalog
.
load_topic_map
()
leaf_topics
=
_leaf_topic_map
(
topic_map
)
parents_by_ref
,
children_by_ref
=
load_topics_and_children
(
source_root
,
topic_map
)
items
:
dict
[
str
,
dict
[
str
,
Any
]]
=
{}
items
:
dict
[
str
,
dict
[
str
,
Any
]]
=
{}
warnings
:
list
[
str
]
=
[]
warnings
:
list
[
str
]
=
[]
processed
=
0
processed
=
0
for
subsection
in
subsections
:
for
key
,
ref
in
sorted
(
leaf_topics
.
items
(),
key
=
lambda
item
:
item
[
1
])
:
if
limit
is
not
None
and
processed
>=
limit
:
if
limit
is
not
None
and
processed
>=
limit
:
break
break
key
=
_subsection_key
(
subsection
)
topic
=
parents_by_ref
.
get
(
ref
)
if
not
key
:
if
topic
is
None
:
warnings
.
append
(
f
"Skipping subsection without key:
{
subsection
.
relative_path
}
"
)
message
=
f
"Missing parent markdown doc for topic '
{
key
}
' (
{
_format_ref
(
ref
)
}
)"
continue
if
strict
:
if
subsection
.
ref
is
None
:
raise
ValueError
(
message
)
warnings
.
append
(
f
"Skipping subsection without resolved index:
{
subsection
.
relative_path
}
"
)
warnings
.
append
(
message
)
continue
continue
children
=
children_by_ref
.
get
(
subsection
.
ref
,
[])
children
=
children_by_ref
.
get
(
ref
,
[])
if
not
children
:
if
not
children
:
message
=
f
"No child chunks matched
subsection
{
subsection
.
relative_path
}
"
message
=
f
"No child chunks matched
topic
{
topic
.
relative_path
}
"
if
strict
:
if
strict
:
raise
ValueError
(
message
)
raise
ValueError
(
message
)
warnings
.
append
(
message
)
warnings
.
append
(
message
)
sources
=
[
subsection
.
relative_path
,
*
[
child
.
relative_path
for
child
in
children
]]
existing_message
=
str
(
existing_items
.
get
(
key
,
{}).
get
(
"inital_message"
)
or
""
).
strip
()
existing_message
=
str
(
existing_items
.
get
(
key
,
{}).
get
(
"inital_message"
)
or
""
).
strip
()
if
existing_message
and
not
force
:
if
existing_message
and
not
force
:
initial_message
=
existing_message
initial_message
=
existing_message
else
:
else
:
initial_message
=
generate_initial_message
(
subsection
,
children
)
initial_message
=
generate_initial_message
(
topic
,
children
)
item
=
{
item
=
{
"subsection"
:
subsection
.
title
,
"label"
:
topic
.
title
,
"index"
:
_format_ref
(
subsection
.
ref
),
"level"
:
task_catalog
.
_parent_ref_level
(
ref
),
"index"
:
_format_ref
(
ref
),
"refs"
:
[
task_catalog
.
_parent_ref_to_list
(
ref
)],
"inital_message"
:
initial_message
,
"inital_message"
:
initial_message
,
"sources"
:
sources
,
"sources"
:
[
topic
.
relative_path
,
*
[
child
.
relative_path
for
child
in
children
]]
,
}
}
items
[
key
]
=
_ordered_item
(
item
,
template_fields
)
items
[
key
]
=
_ordered_item
(
item
,
template_fields
)
processed
+=
1
processed
+=
1
...
@@ -325,7 +363,7 @@ def generate_file(
...
@@ -325,7 +363,7 @@ def generate_file(
def
parse_args
(
argv
:
list
[
str
]
|
None
=
None
)
->
argparse
.
Namespace
:
def
parse_args
(
argv
:
list
[
str
]
|
None
=
None
)
->
argparse
.
Namespace
:
parser
=
argparse
.
ArgumentParser
(
parser
=
argparse
.
ArgumentParser
(
description
=
"Generate pregenerated Socratic initial prompts for
subsection
s."
description
=
"Generate pregenerated Socratic initial prompts for
leaf topic
s."
)
)
parser
.
add_argument
(
"--source-root"
,
type
=
Path
,
default
=
DEFAULT_SOURCE_ROOT
)
parser
.
add_argument
(
"--source-root"
,
type
=
Path
,
default
=
DEFAULT_SOURCE_ROOT
)
parser
.
add_argument
(
"--output"
,
type
=
Path
,
default
=
DEFAULT_OUTPUT
)
parser
.
add_argument
(
"--output"
,
type
=
Path
,
default
=
DEFAULT_OUTPUT
)
...
...
math-tutor/backend/test/generate_socratic_chats_test.py
0 → 100644
View file @
c4ee9dda
from
__future__
import
annotations
import
os
import
shutil
import
sys
import
tempfile
import
unittest
from
pathlib
import
Path
from
unittest.mock
import
patch
os
.
environ
.
setdefault
(
"OPENAI_BASE_URL"
,
"http://localhost:9999"
)
os
.
environ
.
setdefault
(
"OPENAI_API_KEY"
,
"test-key"
)
BACKEND_ROOT
=
Path
(
__file__
).
resolve
().
parents
[
1
]
if
str
(
BACKEND_ROOT
)
not
in
sys
.
path
:
sys
.
path
.
insert
(
0
,
str
(
BACKEND_ROOT
))
from
scripts
import
generate_socratic_chats
def
_write_markdown
(
path
:
Path
,
frontmatter
:
str
,
body
:
str
)
->
None
:
path
.
parent
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
path
.
write_text
(
f
"---
\n
{
frontmatter
}
\n
---
\n
{
body
}
\n
"
,
encoding
=
"utf-8"
)
class
GenerateSocraticChatsTest
(
unittest
.
TestCase
):
def
setUp
(
self
)
->
None
:
self
.
temp_dir
=
Path
(
tempfile
.
mkdtemp
(
prefix
=
"socratic-prompts-"
))
self
.
source_root
=
self
.
temp_dir
/
"lecture_script"
self
.
output_path
=
self
.
temp_dir
/
"initial_prompts.yaml"
self
.
template_path
=
self
.
temp_dir
/
"template.yaml"
self
.
template_path
.
write_text
(
"label: ''
\n
level: ''
\n
index: ''
\n
refs:
\n
- [0, 0, 0, 0]
\n
inital_message: ''
\n
sources:
\n
- ''
\n
"
,
encoding
=
"utf-8"
,
)
def
tearDown
(
self
)
->
None
:
shutil
.
rmtree
(
self
.
temp_dir
,
ignore_errors
=
True
)
def
test_build_initial_prompt_index_uses_lowest_available_leaf_level
(
self
)
->
None
:
_write_markdown
(
self
.
source_root
/
"sections/section-leaf.md"
,
'chapter_index: 1
\n
section_index: 1
\n
title: "Mengen"'
,
"Section leaf body"
,
)
_write_markdown
(
self
.
source_root
/
"subsections/subsection-leaf.md"
,
'chapter_index: 1
\n
section_index: 2
\n
subsection_index: 3
\n
title: "Potenz- und Wurzelrechnung"'
,
"Subsection leaf body"
,
)
_write_markdown
(
self
.
source_root
/
"subsections/subsection-parent.md"
,
'chapter_index: 1
\n
section_index: 2
\n
subsection_index: 2
\n
title: "Grundrechenarten bei reellen Zahlen"'
,
"Subsection parent body"
,
)
_write_markdown
(
self
.
source_root
/
"subsubsections/subsubsection-leaf.md"
,
'chapter_index: 1
\n
section_index: 2
\n
subsection_index: 2
\n
subsubsection_index: 1
\n
title: "Klammerrechnung"'
,
"Subsubsection leaf body"
,
)
_write_markdown
(
self
.
source_root
/
"childs/section-child.md"
,
'chapter_index: 1
\n
section_index: 1
\n
subsection_index: 0
\n
subsubsection_index: 0
\n
child_index: 1
\n
title: "Mengen Child"'
,
"Section child body"
,
)
_write_markdown
(
self
.
source_root
/
"childs/subsection-child.md"
,
'chapter_index: 1
\n
section_index: 2
\n
subsection_index: 3
\n
subsubsection_index: 0
\n
child_index: 1
\n
title: "Potenz Child"'
,
"Subsection child body"
,
)
_write_markdown
(
self
.
source_root
/
"childs/subsubsection-child.md"
,
'chapter_index: 1
\n
section_index: 2
\n
subsection_index: 2
\n
subsubsection_index: 1
\n
child_index: 1
\n
title: "Klammer Child"'
,
"Subsubsection child body"
,
)
_write_markdown
(
self
.
source_root
/
"childs/subsection-parent-child.md"
,
'chapter_index: 1
\n
section_index: 2
\n
subsection_index: 2
\n
subsubsection_index: 0
\n
child_index: 1
\n
title: "Grundrechenarten Child"'
,
"Subsection parent child body"
,
)
topic_map
=
{
"mengen"
:
(
1
,
1
,
0
,
0
),
"grundrechenarten bei reellen zahlen"
:
(
1
,
2
,
2
,
0
),
"klammerrechnung"
:
(
1
,
2
,
2
,
1
),
"potenz und wurzelrechnung"
:
(
1
,
2
,
3
,
0
),
}
with
patch
(
"scripts.generate_socratic_chats.task_catalog.load_topic_map"
,
return_value
=
topic_map
,
),
patch
(
"scripts.generate_socratic_chats.generate_initial_message"
,
side_effect
=
lambda
topic
,
children
:
f
"
{
topic
.
title
}
|
{
len
(
children
)
}
"
,
):
payload
=
generate_socratic_chats
.
build_initial_prompt_index
(
source_root
=
self
.
source_root
,
output_path
=
self
.
output_path
,
template_path
=
self
.
template_path
,
)
items
=
payload
[
"items"
]
self
.
assertEqual
(
sorted
(
items
.
keys
()),
[
"klammerrechnung"
,
"mengen"
,
"potenz und wurzelrechnung"
])
self
.
assertEqual
(
items
[
"mengen"
][
"level"
],
"section"
)
self
.
assertEqual
(
items
[
"mengen"
][
"index"
],
"1-1"
)
self
.
assertEqual
(
items
[
"mengen"
][
"refs"
],
[[
1
,
1
,
0
,
0
]])
self
.
assertEqual
(
items
[
"mengen"
][
"inital_message"
],
"Mengen|1"
)
self
.
assertEqual
(
items
[
"potenz und wurzelrechnung"
][
"level"
],
"subsection"
)
self
.
assertEqual
(
items
[
"potenz und wurzelrechnung"
][
"index"
],
"1-2-3"
)
self
.
assertEqual
(
items
[
"potenz und wurzelrechnung"
][
"refs"
],
[[
1
,
2
,
3
,
0
]])
self
.
assertEqual
(
items
[
"potenz und wurzelrechnung"
][
"inital_message"
],
"Potenz- und Wurzelrechnung|1"
)
self
.
assertEqual
(
items
[
"klammerrechnung"
][
"level"
],
"subsubsection"
)
self
.
assertEqual
(
items
[
"klammerrechnung"
][
"index"
],
"1-2-2-1"
)
self
.
assertEqual
(
items
[
"klammerrechnung"
][
"refs"
],
[[
1
,
2
,
2
,
1
]])
self
.
assertEqual
(
items
[
"klammerrechnung"
][
"inital_message"
],
"Klammerrechnung|1"
)
self
.
assertNotIn
(
"grundrechenarten bei reellen zahlen"
,
items
)
def
test_existing_message_reuse_matches_leaf_key
(
self
)
->
None
:
_write_markdown
(
self
.
source_root
/
"sections/section-leaf.md"
,
'chapter_index: 1
\n
section_index: 1
\n
title: "Mengen"'
,
"Section leaf body"
,
)
_write_markdown
(
self
.
source_root
/
"childs/section-child.md"
,
'chapter_index: 1
\n
section_index: 1
\n
subsection_index: 0
\n
subsubsection_index: 0
\n
child_index: 1
\n
title: "Mengen Child"'
,
"Section child body"
,
)
self
.
output_path
.
write_text
(
"items:
\n
mengen:
\n
inital_message: Bereits vorhanden
\n
"
,
encoding
=
"utf-8"
,
)
with
patch
(
"scripts.generate_socratic_chats.task_catalog.load_topic_map"
,
return_value
=
{
"mengen"
:
(
1
,
1
,
0
,
0
)},
),
patch
(
"scripts.generate_socratic_chats.generate_initial_message"
,
side_effect
=
AssertionError
(
"should not regenerate"
),
):
payload
=
generate_socratic_chats
.
build_initial_prompt_index
(
source_root
=
self
.
source_root
,
output_path
=
self
.
output_path
,
template_path
=
self
.
template_path
,
)
self
.
assertEqual
(
payload
[
"items"
][
"mengen"
][
"inital_message"
],
"Bereits vorhanden"
)
if
__name__
==
"__main__"
:
unittest
.
main
()
math-tutor/backend/test/task_catalog_socratic_test.py
View file @
c4ee9dda
...
@@ -120,16 +120,18 @@ class TaskCatalogSocraticTest(unittest.TestCase):
...
@@ -120,16 +120,18 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"app.deterministic_services.socratic_oranisator.load_initial_prompt_items"
,
"app.deterministic_services.socratic_oranisator.load_initial_prompt_items"
,
return_value
=
{
return_value
=
{
"quadratische gleichungen"
:
{
"quadratische gleichungen"
:
{
"subsection"
:
"Quadratische Gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"index"
:
"1-3-3"
,
"level"
:
"subsection"
,
"refs"
:
[[
1
,
3
,
3
,
0
]],
},
},
"mengen"
:
{
"mengen"
:
{
"subsection"
:
"Mengen"
,
"label"
:
"Mengen"
,
"index"
:
"1-1-1"
,
"level"
:
"section"
,
"refs"
:
[[
1
,
1
,
0
,
0
]],
},
},
},
},
),
patch
(
),
patch
(
"app.deterministic_services.socratic_oranisator.task_catalog.load_
subsection
_summaries"
,
"app.deterministic_services.socratic_oranisator.task_catalog.load_
topic
_summaries"
,
return_value
=
{
return_value
=
{
"mengen"
:
"Mengen summary text"
,
"mengen"
:
"Mengen summary text"
,
"quadratische gleichungen"
:
"Quadratische summary text"
,
"quadratische gleichungen"
:
"Quadratische summary text"
,
...
@@ -143,13 +145,15 @@ class TaskCatalogSocraticTest(unittest.TestCase):
...
@@ -143,13 +145,15 @@ class TaskCatalogSocraticTest(unittest.TestCase):
{
{
"subsection_key"
:
"mengen"
,
"subsection_key"
:
"mengen"
,
"label"
:
"Mengen"
,
"label"
:
"Mengen"
,
"refs"
:
[[
1
,
1
,
1
]],
"level"
:
"section"
,
"refs"
:
[[
1
,
1
,
0
,
0
]],
"summary"
:
"Mengen summary text"
,
"summary"
:
"Mengen summary text"
,
},
},
{
{
"subsection_key"
:
"quadratische gleichungen"
,
"subsection_key"
:
"quadratische gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"refs"
:
[[
1
,
3
,
3
]],
"level"
:
"subsection"
,
"refs"
:
[[
1
,
3
,
3
,
0
]],
"summary"
:
"Quadratische summary text"
,
"summary"
:
"Quadratische summary text"
,
},
},
],
],
...
@@ -177,13 +181,15 @@ class TaskCatalogSocraticTest(unittest.TestCase):
...
@@ -177,13 +181,15 @@ class TaskCatalogSocraticTest(unittest.TestCase):
{
{
"subsection_key"
:
"mengen"
,
"subsection_key"
:
"mengen"
,
"label"
:
"Mengen"
,
"label"
:
"Mengen"
,
"refs"
:
[[
1
,
1
,
1
]],
"level"
:
"section"
,
"refs"
:
[[
1
,
1
,
0
,
0
]],
"summary"
:
"Mengen summary text"
,
"summary"
:
"Mengen summary text"
,
},
},
{
{
"subsection_key"
:
"quadratische gleichungen"
,
"subsection_key"
:
"quadratische gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"refs"
:
[[
1
,
3
,
3
]],
"level"
:
"subsection"
,
"refs"
:
[[
1
,
3
,
3
,
0
]],
"summary"
:
"Quadratische summary text"
,
"summary"
:
"Quadratische summary text"
,
},
},
],
],
...
@@ -289,8 +295,8 @@ Zweite Zeile.
...
@@ -289,8 +295,8 @@ Zweite Zeile.
sheet
:
dict
[
str
,
object
]
=
{}
sheet
:
dict
[
str
,
object
]
=
{}
with
patch
(
with
patch
(
"app.deterministic_services.task_catalog.load_
subsection
_map"
,
"app.deterministic_services.task_catalog.load_
topic
_map"
,
return_value
=
{
"quadratische gleichungen"
:
(
1
,
3
,
3
)},
return_value
=
{
"quadratische gleichungen"
:
(
1
,
3
,
3
,
1
)},
):
):
updated
=
task_catalog
.
select_subsection_by_ids
(
updated
=
task_catalog
.
select_subsection_by_ids
(
sheet
,
sheet
,
...
@@ -301,20 +307,31 @@ Zweite Zeile.
...
@@ -301,20 +307,31 @@ Zweite Zeile.
self
.
assertTrue
(
updated
)
self
.
assertTrue
(
updated
)
self
.
assertEqual
(
sheet
[
"selected_subsection_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
sheet
[
"selected_subsection_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
sheet
[
"selected_subsection_refs"
],
[[
1
,
3
,
3
]])
self
.
assertEqual
(
sheet
[
"selected_subsection_parent_refs"
],
[[
1
,
3
,
3
,
1
]])
self
.
assertEqual
(
sheet
[
"selected_subsection_refs"
],
[])
def
test_select_subsection_by_key_sets_sheet_fields
(
self
)
->
None
:
def
test_select_subsection_by_key_sets_sheet_fields
(
self
)
->
None
:
sheet
:
dict
[
str
,
object
]
=
{}
sheet
:
dict
[
str
,
object
]
=
{}
with
patch
(
with
patch
(
"app.deterministic_services.task_catalog.load_
subsection
_map"
,
"app.deterministic_services.task_catalog.load_
topic
_map"
,
return_value
=
{
"quadratische gleichungen"
:
(
1
,
3
,
3
)},
return_value
=
{
"quadratische gleichungen"
:
(
1
,
3
,
3
,
1
)},
):
):
updated
=
task_catalog
.
select_subsection_by_key
(
sheet
,
"quadratische_gleichungen"
)
updated
=
task_catalog
.
select_subsection_by_key
(
sheet
,
"quadratische_gleichungen"
)
self
.
assertTrue
(
updated
)
self
.
assertTrue
(
updated
)
self
.
assertEqual
(
sheet
[
"selected_subsection_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
sheet
[
"selected_subsection_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
sheet
[
"selected_subsection_refs"
],
[[
1
,
3
,
3
]])
self
.
assertEqual
(
sheet
[
"selected_subsection_parent_refs"
],
[[
1
,
3
,
3
,
1
]])
self
.
assertEqual
(
sheet
[
"selected_subsection_refs"
],
[])
def
test_get_selected_subsection_parent_refs_reads_normalized_refs
(
self
)
->
None
:
sheet
:
dict
[
str
,
object
]
=
{
"selected_subsection_parent_refs"
:
[[
1
,
1
,
0
,
0
],
[
1
,
3
,
2
,
1
],
[
1
,
3
,
2
,
1
]],
}
refs
=
task_catalog
.
get_selected_subsection_parent_refs
(
sheet
)
self
.
assertEqual
(
refs
,
[(
1
,
1
,
0
,
0
),
(
1
,
3
,
2
,
1
)])
def
test_set_selected_task_stores_generic_parent_refs
(
self
)
->
None
:
def
test_set_selected_task_stores_generic_parent_refs
(
self
)
->
None
:
sheet
:
dict
[
str
,
object
]
=
{}
sheet
:
dict
[
str
,
object
]
=
{}
...
@@ -428,7 +445,8 @@ class TaskApiSocraticTest(unittest.TestCase):
...
@@ -428,7 +445,8 @@ class TaskApiSocraticTest(unittest.TestCase):
{
{
"subsection_key"
:
"quadratische gleichungen"
,
"subsection_key"
:
"quadratische gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"refs"
:
[[
1
,
3
,
3
]],
"level"
:
"subsubsection"
,
"refs"
:
[[
1
,
3
,
3
,
1
]],
"summary"
:
"Quadratische summary text"
,
"summary"
:
"Quadratische summary text"
,
}
}
]
]
...
@@ -498,10 +516,10 @@ class ChatBootstrapSocraticTest(unittest.TestCase):
...
@@ -498,10 +516,10 @@ class ChatBootstrapSocraticTest(unittest.TestCase):
),
patch
(
),
patch
(
"app.api.chat.context_store.set_initialized"
"app.api.chat.context_store.set_initialized"
),
patch
(
),
patch
(
"app.api.chat.task_catalog.load_
subsection
_map"
,
"app.api.chat.task_catalog.load_
topic
_map"
,
return_value
=
{
"quadratische gleichungen"
:
(
1
,
3
,
3
)},
return_value
=
{
"quadratische gleichungen"
:
(
1
,
3
,
3
,
1
)},
),
patch
(
),
patch
(
"app.api.chat.retrieval_store.retrieve_for_
subsection
s"
,
"app.api.chat.retrieval_store.retrieve_for_
parent_ref
s"
,
return_value
=
[],
return_value
=
[],
)
as
retrieve_mock
,
patch
(
)
as
retrieve_mock
,
patch
(
"app.api.chat.socratic_oranisator.get_initial_message"
,
"app.api.chat.socratic_oranisator.get_initial_message"
,
...
@@ -517,8 +535,9 @@ class ChatBootstrapSocraticTest(unittest.TestCase):
...
@@ -517,8 +535,9 @@ class ChatBootstrapSocraticTest(unittest.TestCase):
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
json
()[
"reply"
],
"Was sind die Themen dieses Abschnitts?"
)
self
.
assertEqual
(
response
.
json
()[
"reply"
],
"Was sind die Themen dieses Abschnitts?"
)
retrieve_mock
.
assert_called_once_with
(
pg_url
=
"postgresql://localhost/test"
,
subsection
_refs
=
[(
1
,
3
,
3
)])
retrieve_mock
.
assert_called_once_with
(
pg_url
=
"postgresql://localhost/test"
,
parent
_refs
=
[(
1
,
3
,
3
,
1
)])
self
.
assertEqual
(
sheet
[
"selected_subsection_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
sheet
[
"selected_subsection_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
sheet
[
"selected_subsection_parent_refs"
],
[[
1
,
3
,
3
,
1
]])
self
.
assertEqual
(
sheet
[
"history"
],
[{
"role"
:
"assistant"
,
"content"
:
"Was sind die Themen dieses Abschnitts?"
}])
self
.
assertEqual
(
sheet
[
"history"
],
[{
"role"
:
"assistant"
,
"content"
:
"Was sind die Themen dieses Abschnitts?"
}])
...
...
math-tutor/docker/docker-compose.yml
→
math-tutor/docker/docker-compose.y
a
ml
View file @
c4ee9dda
File moved
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