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
72d5b5d1
Commit
72d5b5d1
authored
May 21, 2026
by
Kantz
Browse files
umstellung auf neune Modus auch bei den Tasks
parent
75a26eb5
Changes
14
Show whitespace changes
Inline
Side-by-side
math-tutor/README.md
View file @
72d5b5d1
...
@@ -10,7 +10,7 @@
...
@@ -10,7 +10,7 @@
if you want to use the task mode you need :
if you want to use the task mode you need :
-
task folder in sources with your tasks
-
task folder in sources with your tasks
-
`_subsection_map.yaml`
.
-
`_subsection_map.yaml`
as the topic-to-index map for section, subsection, or subsubsection references
.
## Setup
## Setup
...
...
math-tutor/backend/app/api/tasks.py
View file @
72d5b5d1
...
@@ -26,6 +26,14 @@ class TaskItem(BaseModel):
...
@@ -26,6 +26,14 @@ class TaskItem(BaseModel):
images
:
List
[
TaskImage
]
=
Field
(
default_factory
=
list
)
images
:
List
[
TaskImage
]
=
Field
(
default_factory
=
list
)
class
TopicEntry
(
BaseModel
):
topic_key
:
str
label
:
str
refs
:
List
[
List
[
int
]]
level
:
str
summary
:
str
=
""
class
SubsectionEntry
(
BaseModel
):
class
SubsectionEntry
(
BaseModel
):
subsection_key
:
str
subsection_key
:
str
label
:
str
label
:
str
...
@@ -38,14 +46,15 @@ class TaskFile(BaseModel):
...
@@ -38,14 +46,15 @@ class TaskFile(BaseModel):
title
:
str
title
:
str
intro
:
str
intro
:
str
tasks
:
List
[
TaskItem
]
tasks
:
List
[
TaskItem
]
subsections
:
List
[
str
]
=
Field
(
default_factory
=
list
)
topics
:
List
[
str
]
=
Field
(
default_factory
=
list
)
topic_options
:
List
[
TopicEntry
]
=
Field
(
default_factory
=
list
)
class
TasksResponse
(
BaseModel
):
class
TasksResponse
(
BaseModel
):
orchestrator
:
str
orchestrator
:
str
enabled
:
bool
enabled
:
bool
task_files
:
List
[
TaskFile
]
task_files
:
List
[
TaskFile
]
subsections
:
List
[
Subsection
Entry
]
=
Field
(
default_factory
=
list
)
topics
:
List
[
Topic
Entry
]
=
Field
(
default_factory
=
list
)
class
SocraticResponse
(
BaseModel
):
class
SocraticResponse
(
BaseModel
):
...
@@ -104,12 +113,12 @@ def get_task_config() -> dict[str, object]:
...
@@ -104,12 +113,12 @@ def get_task_config() -> dict[str, object]:
def
list_tasks
()
->
TasksResponse
:
def
list_tasks
()
->
TasksResponse
:
orchestrator
=
config
.
get_orchestrator
()
orchestrator
=
config
.
get_orchestrator
()
task_files
=
task_catalog
.
build_task_catalog
()
task_files
=
task_catalog
.
build_task_catalog
()
subsection
s
=
task_catalog
.
build_
subsection
_catalog
()
topic
s
=
task_catalog
.
build_
topic
_catalog
()
return
TasksResponse
(
return
TasksResponse
(
orchestrator
=
orchestrator
,
orchestrator
=
orchestrator
,
enabled
=
orchestrator
in
TASK_ORCHESTRATORS
,
enabled
=
orchestrator
in
TASK_ORCHESTRATORS
,
task_files
=
task_files
,
task_files
=
task_files
,
subsections
=
subsection
s
,
topics
=
topic
s
,
)
)
...
...
math-tutor/backend/app/deterministic_services/orchestrators/orchestrator_task.py
View file @
72d5b5d1
...
@@ -55,28 +55,27 @@ def _ensure_context_task_fields(state: base.ChatState, query_text: str) -> tuple
...
@@ -55,28 +55,27 @@ def _ensure_context_task_fields(state: base.ChatState, query_text: str) -> tuple
def
_retrieve_context_for_task
(
state
:
base
.
ChatState
,
query_text
:
str
)
->
int
:
def
_retrieve_context_for_task
(
state
:
base
.
ChatState
,
query_text
:
str
)
->
int
:
# Deprecated: this still uses subsection refs until task retrieval is migrated to the new parent model.
refs
=
task_catalog
.
get_selected_task_parent_refs
(
state
.
sheet
)
refs
=
task_catalog
.
get_selected_task_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_task_
subsection
s"
,
"retrieve_context_with_task_
parent
s"
,
{
{
"query"
:
query_text
,
"query"
:
query_text
,
"
subsection
_refs"
:
refs
,
"
parent
_refs"
:
refs
,
},
},
_retrieve
,
_retrieve
,
)
)
...
...
math-tutor/backend/app/deterministic_services/retrieval_store.py
View file @
72d5b5d1
...
@@ -109,3 +109,13 @@ def retrieve_for_subsections(
...
@@ -109,3 +109,13 @@ def retrieve_for_subsections(
pg_url
=
pg_url
,
pg_url
=
pg_url
,
subsection_refs
=
subsection_refs
,
subsection_refs
=
subsection_refs
,
)
)
def
retrieve_for_parent_refs
(
pg_url
:
str
,
parent_refs
:
list
[
vector_store
.
ParentRef
]
|
None
=
None
,
)
->
List
[
Source
]:
return
vector_store
.
load_sources_for_parent_refs
(
pg_url
=
pg_url
,
parent_refs
=
parent_refs
,
)
math-tutor/backend/app/deterministic_services/task_catalog.py
View file @
72d5b5d1
...
@@ -16,8 +16,14 @@ from app.deterministic_services.vector_store import parse_markdown_with_frontmat
...
@@ -16,8 +16,14 @@ from app.deterministic_services.vector_store import parse_markdown_with_frontmat
TASKS_DIR
=
config
.
get_task_folder
()
TASKS_DIR
=
config
.
get_task_folder
()
TASK_IMAGES_DIR
=
TASKS_DIR
/
"images"
TASK_IMAGES_DIR
=
TASKS_DIR
/
"images"
SOURCES_DIR
=
Path
(
__file__
).
resolve
().
parents
[
2
]
/
"sources"
SOURCES_DIR
=
Path
(
__file__
).
resolve
().
parents
[
2
]
/
"sources"
SUBSECTION_MAP_PATH
=
TASKS_DIR
/
"_subsection_map.yaml"
TOPIC_MAP_PATH
=
TASKS_DIR
/
"_topic_to_index_map.yaml"
SUBSECTION_MAP_PATH
=
TOPIC_MAP_PATH
TASK_ASSET_URL_PREFIX
=
"/api/tasks/assets"
TASK_ASSET_URL_PREFIX
=
"/api/tasks/assets"
ParentRef
=
tuple
[
int
,
int
,
int
,
int
]
def
_normalize_parent_ref_key
(
value
:
str
)
->
str
:
return
_normalize_subsection_key
(
value
)
def
_normalize_text
(
value
:
str
)
->
str
:
def
_normalize_text
(
value
:
str
)
->
str
:
return
re
.
sub
(
r
"\s+"
,
" "
,
value
.
strip
().
lower
())
return
re
.
sub
(
r
"\s+"
,
" "
,
value
.
strip
().
lower
())
...
@@ -42,7 +48,80 @@ def _parse_subsection_ref(value: str) -> tuple[int, int, int] | None:
...
@@ -42,7 +48,80 @@ def _parse_subsection_ref(value: str) -> tuple[int, int, int] | None:
return
int
(
match
.
group
(
1
)),
int
(
match
.
group
(
2
)),
int
(
match
.
group
(
3
))
return
int
(
match
.
group
(
1
)),
int
(
match
.
group
(
2
)),
int
(
match
.
group
(
3
))
def
load_subsection_map
(
path
:
Path
=
SUBSECTION_MAP_PATH
)
->
dict
[
str
,
tuple
[
int
,
int
,
int
]]:
def
_parse_parent_ref
(
value
:
Any
)
->
ParentRef
|
None
:
if
isinstance
(
value
,
(
list
,
tuple
)):
parts
=
[
str
(
item
).
strip
()
for
item
in
value
if
str
(
item
).
strip
()]
elif
isinstance
(
value
,
dict
):
try
:
chapter
=
int
(
value
.
get
(
"chapter_index"
))
section
=
int
(
value
.
get
(
"section_index"
))
except
Exception
:
return
None
subsection
=
int
(
value
.
get
(
"subsection_index"
)
or
0
)
subsubsection
=
int
(
value
.
get
(
"subsubsection_index"
)
or
0
)
return
_normalize_parent_ref
((
chapter
,
section
,
subsection
,
subsubsection
))
else
:
token
=
str
(
value
).
strip
()
if
not
token
:
return
None
parts
=
[
part
for
part
in
re
.
split
(
r
"\s*[-.:]\s*"
,
token
)
if
part
]
try
:
ints
=
[
int
(
part
)
for
part
in
parts
]
except
Exception
:
return
None
if
len
(
ints
)
==
2
:
return
_normalize_parent_ref
((
ints
[
0
],
ints
[
1
],
0
,
0
))
if
len
(
ints
)
==
3
:
return
_normalize_parent_ref
((
ints
[
0
],
ints
[
1
],
ints
[
2
],
0
))
if
len
(
ints
)
==
4
:
return
_normalize_parent_ref
((
ints
[
0
],
ints
[
1
],
ints
[
2
],
ints
[
3
]))
return
None
def
_normalize_parent_ref
(
ref
:
ParentRef
)
->
ParentRef
:
chap
,
sec
,
sub
,
subsub
=
[
int
(
part
)
for
part
in
ref
]
if
sub
<=
0
:
return
(
chap
,
sec
,
0
,
0
)
if
subsub
<=
0
:
return
(
chap
,
sec
,
sub
,
0
)
return
(
chap
,
sec
,
sub
,
subsub
)
def
_parent_ref_level
(
ref
:
ParentRef
)
->
str
:
_
,
_
,
sub
,
subsub
=
_normalize_parent_ref
(
ref
)
if
sub
<=
0
:
return
"section"
if
subsub
<=
0
:
return
"subsection"
return
"subsubsection"
def
_parent_ref_to_key
(
ref
:
ParentRef
)
->
str
:
chap
,
sec
,
sub
,
subsub
=
_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
_parent_ref_to_list
(
ref
:
ParentRef
)
->
list
[
int
]:
chap
,
sec
,
sub
,
subsub
=
_normalize_parent_ref
(
ref
)
return
[
chap
,
sec
,
sub
,
subsub
]
def
_format_parent_ref_label
(
ref
:
ParentRef
)
->
str
:
chap
,
sec
,
sub
,
subsub
=
_normalize_parent_ref
(
ref
)
if
sub
<=
0
:
return
f
"Section
{
chap
}
.
{
sec
}
"
if
subsub
<=
0
:
return
f
"Subsection
{
chap
}
.
{
sec
}
.
{
sub
}
"
return
f
"Subsubsection
{
chap
}
.
{
sec
}
.
{
sub
}
.
{
subsub
}
"
def
load_topic_map
(
path
:
Path
=
TOPIC_MAP_PATH
)
->
dict
[
str
,
ParentRef
]:
if
not
path
.
exists
():
if
not
path
.
exists
():
return
{}
return
{}
try
:
try
:
...
@@ -52,13 +131,24 @@ def load_subsection_map(path: Path = SUBSECTION_MAP_PATH) -> dict[str, tuple[int
...
@@ -52,13 +131,24 @@ def load_subsection_map(path: Path = SUBSECTION_MAP_PATH) -> dict[str, tuple[int
if
not
isinstance
(
content
,
dict
):
if
not
isinstance
(
content
,
dict
):
return
{}
return
{}
mapped
:
dict
[
str
,
tuple
[
int
,
int
,
int
]
]
=
{}
mapped
:
dict
[
str
,
ParentRef
]
=
{}
for
raw_key
,
raw_ref
in
content
.
items
():
for
raw_key
,
raw_ref
in
content
.
items
():
key
=
_normalize_
subsection
_key
(
str
(
raw_key
))
key
=
_normalize_
parent_ref
_key
(
str
(
raw_key
))
parsed
=
_parse_
subsection
_ref
(
str
(
raw_ref
)
)
parsed
=
_parse_
parent
_ref
(
raw_ref
)
if
not
key
or
parsed
is
None
:
if
not
key
or
parsed
is
None
:
continue
continue
mapped
[
key
]
=
parsed
mapped
[
key
]
=
_normalize_parent_ref
(
parsed
)
return
mapped
def
load_subsection_map
(
path
:
Path
=
SUBSECTION_MAP_PATH
)
->
dict
[
str
,
tuple
[
int
,
int
,
int
]]:
topic_map
=
load_topic_map
(
path
)
mapped
:
dict
[
str
,
tuple
[
int
,
int
,
int
]]
=
{}
for
key
,
ref
in
topic_map
.
items
():
chap
,
sec
,
sub
,
subsub
=
ref
if
sub
<=
0
or
subsub
>
0
:
continue
mapped
[
key
]
=
(
chap
,
sec
,
sub
)
return
mapped
return
mapped
...
@@ -79,13 +169,18 @@ def _extract_subsection_summary(text: str) -> str:
...
@@ -79,13 +169,18 @@ def _extract_subsection_summary(text: str) -> str:
@
lru_cache
(
maxsize
=
1
)
@
lru_cache
(
maxsize
=
1
)
def
load_
subsection
_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
]
=
{}
for
folder
in
(
folders
=
(
base_dir
/
"lecture_script_new"
/
"sections"
,
base_dir
/
"lecture_script_new"
/
"subsections"
,
base_dir
/
"lecture_script_new"
/
"subsubsections"
,
base_dir
/
"with_chapters"
/
"sections"
,
base_dir
/
"with_chapters"
/
"subsections"
,
base_dir
/
"with_chapters"
/
"subsections"
,
base_dir
/
"child_lvl"
/
"subsections"
,
base_dir
/
"child_lvl"
/
"subsections"
,
base_dir
/
"subsection_lvl"
/
"subsections"
,
base_dir
/
"subsection_lvl"
/
"subsections"
,
):
)
for
folder
in
folders
:
if
not
folder
.
exists
():
if
not
folder
.
exists
():
continue
continue
for
path
in
sorted
(
folder
.
glob
(
"*.md"
)):
for
path
in
sorted
(
folder
.
glob
(
"*.md"
)):
...
@@ -94,8 +189,14 @@ def load_subsection_summaries(base_dir: Path = SOURCES_DIR) -> dict[str, str]:
...
@@ -94,8 +189,14 @@ def load_subsection_summaries(base_dir: Path = SOURCES_DIR) -> dict[str, str]:
except
Exception
:
except
Exception
:
continue
continue
meta
,
body
=
parse_markdown_with_frontmatter
(
text
)
meta
,
body
=
parse_markdown_with_frontmatter
(
text
)
title
=
str
(
meta
.
get
(
"title"
)
or
meta
.
get
(
"subsection_title"
)
or
meta
.
get
(
"section_title"
)
or
""
).
strip
()
title
=
str
(
key
=
_normalize_subsection_key
(
title
)
meta
.
get
(
"title"
)
or
meta
.
get
(
"subsubsection_title"
)
or
meta
.
get
(
"subsection_title"
)
or
meta
.
get
(
"section_title"
)
or
""
).
strip
()
key
=
_normalize_parent_ref_key
(
title
)
summary
=
_extract_subsection_summary
(
body
)
summary
=
_extract_subsection_summary
(
body
)
if
not
key
or
not
summary
:
if
not
key
or
not
summary
:
continue
continue
...
@@ -103,24 +204,114 @@ def load_subsection_summaries(base_dir: Path = SOURCES_DIR) -> dict[str, str]:
...
@@ -103,24 +204,114 @@ def load_subsection_summaries(base_dir: Path = SOURCES_DIR) -> dict[str, str]:
return
summaries
return
summaries
def
load_subsection_summaries
(
base_dir
:
Path
=
SOURCES_DIR
)
->
dict
[
str
,
str
]:
return
load_topic_summaries
(
base_dir
)
def
_normalize_topic_ref_entries
(
raw_entries
:
Any
,
topic_map
:
dict
[
str
,
ParentRef
],
)
->
list
[
dict
[
str
,
Any
]]:
if
not
isinstance
(
raw_entries
,
list
):
return
[]
options
:
list
[
dict
[
str
,
Any
]]
=
[]
seen_refs
:
set
[
ParentRef
]
=
set
()
for
raw_entry
in
raw_entries
:
normalized_key
=
""
label
=
""
summary
=
""
ref
:
ParentRef
|
None
=
None
if
isinstance
(
raw_entry
,
str
):
normalized_key
=
_normalize_parent_ref_key
(
raw_entry
)
label
=
_format_subsection_label
(
raw_entry
)
ref
=
topic_map
.
get
(
normalized_key
)
elif
isinstance
(
raw_entry
,
dict
):
raw_key
=
str
(
raw_entry
.
get
(
"key"
)
or
raw_entry
.
get
(
"topic_key"
)
or
raw_entry
.
get
(
"subsection_key"
)
or
""
).
strip
()
normalized_key
=
_normalize_parent_ref_key
(
raw_key
)
if
raw_key
else
""
label
=
str
(
raw_entry
.
get
(
"label"
)
or
""
).
strip
()
summary
=
str
(
raw_entry
.
get
(
"summary"
)
or
""
).
strip
()
ref
=
_parse_parent_ref
(
raw_entry
)
if
ref
is
None
and
normalized_key
:
ref
=
topic_map
.
get
(
normalized_key
)
else
:
continue
if
ref
is
None
:
continue
normalized_ref
=
_normalize_parent_ref
(
ref
)
if
normalized_ref
in
seen_refs
:
continue
seen_refs
.
add
(
normalized_ref
)
key
=
normalized_key
or
_parent_ref_to_key
(
normalized_ref
)
options
.
append
(
{
"topic_key"
:
key
,
"label"
:
label
or
_format_parent_ref_label
(
normalized_ref
),
"summary"
:
summary
,
"level"
:
_parent_ref_level
(
normalized_ref
),
"refs"
:
[
_parent_ref_to_list
(
normalized_ref
)],
}
)
return
options
def
_resolve_task_topic_options
(
task_file
:
dict
[
str
,
Any
],
topic_map
:
dict
[
str
,
ParentRef
]
|
None
=
None
,
topic_summaries
:
dict
[
str
,
str
]
|
None
=
None
,
)
->
list
[
dict
[
str
,
Any
]]:
mapping
=
topic_map
if
topic_map
is
not
None
else
load_topic_map
()
summaries
=
topic_summaries
if
topic_summaries
is
not
None
else
load_topic_summaries
()
raw_entries
:
list
[
Any
]
=
[]
legacy_topics
=
task_file
.
get
(
"subsections"
,
[])
if
isinstance
(
legacy_topics
,
list
):
raw_entries
.
extend
(
legacy_topics
)
explicit_refs
=
task_file
.
get
(
"topic_refs"
,
[])
if
isinstance
(
explicit_refs
,
list
):
raw_entries
.
extend
(
explicit_refs
)
options
=
_normalize_topic_ref_entries
(
raw_entries
,
mapping
)
for
option
in
options
:
if
not
option
.
get
(
"summary"
):
option
[
"summary"
]
=
summaries
.
get
(
str
(
option
.
get
(
"topic_key"
,
""
)).
strip
(),
""
)
return
options
def
_resolve_task_topic_refs
(
task_file
:
dict
[
str
,
Any
],
topic_map
:
dict
[
str
,
ParentRef
]
|
None
=
None
,
)
->
list
[
ParentRef
]:
refs
:
set
[
ParentRef
]
=
set
()
for
option
in
_resolve_task_topic_options
(
task_file
,
topic_map
=
topic_map
):
refs_raw
=
option
.
get
(
"refs"
,
[])
if
not
isinstance
(
refs_raw
,
list
):
continue
for
item
in
refs_raw
:
parsed
=
_parse_parent_ref
(
item
)
if
parsed
is
not
None
:
refs
.
add
(
_normalize_parent_ref
(
parsed
))
return
sorted
(
refs
)
def
_resolve_task_subsection_refs
(
def
_resolve_task_subsection_refs
(
task_file
:
dict
[
str
,
Any
],
task_file
:
dict
[
str
,
Any
],
subsection_map
:
dict
[
str
,
tuple
[
int
,
int
,
int
]]
|
None
=
None
,
subsection_map
:
dict
[
str
,
tuple
[
int
,
int
,
int
]]
|
None
=
None
,
)
->
list
[
tuple
[
int
,
int
,
int
]]:
)
->
list
[
tuple
[
int
,
int
,
int
]]:
mapping
=
subsection_map
if
subsection_map
is
not
None
else
load_subsection_map
()
mapping
:
dict
[
str
,
ParentRef
]
subsection
s
=
task_file
.
get
(
"subsections"
,
[])
if
subsection
_map
is
None
:
if
not
isinstance
(
subsections
,
list
):
mapping
=
load_topic_map
()
return
[]
else
:
mapping
=
{
key
:
(
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
]),
0
)
for
key
,
ref
in
subsection_map
.
items
()}
refs
:
set
[
tuple
[
int
,
int
,
int
]]
=
set
(
)
refs
=
_resolve_task_topic_refs
(
task_file
,
topic_map
=
mapping
)
f
or
subsection
in
subsections
:
n
or
malized
:
list
[
tuple
[
int
,
int
,
int
]]
=
[]
key
=
_normalize_subsection_key
(
str
(
subsection
))
for
chap
,
sec
,
sub
,
subsub
in
refs
:
if
not
key
:
if
sub
<=
0
or
subsub
>
0
:
continue
continue
ref
=
mapping
.
get
(
key
)
normalized
.
append
((
chap
,
sec
,
sub
))
if
ref
is
not
None
:
return
sorted
(
normalized
)
refs
.
add
((
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
])))
return
sorted
(
refs
)
def
_format_subsection_label
(
value
:
str
)
->
str
:
def
_format_subsection_label
(
value
:
str
)
->
str
:
...
@@ -131,17 +322,36 @@ def _format_subsection_label(value: str) -> str:
...
@@ -131,17 +322,36 @@ def _format_subsection_label(value: str) -> str:
return
cleaned
.
title
()
return
cleaned
.
title
()
def
build_
subsection
_catalog
(
path
:
Path
=
SUBSECTION
_MAP_PATH
)
->
list
[
dict
[
str
,
Any
]]:
def
build_
topic
_catalog
(
path
:
Path
=
TOPIC
_MAP_PATH
)
->
list
[
dict
[
str
,
Any
]]:
subsection
_map
=
load_
subsection
_map
(
path
)
topic
_map
=
load_
topic
_map
(
path
)
subsection
_summaries
=
load_
subsection
_summaries
()
topic
_summaries
=
load_
topic
_summaries
()
response
:
list
[
dict
[
str
,
Any
]]
=
[]
response
:
list
[
dict
[
str
,
Any
]]
=
[]
for
key
,
ref
in
sorted
(
subsection
_map
.
items
(),
key
=
lambda
item
:
(
item
[
0
],
item
[
1
])):
for
key
,
ref
in
sorted
(
topic
_map
.
items
(),
key
=
lambda
item
:
(
item
[
0
],
item
[
1
])):
response
.
append
(
response
.
append
(
{
{
"
subsection
_key"
:
key
,
"
topic
_key"
:
key
,
"label"
:
_format_subsection_label
(
key
),
"label"
:
_format_subsection_label
(
key
),
"level"
:
_parent_ref_level
(
ref
),
"refs"
:
[
_parent_ref_to_list
(
ref
)],
"summary"
:
topic_summaries
.
get
(
key
,
""
),
}
)
return
response
def
build_subsection_catalog
(
path
:
Path
=
SUBSECTION_MAP_PATH
)
->
list
[
dict
[
str
,
Any
]]:
response
:
list
[
dict
[
str
,
Any
]]
=
[]
for
item
in
build_topic_catalog
(
path
):
refs
=
item
.
get
(
"refs"
,
[])
if
item
.
get
(
"level"
)
!=
"subsection"
or
not
refs
:
continue
ref
=
refs
[
0
]
response
.
append
(
{
"subsection_key"
:
item
[
"topic_key"
],
"label"
:
item
[
"label"
],
"refs"
:
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
])]],
"refs"
:
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
])]],
"summary"
:
subsection_summaries
.
get
(
key
,
""
)
,
"summary"
:
item
[
"summary"
]
,
}
}
)
)
return
response
return
response
...
@@ -151,33 +361,28 @@ def _resolve_task_subsection_options(
...
@@ -151,33 +361,28 @@ def _resolve_task_subsection_options(
task_file
:
dict
[
str
,
Any
],
task_file
:
dict
[
str
,
Any
],
subsection_map
:
dict
[
str
,
tuple
[
int
,
int
,
int
]]
|
None
=
None
,
subsection_map
:
dict
[
str
,
tuple
[
int
,
int
,
int
]]
|
None
=
None
,
)
->
list
[
dict
[
str
,
Any
]]:
)
->
list
[
dict
[
str
,
Any
]]:
mapping
=
subsection_map
if
subsection_map
is
not
None
else
load_subsection_map
()
mapping
:
dict
[
str
,
ParentRef
]
subsections
=
task_file
.
get
(
"subsections"
,
[])
if
subsection_map
is
None
:
if
not
isinstance
(
subsections
,
list
):
mapping
=
load_topic_map
()
return
[]
else
:
mapping
=
{
key
:
(
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
]),
0
)
for
key
,
ref
in
subsection_map
.
items
()}
options
:
list
[
dict
[
str
,
Any
]]
=
[]
options
=
_resolve_task_topic_options
(
task_file
,
topic_map
=
mapping
)
seen_refs
:
set
[
tuple
[
int
,
int
,
int
]]
=
set
()
response
:
list
[
dict
[
str
,
Any
]]
=
[]
for
subsection
in
subsections
:
for
option
in
options
:
raw_label
=
str
(
subsection
).
strip
()
refs
=
option
.
get
(
"refs"
,
[])
key
=
_normalize_subsection_key
(
raw_label
)
if
option
.
get
(
"level"
)
!=
"subsection"
or
not
refs
:
if
not
key
:
continue
ref
=
mapping
.
get
(
key
)
if
ref
is
None
:
continue
normalized_ref
=
(
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
]))
if
normalized_ref
in
seen_refs
:
continue
continue
seen_refs
.
add
(
normalized_ref
)
ref
=
refs
[
0
]
opti
ons
.
append
(
resp
ons
e
.
append
(
{
{
"subsection_key"
:
key
,
"subsection_key"
:
option
[
"topic_key"
],
"label"
:
_format_subsection_label
(
raw_label
)
or
raw_label
,
"label"
:
option
[
"label"
],
"refs"
:
[[
normalized_ref
[
0
],
normalized_ref
[
1
],
normalized_ref
[
2
]]],
"refs"
:
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
])]],
"summary"
:
option
.
get
(
"summary"
,
""
),
}
}
)
)
return
opti
ons
return
resp
ons
e
def
_match_score
(
query_text
:
str
,
candidate_text
:
str
)
->
int
:
def
_match_score
(
query_text
:
str
,
candidate_text
:
str
)
->
int
:
...
@@ -289,6 +494,7 @@ def _normalize_yaml_task_file(content: Any, path: Path) -> dict[str, Any] | None
...
@@ -289,6 +494,7 @@ def _normalize_yaml_task_file(content: Any, path: Path) -> dict[str, Any] | None
slug
=
str
(
content
.
get
(
"slug"
,
""
)).
strip
()
or
_slugify
(
title
or
file_id
)
slug
=
str
(
content
.
get
(
"slug"
,
""
)).
strip
()
or
_slugify
(
title
or
file_id
)
level
=
str
(
content
.
get
(
"level"
,
""
)).
strip
()
level
=
str
(
content
.
get
(
"level"
,
""
)).
strip
()
subsections
=
_normalize_exercise_topics
(
content
.
get
(
"exercise_topic"
,
[]))
subsections
=
_normalize_exercise_topics
(
content
.
get
(
"exercise_topic"
,
[]))
topic_refs_raw
=
content
.
get
(
"topic_refs"
,
[])
if
not
title
or
not
file_id
or
not
slug
or
not
level
:
if
not
title
or
not
file_id
or
not
slug
or
not
level
:
return
None
return
None
...
@@ -308,6 +514,7 @@ def _normalize_yaml_task_file(content: Any, path: Path) -> dict[str, Any] | None
...
@@ -308,6 +514,7 @@ def _normalize_yaml_task_file(content: Any, path: Path) -> dict[str, Any] | None
"slug"
:
slug
,
"slug"
:
slug
,
"level"
:
level
,
"level"
:
level
,
"subsections"
:
subsections
,
"subsections"
:
subsections
,
"topic_refs"
:
topic_refs_raw
if
isinstance
(
topic_refs_raw
,
list
)
else
[],
"tasks"
:
tasks
,
"tasks"
:
tasks
,
}
}
...
@@ -381,8 +588,9 @@ def set_selected_task(
...
@@ -381,8 +588,9 @@ def set_selected_task(
store_new
.
set_solution
(
sheet
,
solution
)
store_new
.
set_solution
(
sheet
,
solution
)
sheet
[
"task_file_id"
]
=
str
(
task_file
.
get
(
"_file_id"
,
""
))
sheet
[
"task_file_id"
]
=
str
(
task_file
.
get
(
"_file_id"
,
""
))
sheet
[
"task_id"
]
=
str
(
task_entry
.
get
(
"id"
,
""
)).
zfill
(
2
)
sheet
[
"task_id"
]
=
str
(
task_entry
.
get
(
"id"
,
""
)).
zfill
(
2
)
refs
=
_resolve_task_subsection_refs
(
task_file
)
refs
=
_resolve_task_topic_refs
(
task_file
)
sheet
[
"task_subsection_refs"
]
=
[[
chap
,
sec
,
sub
]
for
chap
,
sec
,
sub
in
refs
]
sheet
[
"task_parent_refs"
]
=
[
_parent_ref_to_list
(
ref
)
for
ref
in
refs
]
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_refs"
,
None
)
sheet
.
pop
(
"selected_subsection_refs"
,
None
)
...
@@ -410,6 +618,7 @@ def set_selected_subsection(
...
@@ -410,6 +618,7 @@ def set_selected_subsection(
sheet
[
"selected_subsection_refs"
]
=
[[
chap
,
sec
,
sub
]
for
chap
,
sec
,
sub
in
sorted
({
*
refs
})]
sheet
[
"selected_subsection_refs"
]
=
[[
chap
,
sec
,
sub
]
for
chap
,
sec
,
sub
in
sorted
({
*
refs
})]
sheet
.
pop
(
"task_id"
,
None
)
sheet
.
pop
(
"task_id"
,
None
)
sheet
.
pop
(
"task_parent_refs"
,
None
)
sheet
.
pop
(
"task_subsection_refs"
,
None
)
sheet
.
pop
(
"task_subsection_refs"
,
None
)
sheet
.
pop
(
"task"
,
None
)
sheet
.
pop
(
"task"
,
None
)
sheet
.
pop
(
"hints"
,
None
)
sheet
.
pop
(
"hints"
,
None
)
...
@@ -475,6 +684,7 @@ def select_subsection_by_key(
...
@@ -475,6 +684,7 @@ def select_subsection_by_key(
sheet
[
"selected_subsection_refs"
]
=
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
])]]
sheet
[
"selected_subsection_refs"
]
=
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
])]]
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_subsection_refs"
,
None
)
sheet
.
pop
(
"task_subsection_refs"
,
None
)
sheet
.
pop
(
"task"
,
None
)
sheet
.
pop
(
"task"
,
None
)
sheet
.
pop
(
"hints"
,
None
)
sheet
.
pop
(
"hints"
,
None
)
...
@@ -508,18 +718,25 @@ def get_selected_subsection_refs(sheet: dict[str, Any]) -> list[tuple[int, int,
...
@@ -508,18 +718,25 @@ def get_selected_subsection_refs(sheet: dict[str, Any]) -> list[tuple[int, int,
return
sorted
(
refs
)
return
sorted
(
refs
)
def
get_selected_task_
subsection
_refs
(
sheet
:
dict
[
str
,
Any
])
->
list
[
tuple
[
int
,
int
,
int
]
]:
def
get_selected_task_
parent
_refs
(
sheet
:
dict
[
str
,
Any
])
->
list
[
ParentRef
]:
refs_raw
=
sheet
.
get
(
"task_
subsection
_refs"
,
[])
refs_raw
=
sheet
.
get
(
"task_
parent
_refs"
,
[])
if
not
isinstance
(
refs_raw
,
list
):
if
not
isinstance
(
refs_raw
,
list
):
return
[]
return
[]
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
:
return
sorted
(
refs
)
def
get_selected_task_subsection_refs
(
sheet
:
dict
[
str
,
Any
])
->
list
[
tuple
[
int
,
int
,
int
]]:
refs
:
list
[
tuple
[
int
,
int
,
int
]]
=
[]
for
chap
,
sec
,
sub
,
subsub
in
get_selected_task_parent_refs
(
sheet
):
if
sub
<=
0
or
subsub
>
0
:
continue
continue
refs
.
append
((
chap
,
sec
,
sub
))
return
sorted
(
refs
)
return
sorted
(
refs
)
...
@@ -600,8 +817,12 @@ def build_task_catalog(task_files: list[dict[str, Any]] | None = None) -> list[d
...
@@ -600,8 +817,12 @@ def build_task_catalog(task_files: list[dict[str, Any]] | None = None) -> list[d
file_id
=
str
(
task_file
.
get
(
"_file_id"
,
""
))
file_id
=
str
(
task_file
.
get
(
"_file_id"
,
""
))
title
=
str
(
task_file
.
get
(
"title"
,
""
)).
strip
()
title
=
str
(
task_file
.
get
(
"title"
,
""
)).
strip
()
intro
=
str
(
task_file
.
get
(
"intro"
,
""
)).
strip
()
intro
=
str
(
task_file
.
get
(
"intro"
,
""
)).
strip
()
subsections
=
task_file
.
get
(
"subsections"
,
[])
topic_options
=
_resolve_task_topic_options
(
task_file
)
subsection_options
=
_resolve_task_subsection_options
(
task_file
)
topics_raw
=
task_file
.
get
(
"subsections"
,
[])
if
isinstance
(
topics_raw
,
list
)
and
topics_raw
:
topics
=
[
str
(
item
).
strip
()
for
item
in
topics_raw
if
str
(
item
).
strip
()]
else
:
topics
=
[
str
(
item
.
get
(
"topic_key"
,
""
)).
strip
()
for
item
in
topic_options
if
str
(
item
.
get
(
"topic_key"
,
""
)).
strip
()]
tasks
:
list
[
dict
[
str
,
str
]]
=
[]
tasks
:
list
[
dict
[
str
,
str
]]
=
[]
for
item
in
task_file
.
get
(
"tasks"
,
[]):
for
item
in
task_file
.
get
(
"tasks"
,
[]):
if
not
isinstance
(
item
,
dict
):
if
not
isinstance
(
item
,
dict
):
...
@@ -625,8 +846,8 @@ def build_task_catalog(task_files: list[dict[str, Any]] | None = None) -> list[d
...
@@ -625,8 +846,8 @@ def build_task_catalog(task_files: list[dict[str, Any]] | None = None) -> list[d
"title"
:
title
,
"title"
:
title
,
"intro"
:
intro
,
"intro"
:
intro
,
"tasks"
:
tasks
,
"tasks"
:
tasks
,
"
subsections"
:
subsection
s
,
"
topics"
:
topic
s
,
"
subsection
_options"
:
subsection
_options
,
"
topic
_options"
:
topic
_options
,
}
}
)
)
return
response
return
response
math-tutor/backend/app/deterministic_services/vector_store.py
View file @
72d5b5d1
...
@@ -862,6 +862,34 @@ def retrieve(
...
@@ -862,6 +862,34 @@ def retrieve(
# Deprecated: subsection refs only represent the legacy subsection-centric task/socratic flow.
# Deprecated: subsection refs only represent the legacy subsection-centric task/socratic flow.
SubsectionRef
=
Tuple
[
int
,
int
,
int
]
SubsectionRef
=
Tuple
[
int
,
int
,
int
]
ParentRef
=
Tuple
[
int
,
int
,
int
,
int
]
def
_normalize_parent_refs
(
parent_refs
:
Optional
[
List
[
ParentRef
]],
)
->
List
[
ParentRef
]:
if
not
parent_refs
:
return
[]
normalized
:
set
[
ParentRef
]
=
set
()
for
chap
,
sec
,
sub
,
subsub
in
parent_refs
:
chap_i
,
sec_i
,
sub_i
,
subsub_i
=
int
(
chap
),
int
(
sec
),
int
(
sub
),
int
(
subsub
)
if
sub_i
<=
0
:
normalized
.
add
((
chap_i
,
sec_i
,
0
,
0
))
elif
subsub_i
<=
0
:
normalized
.
add
((
chap_i
,
sec_i
,
sub_i
,
0
))
else
:
normalized
.
add
((
chap_i
,
sec_i
,
sub_i
,
subsub_i
))
return
sorted
(
normalized
)
def
_parent_ref_scope
(
ref
:
ParentRef
)
->
DominantScope
:
chap
,
sec
,
sub
,
subsub
=
ref
if
sub
<=
0
:
return
DominantScope
(
chapter_index
=
chap
,
section_index
=
sec
,
subsection_index
=
None
,
subsubsection_index
=
None
,
level
=
"section"
)
if
subsub
<=
0
:
return
DominantScope
(
chapter_index
=
chap
,
section_index
=
sec
,
subsection_index
=
sub
,
subsubsection_index
=
None
,
level
=
"subsection"
)
return
DominantScope
(
chapter_index
=
chap
,
section_index
=
sec
,
subsection_index
=
sub
,
subsubsection_index
=
subsub
,
level
=
"subsubsection"
)
def
_normalize_subsection_refs
(
def
_normalize_subsection_refs
(
...
@@ -917,6 +945,143 @@ def load_children_for_subsections(
...
@@ -917,6 +945,143 @@ def load_children_for_subsections(
return
_retrivla_to_sources
({
"task_childs"
:
children
})
return
_retrivla_to_sources
({
"task_childs"
:
children
})
def
load_sources_for_parent_refs
(
pg_url
:
str
,
parent_refs
:
Optional
[
List
[
ParentRef
]],
)
->
List
[
Source
]:
refs
=
_normalize_parent_refs
(
parent_refs
)
if
not
refs
:
return
[]
groups
:
Dict
[
str
,
List
[
Retrieved
]]
=
{
"sections"
:
[],
"subsections"
:
[],
"subsubsections"
:
[],
"task_childs"
:
[],
}
with
psycopg
.
connect
(
pg_url
,
row_factory
=
dict_row
)
as
conn
:
with
conn
.
cursor
()
as
cur
:
for
ref
in
refs
:
scope
=
_parent_ref_scope
(
ref
)
if
scope
.
level
==
"section"
:
cur
.
execute
(
"""
SELECT
d.uid, d.doc_type,
d.chapter_index, d.section_index, d.subsection_index, d.subsubsection_index, d.child_index,
d.chapter_title, d.section_title, d.subsection_title, d.subsubsection_title, d.title, d.source_type,
d.path, d.markdown,
1.0 AS score
FROM docs d
WHERE d.doc_type = 'section'
AND d.chapter_index = %(cpt)s
AND d.section_index = %(sec)s
"""
,
{
"cpt"
:
scope
.
chapter_index
,
"sec"
:
scope
.
section_index
},
)
groups
[
"sections"
].
extend
([
_row_to_retrieved
(
row
,
source_type
=
"section"
)
for
row
in
cur
.
fetchall
()])
cur
.
execute
(
"""
SELECT
d.uid, d.doc_type,
d.chapter_index, d.section_index, d.subsection_index, d.subsubsection_index, d.child_index,
d.chapter_title, d.section_title, d.subsection_title, d.subsubsection_title, d.title, d.source_type,
d.path, d.markdown,
1.0 AS score
FROM docs d
WHERE d.doc_type = 'child'
AND d.chapter_index = %(cpt)s
AND d.section_index = %(sec)s
AND d.subsection_index = 0
AND d.subsubsection_index = 0
ORDER BY d.child_index
"""
,
{
"cpt"
:
scope
.
chapter_index
,
"sec"
:
scope
.
section_index
},
)
groups
[
"task_childs"
].
extend
([
_row_to_retrieved
(
row
)
for
row
in
cur
.
fetchall
()])
continue
if
scope
.
level
==
"subsection"
:
cur
.
execute
(
"""
SELECT
d.uid, d.doc_type,
d.chapter_index, d.section_index, d.subsection_index, d.subsubsection_index, d.child_index,
d.chapter_title, d.section_title, d.subsection_title, d.subsubsection_title, d.title, d.source_type,
d.path, d.markdown,
1.0 AS score
FROM docs d
WHERE d.doc_type = 'subsection'
AND d.chapter_index = %(cpt)s
AND d.section_index = %(sec)s
AND d.subsection_index = %(sub)s
"""
,
{
"cpt"
:
scope
.
chapter_index
,
"sec"
:
scope
.
section_index
,
"sub"
:
scope
.
subsection_index
},
)
groups
[
"subsections"
].
extend
([
_row_to_retrieved
(
row
,
source_type
=
"subsection"
)
for
row
in
cur
.
fetchall
()])
cur
.
execute
(
"""
SELECT
d.uid, d.doc_type,
d.chapter_index, d.section_index, d.subsection_index, d.subsubsection_index, d.child_index,
d.chapter_title, d.section_title, d.subsection_title, d.subsubsection_title, d.title, d.source_type,
d.path, d.markdown,
1.0 AS score
FROM docs d
WHERE d.doc_type = 'child'
AND d.chapter_index = %(cpt)s
AND d.section_index = %(sec)s
AND d.subsection_index = %(sub)s
AND d.subsubsection_index = 0
ORDER BY d.child_index
"""
,
{
"cpt"
:
scope
.
chapter_index
,
"sec"
:
scope
.
section_index
,
"sub"
:
scope
.
subsection_index
},
)
groups
[
"task_childs"
].
extend
([
_row_to_retrieved
(
row
)
for
row
in
cur
.
fetchall
()])
continue
cur
.
execute
(
"""
SELECT
d.uid, d.doc_type,
d.chapter_index, d.section_index, d.subsection_index, d.subsubsection_index, d.child_index,
d.chapter_title, d.section_title, d.subsection_title, d.subsubsection_title, d.title, d.source_type,
d.path, d.markdown,
1.0 AS score
FROM docs d
WHERE d.doc_type = 'subsubsection'
AND d.chapter_index = %(cpt)s
AND d.section_index = %(sec)s
AND d.subsection_index = %(sub)s
AND d.subsubsection_index = %(subsub)s
"""
,
{
"cpt"
:
scope
.
chapter_index
,
"sec"
:
scope
.
section_index
,
"sub"
:
scope
.
subsection_index
,
"subsub"
:
scope
.
subsubsection_index
},
)
groups
[
"subsubsections"
].
extend
([
_row_to_retrieved
(
row
,
source_type
=
"subsubsection"
)
for
row
in
cur
.
fetchall
()])
cur
.
execute
(
"""
SELECT
d.uid, d.doc_type,
d.chapter_index, d.section_index, d.subsection_index, d.subsubsection_index, d.child_index,
d.chapter_title, d.section_title, d.subsection_title, d.subsubsection_title, d.title, d.source_type,
d.path, d.markdown,
1.0 AS score
FROM docs d
WHERE d.doc_type = 'child'
AND d.chapter_index = %(cpt)s
AND d.section_index = %(sec)s
AND d.subsection_index = %(sub)s
AND d.subsubsection_index = %(subsub)s
ORDER BY d.child_index
"""
,
{
"cpt"
:
scope
.
chapter_index
,
"sec"
:
scope
.
section_index
,
"sub"
:
scope
.
subsection_index
,
"subsub"
:
scope
.
subsubsection_index
},
)
groups
[
"task_childs"
].
extend
([
_row_to_retrieved
(
row
)
for
row
in
cur
.
fetchall
()])
return
merge_sources
([],
_retrivla_to_sources
(
groups
))
def
merge_sources
(
primary
:
List
[
Source
],
additional
:
List
[
Source
])
->
List
[
Source
]:
def
merge_sources
(
primary
:
List
[
Source
],
additional
:
List
[
Source
])
->
List
[
Source
]:
merged
:
List
[
Source
]
=
[]
merged
:
List
[
Source
]
=
[]
index_by_key
:
Dict
[
Tuple
[
str
,
str
,
str
,
str
,
str
,
str
,
str
],
int
]
=
{}
index_by_key
:
Dict
[
Tuple
[
str
,
str
,
str
,
str
,
str
,
str
,
str
],
int
]
=
{}
...
...
math-tutor/backend/scripts/convert_tasks_json_to_yaml.py
View file @
72d5b5d1
...
@@ -223,7 +223,7 @@ def convert_all(tasks_dir: Path = TASKS_DIR, dry_run: bool = False) -> tuple[lis
...
@@ -223,7 +223,7 @@ def convert_all(tasks_dir: Path = TASKS_DIR, dry_run: bool = False) -> tuple[lis
images_root
=
tasks_dir
/
"images"
images_root
=
tasks_dir
/
"images"
used_names
:
set
[
str
]
=
set
()
used_names
:
set
[
str
]
=
set
()
for
json_path
in
sorted
(
tasks_dir
.
glob
(
"*.json"
)):
for
json_path
in
sorted
(
tasks_dir
.
glob
(
"*.json"
)):
if
json_path
.
name
==
"_
subsection_
map.json"
:
if
json_path
.
name
==
"_
topic-to-index-
map.json"
:
continue
continue
payload
=
load_json_task
(
json_path
)
payload
=
load_json_task
(
json_path
)
source_file_id
=
json_path
.
stem
source_file_id
=
json_path
.
stem
...
...
math-tutor/backend/test/task_catalog_socratic_test.py
View file @
72d5b5d1
...
@@ -62,6 +62,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
...
@@ -62,6 +62,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"title"
:
"Analysis"
,
"title"
:
"Analysis"
,
"intro"
:
"Intro"
,
"intro"
:
"Intro"
,
"subsections"
:
[
"quadratische_gleichungen"
],
"subsections"
:
[
"quadratische_gleichungen"
],
"topic_refs"
:
[],
"tasks"
:
[
"tasks"
:
[
{
{
"id"
:
"01"
,
"id"
:
"01"
,
...
@@ -92,6 +93,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
...
@@ -92,6 +93,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
}
}
],
],
)
)
self
.
assertEqual
(
catalog
[
0
][
"topics"
],
[
"quadratische_gleichungen"
])
def
test_socratic_build_subsection_catalog_returns_response_shape
(
self
)
->
None
:
def
test_socratic_build_subsection_catalog_returns_response_shape
(
self
)
->
None
:
with
patch
(
with
patch
(
...
@@ -135,13 +137,13 @@ class TaskCatalogSocraticTest(unittest.TestCase):
...
@@ -135,13 +137,13 @@ class TaskCatalogSocraticTest(unittest.TestCase):
def
test_build_subsection_catalog_includes_summary
(
self
)
->
None
:
def
test_build_subsection_catalog_includes_summary
(
self
)
->
None
:
with
patch
(
with
patch
(
"app.deterministic_services.task_catalog.load_
subsection
_map"
,
"app.deterministic_services.task_catalog.load_
topic
_map"
,
return_value
=
{
return_value
=
{
"quadratische gleichungen"
:
(
1
,
3
,
3
),
"quadratische gleichungen"
:
(
1
,
3
,
3
,
0
),
"mengen"
:
(
1
,
1
,
1
),
"mengen"
:
(
1
,
1
,
1
,
0
),
},
},
),
patch
(
),
patch
(
"app.deterministic_services.task_catalog.load_
subsection
_summaries"
,
"app.deterministic_services.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"
,
...
@@ -167,6 +169,51 @@ class TaskCatalogSocraticTest(unittest.TestCase):
...
@@ -167,6 +169,51 @@ class TaskCatalogSocraticTest(unittest.TestCase):
],
],
)
)
def
test_build_topic_catalog_supports_mixed_parent_levels
(
self
)
->
None
:
with
patch
(
"app.deterministic_services.task_catalog.load_topic_map"
,
return_value
=
{
"mengen"
:
(
1
,
1
,
0
,
0
),
"quadratische gleichungen"
:
(
1
,
3
,
2
,
0
),
"hornerschema"
:
(
1
,
3
,
3
,
1
),
},
),
patch
(
"app.deterministic_services.task_catalog.load_topic_summaries"
,
return_value
=
{
"mengen"
:
"Mengen summary text"
,
"quadratische gleichungen"
:
"Quadratische summary text"
,
"hornerschema"
:
"Hornerschema summary text"
,
},
):
catalog
=
task_catalog
.
build_topic_catalog
()
self
.
assertEqual
(
catalog
,
[
{
"topic_key"
:
"hornerschema"
,
"label"
:
"Hornerschema"
,
"level"
:
"subsubsection"
,
"refs"
:
[[
1
,
3
,
3
,
1
]],
"summary"
:
"Hornerschema summary text"
,
},
{
"topic_key"
:
"mengen"
,
"label"
:
"Mengen"
,
"level"
:
"section"
,
"refs"
:
[[
1
,
1
,
0
,
0
]],
"summary"
:
"Mengen summary text"
,
},
{
"topic_key"
:
"quadratische gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"level"
:
"subsection"
,
"refs"
:
[[
1
,
3
,
2
,
0
]],
"summary"
:
"Quadratische summary text"
,
},
],
)
def
test_load_subsection_summaries_extracts_body
(
self
)
->
None
:
def
test_load_subsection_summaries_extracts_body
(
self
)
->
None
:
temp_dir
=
Path
(
__file__
).
resolve
().
parent
/
"_tmp_subsection_summaries"
temp_dir
=
Path
(
__file__
).
resolve
().
parent
/
"_tmp_subsection_summaries"
summary_root
=
temp_dir
/
"child_lvl"
/
"subsections"
summary_root
=
temp_dir
/
"child_lvl"
/
"subsections"
...
@@ -249,6 +296,42 @@ Zweite Zeile.
...
@@ -249,6 +296,42 @@ Zweite Zeile.
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_refs"
],
[[
1
,
3
,
3
]])
def
test_set_selected_task_stores_generic_parent_refs
(
self
)
->
None
:
sheet
:
dict
[
str
,
object
]
=
{}
task_file
=
{
"_file_id"
:
"analysis_1"
,
"title"
:
"Analysis"
,
"subsections"
:
[
"mengen"
],
"topic_refs"
:
[
{
"chapter_index"
:
1
,
"section_index"
:
3
,
"subsection_index"
:
2
,
"subsubsection_index"
:
1
,
"key"
:
"hornerschema"
,
"label"
:
"Hornerschema"
,
}
],
}
task_entry
=
{
"id"
:
"01"
,
"statement"
:
"Bestimme f(x)."
,
"hints"
:
[],
"solution"
:
""
,
"images"
:
[]}
with
patch
(
"app.deterministic_services.task_catalog.load_topic_map"
,
return_value
=
{
"mengen"
:
(
1
,
1
,
0
,
0
)},
):
task_catalog
.
set_selected_task
(
sheet
,
task_file
,
task_entry
)
self
.
assertEqual
(
sheet
[
"task_parent_refs"
],
[[
1
,
1
,
0
,
0
],
[
1
,
3
,
2
,
1
]])
def
test_get_selected_task_parent_refs_reads_normalized_refs
(
self
)
->
None
:
sheet
:
dict
[
str
,
object
]
=
{
"task_parent_refs"
:
[[
1
,
1
,
0
,
0
],
[
1
,
3
,
2
,
1
],
[
1
,
3
,
2
,
1
]],
}
refs
=
task_catalog
.
get_selected_task_parent_refs
(
sheet
)
self
.
assertEqual
(
refs
,
[(
1
,
1
,
0
,
0
),
(
1
,
3
,
2
,
1
)])
class
TaskApiSocraticTest
(
unittest
.
TestCase
):
class
TaskApiSocraticTest
(
unittest
.
TestCase
):
def
setUp
(
self
)
->
None
:
def
setUp
(
self
)
->
None
:
...
@@ -256,20 +339,23 @@ class TaskApiSocraticTest(unittest.TestCase):
...
@@ -256,20 +339,23 @@ class TaskApiSocraticTest(unittest.TestCase):
app
.
include_router
(
tasks
.
router
)
app
.
include_router
(
tasks
.
router
)
self
.
client
=
TestClient
(
app
)
self
.
client
=
TestClient
(
app
)
def
test_list_tasks_includes_
subsection
_options
(
self
)
->
None
:
def
test_list_tasks_includes_
topic
_options
(
self
)
->
None
:
payload
=
[
payload
=
[
{
{
"file_id"
:
"analysis_1"
,
"file_id"
:
"analysis_1"
,
"title"
:
"Analysis"
,
"title"
:
"Analysis"
,
"intro"
:
"Intro"
,
"intro"
:
"Intro"
,
"tasks"
:
[],
"tasks"
:
[],
"topics"
:
[
"quadratische gleichungen"
],
"topic_options"
:
[],
}
}
]
]
subsection
s
=
[
topic
s
=
[
{
{
"
subsection
_key"
:
"quadratische gleichungen"
,
"
topic
_key"
:
"quadratische gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"refs"
:
[[
1
,
3
,
3
]],
"refs"
:
[[
1
,
3
,
3
,
0
]],
"level"
:
"subsection"
,
}
}
]
]
...
@@ -277,15 +363,15 @@ class TaskApiSocraticTest(unittest.TestCase):
...
@@ -277,15 +363,15 @@ class TaskApiSocraticTest(unittest.TestCase):
"app.api.tasks.task_catalog.build_task_catalog"
,
"app.api.tasks.task_catalog.build_task_catalog"
,
return_value
=
payload
,
return_value
=
payload
,
),
patch
(
),
patch
(
"app.api.tasks.task_catalog.build_
subsection
_catalog"
,
"app.api.tasks.task_catalog.build_
topic
_catalog"
,
return_value
=
subsection
s
,
return_value
=
topic
s
,
):
):
response
=
self
.
client
.
get
(
"/api/tasks"
)
response
=
self
.
client
.
get
(
"/api/tasks"
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
status_code
,
200
)
body
=
response
.
json
()
body
=
response
.
json
()
self
.
assertEqual
(
body
[
"orchestrator"
],
"socratic"
)
self
.
assertEqual
(
body
[
"orchestrator"
],
"socratic"
)
self
.
assertEqual
(
body
[
"
subsections"
][
0
][
"subsection
_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
body
[
"
topics"
][
0
][
"topic
_key"
],
"quadratische gleichungen"
)
def
test_task_asset_endpoint_serves_files_from_task_image_dir
(
self
)
->
None
:
def
test_task_asset_endpoint_serves_files_from_task_image_dir
(
self
)
->
None
:
temp_dir
=
Path
(
tempfile
.
mkdtemp
(
prefix
=
"task-assets-"
))
temp_dir
=
Path
(
tempfile
.
mkdtemp
(
prefix
=
"task-assets-"
))
...
...
math-tutor/backend/test/task_yaml_migration_test.py
View file @
72d5b5d1
...
@@ -142,6 +142,7 @@ class TaskYamlMigrationTest(unittest.TestCase):
...
@@ -142,6 +142,7 @@ class TaskYamlMigrationTest(unittest.TestCase):
self
.
assertEqual
(
catalog
[
0
][
"file_id"
],
"analysis_1"
)
self
.
assertEqual
(
catalog
[
0
][
"file_id"
],
"analysis_1"
)
self
.
assertEqual
(
catalog
[
0
][
"title"
],
"Analysis"
)
self
.
assertEqual
(
catalog
[
0
][
"title"
],
"Analysis"
)
self
.
assertEqual
(
catalog
[
0
][
"intro"
],
"Intro"
)
self
.
assertEqual
(
catalog
[
0
][
"intro"
],
"Intro"
)
self
.
assertEqual
(
catalog
[
0
][
"topics"
],
[
"quadratische-gleichungen"
])
self
.
assertEqual
(
catalog
[
0
][
"tasks"
][
0
][
"task_id"
],
"01"
)
self
.
assertEqual
(
catalog
[
0
][
"tasks"
][
0
][
"task_id"
],
"01"
)
self
.
assertEqual
(
catalog
[
0
][
"tasks"
][
0
][
"statement"
],
"Bestimme f(x)."
)
self
.
assertEqual
(
catalog
[
0
][
"tasks"
][
0
][
"statement"
],
"Bestimme f(x)."
)
self
.
assertEqual
(
self
.
assertEqual
(
...
...
math-tutor/frontend/src/api/taskApi.ts
View file @
72d5b5d1
...
@@ -15,8 +15,16 @@ export type TaskFile = {
...
@@ -15,8 +15,16 @@ export type TaskFile = {
title
:
string
;
title
:
string
;
intro
:
string
;
intro
:
string
;
tasks
:
TaskItem
[];
tasks
:
TaskItem
[];
subsections
?:
string
[];
topics
?:
string
[];
subsection_options
?:
SubsectionOption
[];
topic_options
?:
TopicOption
[];
};
export
type
TopicOption
=
{
topic_key
:
string
;
label
:
string
;
refs
:
[
number
,
number
,
number
,
number
][];
level
:
string
;
summary
:
string
;
};
};
export
type
SubsectionOption
=
{
export
type
SubsectionOption
=
{
...
@@ -30,7 +38,7 @@ export type TasksResponse = {
...
@@ -30,7 +38,7 @@ export type TasksResponse = {
orchestrator
:
string
;
orchestrator
:
string
;
enabled
:
boolean
;
enabled
:
boolean
;
task_files
:
TaskFile
[];
task_files
:
TaskFile
[];
subsections
:
Subsection
Option
[];
topics
:
Topic
Option
[];
};
};
export
type
SocraticResponse
=
{
export
type
SocraticResponse
=
{
...
...
math-tutor/frontend/src/components/Retrieval/DocPanel.tsx
View file @
72d5b5d1
...
@@ -9,9 +9,11 @@ export type RetrievedDoc = {
...
@@ -9,9 +9,11 @@ export type RetrievedDoc = {
metadata
:
{
metadata
:
{
section_index
?:
number
|
null
;
section_index
?:
number
|
null
;
subsection_index
?:
number
|
null
;
subsection_index
?:
number
|
null
;
subsubsection_index
?:
number
|
null
;
child_index
?:
number
|
null
;
child_index
?:
number
|
null
;
section_title
?:
string
|
null
;
section_title
?:
string
|
null
;
subsection_title
?:
string
|
null
;
subsection_title
?:
string
|
null
;
subsubsection_title
?:
string
|
null
;
title
?:
string
|
null
;
title
?:
string
|
null
;
type
?:
string
|
null
;
type
?:
string
|
null
;
box_hint
?:
string
|
null
;
box_hint
?:
string
|
null
;
...
@@ -25,6 +27,7 @@ type DocPanelProps = {
...
@@ -25,6 +27,7 @@ type DocPanelProps = {
taskChildren
:
RetrievedDoc
[];
taskChildren
:
RetrievedDoc
[];
indirectChildren
:
RetrievedDoc
[];
indirectChildren
:
RetrievedDoc
[];
subsections
:
RetrievedDoc
[];
subsections
:
RetrievedDoc
[];
subsubsections
:
RetrievedDoc
[];
sections
:
RetrievedDoc
[];
sections
:
RetrievedDoc
[];
isLoading
?:
boolean
;
isLoading
?:
boolean
;
error
?:
string
|
null
;
error
?:
string
|
null
;
...
@@ -36,6 +39,7 @@ const buildTitle = (doc: RetrievedDoc) => {
...
@@ -36,6 +39,7 @@ const buildTitle = (doc: RetrievedDoc) => {
const
meta
=
doc
.
metadata
;
const
meta
=
doc
.
metadata
;
return
(
return
(
meta
.
title
||
meta
.
title
||
meta
.
subsubsection_title
||
meta
.
subsection_title
||
meta
.
subsection_title
||
meta
.
section_title
||
meta
.
section_title
||
meta
.
path
||
meta
.
path
||
...
@@ -52,6 +56,9 @@ const buildSubtitle = (doc: RetrievedDoc) => {
...
@@ -52,6 +56,9 @@ const buildSubtitle = (doc: RetrievedDoc) => {
if
(
meta
.
subsection_index
!==
null
&&
meta
.
subsection_index
!==
undefined
)
{
if
(
meta
.
subsection_index
!==
null
&&
meta
.
subsection_index
!==
undefined
)
{
parts
.
push
(
`ss
${
meta
.
subsection_index
}
`
);
parts
.
push
(
`ss
${
meta
.
subsection_index
}
`
);
}
}
if
(
meta
.
subsubsection_index
!==
null
&&
meta
.
subsubsection_index
!==
undefined
)
{
parts
.
push
(
`sss
${
meta
.
subsubsection_index
}
`
);
}
if
(
meta
.
child_index
!==
null
&&
meta
.
child_index
!==
undefined
)
{
if
(
meta
.
child_index
!==
null
&&
meta
.
child_index
!==
undefined
)
{
parts
.
push
(
`c
${
meta
.
child_index
}
`
);
parts
.
push
(
`c
${
meta
.
child_index
}
`
);
}
}
...
@@ -102,6 +109,7 @@ export default function DocPanel({
...
@@ -102,6 +109,7 @@ export default function DocPanel({
taskChildren
,
taskChildren
,
indirectChildren
,
indirectChildren
,
subsections
,
subsections
,
subsubsections
,
sections
,
sections
,
isLoading
,
isLoading
,
error
,
error
,
...
@@ -113,6 +121,7 @@ export default function DocPanel({
...
@@ -113,6 +121,7 @@ export default function DocPanel({
taskChildren
.
length
+
taskChildren
.
length
+
indirectChildren
.
length
+
indirectChildren
.
length
+
subsections
.
length
+
subsections
.
length
+
subsubsections
.
length
+
sections
.
length
;
sections
.
length
;
return
(
return
(
...
@@ -126,6 +135,7 @@ export default function DocPanel({
...
@@ -126,6 +135,7 @@ export default function DocPanel({
{
renderGroup
(
t
(
"
taskChildren
"
),
taskChildren
,
onCiteDoc
,
onInspectDoc
)
}
{
renderGroup
(
t
(
"
taskChildren
"
),
taskChildren
,
onCiteDoc
,
onInspectDoc
)
}
{
renderGroup
(
t
(
"
directChildren
"
),
directChildren
,
onCiteDoc
,
onInspectDoc
)
}
{
renderGroup
(
t
(
"
directChildren
"
),
directChildren
,
onCiteDoc
,
onInspectDoc
)
}
{
renderGroup
(
t
(
"
indirectChildren
"
),
indirectChildren
,
onCiteDoc
,
onInspectDoc
)
}
{
renderGroup
(
t
(
"
indirectChildren
"
),
indirectChildren
,
onCiteDoc
,
onInspectDoc
)
}
{
renderGroup
(
t
(
"
subsubsectionSummary
"
),
subsubsections
,
onCiteDoc
,
onInspectDoc
)
}
{
renderGroup
(
t
(
"
subsectionSummary
"
),
subsections
,
onCiteDoc
,
onInspectDoc
)
}
{
renderGroup
(
t
(
"
subsectionSummary
"
),
subsections
,
onCiteDoc
,
onInspectDoc
)
}
{
renderGroup
(
t
(
"
sectionSummary
"
),
sections
,
onCiteDoc
,
onInspectDoc
)
}
{
renderGroup
(
t
(
"
sectionSummary
"
),
sections
,
onCiteDoc
,
onInspectDoc
)
}
</
div
>
</
div
>
...
...
math-tutor/frontend/src/i18n.ts
View file @
72d5b5d1
...
@@ -31,6 +31,7 @@
...
@@ -31,6 +31,7 @@
directChildren
:
"
Direct children
"
,
directChildren
:
"
Direct children
"
,
taskChildren
:
"
Task sources
"
,
taskChildren
:
"
Task sources
"
,
indirectChildren
:
"
Indirect children
"
,
indirectChildren
:
"
Indirect children
"
,
subsubsectionSummary
:
"
Subsubsection summary
"
,
subsectionSummary
:
"
Subsection summary
"
,
subsectionSummary
:
"
Subsection summary
"
,
sectionSummary
:
"
Section summary
"
,
sectionSummary
:
"
Section summary
"
,
untitled
:
"
Untitled
"
,
untitled
:
"
Untitled
"
,
...
@@ -130,6 +131,7 @@
...
@@ -130,6 +131,7 @@
directChildren
:
"
Direkte Quellen
"
,
directChildren
:
"
Direkte Quellen
"
,
taskChildren
:
"
Aufgaben-Quellen
"
,
taskChildren
:
"
Aufgaben-Quellen
"
,
indirectChildren
:
"
Indirekte Quellen
"
,
indirectChildren
:
"
Indirekte Quellen
"
,
subsubsectionSummary
:
"
Unterunterabschnitt-Zusammenfassung
"
,
subsectionSummary
:
"
Unterabschnitt-Zusammenfassung
"
,
subsectionSummary
:
"
Unterabschnitt-Zusammenfassung
"
,
sectionSummary
:
"
Abschnitt-Zusammenfassung
"
,
sectionSummary
:
"
Abschnitt-Zusammenfassung
"
,
untitled
:
"
Ohne Titel
"
,
untitled
:
"
Ohne Titel
"
,
...
...
math-tutor/frontend/src/pages/ChatPage.tsx
View file @
72d5b5d1
...
@@ -61,6 +61,7 @@ type ContextSource = {
...
@@ -61,6 +61,7 @@ type ContextSource = {
chapter_title
?:
string
|
null
;
chapter_title
?:
string
|
null
;
section_title
?:
string
|
null
;
section_title
?:
string
|
null
;
subsection_title
?:
string
|
null
;
subsection_title
?:
string
|
null
;
subsubsection_title
?:
string
|
null
;
title
:
string
;
title
:
string
;
doc_type
:
string
;
doc_type
:
string
;
};
};
...
@@ -75,6 +76,7 @@ const sourceIdToUid = (source: ContextSource, index: number) => {
...
@@ -75,6 +76,7 @@ const sourceIdToUid = (source: ContextSource, index: number) => {
source
.
source_id
.
chapter_title
,
source
.
source_id
.
chapter_title
,
source
.
source_id
.
section_title
,
source
.
source_id
.
section_title
,
source
.
source_id
.
subsection_title
,
source
.
source_id
.
subsection_title
,
source
.
source_id
.
subsubsection_title
,
source
.
source_id
.
title
,
source
.
source_id
.
title
,
source
.
source_id
.
doc_type
,
source
.
source_id
.
doc_type
,
].
filter
(
Boolean
);
].
filter
(
Boolean
);
...
@@ -86,6 +88,7 @@ const sourceIdToKey = (sourceId: ContextSource["source_id"]) => {
...
@@ -86,6 +88,7 @@ const sourceIdToKey = (sourceId: ContextSource["source_id"]) => {
sourceId
.
chapter_title
??
""
,
sourceId
.
chapter_title
??
""
,
sourceId
.
section_title
??
""
,
sourceId
.
section_title
??
""
,
sourceId
.
subsection_title
??
""
,
sourceId
.
subsection_title
??
""
,
sourceId
.
subsubsection_title
??
""
,
sourceId
.
title
??
""
,
sourceId
.
title
??
""
,
sourceId
.
doc_type
??
""
,
sourceId
.
doc_type
??
""
,
];
];
...
@@ -97,6 +100,7 @@ const sourceIdToPath = (source: ContextSource) => {
...
@@ -97,6 +100,7 @@ const sourceIdToPath = (source: ContextSource) => {
source
.
source_id
.
chapter_title
,
source
.
source_id
.
chapter_title
,
source
.
source_id
.
section_title
,
source
.
source_id
.
section_title
,
source
.
source_id
.
subsection_title
,
source
.
source_id
.
subsection_title
,
source
.
source_id
.
subsubsection_title
,
].
filter
(
Boolean
);
].
filter
(
Boolean
);
return
parts
.
length
?
parts
.
join
(
"
/
"
)
:
null
;
return
parts
.
length
?
parts
.
join
(
"
/
"
)
:
null
;
};
};
...
@@ -109,6 +113,7 @@ const toRetrievedDoc = (source: ContextSource, index: number): RetrievedDoc => (
...
@@ -109,6 +113,7 @@ const toRetrievedDoc = (source: ContextSource, index: number): RetrievedDoc => (
metadata
:
{
metadata
:
{
section_title
:
source
.
source_id
.
section_title
??
null
,
section_title
:
source
.
source_id
.
section_title
??
null
,
subsection_title
:
source
.
source_id
.
subsection_title
??
null
,
subsection_title
:
source
.
source_id
.
subsection_title
??
null
,
subsubsection_title
:
source
.
source_id
.
subsubsection_title
??
null
,
title
:
source
.
source_id
.
title
??
null
,
title
:
source
.
source_id
.
title
??
null
,
type
:
source
.
source_type
??
null
,
type
:
source
.
source_type
??
null
,
path
:
sourceIdToPath
(
source
),
path
:
sourceIdToPath
(
source
),
...
@@ -123,6 +128,7 @@ const applyRetrievedSources = (
...
@@ -123,6 +128,7 @@ const applyRetrievedSources = (
setTaskChildren
:
(
value
:
RetrievedDoc
[])
=>
void
;
setTaskChildren
:
(
value
:
RetrievedDoc
[])
=>
void
;
setIndirectChildren
:
(
value
:
RetrievedDoc
[])
=>
void
;
setIndirectChildren
:
(
value
:
RetrievedDoc
[])
=>
void
;
setSubsections
:
(
value
:
RetrievedDoc
[])
=>
void
;
setSubsections
:
(
value
:
RetrievedDoc
[])
=>
void
;
setSubsubsections
:
(
value
:
RetrievedDoc
[])
=>
void
;
setSections
:
(
value
:
RetrievedDoc
[])
=>
void
;
setSections
:
(
value
:
RetrievedDoc
[])
=>
void
;
}
}
)
=>
{
)
=>
{
...
@@ -130,6 +136,7 @@ const applyRetrievedSources = (
...
@@ -130,6 +136,7 @@ const applyRetrievedSources = (
const
nextTask
:
RetrievedDoc
[]
=
[];
const
nextTask
:
RetrievedDoc
[]
=
[];
const
nextIndirect
:
RetrievedDoc
[]
=
[];
const
nextIndirect
:
RetrievedDoc
[]
=
[];
const
nextSubsections
:
RetrievedDoc
[]
=
[];
const
nextSubsections
:
RetrievedDoc
[]
=
[];
const
nextSubsubsections
:
RetrievedDoc
[]
=
[];
const
nextSections
:
RetrievedDoc
[]
=
[];
const
nextSections
:
RetrievedDoc
[]
=
[];
sources
.
forEach
((
source
,
index
)
=>
{
sources
.
forEach
((
source
,
index
)
=>
{
...
@@ -147,6 +154,9 @@ const applyRetrievedSources = (
...
@@ -147,6 +154,9 @@ const applyRetrievedSources = (
case
"
subsections
"
:
case
"
subsections
"
:
nextSubsections
.
push
(
doc
);
nextSubsections
.
push
(
doc
);
break
;
break
;
case
"
subsubsections
"
:
nextSubsubsections
.
push
(
doc
);
break
;
case
"
sections
"
:
case
"
sections
"
:
nextSections
.
push
(
doc
);
nextSections
.
push
(
doc
);
break
;
break
;
...
@@ -162,6 +172,7 @@ const applyRetrievedSources = (
...
@@ -162,6 +172,7 @@ const applyRetrievedSources = (
setters
.
setTaskChildren
(
nextTask
);
setters
.
setTaskChildren
(
nextTask
);
setters
.
setIndirectChildren
(
nextIndirect
);
setters
.
setIndirectChildren
(
nextIndirect
);
setters
.
setSubsections
(
nextSubsections
);
setters
.
setSubsections
(
nextSubsections
);
setters
.
setSubsubsections
(
nextSubsubsections
);
setters
.
setSections
(
nextSections
);
setters
.
setSections
(
nextSections
);
};
};
...
@@ -210,6 +221,7 @@ export default function ChatPage() {
...
@@ -210,6 +221,7 @@ export default function ChatPage() {
const
[
taskChildren
,
setTaskChildren
]
=
useState
<
RetrievedDoc
[]
>
([]);
const
[
taskChildren
,
setTaskChildren
]
=
useState
<
RetrievedDoc
[]
>
([]);
const
[
indirectChildren
,
setIndirectChildren
]
=
useState
<
RetrievedDoc
[]
>
([]);
const
[
indirectChildren
,
setIndirectChildren
]
=
useState
<
RetrievedDoc
[]
>
([]);
const
[
subsections
,
setSubsections
]
=
useState
<
RetrievedDoc
[]
>
([]);
const
[
subsections
,
setSubsections
]
=
useState
<
RetrievedDoc
[]
>
([]);
const
[
subsubsections
,
setSubsubsections
]
=
useState
<
RetrievedDoc
[]
>
([]);
const
[
sections
,
setSections
]
=
useState
<
RetrievedDoc
[]
>
([]);
const
[
sections
,
setSections
]
=
useState
<
RetrievedDoc
[]
>
([]);
const
[
retrievalLoading
,
setRetrievalLoading
]
=
useState
(
false
);
const
[
retrievalLoading
,
setRetrievalLoading
]
=
useState
(
false
);
const
[
retrievalError
,
setRetrievalError
]
=
useState
<
string
|
null
>
(
null
);
const
[
retrievalError
,
setRetrievalError
]
=
useState
<
string
|
null
>
(
null
);
...
@@ -420,6 +432,7 @@ export default function ChatPage() {
...
@@ -420,6 +432,7 @@ export default function ChatPage() {
setTaskChildren
,
setTaskChildren
,
setIndirectChildren
,
setIndirectChildren
,
setSubsections
,
setSubsections
,
setSubsubsections
,
setSections
,
setSections
,
});
});
socraticBootstrapRef
.
current
=
bootstrapKey
;
socraticBootstrapRef
.
current
=
bootstrapKey
;
...
@@ -492,6 +505,7 @@ export default function ChatPage() {
...
@@ -492,6 +505,7 @@ export default function ChatPage() {
...
taskChildren
,
...
taskChildren
,
...
indirectChildren
,
...
indirectChildren
,
...
subsections
,
...
subsections
,
...
subsubsections
,
...
sections
,
...
sections
,
];
];
...
@@ -522,7 +536,7 @@ export default function ChatPage() {
...
@@ -522,7 +536,7 @@ export default function ChatPage() {
});
});
return
{
bySourceKey
,
bySlug
};
return
{
bySourceKey
,
bySlug
};
},
[
directChildren
,
taskChildren
,
indirectChildren
,
subsections
,
sections
]);
},
[
directChildren
,
taskChildren
,
indirectChildren
,
subsections
,
subsubsections
,
sections
]);
const
nextTaskRef
=
useMemo
(()
=>
{
const
nextTaskRef
=
useMemo
(()
=>
{
if
(
!
selectedTaskFile
||
!
selectedTaskRef
)
{
if
(
!
selectedTaskFile
||
!
selectedTaskRef
)
{
...
@@ -734,6 +748,7 @@ export default function ChatPage() {
...
@@ -734,6 +748,7 @@ export default function ChatPage() {
setTaskChildren
,
setTaskChildren
,
setIndirectChildren
,
setIndirectChildren
,
setSubsections
,
setSubsections
,
setSubsubsections
,
setSections
,
setSections
,
});
});
}
catch
(
error
)
{
}
catch
(
error
)
{
...
@@ -806,6 +821,7 @@ export default function ChatPage() {
...
@@ -806,6 +821,7 @@ export default function ChatPage() {
setTaskChildren
([]);
setTaskChildren
([]);
setIndirectChildren
([]);
setIndirectChildren
([]);
setSubsections
([]);
setSubsections
([]);
setSubsubsections
([]);
setSections
([]);
setSections
([]);
setRetrievalLoading
(
false
);
setRetrievalLoading
(
false
);
setRetrievalError
(
null
);
setRetrievalError
(
null
);
...
@@ -1281,6 +1297,7 @@ export default function ChatPage() {
...
@@ -1281,6 +1297,7 @@ export default function ChatPage() {
taskChildren
=
{
taskChildren
}
taskChildren
=
{
taskChildren
}
indirectChildren
=
{
indirectChildren
}
indirectChildren
=
{
indirectChildren
}
subsections
=
{
subsections
}
subsections
=
{
subsections
}
subsubsections
=
{
subsubsections
}
sections
=
{
sections
}
sections
=
{
sections
}
isLoading
=
{
retrievalLoading
}
isLoading
=
{
retrievalLoading
}
error
=
{
retrievalError
}
error
=
{
retrievalError
}
...
...
math-tutor/frontend/src/state/tutorSession.tsx
View file @
72d5b5d1
...
@@ -20,6 +20,7 @@ import {
...
@@ -20,6 +20,7 @@ import {
type
SelectedSubsectionRef
,
type
SelectedSubsectionRef
,
type
SelectedTaskRef
,
type
SelectedTaskRef
,
type
SubsectionOption
,
type
SubsectionOption
,
type
TopicOption
,
type
TaskImage
,
type
TaskImage
,
type
TaskFile
,
type
TaskFile
,
}
from
"
../api/taskApi
"
;
}
from
"
../api/taskApi
"
;
...
@@ -44,6 +45,7 @@ export type SelectedSubsection = SelectedSubsectionRef & {
...
@@ -44,6 +45,7 @@ export type SelectedSubsection = SelectedSubsectionRef & {
export
type
TaskSelectionState
=
{
export
type
TaskSelectionState
=
{
taskFiles
:
TaskFile
[];
taskFiles
:
TaskFile
[];
topics
:
TopicOption
[];
selectedTaskRef
:
SelectedTaskRef
|
null
;
selectedTaskRef
:
SelectedTaskRef
|
null
;
selectedTask
:
SelectedTask
|
null
;
selectedTask
:
SelectedTask
|
null
;
selectedTaskFile
:
TaskFile
|
null
;
selectedTaskFile
:
TaskFile
|
null
;
...
@@ -88,7 +90,8 @@ export const getDefaultTaskId = (tasks: Array<{ task_id: string }>): string =>
...
@@ -88,7 +90,8 @@ export const getDefaultTaskId = (tasks: Array<{ task_id: string }>): string =>
tasks
.
find
((
task
)
=>
task
.
task_id
===
"
01
"
)?.
task_id
||
tasks
[
0
]?.
task_id
||
""
;
tasks
.
find
((
task
)
=>
task
.
task_id
===
"
01
"
)?.
task_id
||
tasks
[
0
]?.
task_id
||
""
;
const
isSelectableTaskFile
=
(
file
:
TaskFile
):
boolean
=>
const
isSelectableTaskFile
=
(
file
:
TaskFile
):
boolean
=>
Array
.
isArray
(
file
.
subsections
)
&&
file
.
subsections
.
length
>
0
;
(
Array
.
isArray
(
file
.
topics
)
&&
file
.
topics
.
length
>
0
)
||
(
Array
.
isArray
(
file
.
topic_options
)
&&
file
.
topic_options
.
length
>
0
);
const
isTaskCoupledOrchestrator
=
(
value
:
OrchestratorName
):
boolean
=>
const
isTaskCoupledOrchestrator
=
(
value
:
OrchestratorName
):
boolean
=>
value
===
"
task
"
||
value
===
"
socratic
"
;
value
===
"
task
"
||
value
===
"
socratic
"
;
...
@@ -107,6 +110,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -107,6 +110,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const
[
orchestratorError
,
setOrchestratorError
]
=
useState
<
string
|
null
>
(
null
);
const
[
orchestratorError
,
setOrchestratorError
]
=
useState
<
string
|
null
>
(
null
);
const
[
isTasksInitialized
,
setIsTasksInitialized
]
=
useState
(
false
);
const
[
isTasksInitialized
,
setIsTasksInitialized
]
=
useState
(
false
);
const
[
taskFiles
,
setTaskFiles
]
=
useState
<
TaskFile
[]
>
([]);
const
[
taskFiles
,
setTaskFiles
]
=
useState
<
TaskFile
[]
>
([]);
const
[
topics
,
setTopics
]
=
useState
<
TopicOption
[]
>
([]);
const
[
subsections
,
setSubsections
]
=
useState
<
SubsectionOption
[]
>
([]);
const
[
subsections
,
setSubsections
]
=
useState
<
SubsectionOption
[]
>
([]);
const
[
selectedTaskRef
,
setSelectedTaskRef
]
=
useState
<
SelectedTaskRef
|
null
>
(
null
);
const
[
selectedTaskRef
,
setSelectedTaskRef
]
=
useState
<
SelectedTaskRef
|
null
>
(
null
);
const
[
selectedSubsectionRef
,
setSelectedSubsectionRef
]
=
const
[
selectedSubsectionRef
,
setSelectedSubsectionRef
]
=
...
@@ -199,6 +203,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -199,6 +203,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
try
{
try
{
const
isSocratic
=
orchestrator
===
"
socratic
"
;
const
isSocratic
=
orchestrator
===
"
socratic
"
;
let
files
:
TaskFile
[]
=
[];
let
files
:
TaskFile
[]
=
[];
let
topicsPayload
:
TopicOption
[]
=
[];
let
subsectionsPayload
:
SubsectionOption
[]
=
[];
let
subsectionsPayload
:
SubsectionOption
[]
=
[];
if
(
isSocratic
)
{
if
(
isSocratic
)
{
...
@@ -207,12 +212,13 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -207,12 +212,13 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
}
else
{
}
else
{
const
payload
=
await
fetchTasks
();
const
payload
=
await
fetchTasks
();
files
=
payload
.
task_files
||
[];
files
=
payload
.
task_files
||
[];
subsection
sPayload
=
payload
.
subsection
s
||
[];
topic
sPayload
=
payload
.
topic
s
||
[];
}
}
const
selectableFiles
=
files
.
filter
((
file
)
=>
isSelectableTaskFile
(
file
));
const
selectableFiles
=
files
.
filter
((
file
)
=>
isSelectableTaskFile
(
file
));
setTaskFiles
(
files
);
setTaskFiles
(
files
);
setTopics
(
topicsPayload
);
setSubsections
(
subsectionsPayload
);
setSubsections
(
subsectionsPayload
);
setSelectedTaskRef
((
prev
)
=>
{
setSelectedTaskRef
((
prev
)
=>
{
if
(
isSocratic
)
{
if
(
isSocratic
)
{
...
@@ -257,6 +263,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -257,6 +263,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
}
catch
(
error
)
{
}
catch
(
error
)
{
setTasksError
(
t
(
"
failedLoadTasks
"
));
setTasksError
(
t
(
"
failedLoadTasks
"
));
setTaskFiles
([]);
setTaskFiles
([]);
setTopics
([]);
setSubsections
([]);
setSubsections
([]);
setSelectedTaskRef
(
null
);
setSelectedTaskRef
(
null
);
setSelectedSubsectionRef
(
null
);
setSelectedSubsectionRef
(
null
);
...
@@ -384,6 +391,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -384,6 +391,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
isOrchestratorSelectable
,
isOrchestratorSelectable
,
orchestratorError
,
orchestratorError
,
taskFiles
,
taskFiles
,
topics
,
subsections
,
subsections
,
selectedTaskRef
,
selectedTaskRef
,
selectedTask
,
selectedTask
,
...
...
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