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
134a9c5c
Commit
134a9c5c
authored
Jun 02, 2026
by
Kantz
Browse files
subsection struktur elemente entfertn
parent
f3444904
Changes
14
Hide whitespace changes
Inline
Side-by-side
math-tutor/README.md
View file @
134a9c5c
...
...
@@ -79,7 +79,7 @@ You can open a socratic chat directly with URL query parameters:
Example:
`http://localhost:5173/chat?orchestrator=socratic&subsection_key=
quadratische-gleichu
ngen`
`http://localhost:5173/chat?orchestrator=socratic&subsection_key=
me
ngen`
Notes:
...
...
math-tutor/backend/app/api/chat.py
View file @
134a9c5c
...
...
@@ -29,7 +29,7 @@ class ChatRequest(BaseModel):
messages
:
List
[
ChatMessage
]
draft
:
Optional
[
str
]
=
None
selected_task
:
Optional
[
dict
[
str
,
str
]]
=
None
selected_
subsection
:
Optional
[
dict
[
str
,
str
]]
=
None
selected_
topic
:
Optional
[
dict
[
str
,
str
]]
=
None
orchestrator
:
Optional
[
str
]
=
None
...
...
@@ -56,8 +56,8 @@ class SelectedTaskRef(BaseModel):
task_id
:
str
class
Selected
Subsection
Ref
(
BaseModel
):
subsection
_key
:
str
class
Selected
Topic
Ref
(
BaseModel
):
topic
_key
:
str
class
ChatArchiveDetail
(
BaseModel
):
...
...
@@ -65,13 +65,13 @@ class ChatArchiveDetail(BaseModel):
saved_at
:
str
history
:
List
[
ChatMessage
]
selected_task
:
Optional
[
SelectedTaskRef
]
=
None
selected_
subsection
:
Optional
[
Selected
Subsection
Ref
]
=
None
selected_
topic
:
Optional
[
Selected
Topic
Ref
]
=
None
orchestrator
:
str
class
SocraticBootstrapRequest
(
BaseModel
):
draft
:
str
=
Field
(...,
min_length
=
1
)
subsection
_key
:
str
=
Field
(...,
min_length
=
1
)
topic
_key
:
str
=
Field
(...,
min_length
=
1
)
class
SocraticBootstrapResponse
(
BaseModel
):
...
...
@@ -99,7 +99,7 @@ def chat(request: ChatRequest) -> ChatResponse:
payload_messages
,
draft
=
request
.
draft
,
selected_task
=
request
.
selected_task
,
selected_
subsection
=
request
.
selected_
subsection
,
selected_
topic
=
request
.
selected_
topic
,
)
else
:
result
=
orchestrator_impl
.
run_chat
(
...
...
@@ -145,19 +145,19 @@ def get_archive(chat_id: str = Path(..., min_length=1)) -> ChatArchiveDetail:
if
file_id
and
task_id
:
selected_task
=
SelectedTaskRef
(
file_id
=
file_id
,
task_id
=
task_id
)
selected_
subsection
_raw
=
record
.
get
(
"selected_
subsection
"
)
selected_
subsection
:
Optional
[
Selected
Subsection
Ref
]
=
None
if
isinstance
(
selected_
subsection
_raw
,
dict
):
subsection
_key
=
str
(
selected_
subsection_raw
.
get
(
"subsection
_key"
,
""
)).
strip
()
if
subsection
_key
:
selected_
subsection
=
Selected
SubsectionRef
(
subsection_key
=
subsection
_key
)
selected_
topic
_raw
=
record
.
get
(
"selected_
topic
"
)
selected_
topic
:
Optional
[
Selected
Topic
Ref
]
=
None
if
isinstance
(
selected_
topic
_raw
,
dict
):
topic
_key
=
str
(
selected_
topic_raw
.
get
(
"topic
_key"
,
""
)).
strip
()
if
topic
_key
:
selected_
topic
=
Selected
TopicRef
(
topic_key
=
topic
_key
)
return
ChatArchiveDetail
(
chat_id
=
record
[
"chat_id"
],
saved_at
=
record
.
get
(
"saved_at"
,
""
),
history
=
[
ChatMessage
(
role
=
item
[
"role"
],
text
=
item
[
"text"
])
for
item
in
record
[
"history"
]],
selected_task
=
selected_task
,
selected_
subsection
=
selected_subsection
,
selected_
topic
=
selected_topic
,
orchestrator
=
record
.
get
(
"orchestrator"
)
or
get_default_orchestrator
(),
)
...
...
@@ -192,10 +192,13 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe
if
not
sheet
:
sheet
=
context_store
.
context_store_new
.
init_sheet
(
chat_id
,
[])
if
not
task_catalog
.
select_subsection_by_key
(
sheet
,
request
.
subsection_key
):
raise
HTTPException
(
status_code
=
404
,
detail
=
"subsection not found"
)
try
:
topic_entry
=
socratic_oranisator
.
get_topic_entry
(
request
.
topic_key
)
except
ValueError
as
exc
:
raise
HTTPException
(
status_code
=
404
,
detail
=
"topic not found"
)
from
exc
refs
=
task_catalog
.
get_selected_subsection_parent_refs
(
sheet
)
task_catalog
.
set_selected_topic
(
sheet
,
{},
topic_entry
)
refs
=
task_catalog
.
get_selected_topic_parent_refs
(
sheet
)
sources
=
retrieval_store
.
retrieve_for_parent_refs
(
pg_url
=
config
.
get_postgres_url
(),
parent_refs
=
refs
,
...
...
@@ -205,7 +208,7 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe
context_store
.
save_sheet
(
sheet
)
try
:
reply
=
socratic_oranisator
.
get_initial_message
(
request
.
subsection
_key
)
reply
=
socratic_oranisator
.
get_initial_message
(
request
.
topic
_key
)
except
ValueError
as
exc
:
logger
.
exception
(
"Socratic bootstrap prompt lookup failed"
)
raise
HTTPException
(
status_code
=
500
,
detail
=
str
(
exc
))
from
exc
...
...
math-tutor/backend/app/api/tasks.py
View file @
134a9c5c
...
...
@@ -23,8 +23,8 @@ class TaskItem(BaseModel):
task_id
:
str
class
Subsection
Entry
(
BaseModel
):
subsection
_key
:
str
class
Topic
Entry
(
BaseModel
):
topic
_key
:
str
label
:
str
level
:
str
=
""
refs
:
List
[
List
[
int
]]
...
...
@@ -54,7 +54,7 @@ class TaskDetailsResponse(BaseModel):
class
SocraticResponse
(
BaseModel
):
orchestrator
:
str
enabled
:
bool
subsections
:
List
[
Subsection
Entry
]
=
Field
(
default_factory
=
list
)
topics
:
List
[
Topic
Entry
]
=
Field
(
default_factory
=
list
)
class
SelectTaskRequest
(
BaseModel
):
draft
:
str
=
Field
(...,
min_length
=
1
)
...
...
@@ -68,14 +68,23 @@ class SelectTaskResponse(BaseModel):
task_id
:
str
class
Select
Subsection
Request
(
BaseModel
):
class
Select
Topic
Request
(
BaseModel
):
draft
:
str
=
Field
(...,
min_length
=
1
)
subsection
_key
:
str
=
Field
(...,
min_length
=
1
)
topic
_key
:
str
=
Field
(...,
min_length
=
1
)
class
Select
Subsection
Response
(
BaseModel
):
class
Select
Topic
Response
(
BaseModel
):
status
:
str
subsection_key
:
str
topic_key
:
str
def
_set_selected_socratic_topic
(
sheet
:
dict
[
str
,
object
],
topic_key
:
str
)
->
str
:
entry
=
socratic_oranisator
.
get_topic_entry
(
topic_key
)
task_catalog
.
set_selected_topic
(
sheet
,
{},
entry
)
_
,
selected_topic_key
=
task_catalog
.
get_selected_topic_ids
(
sheet
)
if
not
selected_topic_key
:
raise
HTTPException
(
status_code
=
404
,
detail
=
"topic not found"
)
return
selected_topic_key
@
router
.
get
(
"/api/tasks/assets/{asset_path:path}"
)
...
...
@@ -122,14 +131,14 @@ def get_task_details(file_id: str, task_id: str) -> TaskDetailsResponse:
return
TaskDetailsResponse
(
**
payload
)
@
router
.
get
(
"/api/tasks/socratic-
subsection
s"
,
response_model
=
SocraticResponse
)
def
list_socratic_
subsection
s
()
->
SocraticResponse
:
@
router
.
get
(
"/api/tasks/socratic-
topic
s"
,
response_model
=
SocraticResponse
)
def
list_socratic_
topic
s
()
->
SocraticResponse
:
orchestrator
=
config
.
get_orchestrator
()
subsection
s
=
socratic_oranisator
.
build_
subsection
_catalog
()
topic
s
=
socratic_oranisator
.
build_
topic
_catalog
()
return
SocraticResponse
(
orchestrator
=
orchestrator
,
enabled
=
orchestrator
==
"socratic"
,
subsections
=
subsection
s
,
topics
=
topic
s
,
)
@
router
.
post
(
"/api/tasks/select"
,
response_model
=
SelectTaskResponse
)
...
...
@@ -156,20 +165,20 @@ def select_task(request: SelectTaskRequest) -> SelectTaskResponse:
)
@
router
.
post
(
"/api/tasks/select-
subsection
"
,
response_model
=
Select
Subsection
Response
)
def
select_
subsection
(
request
:
Select
Subsection
Request
)
->
Select
Subsection
Response
:
@
router
.
post
(
"/api/tasks/select-
topic
"
,
response_model
=
Select
Topic
Response
)
def
select_
topic
(
request
:
Select
Topic
Request
)
->
Select
Topic
Response
:
chat_id
=
context_store
.
get_chat_id
([],
draft
=
request
.
draft
)
sheet
=
context_store
.
load_sheet
(
chat_id
)
if
not
sheet
:
sheet
=
context_store
.
context_store_new
.
init_sheet
(
chat_id
,
[])
updated
=
task_catalog
.
select_subsection_by_key
(
sheet
,
subsection_key
=
request
.
subsection_key
)
if
not
updated
:
raise
HTTPException
(
status_code
=
404
,
detail
=
"subsection not found"
)
try
:
topic_key
=
_set_selected_socratic_topic
(
sheet
,
request
.
topic_key
)
except
ValueError
as
exc
:
raise
HTTPException
(
status_code
=
404
,
detail
=
"topic not found"
)
from
exc
context_store
.
save_sheet
(
sheet
)
_
,
subsection_key
=
task_catalog
.
get_selected_subsection_ids
(
sheet
)
return
SelectSubsectionResponse
(
return
SelectTopicResponse
(
status
=
"ok"
,
subsection_key
=
subsection_key
or
request
.
subsection
_key
,
topic_key
=
topic
_key
,
)
math-tutor/backend/app/deterministic_services/orchestrators/orchestrator_socratic.py
View file @
134a9c5c
...
...
@@ -6,22 +6,22 @@ from app.deterministic_services import context_store, retrieval_store, task_cata
from
app.deterministic_services.orchestrators
import
orchestrator_base
as
base
def
_apply_selected_
subsection
(
def
_apply_selected_
topic
(
state
:
base
.
ChatState
,
selected_
subsection
:
dict
|
None
,
selected_
topic
:
dict
|
None
,
)
->
None
:
if
not
selected_
subsection
:
if
not
selected_
topic
:
return
subsection
_key
=
str
(
selected_
subsection
.
get
(
"subsection
_key"
,
""
)).
strip
()
if
not
subsection
_key
:
topic
_key
=
str
(
selected_
topic
.
get
(
"topic
_key"
,
""
)).
strip
()
if
not
topic
_key
:
return
task_catalog
.
select_
subsection
_by_key
(
state
.
sheet
,
subsection
_key
)
task_catalog
.
select_
topic
_by_key
(
state
.
sheet
,
topic
_key
)
def
_retrieve_context_for_
subsection
(
state
:
base
.
ChatState
,
query_text
:
str
)
->
int
:
refs
=
task_catalog
.
get_selected_
subsection
_parent_refs
(
state
.
sheet
)
def
_retrieve_context_for_
topic
(
state
:
base
.
ChatState
,
query_text
:
str
)
->
int
:
refs
=
task_catalog
.
get_selected_
topic
_parent_refs
(
state
.
sheet
)
if
not
refs
:
return
0
...
...
@@ -49,17 +49,17 @@ def _retrieve_context_for_subsection(state: base.ChatState, query_text: str) ->
def
_on_bootstrap
(
state
:
base
.
ChatState
,
query_text
:
str
)
->
None
:
_retrieve_context_for_
subsection
(
state
,
query_text
)
_retrieve_context_for_
topic
(
state
,
query_text
)
def
_on_turn_logic
(
state
:
base
.
ChatState
)
->
None
:
if
not
task_catalog
.
get_selected_
subsection
_parent_refs
(
state
.
sheet
):
if
not
task_catalog
.
get_selected_
topic
_parent_refs
(
state
.
sheet
):
return
def
_on_build_reply
(
state
:
base
.
ChatState
)
->
str
|
None
:
history_turns
=
context_store
.
get_history_turns
(
state
.
sheet
)
parent_refs
=
task_catalog
.
get_selected_
subsection
_parent_refs
(
state
.
sheet
)
parent_refs
=
task_catalog
.
get_selected_
topic
_parent_refs
(
state
.
sheet
)
args
=
{
"query"
:
state
.
last_user
,
"parent_refs"
:
parent_refs
,
...
...
@@ -77,11 +77,11 @@ def _on_build_reply(state: base.ChatState) -> str | None:
def
run_chat
(
messages
:
list
[
dict
],
draft
:
str
|
None
=
None
,
selected_
subsection
:
dict
|
None
=
None
,
selected_
topic
:
dict
|
None
=
None
,
selected_task
:
dict
|
None
=
None
,
)
->
dict
:
def
_apply_selected_context
(
state
:
base
.
ChatState
)
->
None
:
_apply_selected_
subsection
(
state
,
selected_
subsection
)
_apply_selected_
topic
(
state
,
selected_
topic
)
def
on_bootstrap
(
state
:
base
.
ChatState
,
query_text
:
str
)
->
None
:
_apply_selected_context
(
state
)
...
...
math-tutor/backend/app/deterministic_services/orchestrators/orchestrator_task.py
View file @
134a9c5c
...
...
@@ -123,7 +123,7 @@ def run_chat(
messages
:
list
[
dict
],
draft
:
str
|
None
=
None
,
selected_task
:
dict
|
None
=
None
,
selected_
subsection
:
dict
|
None
=
None
,
selected_
topic
:
dict
|
None
=
None
,
)
->
dict
:
def
_apply_selected_task
(
state
:
base
.
ChatState
)
->
None
:
if
not
selected_task
:
...
...
math-tutor/backend/app/deterministic_services/session_store.py
View file @
134a9c5c
...
...
@@ -26,11 +26,11 @@ def _extract_selected_task(sheet: dict[str, Any]) -> dict[str, str] | None:
return
{
"file_id"
:
file_id
,
"task_id"
:
task_id
}
def
_extract_selected_
subsection
(
sheet
:
dict
[
str
,
Any
])
->
dict
[
str
,
str
]
|
None
:
subsection
_key
=
str
(
sheet
.
get
(
"selected_
subsection
_key"
,
""
)).
strip
()
if
not
subsection
_key
:
def
_extract_selected_
topic
(
sheet
:
dict
[
str
,
Any
])
->
dict
[
str
,
str
]
|
None
:
topic
_key
=
str
(
sheet
.
get
(
"selected_
topic
_key"
,
""
)).
strip
()
if
not
topic
_key
:
return
None
return
{
"
subsection_key"
:
subsection
_key
}
return
{
"
topic_key"
:
topic
_key
}
def
archive_chat
(
...
...
@@ -56,7 +56,7 @@ def archive_chat(
"math_solutions"
:
sheet
.
get
(
"math_solutions"
,
[]),
"sources"
:
sheet
.
get
(
"sources"
,
[]),
"selected_task"
:
_extract_selected_task
(
sheet
),
"selected_
subsection
"
:
_extract_selected_
subsection
(
sheet
),
"selected_
topic
"
:
_extract_selected_
topic
(
sheet
),
}
os
.
makedirs
(
_LOG_DIR
,
exist_ok
=
True
)
...
...
@@ -134,12 +134,12 @@ def load_archive(chat_id: str) -> dict[str, Any] | None:
if
file_id
and
task_id
:
selected_task
=
{
"file_id"
:
file_id
,
"task_id"
:
task_id
}
selected_
subsection
_raw
=
record
.
get
(
"selected_
subsection
"
)
selected_
subsection
:
dict
[
str
,
str
]
|
None
=
None
if
isinstance
(
selected_
subsection
_raw
,
dict
):
subsection
_key
=
str
(
selected_
subsection_raw
.
get
(
"subsection
_key"
,
""
)).
strip
()
if
subsection
_key
:
selected_
subsection
=
{
"subsection_key"
:
subsection
_key
}
selected_
topic
_raw
=
record
.
get
(
"selected_
topic
"
)
selected_
topic
:
dict
[
str
,
str
]
|
None
=
None
if
isinstance
(
selected_
topic
_raw
,
dict
):
topic
_key
=
str
(
selected_
topic_raw
.
get
(
"topic
_key"
,
""
)).
strip
()
if
topic
_key
:
selected_
topic
=
{
"topic_key"
:
topic
_key
}
return
{
"chat_id"
:
record
.
get
(
"chat_id"
,
chat_id
),
...
...
@@ -147,6 +147,6 @@ def load_archive(chat_id: str) -> dict[str, Any] | None:
"orchestrator"
:
record
.
get
(
"orchestrator"
),
"history"
:
history
,
"selected_task"
:
selected_task
,
"selected_
subsection
"
:
selected_
subsection
,
"selected_
topic
"
:
selected_
topic
,
}
return
None
math-tutor/backend/app/deterministic_services/socratic_oranisator.py
View file @
134a9c5c
...
...
@@ -35,60 +35,75 @@ def load_initial_prompt_items(path: Path = PROMPTS_PATH) -> dict[str, dict[str,
return
items
def
get_initial_message
(
subsection
_key
:
str
,
path
:
Path
=
PROMPTS_PATH
)
->
str
:
normalized_key
=
task_catalog
.
_normalize_parent_ref_key
(
subsection
_key
)
def
get_initial_message
(
topic
_key
:
str
,
path
:
Path
=
PROMPTS_PATH
)
->
str
:
normalized_key
=
task_catalog
.
_normalize_parent_ref_key
(
topic
_key
)
if
not
normalized_key
:
raise
ValueError
(
"invalid
subsection
key"
)
raise
ValueError
(
"invalid
topic
key"
)
items
=
load_initial_prompt_items
(
path
)
item
=
items
.
get
(
normalized_key
)
if
item
is
None
:
raise
ValueError
(
f
"missing socratic bootstrap prompt for
subsection
'
{
normalized_key
}
'"
)
raise
ValueError
(
f
"missing socratic bootstrap prompt for
topic
'
{
normalized_key
}
'"
)
message
=
str
(
item
.
get
(
"inital_message"
)
or
""
).
strip
()
if
not
message
:
raise
ValueError
(
f
"empty socratic bootstrap prompt for
subsection
'
{
normalized_key
}
'"
)
raise
ValueError
(
f
"empty socratic bootstrap prompt for
topic
'
{
normalized_key
}
'"
)
return
message
def
build_subsection_catalog
(
path
:
Path
=
PROMPTS_PATH
)
->
list
[
dict
[
str
,
Any
]]:
def
get_topic_entry
(
topic_key
:
str
,
path
:
Path
=
PROMPTS_PATH
)
->
dict
[
str
,
Any
]:
normalized_key
=
task_catalog
.
_normalize_parent_ref_key
(
topic_key
)
if
not
normalized_key
:
raise
ValueError
(
"invalid topic key"
)
items
=
load_initial_prompt_items
(
path
)
item
=
items
.
get
(
normalized_key
)
if
item
is
None
:
raise
ValueError
(
f
"missing socratic bootstrap prompt for topic '
{
normalized_key
}
'"
)
refs
:
list
[
list
[
int
]]
=
[]
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
))
if
not
refs
:
raise
ValueError
(
f
"missing refs for topic '
{
normalized_key
}
'"
)
label_source
=
str
(
item
.
get
(
"label"
)
or
item
.
get
(
"topic"
)
or
item
.
get
(
"subsection"
)
or
normalized_key
)
return
{
"topic_key"
:
normalized_key
,
"label"
:
task_catalog
.
_format_topic_label
(
label_source
)
or
task_catalog
.
_format_topic_label
(
normalized_key
),
"level"
:
str
(
item
.
get
(
"level"
)
or
task_catalog
.
_parent_ref_level
(
tuple
(
refs
[
0
]))).
strip
(),
"refs"
:
refs
,
}
def
build_topic_catalog
(
path
:
Path
=
PROMPTS_PATH
)
->
list
[
dict
[
str
,
Any
]]:
items
=
load_initial_prompt_items
(
path
)
topic_summaries
=
task_catalog
.
load_topic_summaries
()
response
:
list
[
dict
[
str
,
Any
]]
=
[]
for
raw_key
,
item
in
items
.
items
():
subsection
_key
=
task_catalog
.
_normalize_parent_ref_key
(
str
(
raw_key
))
if
not
subsection
_key
:
topic
_key
=
task_catalog
.
_normalize_parent_ref_key
(
str
(
raw_key
))
if
not
topic
_key
:
continue
label_source
=
str
(
item
.
get
(
"label"
)
or
item
.
get
(
"subsection"
)
or
subsection_key
)
label
=
task_catalog
.
_format_subsection_label
(
label_source
)
refs
:
list
[
list
[
int
]]
=
[]
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
(
{
"subsection_key"
:
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
,
"summary"
:
topic_summaries
.
get
(
subsection_key
,
""
),
}
)
try
:
entry
=
get_topic_entry
(
topic_key
,
path
)
except
ValueError
:
continue
entry
[
"summary"
]
=
topic_summaries
.
get
(
topic_key
,
""
)
response
.
append
(
entry
)
response
.
sort
(
key
=
lambda
item
:
(
item
[
"refs"
][
0
]
if
item
[
"refs"
]
else
(
9999
,
9999
,
9999
,
9999
),
item
[
"
subsection
_key"
],
item
[
"
topic
_key"
],
)
)
return
response
math-tutor/backend/app/deterministic_services/task_catalog.py
View file @
134a9c5c
...
...
@@ -18,13 +18,12 @@ TASKS_DIR = config.get_task_folder()
TASK_IMAGES_DIR
=
TASKS_DIR
/
"images"
SOURCES_DIR
=
Path
(
__file__
).
resolve
().
parents
[
2
]
/
"sources"
TOPIC_MAP_PATH
=
TASKS_DIR
/
"_topic_to_index_map.yaml"
SUBSECTION_MAP_PATH
=
TOPIC_MAP_PATH
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
)
return
_normalize_
topic
_key
(
value
)
def
_normalize_text
(
value
:
str
)
->
str
:
return
re
.
sub
(
r
"\s+"
,
" "
,
value
.
strip
().
lower
())
...
...
@@ -34,7 +33,7 @@ def _tokenize(value: str) -> set[str]:
return
set
(
re
.
findall
(
r
"[a-z0-9_]+"
,
_normalize_text
(
value
)))
def
_normalize_
subsection
_key
(
value
:
str
)
->
str
:
def
_normalize_
topic
_key
(
value
:
str
)
->
str
:
normalized
=
unicodedata
.
normalize
(
"NFKD"
,
str
(
value
))
ascii_value
=
normalized
.
encode
(
"ascii"
,
"ignore"
).
decode
(
"ascii"
)
collapsed
=
re
.
sub
(
r
"[-_]+"
,
" "
,
ascii_value
.
strip
().
lower
())
...
...
@@ -42,13 +41,6 @@ def _normalize_subsection_key(value: str) -> str:
return
re
.
sub
(
r
"\s+"
,
" "
,
collapsed
).
strip
()
def
_parse_subsection_ref
(
value
:
str
)
->
tuple
[
int
,
int
,
int
]
|
None
:
match
=
re
.
match
(
r
"^\s*(\d+)\s*[-.]\s*(\d+)\s*[-.]\s*(\d+)\s*$"
,
str
(
value
))
if
not
match
:
return
None
return
int
(
match
.
group
(
1
)),
int
(
match
.
group
(
2
)),
int
(
match
.
group
(
3
))
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
()]
...
...
@@ -142,18 +134,7 @@ def load_topic_map(path: Path = TOPIC_MAP_PATH) -> dict[str, ParentRef]:
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
def
_extract_subsection_summary
(
text
:
str
)
->
str
:
def
_extract_topic_summary
(
text
:
str
)
->
str
:
body
=
text
.
replace
(
"
\r\n
"
,
"
\n
"
).
strip
()
if
not
body
:
return
""
...
...
@@ -204,17 +185,13 @@ def load_topic_summaries(base_dir: Path = SOURCES_DIR) -> dict[str, str]:
or
""
).
strip
()
key
=
_normalize_parent_ref_key
(
title
)
summary
=
_extract_
subsection
_summary
(
body
)
summary
=
_extract_
topic
_summary
(
body
)
if
not
key
or
not
summary
:
continue
summaries
.
setdefault
(
key
,
summary
)
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
],
...
...
@@ -232,10 +209,10 @@ def _normalize_topic_ref_entries(
if
isinstance
(
raw_entry
,
str
):
normalized_key
=
_normalize_parent_ref_key
(
raw_entry
)
label
=
_format_
subsection
_label
(
raw_entry
)
label
=
_format_
topic
_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
()
raw_key
=
str
(
raw_entry
.
get
(
"key"
)
or
raw_entry
.
get
(
"topic_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
()
...
...
@@ -273,9 +250,9 @@ def _resolve_task_topic_options(
summaries
=
topic_summaries
if
topic_summaries
is
not
None
else
load_topic_summaries
()
raw_entries
:
list
[
Any
]
=
[]
legacy_
topics
=
task_file
.
get
(
"
subsection
s"
,
[])
if
isinstance
(
legacy_
topics
,
list
):
raw_entries
.
extend
(
legacy_
topics
)
topics
=
task_file
.
get
(
"
topic
s"
,
[])
if
isinstance
(
topics
,
list
):
raw_entries
.
extend
(
topics
)
explicit_refs
=
task_file
.
get
(
"topic_refs"
,
[])
if
isinstance
(
explicit_refs
,
list
):
raw_entries
.
extend
(
explicit_refs
)
...
...
@@ -303,25 +280,7 @@ def _resolve_task_topic_refs(
return
sorted
(
refs
)
def
_resolve_task_subsection_refs
(
task_file
:
dict
[
str
,
Any
],
subsection_map
:
dict
[
str
,
tuple
[
int
,
int
,
int
]]
|
None
=
None
,
)
->
list
[
tuple
[
int
,
int
,
int
]]:
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
()}
refs
=
_resolve_task_topic_refs
(
task_file
,
topic_map
=
mapping
)
normalized
:
list
[
tuple
[
int
,
int
,
int
]]
=
[]
for
chap
,
sec
,
sub
,
subsub
in
refs
:
if
sub
<=
0
or
subsub
>
0
:
continue
normalized
.
append
((
chap
,
sec
,
sub
))
return
sorted
(
normalized
)
def
_format_subsection_label
(
value
:
str
)
->
str
:
def
_format_topic_label
(
value
:
str
)
->
str
:
cleaned
=
re
.
sub
(
r
"[-_]+"
,
" "
,
value
.
strip
())
cleaned
=
re
.
sub
(
r
"\s+"
,
" "
,
cleaned
).
strip
()
if
not
cleaned
:
...
...
@@ -337,7 +296,7 @@ def build_topic_catalog(path: Path = TOPIC_MAP_PATH) -> list[dict[str, Any]]:
response
.
append
(
{
"topic_key"
:
key
,
"label"
:
_format_
subsection
_label
(
key
),
"label"
:
_format_
topic
_label
(
key
),
"level"
:
_parent_ref_level
(
ref
),
"refs"
:
[
_parent_ref_to_list
(
ref
)],
"summary"
:
topic_summaries
.
get
(
key
,
""
),
...
...
@@ -346,54 +305,6 @@ def build_topic_catalog(path: Path = TOPIC_MAP_PATH) -> list[dict[str, Any]]:
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
not
refs
:
continue
ref
=
refs
[
0
]
response
.
append
(
{
"subsection_key"
:
item
[
"topic_key"
],
"label"
:
item
[
"label"
],
"level"
:
item
[
"level"
],
"refs"
:
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
]),
int
(
ref
[
3
])]],
"summary"
:
item
[
"summary"
],
}
)
return
response
def
_resolve_task_subsection_options
(
task_file
:
dict
[
str
,
Any
],
subsection_map
:
dict
[
str
,
tuple
[
int
,
int
,
int
]]
|
None
=
None
,
)
->
list
[
dict
[
str
,
Any
]]:
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
()}
options
=
_resolve_task_topic_options
(
task_file
,
topic_map
=
mapping
)
response
:
list
[
dict
[
str
,
Any
]]
=
[]
for
option
in
options
:
refs
=
option
.
get
(
"refs"
,
[])
if
not
refs
:
continue
ref
=
refs
[
0
]
response
.
append
(
{
"subsection_key"
:
option
[
"topic_key"
],
"label"
:
option
[
"label"
],
"level"
:
option
[
"level"
],
"refs"
:
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
]),
int
(
ref
[
3
])]],
"summary"
:
option
.
get
(
"summary"
,
""
),
}
)
return
response
def
_match_score
(
query_text
:
str
,
candidate_text
:
str
)
->
int
:
query_tokens
=
_tokenize
(
query_text
)
if
not
query_tokens
:
...
...
@@ -501,7 +412,7 @@ def _normalize_yaml_task_file(content: Any, path: Path) -> dict[str, Any] | None
intro
=
str
(
content
.
get
(
"description"
,
""
)).
strip
()
file_id
=
str
(
content
.
get
(
"id"
,
""
)).
strip
()
or
path
.
stem
slug
=
str
(
content
.
get
(
"slug"
,
""
)).
strip
()
or
_slugify
(
title
or
file_id
)
subsection
s
=
_normalize_exercise_topics
(
content
.
get
(
"exercise_topic"
,
[]))
topic
s
=
_normalize_exercise_topics
(
content
.
get
(
"exercise_topic"
,
[]))
topic_refs_raw
=
content
.
get
(
"topic_refs"
,
[])
if
not
title
or
not
file_id
or
not
slug
:
...
...
@@ -520,7 +431,7 @@ def _normalize_yaml_task_file(content: Any, path: Path) -> dict[str, Any] | None
"title"
:
title
,
"intro"
:
intro
,
"slug"
:
slug
,
"
subsections"
:
subsection
s
,
"
topics"
:
topic
s
,
"topic_refs"
:
topic_refs_raw
if
isinstance
(
topic_refs_raw
,
list
)
else
[],
"tasks"
:
tasks
,
}
...
...
@@ -629,19 +540,17 @@ def set_selected_task(
sheet
[
"task_id"
]
=
str
(
task_entry
.
get
(
"id"
,
""
)).
zfill
(
2
)
refs
=
_resolve_task_topic_refs
(
task_file
)
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_label"
,
None
)
sheet
.
pop
(
"selected_subsection_parent_refs"
,
None
)
sheet
.
pop
(
"selected_subsection_refs"
,
None
)
sheet
.
pop
(
"selected_topic_key"
,
None
)
sheet
.
pop
(
"selected_topic_label"
,
None
)
sheet
.
pop
(
"selected_topic_parent_refs"
,
None
)
def
set_selected_
subsection
(
def
set_selected_
topic
(
sheet
:
dict
[
str
,
Any
],
task_file
:
dict
[
str
,
Any
],
subsection
_option
:
dict
[
str
,
Any
],
topic
_option
:
dict
[
str
,
Any
],
)
->
None
:
refs_raw
=
subsection
_option
.
get
(
"refs"
,
[])
refs_raw
=
topic
_option
.
get
(
"refs"
,
[])
refs
:
list
[
ParentRef
]
=
[]
if
isinstance
(
refs_raw
,
list
):
for
item
in
refs_raw
:
...
...
@@ -651,21 +560,15 @@ def set_selected_subsection(
if
not
refs
:
return
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
()
key
=
str
(
topic
_option
.
get
(
"topic_key"
)
or
""
).
strip
()
sheet
[
"selected_
topic
_key"
]
=
key
sheet
[
"selected_
topic
_label"
]
=
str
(
topic
_option
.
get
(
"label"
,
""
)).
strip
()
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
[
"selected_topic_parent_refs"
]
=
[
_parent_ref_to_list
(
ref
)
for
ref
in
parent_refs
]
sheet
.
pop
(
"task_file_id"
,
None
)
sheet
.
pop
(
"task_id"
,
None
)
sheet
.
pop
(
"task_parent_refs"
,
None
)
sheet
.
pop
(
"task_subsection_refs"
,
None
)
sheet
.
pop
(
"task"
,
None
)
sheet
.
pop
(
"hints"
,
None
)
sheet
.
pop
(
"solution"
,
None
)
...
...
@@ -688,10 +591,10 @@ def select_task_by_ids(
return
True
def
select_
subsection
_by_ids
(
def
select_
topic
_by_ids
(
sheet
:
dict
[
str
,
Any
],
file_id
:
str
,
subsection
_key
:
str
,
topic
_key
:
str
,
task_files
:
list
[
dict
[
str
,
Any
]]
|
None
=
None
,
)
->
bool
:
catalog
=
task_files
if
task_files
is
not
None
else
load_cached_task_files
()
...
...
@@ -699,48 +602,38 @@ def select_subsection_by_ids(
if
not
task_file
:
return
False
options
=
_resolve_task_
subsection
_options
(
task_file
)
normalized_key
=
_normalize_
subsection_key
(
subsection
_key
)
options
=
_resolve_task_
topic
_options
(
task_file
)
normalized_key
=
_normalize_
topic_key
(
topic
_key
)
if
not
normalized_key
:
return
False
for
option
in
options
:
if
str
(
option
.
get
(
"
subsection
_key"
,
""
)).
strip
()
==
normalized_key
:
set_selected_
subsection
(
sheet
,
task_file
,
option
)
if
str
(
option
.
get
(
"
topic
_key"
,
""
)).
strip
()
==
normalized_key
:
set_selected_
topic
(
sheet
,
task_file
,
option
)
return
True
return
False
def
select_
subsection
_by_key
(
def
select_
topic
_by_key
(
sheet
:
dict
[
str
,
Any
],
subsection
_key
:
str
,
subsection
_map
:
dict
[
str
,
tuple
[
int
,
int
,
int
]
]
|
None
=
None
,
topic
_key
:
str
,
topic
_map
:
dict
[
str
,
ParentRef
]
|
None
=
None
,
)
->
bool
:
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
)
mapping
=
topic_map
if
topic_map
is
not
None
else
load_topic_map
()
normalized_key
=
_normalize_topic_key
(
topic_key
)
if
not
normalized_key
:
return
False
ref
=
mapping
.
get
(
normalized_key
)
if
ref
is
None
:
return
False
sheet
[
"selected_
subsection
_key"
]
=
normalized_key
sheet
[
"selected_
subsection
_label"
]
=
_format_
subsection
_label
(
normalized_key
)
sheet
[
"selected_
topic
_key"
]
=
normalized_key
sheet
[
"selected_
topic
_label"
]
=
_format_
topic
_label
(
normalized_key
)
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
[
"selected_topic_parent_refs"
]
=
[
_parent_ref_to_list
(
normalized_ref
)]
sheet
.
pop
(
"task_file_id"
,
None
)
sheet
.
pop
(
"task_id"
,
None
)
sheet
.
pop
(
"task_parent_refs"
,
None
)
sheet
.
pop
(
"task_subsection_refs"
,
None
)
sheet
.
pop
(
"task"
,
None
)
sheet
.
pop
(
"hints"
,
None
)
sheet
.
pop
(
"solution"
,
None
)
...
...
@@ -753,25 +646,13 @@ def get_selected_task_ids(sheet: dict[str, Any]) -> tuple[str | None, str | None
return
(
file_id
or
None
,
task_id
or
None
)
def
get_selected_subsection_ids
(
sheet
:
dict
[
str
,
Any
])
->
tuple
[
str
|
None
,
str
|
None
]:
subsection_key
=
str
(
sheet
.
get
(
"selected_subsection_key"
,
""
)).
strip
()
return
(
None
,
subsection_key
or
None
)
def
get_selected_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_subsection_parent_refs
(
sheet
):
if
sub
<=
0
or
subsub
>
0
:
continue
refs
.
append
((
chap
,
sec
,
sub
))
return
sorted
(
set
(
refs
))
def
get_selected_topic_ids
(
sheet
:
dict
[
str
,
Any
])
->
tuple
[
str
|
None
,
str
|
None
]:
topic_key
=
str
(
sheet
.
get
(
"selected_topic_key"
,
""
)).
strip
()
return
(
None
,
topic_key
or
None
)
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
):
refs_raw
=
sheet
.
get
(
"selected_subsection_refs"
,
[])
def
get_selected_topic_parent_refs
(
sheet
:
dict
[
str
,
Any
])
->
list
[
ParentRef
]:
refs_raw
=
sheet
.
get
(
"selected_topic_parent_refs"
,
[])
refs
:
set
[
ParentRef
]
=
set
()
for
item
in
refs_raw
:
parsed
=
_parse_parent_ref
(
item
)
...
...
@@ -793,15 +674,6 @@ def get_selected_task_parent_refs(sheet: dict[str, Any]) -> list[ParentRef]:
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
refs
.
append
((
chap
,
sec
,
sub
))
return
sorted
(
refs
)
def
select_task_for_context
(
sheet
:
dict
[
str
,
Any
],
query_text
:
str
,
...
...
@@ -880,7 +752,7 @@ def build_task_catalog(task_files: list[dict[str, Any]] | None = None) -> list[d
title
=
str
(
task_file
.
get
(
"title"
,
""
)).
strip
()
intro
=
str
(
task_file
.
get
(
"intro"
,
""
)).
strip
()
topic_options
=
_resolve_task_topic_options
(
task_file
)
topics_raw
=
task_file
.
get
(
"
subsection
s"
,
[])
topics_raw
=
task_file
.
get
(
"
topic
s"
,
[])
if
isinstance
(
topics_raw
,
list
)
and
topics_raw
:
topics
=
[
str
(
item
).
strip
()
for
item
in
topics_raw
if
str
(
item
).
strip
()]
else
:
...
...
math-tutor/backend/test/task_catalog_socratic_test.py
View file @
134a9c5c
...
...
@@ -38,7 +38,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"description"
:
"Pfeildiagramm mit einer fehlenden Abbildung."
,
}
],
"hints"
:
[
"Pr
ü
fe jedes Element."
],
"hints"
:
[
"Pr
ü
fe jedes Element."
],
"solution"
:
"Keine Funktion."
,
}
...
...
@@ -47,7 +47,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
self
.
assertIn
(
"Grundlagen von Funktionen"
,
task_text
)
self
.
assertIn
(
"Ordnen Sie zu."
,
task_text
)
self
.
assertIn
(
"Bildbeschreibung: Pfeildiagramm mit einer fehlenden Abbildung."
,
task_text
)
self
.
assertEqual
(
hints
,
[
"Pr
ü
fe jedes Element."
])
self
.
assertEqual
(
hints
,
[
"Pr
ü
fe jedes Element."
])
self
.
assertEqual
(
solution
,
"Keine Funktion."
)
self
.
assertEqual
(
images
,
...
...
@@ -65,7 +65,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"_file_id"
:
"analysis_1"
,
"title"
:
"Analysis"
,
"intro"
:
"Intro"
,
"
subsection
s"
:
[
"quadratische_gleichungen"
],
"
topic
s"
:
[
"quadratische_gleichungen"
],
"topic_refs"
:
[],
"tasks"
:
[
{
...
...
@@ -105,7 +105,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"_file_id"
:
"analysis_1"
,
"title"
:
"Analysis"
,
"intro"
:
"Intro"
,
"
subsection
s"
:
[
"quadratische_gleichungen"
],
"
topic
s"
:
[
"quadratische_gleichungen"
],
"topic_refs"
:
[],
"tasks"
:
[
{
...
...
@@ -163,7 +163,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
self
.
assertEqual
(
normalized
[
"hints"
],
[
"Nutze den Graphen."
])
self
.
assertEqual
(
normalized
[
"images"
],
[])
def
test_socratic_build_
subsection
_catalog_returns_response_shape
(
self
)
->
None
:
def
test_socratic_build_
topic
_catalog_returns_response_shape
(
self
)
->
None
:
with
patch
(
"app.deterministic_services.socratic_oranisator.load_initial_prompt_items"
,
return_value
=
{
...
...
@@ -185,20 +185,20 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"quadratische gleichungen"
:
"Quadratische summary text"
,
},
):
catalog
=
socratic_oranisator
.
build_
subsection
_catalog
()
catalog
=
socratic_oranisator
.
build_
topic
_catalog
()
self
.
assertEqual
(
catalog
,
[
{
"
subsection
_key"
:
"mengen"
,
"
topic
_key"
:
"mengen"
,
"label"
:
"Mengen"
,
"level"
:
"section"
,
"refs"
:
[[
1
,
1
,
0
,
0
]],
"summary"
:
"Mengen summary text"
,
},
{
"
subsection
_key"
:
"quadratische gleichungen"
,
"
topic
_key"
:
"quadratische gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"level"
:
"subsection"
,
"refs"
:
[[
1
,
3
,
3
,
0
]],
...
...
@@ -207,12 +207,12 @@ class TaskCatalogSocraticTest(unittest.TestCase):
],
)
def
test_build_
subsection
_catalog_includes_summary
(
self
)
->
None
:
def
test_build_
topic
_catalog_includes_summary
(
self
)
->
None
:
with
patch
(
"app.deterministic_services.task_catalog.load_topic_map"
,
return_value
=
{
"quadratische gleichungen"
:
(
1
,
3
,
3
,
0
),
"mengen"
:
(
1
,
1
,
1
,
0
),
"mengen"
:
(
1
,
1
,
0
,
0
),
},
),
patch
(
"app.deterministic_services.task_catalog.load_topic_summaries"
,
...
...
@@ -221,20 +221,20 @@ class TaskCatalogSocraticTest(unittest.TestCase):
"quadratische gleichungen"
:
"Quadratische summary text"
,
},
):
catalog
=
task_catalog
.
build_
subsection
_catalog
()
catalog
=
task_catalog
.
build_
topic
_catalog
()
self
.
assertEqual
(
catalog
,
[
{
"
subsection
_key"
:
"mengen"
,
"
topic
_key"
:
"mengen"
,
"label"
:
"Mengen"
,
"level"
:
"section"
,
"refs"
:
[[
1
,
1
,
0
,
0
]],
"summary"
:
"Mengen summary text"
,
},
{
"
subsection
_key"
:
"quadratische gleichungen"
,
"
topic
_key"
:
"quadratische gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"level"
:
"subsection"
,
"refs"
:
[[
1
,
3
,
3
,
0
]],
...
...
@@ -288,7 +288,7 @@ class TaskCatalogSocraticTest(unittest.TestCase):
],
)
def
test_load_
subsection
_summaries_extracts_body
(
self
)
->
None
:
def
test_load_
topic
_summaries_extracts_body
(
self
)
->
None
:
temp_dir
=
Path
(
__file__
).
resolve
().
parent
/
"_tmp_subsection_summaries"
summary_root
=
temp_dir
/
"child_lvl"
/
"subsections"
summary_root
.
mkdir
(
parents
=
True
,
exist_ok
=
True
)
...
...
@@ -315,7 +315,7 @@ Zweite Zeile.
)
try
:
summaries
=
task_catalog
.
load_
subsection
_summaries
(
temp_dir
)
summaries
=
task_catalog
.
load_
topic
_summaries
(
temp_dir
)
finally
:
if
md_path
.
exists
():
md_path
.
unlink
()
...
...
@@ -331,12 +331,12 @@ Zweite Zeile.
self
.
assertTrue
(
summaries
[
"mengen"
].
startswith
(
"### **Zusammenfassung: Mengen**"
))
self
.
assertNotIn
(
"Child-Chunks"
,
summaries
[
"mengen"
])
def
test_select_
subsection
_by_ids_sets_sheet_fields
(
self
)
->
None
:
def
test_select_
topic
_by_ids_sets_sheet_fields
(
self
)
->
None
:
task_files
=
[
{
"_file_id"
:
"analysis_1"
,
"title"
:
"Analysis"
,
"
subsection
s"
:
[
"quadratische_gleichungen"
],
"
topic
s"
:
[
"quadratische_gleichungen"
],
"tasks"
:
[],
}
]
...
...
@@ -346,7 +346,7 @@ Zweite Zeile.
"app.deterministic_services.task_catalog.load_topic_map"
,
return_value
=
{
"quadratische gleichungen"
:
(
1
,
3
,
3
,
1
)},
):
updated
=
task_catalog
.
select_
subsection
_by_ids
(
updated
=
task_catalog
.
select_
topic
_by_ids
(
sheet
,
"analysis_1"
,
"quadratische_gleichungen"
,
...
...
@@ -354,30 +354,28 @@ Zweite Zeile.
)
self
.
assertTrue
(
updated
)
self
.
assertEqual
(
sheet
[
"selected_subsection_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
sheet
[
"selected_subsection_parent_refs"
],
[[
1
,
3
,
3
,
1
]])
self
.
assertEqual
(
sheet
[
"selected_subsection_refs"
],
[])
self
.
assertEqual
(
sheet
[
"selected_topic_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
sheet
[
"selected_topic_parent_refs"
],
[[
1
,
3
,
3
,
1
]])
def
test_select_
subsection
_by_key_sets_sheet_fields
(
self
)
->
None
:
def
test_select_
topic
_by_key_sets_sheet_fields
(
self
)
->
None
:
sheet
:
dict
[
str
,
object
]
=
{}
with
patch
(
"app.deterministic_services.task_catalog.load_topic_map"
,
return_value
=
{
"quadratische gleichungen"
:
(
1
,
3
,
3
,
1
)},
):
updated
=
task_catalog
.
select_
subsection
_by_key
(
sheet
,
"quadratische_gleichungen"
)
updated
=
task_catalog
.
select_
topic
_by_key
(
sheet
,
"quadratische_gleichungen"
)
self
.
assertTrue
(
updated
)
self
.
assertEqual
(
sheet
[
"selected_subsection_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
sheet
[
"selected_subsection_parent_refs"
],
[[
1
,
3
,
3
,
1
]])
self
.
assertEqual
(
sheet
[
"selected_subsection_refs"
],
[])
self
.
assertEqual
(
sheet
[
"selected_topic_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
sheet
[
"selected_topic_parent_refs"
],
[[
1
,
3
,
3
,
1
]])
def
test_get_selected_
subsection
_parent_refs_reads_normalized_refs
(
self
)
->
None
:
def
test_get_selected_
topic
_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
]],
"selected_
topic
_parent_refs"
:
[[
1
,
1
,
0
,
0
],
[
1
,
3
,
2
,
1
],
[
1
,
3
,
2
,
1
]],
}
refs
=
task_catalog
.
get_selected_
subsection
_parent_refs
(
sheet
)
refs
=
task_catalog
.
get_selected_
topic
_parent_refs
(
sheet
)
self
.
assertEqual
(
refs
,
[(
1
,
1
,
0
,
0
),
(
1
,
3
,
2
,
1
)])
...
...
@@ -386,7 +384,7 @@ Zweite Zeile.
task_file
=
{
"_file_id"
:
"analysis_1"
,
"title"
:
"Analysis"
,
"
subsection
s"
:
[
"mengen"
],
"
topic
s"
:
[
"mengen"
],
"topic_refs"
:
[
{
"chapter_index"
:
1
,
...
...
@@ -496,10 +494,10 @@ class TaskApiSocraticTest(unittest.TestCase):
self
.
assertEqual
(
response
.
status_code
,
404
)
def
test_list_socratic_
subsection
s_returns_catalog
(
self
)
->
None
:
subsection
s
=
[
def
test_list_socratic_
topic
s_returns_catalog
(
self
)
->
None
:
topic
s
=
[
{
"
subsection
_key"
:
"quadratische gleichungen"
,
"
topic
_key"
:
"quadratische gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"level"
:
"subsubsection"
,
"refs"
:
[[
1
,
3
,
3
,
1
]],
...
...
@@ -508,18 +506,18 @@ class TaskApiSocraticTest(unittest.TestCase):
]
with
patch
(
"app.api.tasks.config.get_orchestrator"
,
return_value
=
"socratic"
),
patch
(
"app.api.tasks.socratic_oranisator.build_
subsection
_catalog"
,
return_value
=
subsection
s
,
"app.api.tasks.socratic_oranisator.build_
topic
_catalog"
,
return_value
=
topic
s
,
):
response
=
self
.
client
.
get
(
"/api/tasks/socratic-
subsection
s"
)
response
=
self
.
client
.
get
(
"/api/tasks/socratic-
topic
s"
)
self
.
assertEqual
(
response
.
status_code
,
200
)
body
=
response
.
json
()
self
.
assertEqual
(
body
[
"orchestrator"
],
"socratic"
)
self
.
assertTrue
(
body
[
"enabled"
])
self
.
assertEqual
(
body
[
"
subsections"
],
subsection
s
)
self
.
assertEqual
(
body
[
"
topics"
],
topic
s
)
def
test_select_
subsection
_endpoint_returns_selected_key
(
self
)
->
None
:
def
test_select_
topic
_endpoint_returns_selected_key
(
self
)
->
None
:
sheet
:
dict
[
str
,
object
]
=
{}
with
patch
(
"app.api.tasks.context_store.get_chat_id"
,
return_value
=
"chat-1"
),
patch
(
...
...
@@ -529,25 +527,25 @@ class TaskApiSocraticTest(unittest.TestCase):
"app.api.tasks.context_store.context_store_new.init_sheet"
,
return_value
=
sheet
,
),
patch
(
"app.api.tasks.task_catalog.select_
subsection
_by_key"
,
"app.api.tasks.task_catalog.select_
topic
_by_key"
,
return_value
=
True
,
)
as
select_mock
,
patch
(
"app.api.tasks.context_store.save_sheet"
),
patch
(
"app.api.tasks.task_catalog.get_selected_
subsection
_ids"
,
"app.api.tasks.task_catalog.get_selected_
topic
_ids"
,
return_value
=
(
None
,
"quadratische gleichungen"
),
):
response
=
self
.
client
.
post
(
"/api/tasks/select-
subsection
"
,
"/api/tasks/select-
topic
"
,
json
=
{
"draft"
:
"chat-1"
,
"
subsection
_key"
:
"quadratische_gleichungen"
,
"
topic
_key"
:
"quadratische_gleichungen"
,
},
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
json
()[
"
subsection
_key"
],
"quadratische gleichungen"
)
select_mock
.
assert_called_once_with
(
sheet
,
subsection
_key
=
"quadratische_gleichungen"
)
self
.
assertEqual
(
response
.
json
()[
"
topic
_key"
],
"quadratische gleichungen"
)
select_mock
.
assert_called_once_with
(
sheet
,
topic
_key
=
"quadratische_gleichungen"
)
class
ChatBootstrapSocraticTest
(
unittest
.
TestCase
):
...
...
@@ -585,28 +583,28 @@ class ChatBootstrapSocraticTest(unittest.TestCase):
"/api/chat/bootstrap-socratic"
,
json
=
{
"draft"
:
"chat-1"
,
"
subsection
_key"
:
"quadratische_gleichungen"
,
"
topic
_key"
:
"quadratische_gleichungen"
,
},
)
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertEqual
(
response
.
json
()[
"reply"
],
"Was sind die Themen dieses Abschnitts?"
)
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
_parent_refs"
],
[[
1
,
3
,
3
,
1
]])
self
.
assertEqual
(
sheet
[
"selected_
topic
_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
sheet
[
"selected_
topic
_parent_refs"
],
[[
1
,
3
,
3
,
1
]])
self
.
assertEqual
(
sheet
[
"history"
],
[{
"role"
:
"assistant"
,
"content"
:
"Was sind die Themen dieses Abschnitts?"
}])
class
SessionStoreSocraticTest
(
unittest
.
TestCase
):
def
test_load_archive_restores_selected_
subsection
(
self
)
->
None
:
def
test_load_archive_restores_selected_
topic
(
self
)
->
None
:
record
=
{
"chat_id"
:
"chat-1"
,
"saved_at"
:
"2026-03-31T10:00:00Z"
,
"orchestrator"
:
"socratic"
,
"history"
:
[],
"selected_task"
:
None
,
"selected_
subsection
"
:
{
"
subsection
_key"
:
"quadratische gleichungen"
,
"selected_
topic
"
:
{
"
topic
_key"
:
"quadratische gleichungen"
,
},
}
...
...
@@ -624,7 +622,7 @@ class SessionStoreSocraticTest(unittest.TestCase):
temp_dir
.
rmdir
()
self
.
assertIsNotNone
(
archive
)
self
.
assertEqual
(
archive
[
"selected_
subsection
"
],
record
[
"selected_
subsection
"
])
self
.
assertEqual
(
archive
[
"selected_
topic
"
],
record
[
"selected_
topic
"
])
if
__name__
==
"__main__"
:
...
...
math-tutor/backend/test/task_yaml_migration_test.py
View file @
134a9c5c
...
...
@@ -59,7 +59,7 @@ class TaskYamlMigrationTest(unittest.TestCase):
self
.
assertEqual
(
loaded
[
0
][
"_file_id"
],
"analysis_1"
)
self
.
assertEqual
(
loaded
[
0
][
"title"
],
"Analysis"
)
self
.
assertEqual
(
loaded
[
0
][
"intro"
],
"Intro"
)
self
.
assertEqual
(
loaded
[
0
][
"
subsection
s"
],
[
"quadratische-gleichungen"
])
self
.
assertEqual
(
loaded
[
0
][
"
topic
s"
],
[
"quadratische-gleichungen"
])
self
.
assertEqual
(
loaded
[
0
][
"tasks"
][
0
][
"id"
],
"01"
)
self
.
assertEqual
(
loaded
[
0
][
"tasks"
][
0
][
"statement"
],
"Bestimme f(x)."
)
self
.
assertEqual
(
loaded
[
0
][
"tasks"
][
0
][
"hints"
],
[
"Nutze die Ableitung."
])
...
...
@@ -124,7 +124,7 @@ class TaskYamlMigrationTest(unittest.TestCase):
"_file_id"
:
"analysis_1"
,
"title"
:
"Analysis"
,
"intro"
:
"Intro"
,
"
subsection
s"
:
[
"quadratische-gleichungen"
],
"
topic
s"
:
[
"quadratische-gleichungen"
],
"tasks"
:
[
{
"id"
:
"01"
,
...
...
math-tutor/frontend/src/api/taskApi.ts
View file @
134a9c5c
...
...
@@ -13,10 +13,10 @@ export type TaskFile = {
tasks
:
TaskListItem
[];
};
export
type
Subsection
Option
=
{
subsection
_key
:
string
;
export
type
Topic
Option
=
{
topic
_key
:
string
;
label
:
string
;
refs
:
[
number
,
number
,
number
][];
refs
:
[
number
,
number
,
number
,
number
][];
summary
:
string
;
};
...
...
@@ -37,7 +37,7 @@ export type TaskDetailsResponse = {
export
type
SocraticResponse
=
{
orchestrator
:
string
;
enabled
:
boolean
;
subsections
:
Subsection
Option
[];
topics
:
Topic
Option
[];
};
export
type
SelectedTaskRef
=
{
...
...
@@ -45,8 +45,8 @@ export type SelectedTaskRef = {
taskId
:
string
;
};
export
type
Selected
Subsection
Ref
=
{
subsection
Key
:
string
;
export
type
Selected
Topic
Ref
=
{
topic
Key
:
string
;
};
export
type
SelectTaskResponse
=
{
...
...
@@ -55,9 +55,9 @@ export type SelectTaskResponse = {
task_id
:
string
;
};
export
type
Select
Subsection
Response
=
{
export
type
Select
Topic
Response
=
{
status
:
string
;
subsection
_key
:
string
;
topic
_key
:
string
;
};
export
async
function
fetchTasks
():
Promise
<
TasksResponse
>
{
...
...
@@ -81,10 +81,10 @@ export async function fetchTaskDetails(input: {
return
response
.
json
();
}
export
async
function
fetchSocratic
Subsection
s
():
Promise
<
SocraticResponse
>
{
const
response
=
await
fetch
(
"
/api/tasks/socratic-
subsection
s
"
);
export
async
function
fetchSocratic
Topic
s
():
Promise
<
SocraticResponse
>
{
const
response
=
await
fetch
(
"
/api/tasks/socratic-
topic
s
"
);
if
(
!
response
.
ok
)
{
throw
new
Error
(
`Socratic
subsection
s failed:
${
response
.
status
}
`
);
throw
new
Error
(
`Socratic
topic
s failed:
${
response
.
status
}
`
);
}
return
response
.
json
();
}
...
...
@@ -110,21 +110,21 @@ export async function selectTask(input: {
return
response
.
json
();
}
export
async
function
select
Subsection
(
input
:
{
export
async
function
select
Topic
(
input
:
{
draft
:
string
;
subsection
Key
:
string
;
}):
Promise
<
Select
Subsection
Response
>
{
const
response
=
await
fetch
(
"
/api/tasks/select-
subsection
"
,
{
topic
Key
:
string
;
}):
Promise
<
Select
Topic
Response
>
{
const
response
=
await
fetch
(
"
/api/tasks/select-
topic
"
,
{
method
:
"
POST
"
,
headers
:
{
"
Content-Type
"
:
"
application/json
"
},
body
:
JSON
.
stringify
({
draft
:
input
.
draft
,
subsection
_key
:
input
.
subsection
Key
,
topic
_key
:
input
.
topic
Key
,
}),
});
if
(
!
response
.
ok
)
{
throw
new
Error
(
`
Subsection
selection failed:
${
response
.
status
}
`
);
throw
new
Error
(
`
Topic
selection failed:
${
response
.
status
}
`
);
}
return
response
.
json
();
}
math-tutor/frontend/src/pages/ChatPage.tsx
View file @
134a9c5c
...
...
@@ -10,11 +10,11 @@ import TaskPanel from "../components/Task/TaskPanel";
import
type
{
ChatMessage
}
from
"
../components/Chat/MessageList
"
;
import
type
{
RetrievedDoc
}
from
"
../components/Retrieval/DocPanel
"
;
import
type
{
OrchestratorName
}
from
"
../api/orchestratorApi
"
;
import
{
select
Subsection
,
selectT
ask
}
from
"
../api/taskApi
"
;
import
{
select
Task
,
selectT
opic
}
from
"
../api/taskApi
"
;
import
{
t
}
from
"
../i18n
"
;
import
{
createSessionId
,
useTutorSession
}
from
"
../state/tutorSession
"
;
import
{
getSelectionRouteForOrchestrator
,
isSocraticOrchestrator
}
from
"
../utils/orchestratorRoutes
"
;
import
{
normalizeSubsectionKey
}
from
"
../utils/subsectionKey
"
;
import
{
normalizeSubsectionKey
as
normalizeTopicKey
}
from
"
../utils/subsectionKey
"
;
import
sumintLogo
from
"
../../SuMINT-Logo.png
"
;
const
initialMessages
:
ChatMessage
[]
=
[];
...
...
@@ -51,8 +51,8 @@ type ArchivedChatDetail = {
file_id
:
string
;
task_id
:
string
;
}
|
null
;
selected_
subsection
?:
{
subsection
_key
:
string
;
selected_
topic
?:
{
topic
_key
:
string
;
}
|
null
;
};
...
...
@@ -191,8 +191,8 @@ export default function ChatPage() {
selectedTaskRef
,
selectedTask
,
selectedTaskFile
,
selected
Subsection
Ref
,
selected
Subsection
,
selected
Topic
Ref
,
selected
Topic
,
selectedOrchestrator
,
availableOrchestrators
,
setSelectedOrchestrator
,
...
...
@@ -202,7 +202,7 @@ export default function ChatPage() {
lockTask
,
unlockTask
,
setTaskRef
,
set
Subsection
Ref
,
set
Topic
Ref
,
resetForNewChat
,
}
=
useTutorSession
();
...
...
@@ -239,31 +239,33 @@ export default function ChatPage() {
.
toLowerCase
();
const
fileId
=
String
(
searchParams
.
get
(
"
file_id
"
)
||
""
).
trim
();
const
rawTaskId
=
String
(
searchParams
.
get
(
"
task_id
"
)
||
""
).
trim
();
const
rawSubsectionKey
=
String
(
searchParams
.
get
(
"
subsection_key
"
)
||
""
).
trim
();
const
rawTopicKey
=
String
(
searchParams
.
get
(
"
topic_key
"
)
||
searchParams
.
get
(
"
subsection_key
"
)
||
""
).
trim
();
const
taskId
=
/^
\d{1,2}
$/
.
test
(
rawTaskId
)
&&
rawTaskId
.
length
<
2
?
rawTaskId
.
padStart
(
2
,
"
0
"
)
:
rawTaskId
;
const
subsectionKey
=
rawSubsectionKey
?
normalizeSubsectionKey
(
rawSubsection
Key
)
:
""
;
const
topicKey
=
rawTopicKey
?
normalizeTopicKey
(
rawTopic
Key
)
:
""
;
const
hasFileId
=
Boolean
(
fileId
);
const
hasTaskId
=
Boolean
(
taskId
);
const
has
Subsection
Key
=
Boolean
(
subsection
Key
);
const
has
Topic
Key
=
Boolean
(
topic
Key
);
const
isTaskOrchestrator
=
isTaskCoupledOrchestrator
(
orchestrator
);
const
isSocratic
=
isSocraticOrchestrator
(
orchestrator
as
OrchestratorName
);
const
hasAnyTaskParam
=
hasFileId
||
hasTaskId
||
has
Subsection
Key
;
const
hasAnyTaskParam
=
hasFileId
||
hasTaskId
||
has
Topic
Key
;
const
hasRequiredTaskParams
=
hasFileId
&&
hasTaskId
;
const
hasRequired
Subsection
Params
=
has
Subsection
Key
;
const
key
=
`
${
orchestrator
}
|
${
fileId
}
|
${
taskId
}
|
${
subsection
Key
}
`
;
const
hasRequired
Topic
Params
=
has
Topic
Key
;
const
key
=
`
${
orchestrator
}
|
${
fileId
}
|
${
taskId
}
|
${
topic
Key
}
`
;
return
{
fileId
,
taskId
,
subsection
Key
,
topic
Key
,
isTaskOrchestrator
,
isSocratic
,
hasAnyTaskParam
,
hasRequiredTaskParams
,
hasRequired
Subsection
Params
,
hasRequired
Topic
Params
,
key
,
};
},
[
searchParams
]);
...
...
@@ -286,13 +288,13 @@ export default function ChatPage() {
if
(
(
deepLinkTarget
.
isTaskOrchestrator
&&
!
deepLinkTarget
.
hasRequiredTaskParams
)
||
(
deepLinkTarget
.
isSocratic
&&
!
deepLinkTarget
.
hasRequired
Subsection
Params
)
(
deepLinkTarget
.
isSocratic
&&
!
deepLinkTarget
.
hasRequired
Topic
Params
)
)
{
setDeepLinkError
(
deepLinkTarget
.
isSocratic
?
t
(
"
deepLinkInvalidSubsection
"
)
:
t
(
"
deepLinkInvalidTask
"
)
);
setTaskRef
(
null
);
set
Subsection
Ref
(
null
);
set
Topic
Ref
(
null
);
unlockTask
();
navigate
(
targetRoute
,
{
replace
:
true
});
return
;
...
...
@@ -301,13 +303,13 @@ export default function ChatPage() {
if
(
deepLinkTarget
.
isSocratic
)
{
setSelectedOrchestrator
(
"
socratic
"
);
setTaskRef
(
null
);
set
SubsectionRef
({
subsection
Key
:
deepLinkTarget
.
subsection
Key
});
set
TopicRef
({
topic
Key
:
deepLinkTarget
.
topic
Key
});
}
else
{
const
selectedFile
=
taskFiles
.
find
((
file
)
=>
file
.
file_id
===
deepLinkTarget
.
fileId
);
if
(
!
selectedFile
)
{
setDeepLinkError
(
t
(
"
deepLinkInvalidTask
"
));
setTaskRef
(
null
);
set
Subsection
Ref
(
null
);
set
Topic
Ref
(
null
);
unlockTask
();
navigate
(
targetRoute
,
{
replace
:
true
});
return
;
...
...
@@ -316,13 +318,13 @@ export default function ChatPage() {
if
(
!
selectedTask
)
{
setDeepLinkError
(
t
(
"
deepLinkInvalidTask
"
));
setTaskRef
(
null
);
set
Subsection
Ref
(
null
);
set
Topic
Ref
(
null
);
unlockTask
();
navigate
(
targetRoute
,
{
replace
:
true
});
return
;
}
setSelectedOrchestrator
(
"
task
"
);
set
Subsection
Ref
(
null
);
set
Topic
Ref
(
null
);
setTaskRef
({
fileId
:
selectedFile
.
file_id
,
taskId
:
selectedTask
.
task_id
});
}
unlockTask
();
...
...
@@ -331,9 +333,9 @@ export default function ChatPage() {
void
(
async
()
=>
{
try
{
if
(
deepLinkTarget
.
isSocratic
)
{
await
select
Subsection
({
await
select
Topic
({
draft
:
chatSessionId
,
subsection
Key
:
deepLinkTarget
.
subsection
Key
,
topic
Key
:
deepLinkTarget
.
topic
Key
,
});
}
else
{
const
selectedFile
=
taskFiles
.
find
((
file
)
=>
file
.
file_id
===
deepLinkTarget
.
fileId
);
...
...
@@ -356,7 +358,7 @@ export default function ChatPage() {
deepLinkTarget
.
isSocratic
?
t
(
"
deepLinkInitFailedSubsection
"
)
:
t
(
"
deepLinkInitFailed
"
)
);
setTaskRef
(
null
);
set
Subsection
Ref
(
null
);
set
Topic
Ref
(
null
);
unlockTask
();
navigate
(
targetRoute
,
{
replace
:
true
});
}
...
...
@@ -375,7 +377,7 @@ export default function ChatPage() {
searchParams
,
setSelectedOrchestrator
,
setTaskRef
,
set
Subsection
Ref
,
set
Topic
Ref
,
taskFiles
,
unlockTask
,
]);
...
...
@@ -384,13 +386,13 @@ export default function ChatPage() {
if
(
!
isTasksInitialized
||
selectedOrchestrator
!==
"
socratic
"
||
!
selected
Subsection
Ref
||
!
selected
Topic
Ref
||
messages
.
length
>
0
)
{
return
;
}
const
bootstrapKey
=
`
${
chatSessionId
}
|
${
selected
SubsectionRef
.
subsection
Key
}
`
;
const
bootstrapKey
=
`
${
chatSessionId
}
|
${
selected
TopicRef
.
topic
Key
}
`
;
if
(
socraticBootstrapRef
.
current
===
bootstrapKey
)
{
return
;
}
...
...
@@ -402,11 +404,11 @@ export default function ChatPage() {
const
response
=
await
fetch
(
`/api/chat/bootstrap-socratic`
,
{
method
:
"
POST
"
,
headers
:
{
"
Content-Type
"
:
"
application/json
"
},
body
:
JSON
.
stringify
({
draft
:
chatSessionId
,
subsection
_key
:
selected
SubsectionRef
.
subsection
Key
,
}),
});
body
:
JSON
.
stringify
({
draft
:
chatSessionId
,
topic
_key
:
selected
TopicRef
.
topic
Key
,
}),
});
if
(
!
response
.
ok
)
{
throw
new
Error
(
`Bootstrap failed:
${
response
.
status
}
`
);
...
...
@@ -464,7 +466,7 @@ export default function ChatPage() {
isTasksInitialized
,
messages
.
length
,
selectedOrchestrator
,
selected
Subsection
Ref
,
selected
Topic
Ref
,
]);
useEffect
(()
=>
{
...
...
@@ -479,7 +481,7 @@ export default function ChatPage() {
return
;
}
if
(
selectedOrchestrator
===
"
socratic
"
)
{
if
(
!
selected
Subsection
Ref
)
{
if
(
!
selected
Topic
Ref
)
{
navigate
(
"
/select-socratic
"
,
{
replace
:
true
});
}
return
;
...
...
@@ -493,7 +495,7 @@ export default function ChatPage() {
isTasksInitialized
,
navigate
,
selectedOrchestrator
,
selected
Subsection
Ref
,
selected
Topic
Ref
,
selectedTaskRef
,
]);
...
...
@@ -674,7 +676,7 @@ export default function ChatPage() {
draft
:
string
;
orchestrator
:
OrchestratorName
;
selected_task
?:
{
file_id
:
string
;
task_id
:
string
};
selected_
subsection
?:
{
subsection
_key
:
string
};
selected_
topic
?:
{
topic
_key
:
string
};
}
=
{
messages
:
[...
messages
,
userMessage
].
map
((
message
)
=>
({
role
:
message
.
role
,
...
...
@@ -684,9 +686,9 @@ export default function ChatPage() {
orchestrator
:
selectedOrchestrator
,
};
if
(
isTaskModeEnabled
)
{
if
(
selectedOrchestrator
===
"
socratic
"
&&
selected
Subsection
Ref
)
{
chatPayload
.
selected_
subsection
=
{
subsection
_key
:
selected
SubsectionRef
.
subsection
Key
,
if
(
selectedOrchestrator
===
"
socratic
"
&&
selected
Topic
Ref
)
{
chatPayload
.
selected_
topic
=
{
topic
_key
:
selected
TopicRef
.
topic
Key
,
};
}
else
if
(
selectedTaskRef
)
{
chatPayload
.
selected_task
=
{
...
...
@@ -872,27 +874,27 @@ export default function ChatPage() {
if
(
nextOrchestrator
===
"
socratic
"
||
isTaskCoupledOrchestrator
(
nextOrchestrator
))
{
if
(
nextOrchestrator
===
"
socratic
"
)
{
const
restoredSubsection
=
payload
.
selected_
subsection
;
if
(
restoredSubsection
?.
subsection
_key
)
{
const
restoredSubsection
=
payload
.
selected_
topic
;
if
(
restoredSubsection
?.
topic
_key
)
{
setTaskRef
(
null
);
set
SubsectionRef
({
subsection
Key
:
restoredSubsection
.
subsection
_key
});
set
TopicRef
({
topic
Key
:
restoredSubsection
.
topic
_key
});
lockTask
();
try
{
await
select
Subsection
({
await
select
Topic
({
draft
:
payload
.
chat_id
,
subsection
Key
:
restoredSubsection
.
subsection
_key
,
topic
Key
:
restoredSubsection
.
topic
_key
,
});
}
catch
(
error
)
{
void
error
;
}
}
else
{
setTaskRef
(
null
);
set
Subsection
Ref
(
null
);
set
Topic
Ref
(
null
);
unlockTask
();
navigate
(
"
/select-socratic
"
,
{
replace
:
true
});
}
}
else
{
set
Subsection
Ref
(
null
);
set
Topic
Ref
(
null
);
const
restoredTask
=
payload
.
selected_task
;
if
(
restoredTask
?.
file_id
&&
restoredTask
?.
task_id
)
{
setTaskRef
({
...
...
@@ -917,7 +919,7 @@ export default function ChatPage() {
}
}
else
{
setTaskRef
(
null
);
set
Subsection
Ref
(
null
);
set
Topic
Ref
(
null
);
unlockTask
();
navigate
(
"
/chat
"
,
{
replace
:
true
});
}
...
...
@@ -1268,11 +1270,11 @@ export default function ChatPage() {
</
section
>
<
aside
className
=
{
`retrieval-column
${
isTaskModeEnabled
?
"
retrieval-column-task-mode
"
:
""
}
`
}
>
{
selectedOrchestrator
===
"
socratic
"
&&
selected
Subsection
?
(
{
selectedOrchestrator
===
"
socratic
"
&&
selected
Topic
?
(
<
SocraticPanel
selectedSubsectionLabel
=
{
selected
Subsection
.
label
}
selectedSubsectionKey
=
{
selected
Subsection
.
subsection
Key
}
selectedSubsectionSummary
=
{
selected
Subsection
.
summary
}
selectedSubsectionLabel
=
{
selected
Topic
.
label
}
selectedSubsectionKey
=
{
selected
Topic
.
topic
Key
}
selectedSubsectionSummary
=
{
selected
Topic
.
summary
}
onChangeSelection
=
{
handleChangeTaskArea
}
/>
)
:
null
}
...
...
math-tutor/frontend/src/pages/SocraticSelectionPage.tsx
View file @
134a9c5c
import
{
useEffect
,
useMemo
,
useRef
}
from
"
react
"
;
import
{
useNavigate
}
from
"
react-router-dom
"
;
import
type
{
OrchestratorName
}
from
"
../api/orchestratorApi
"
;
import
{
select
Subsection
}
from
"
../api/taskApi
"
;
import
{
select
Topic
}
from
"
../api/taskApi
"
;
import
OrchestratorSelect
from
"
../components/Orchestrator/OrchestratorSelect
"
;
import
{
t
}
from
"
../i18n
"
;
import
{
useTutorSession
}
from
"
../state/tutorSession
"
;
...
...
@@ -18,11 +18,11 @@ export default function SocraticSelectionPage() {
switchOrchestrator
,
isOrchestratorSelectable
,
orchestratorError
,
subsection
s
,
selected
Subsection
Ref
,
selected
Subsection
,
topic
s
,
selected
Topic
Ref
,
selected
Topic
,
tasksError
,
set
Subsection
Key
,
set
Topic
Key
,
lockTask
,
unlockTask
,
isTasksInitialized
,
...
...
@@ -31,11 +31,11 @@ export default function SocraticSelectionPage() {
const
subsectionMenuOptions
=
useMemo
(
()
=>
subsection
s
.
map
((
option
)
=>
({
value
:
option
.
subsection
_key
,
label
:
option
.
label
||
option
.
subsection
_key
,
topic
s
.
map
((
option
)
=>
({
value
:
option
.
topic
_key
,
label
:
option
.
label
||
option
.
topic
_key
,
})),
[
subsection
s
]
[
topic
s
]
);
useEffect
(()
=>
{
...
...
@@ -50,7 +50,7 @@ export default function SocraticSelectionPage() {
},
[
isTasksInitialized
,
navigate
,
selectedOrchestrator
,
unlockTask
]);
useEffect
(()
=>
{
if
(
!
selected
Subsection
?.
label
||
!
subsectionDisplayRef
.
current
)
{
if
(
!
selected
Topic
?.
label
||
!
subsectionDisplayRef
.
current
)
{
return
;
}
const
mathjax
=
window
.
MathJax
;
...
...
@@ -58,30 +58,30 @@ export default function SocraticSelectionPage() {
return
;
}
mathjax
.
typesetPromise
([
subsectionDisplayRef
.
current
]).
catch
(()
=>
undefined
);
},
[
selected
Subsection
?.
label
]);
},
[
selected
Topic
?.
label
]);
useEffect
(()
=>
{
if
(
!
subsectionMenuOptions
.
length
)
{
return
;
}
if
(
selected
Subsection
Ref
&&
subsectionMenuOptions
.
some
((
option
)
=>
option
.
value
===
selected
SubsectionRef
.
subsection
Key
)
selected
Topic
Ref
&&
subsectionMenuOptions
.
some
((
option
)
=>
option
.
value
===
selected
TopicRef
.
topic
Key
)
)
{
return
;
}
set
Subsection
Key
(
subsectionMenuOptions
[
0
].
value
);
},
[
selected
SubsectionRef
,
setSubsection
Key
,
subsectionMenuOptions
]);
set
Topic
Key
(
subsectionMenuOptions
[
0
].
value
);
},
[
selected
TopicRef
,
setTopic
Key
,
subsectionMenuOptions
]);
const
handleStartSocratic
=
async
()
=>
{
if
(
!
selected
Subsection
Ref
)
{
if
(
!
selected
Topic
Ref
)
{
return
;
}
try
{
await
select
Subsection
({
await
select
Topic
({
draft
:
chatSessionId
,
subsection
Key
:
selected
SubsectionRef
.
subsection
Key
,
topic
Key
:
selected
TopicRef
.
topic
Key
,
});
lockTask
();
navigate
(
"
/chat
"
);
...
...
@@ -137,8 +137,8 @@ export default function SocraticSelectionPage() {
<
select
id
=
"socratic-subsection-select"
className
=
"task-select"
value
=
{
selected
SubsectionRef
?.
subsection
Key
||
""
}
onChange
=
{
(
event
)
=>
set
Subsection
Key
(
event
.
target
.
value
)
}
value
=
{
selected
TopicRef
?.
topic
Key
||
""
}
onChange
=
{
(
event
)
=>
set
Topic
Key
(
event
.
target
.
value
)
}
disabled
=
{
!
subsectionMenuOptions
.
length
}
>
{
subsectionMenuOptions
.
length
?
(
...
...
@@ -159,7 +159,7 @@ export default function SocraticSelectionPage() {
type
=
"button"
className
=
"btn primary task-solve-btn"
onClick
=
{
handleStartSocratic
}
disabled
=
{
!
selected
Subsection
Ref
}
disabled
=
{
!
selected
Topic
Ref
}
>
{
t
(
"
startSocratic
"
)
}
</
button
>
...
...
math-tutor/frontend/src/state/tutorSession.tsx
View file @
134a9c5c
...
...
@@ -17,11 +17,11 @@ import {
}
from
"
../api/orchestratorApi
"
;
import
{
fetchTaskDetails
,
fetchSocratic
Subsection
s
,
fetchSocratic
Topic
s
,
fetchTasks
,
type
Selected
Subsection
Ref
,
type
Selected
Topic
Ref
,
type
SelectedTaskRef
,
type
Subsection
Option
,
type
Topic
Option
,
type
TaskImage
,
type
TaskDetailsResponse
,
type
TaskFile
,
...
...
@@ -38,9 +38,9 @@ export type SelectedTask = SelectedTaskRef & {
images
:
TaskImage
[];
};
export
type
Selected
Subsection
=
Selected
Subsection
Ref
&
{
export
type
Selected
Topic
=
Selected
Topic
Ref
&
{
label
:
string
;
refs
:
[
number
,
number
,
number
][];
refs
:
[
number
,
number
,
number
,
number
][];
refsText
:
string
;
summary
:
string
;
};
...
...
@@ -52,10 +52,10 @@ export type TaskSelectionState = {
selectedTaskFile
:
TaskFile
|
null
;
taskFileOptions
:
SelectOption
[];
taskOptions
:
SelectOption
[];
selected
Subsection
Ref
:
Selected
Subsection
Ref
|
null
;
selected
Subsection
:
SelectedSubsection
|
null
;
subsections
:
Subsection
Option
[];
subsection
Options
:
SelectOption
[];
selected
Topic
Ref
:
Selected
Topic
Ref
|
null
;
selected
Topic
:
SelectedTopic
|
null
;
topics
:
Topic
Option
[];
topic
Options
:
SelectOption
[];
tasksError
:
string
|
null
;
isTaskModeEnabled
:
boolean
;
isTasksInitialized
:
boolean
;
...
...
@@ -73,10 +73,10 @@ export type TutorSessionState = TaskSelectionState & {
isOrchestratorSelectable
:
boolean
;
orchestratorError
:
string
|
null
;
setTaskRef
:
(
value
:
SelectedTaskRef
|
null
)
=>
void
;
set
Subsection
Ref
:
(
value
:
Selected
Subsection
Ref
|
null
)
=>
void
;
set
Topic
Ref
:
(
value
:
Selected
Topic
Ref
|
null
)
=>
void
;
setTaskFile
:
(
fileId
:
string
)
=>
void
;
setTaskId
:
(
taskId
:
string
)
=>
void
;
set
SubsectionKey
:
(
subsection
Key
:
string
)
=>
void
;
set
TopicKey
:
(
topic
Key
:
string
)
=>
void
;
lockTask
:
()
=>
void
;
unlockTask
:
()
=>
void
;
resetForNewChat
:
()
=>
void
;
...
...
@@ -96,7 +96,7 @@ const isSelectableTaskFile = (file: TaskFile): boolean =>
const
isTaskCoupledOrchestrator
=
(
value
:
OrchestratorName
):
boolean
=>
value
===
"
task
"
||
value
===
"
socratic
"
;
const
format
Subsection
Refs
=
(
refs
:
[
number
,
number
,
number
][]):
string
=>
const
format
Topic
Refs
=
(
refs
:
[
number
,
number
,
number
,
number
][]):
string
=>
refs
.
map
((
ref
)
=>
ref
.
join
(
"
:
"
)).
join
(
"
,
"
);
export
function
TutorSessionProvider
({
children
}:
PropsWithChildren
)
{
...
...
@@ -110,10 +110,10 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const
[
orchestratorError
,
setOrchestratorError
]
=
useState
<
string
|
null
>
(
null
);
const
[
isTasksInitialized
,
setIsTasksInitialized
]
=
useState
(
false
);
const
[
taskFiles
,
setTaskFiles
]
=
useState
<
TaskFile
[]
>
([]);
const
[
subsections
,
setSubsection
s
]
=
useState
<
Subsection
Option
[]
>
([]);
const
[
topics
,
setTopic
s
]
=
useState
<
Topic
Option
[]
>
([]);
const
[
selectedTaskRef
,
setSelectedTaskRef
]
=
useState
<
SelectedTaskRef
|
null
>
(
null
);
const
[
selected
Subsection
Ref
,
setSelected
Subsection
Ref
]
=
useState
<
Selected
Subsection
Ref
|
null
>
(
null
);
const
[
selected
Topic
Ref
,
setSelected
Topic
Ref
]
=
useState
<
Selected
Topic
Ref
|
null
>
(
null
);
const
[
tasksError
,
setTasksError
]
=
useState
<
string
|
null
>
(
null
);
const
[
taskLocked
,
setTaskLocked
]
=
useState
(
false
);
const
[
selectedTaskDetails
,
setSelectedTaskDetails
]
=
useState
<
TaskDetailsResponse
|
null
>
(
null
);
...
...
@@ -168,34 +168,34 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
[
selectedTaskFile
]
);
const
subsection
Options
=
useMemo
(
const
topic
Options
=
useMemo
(
()
=>
subsection
s
.
map
((
option
)
=>
({
value
:
option
.
subsection
_key
,
label
:
option
.
label
||
option
.
subsection
_key
,
topic
s
.
map
((
option
)
=>
({
value
:
option
.
topic
_key
,
label
:
option
.
label
||
option
.
topic
_key
,
})),
[
subsection
s
]
[
topic
s
]
);
const
selected
Subsection
=
useMemo
<
Selected
Subsection
|
null
>
(()
=>
{
if
(
!
selected
Subsection
Ref
)
{
const
selected
Topic
=
useMemo
<
Selected
Topic
|
null
>
(()
=>
{
if
(
!
selected
Topic
Ref
)
{
return
null
;
}
const
option
=
subsection
s
.
find
(
(
item
)
=>
item
.
subsection
_key
===
selected
SubsectionRef
.
subsection
Key
const
option
=
topic
s
.
find
(
(
item
)
=>
item
.
topic
_key
===
selected
TopicRef
.
topic
Key
);
if
(
!
option
)
{
return
null
;
}
const
refs
=
option
.
refs
??
[];
return
{
subsection
Key
:
option
.
subsection
_key
,
topic
Key
:
option
.
topic
_key
,
label
:
option
.
label
,
refs
,
refsText
:
format
Subsection
Refs
(
refs
),
refsText
:
format
Topic
Refs
(
refs
),
summary
:
option
.
summary
||
""
,
};
},
[
selected
SubsectionRef
,
subsection
s
]);
},
[
selected
TopicRef
,
topic
s
]);
const
loadTaskDetails
=
useCallback
(
async
(
taskRef
:
SelectedTaskRef
|
null
)
=>
{
if
(
!
taskRef
)
{
...
...
@@ -228,11 +228,11 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
try
{
const
isSocratic
=
orchestrator
===
"
socratic
"
;
let
files
:
TaskFile
[]
=
[];
let
subsectionsPayload
:
Subsection
Option
[]
=
[];
let
topicsPayload
:
Topic
Option
[]
=
[];
if
(
isSocratic
)
{
const
payload
=
await
fetchSocratic
Subsection
s
();
subsection
sPayload
=
payload
.
subsection
s
||
[];
const
payload
=
await
fetchSocratic
Topic
s
();
topic
sPayload
=
payload
.
topic
s
||
[];
}
else
{
const
payload
=
await
fetchTasks
();
files
=
payload
.
task_files
||
[];
...
...
@@ -240,10 +240,10 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const
selectableFiles
=
files
.
filter
((
file
)
=>
Array
.
isArray
(
file
.
tasks
)
&&
file
.
tasks
.
length
>
0
);
let
nextTaskRef
:
SelectedTaskRef
|
null
=
null
;
let
next
Subsection
Ref
:
Selected
Subsection
Ref
|
null
=
null
;
let
next
Topic
Ref
:
Selected
Topic
Ref
|
null
=
null
;
setTaskFiles
(
files
);
set
Subsections
(
subsection
sPayload
);
set
Topics
(
topic
sPayload
);
setSelectedTaskRef
((
prev
)
=>
{
if
(
isSocratic
)
{
return
null
;
...
...
@@ -267,26 +267,26 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
nextTaskRef
=
{
fileId
:
firstFile
.
file_id
,
taskId
:
defaultTaskId
};
return
nextTaskRef
;
});
setSelected
Subsection
Ref
((
prev
)
=>
{
setSelected
Topic
Ref
((
prev
)
=>
{
if
(
!
isSocratic
)
{
return
null
;
}
if
(
prev
)
{
const
option
=
subsection
sPayload
.
find
(
(
item
)
=>
item
.
subsection
_key
===
prev
.
subsection
Key
const
option
=
topic
sPayload
.
find
(
(
item
)
=>
item
.
topic
_key
===
prev
.
topic
Key
);
if
(
option
)
{
next
Subsection
Ref
=
prev
;
next
Topic
Ref
=
prev
;
return
prev
;
}
}
const
firstOption
=
subsection
sPayload
[
0
];
const
firstOption
=
topic
sPayload
[
0
];
if
(
!
firstOption
)
{
return
null
;
}
next
SubsectionRef
=
{
subsection
Key
:
firstOption
.
subsection
_key
};
return
next
Subsection
Ref
;
next
TopicRef
=
{
topic
Key
:
firstOption
.
topic
_key
};
return
next
Topic
Ref
;
});
if
(
isSocratic
)
{
taskDetailsRequestRef
.
current
+=
1
;
...
...
@@ -299,13 +299,13 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
void
error
;
}
}
void
next
Subsection
Ref
;
void
next
Topic
Ref
;
}
catch
(
error
)
{
setTasksError
(
t
(
"
failedLoadTasks
"
));
setTaskFiles
([]);
set
Subsection
s
([]);
set
Topic
s
([]);
setSelectedTaskRef
(
null
);
setSelected
Subsection
Ref
(
null
);
setSelected
Topic
Ref
(
null
);
taskDetailsRequestRef
.
current
+=
1
;
setSelectedTaskDetails
(
null
);
void
error
;
...
...
@@ -406,12 +406,12 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
[
loadTaskDetails
,
selectedTaskRef
]
);
const
set
Subsection
Key
=
useCallback
(
(
subsection
Key
:
string
)
=>
{
if
(
!
subsection
Key
)
{
const
set
Topic
Key
=
useCallback
(
(
topic
Key
:
string
)
=>
{
if
(
!
topic
Key
)
{
return
;
}
setSelected
SubsectionRef
({
subsection
Key
});
setSelected
TopicRef
({
topic
Key
});
},
[]
);
...
...
@@ -434,7 +434,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
setSelectedTaskDetails
(
null
);
}
if
(
value
!==
"
socratic
"
)
{
setSelected
Subsection
Ref
(
null
);
setSelected
Topic
Ref
(
null
);
}
},
[]);
...
...
@@ -449,7 +449,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const
resetForNewChat
=
useCallback
(()
=>
{
setChatSessionId
(
createSessionId
());
setSelectedTaskRef
(
null
);
setSelected
Subsection
Ref
(
null
);
setSelected
Topic
Ref
(
null
);
setTaskLocked
(
false
);
taskDetailsRequestRef
.
current
+=
1
;
setSelectedTaskDetails
(
null
);
...
...
@@ -466,20 +466,20 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
isOrchestratorSelectable
,
orchestratorError
,
taskFiles
,
subsection
s
,
topic
s
,
selectedTaskRef
,
selectedTask
,
selectedTaskFile
,
taskFileOptions
,
taskOptions
,
selected
Subsection
Ref
,
selected
Subsection
,
subsection
Options
,
selected
Topic
Ref
,
selected
Topic
,
topic
Options
,
setTaskRef
,
set
Subsection
Ref
:
setSelected
Subsection
Ref
,
set
Topic
Ref
:
setSelected
Topic
Ref
,
setTaskFile
,
setTaskId
,
set
Subsection
Key
,
set
Topic
Key
,
tasksError
,
isTaskModeEnabled
,
isTasksInitialized
,
...
...
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