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
Show 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,7 +5,11 @@ from typing import Iterable
...
@@ -5,7 +5,11 @@ 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.
...
@@ -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