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
18be0686
Commit
18be0686
authored
May 06, 2026
by
Kantz
Browse files
socratesmodus zieht auswahl aus inital_prompts
parent
52b14c87
Changes
6
Hide whitespace changes
Inline
Side-by-side
math-tutor/backend/app/api/chat.py
View file @
18be0686
...
@@ -6,7 +6,7 @@ from typing import List, Optional
...
@@ -6,7 +6,7 @@ from typing import List, Optional
import
app.config
as
config
import
app.config
as
config
from
app.deterministic_services
import
session_store
from
app.deterministic_services
import
session_store
from
app.deterministic_services
import
context_store
,
retrieval_store
,
task_catalog
from
app.deterministic_services
import
context_store
,
retrieval_store
,
task_catalog
from
app.deterministic_services
import
socratic_
bootstrap_prompts
from
app.deterministic_services
import
socratic_
oranisator
from
app.deterministic_services.orchestrators.registry
import
(
from
app.deterministic_services.orchestrators.registry
import
(
get_default_orchestrator
,
get_default_orchestrator
,
is_valid_orchestrator
,
is_valid_orchestrator
,
...
@@ -205,7 +205,7 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe
...
@@ -205,7 +205,7 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe
context_store
.
save_sheet
(
sheet
)
context_store
.
save_sheet
(
sheet
)
try
:
try
:
reply
=
socratic_
bootstrap_prompts
.
get_initial_message
(
request
.
subsection_key
)
reply
=
socratic_
oranisator
.
get_initial_message
(
request
.
subsection_key
)
except
ValueError
as
exc
:
except
ValueError
as
exc
:
logger
.
exception
(
"Socratic bootstrap prompt lookup failed"
)
logger
.
exception
(
"Socratic bootstrap prompt lookup failed"
)
raise
HTTPException
(
status_code
=
500
,
detail
=
str
(
exc
))
from
exc
raise
HTTPException
(
status_code
=
500
,
detail
=
str
(
exc
))
from
exc
...
...
math-tutor/backend/app/api/tasks.py
View file @
18be0686
...
@@ -6,7 +6,7 @@ from fastapi import APIRouter, HTTPException
...
@@ -6,7 +6,7 @@ from fastapi import APIRouter, HTTPException
from
pydantic
import
BaseModel
,
Field
from
pydantic
import
BaseModel
,
Field
import
app.config
as
config
import
app.config
as
config
from
app.deterministic_services
import
context_store
,
task_catalog
from
app.deterministic_services
import
context_store
,
task_catalog
,
socratic_oranisator
router
=
APIRouter
()
router
=
APIRouter
()
...
@@ -41,6 +41,11 @@ class TasksResponse(BaseModel):
...
@@ -41,6 +41,11 @@ class TasksResponse(BaseModel):
subsections
:
List
[
SubsectionEntry
]
=
Field
(
default_factory
=
list
)
subsections
:
List
[
SubsectionEntry
]
=
Field
(
default_factory
=
list
)
class
SocraticResponse
(
BaseModel
):
orchestrator
:
str
enabled
:
bool
subsections
:
List
[
SubsectionEntry
]
=
Field
(
default_factory
=
list
)
class
SelectTaskRequest
(
BaseModel
):
class
SelectTaskRequest
(
BaseModel
):
draft
:
str
=
Field
(...,
min_length
=
1
)
draft
:
str
=
Field
(...,
min_length
=
1
)
file_id
:
str
=
Field
(...,
min_length
=
1
)
file_id
:
str
=
Field
(...,
min_length
=
1
)
...
@@ -81,7 +86,16 @@ def list_tasks() -> TasksResponse:
...
@@ -81,7 +86,16 @@ def list_tasks() -> TasksResponse:
subsections
=
subsections
,
subsections
=
subsections
,
)
)
# Eigentlich sollte die Context-Selection erst passieren wen das schon fest steht
@
router
.
get
(
"/api/tasks/socratic-subsections"
,
response_model
=
SocraticResponse
)
def
list_socratic_subsections
()
->
SocraticResponse
:
orchestrator
=
config
.
get_orchestrator
()
subsections
=
socratic_oranisator
.
build_subsection_catalog
()
return
SocraticResponse
(
orchestrator
=
orchestrator
,
enabled
=
orchestrator
==
"socratic"
,
subsections
=
subsections
,
)
@
router
.
post
(
"/api/tasks/select"
,
response_model
=
SelectTaskResponse
)
@
router
.
post
(
"/api/tasks/select"
,
response_model
=
SelectTaskResponse
)
def
select_task
(
request
:
SelectTaskRequest
)
->
SelectTaskResponse
:
def
select_task
(
request
:
SelectTaskRequest
)
->
SelectTaskResponse
:
...
...
math-tutor/backend/app/deterministic_services/socratic_
bootstrap_prompts
.py
→
math-tutor/backend/app/deterministic_services/socratic_
oranisator
.py
View file @
18be0686
...
@@ -49,3 +49,36 @@ def get_initial_message(subsection_key: str, path: Path = PROMPTS_PATH) -> str:
...
@@ -49,3 +49,36 @@ def get_initial_message(subsection_key: str, path: Path = PROMPTS_PATH) -> str:
if
not
message
:
if
not
message
:
raise
ValueError
(
f
"empty socratic bootstrap prompt for subsection '
{
normalized_key
}
'"
)
raise
ValueError
(
f
"empty socratic bootstrap prompt for subsection '
{
normalized_key
}
'"
)
return
message
return
message
def
build_subsection_catalog
(
path
:
Path
=
PROMPTS_PATH
)
->
list
[
dict
[
str
,
Any
]]:
items
=
load_initial_prompt_items
(
path
)
subsection_summaries
=
task_catalog
.
load_subsection_summaries
()
response
:
list
[
dict
[
str
,
Any
]]
=
[]
for
raw_key
,
item
in
items
.
items
():
subsection_key
=
task_catalog
.
_normalize_subsection_key
(
str
(
raw_key
))
if
not
subsection_key
:
continue
label_source
=
str
(
item
.
get
(
"subsection"
)
or
subsection_key
)
label
=
task_catalog
.
_format_subsection_label
(
label_source
)
ref
=
task_catalog
.
_parse_subsection_ref
(
str
(
item
.
get
(
"index"
)
or
""
))
refs
=
[[
int
(
ref
[
0
]),
int
(
ref
[
1
]),
int
(
ref
[
2
])]]
if
ref
is
not
None
else
[]
response
.
append
(
{
"subsection_key"
:
subsection_key
,
"label"
:
label
or
task_catalog
.
_format_subsection_label
(
subsection_key
),
"refs"
:
refs
,
"summary"
:
subsection_summaries
.
get
(
subsection_key
,
""
),
}
)
response
.
sort
(
key
=
lambda
item
:
(
item
[
"refs"
][
0
]
if
item
[
"refs"
]
else
(
9999
,
9999
,
9999
),
item
[
"subsection_key"
],
)
)
return
response
math-tutor/backend/test/task_catalog_socratic_test.py
View file @
18be0686
...
@@ -14,10 +14,50 @@ from fastapi import FastAPI
...
@@ -14,10 +14,50 @@ from fastapi import FastAPI
from
fastapi.testclient
import
TestClient
from
fastapi.testclient
import
TestClient
from
app.api
import
chat
,
tasks
from
app.api
import
chat
,
tasks
from
app.deterministic_services
import
session_store
,
task_catalog
from
app.deterministic_services
import
session_store
,
socratic_oranisator
,
task_catalog
class
TaskCatalogSocraticTest
(
unittest
.
TestCase
):
class
TaskCatalogSocraticTest
(
unittest
.
TestCase
):
def
test_socratic_build_subsection_catalog_returns_response_shape
(
self
)
->
None
:
with
patch
(
"app.deterministic_services.socratic_oranisator.load_initial_prompt_items"
,
return_value
=
{
"quadratische gleichungen"
:
{
"subsection"
:
"Quadratische Gleichungen"
,
"index"
:
"1:3:3"
,
},
"mengen"
:
{
"subsection"
:
"Mengen"
,
"index"
:
"1:1:1"
,
},
},
),
patch
(
"app.deterministic_services.socratic_oranisator.task_catalog.load_subsection_summaries"
,
return_value
=
{
"mengen"
:
"Mengen summary text"
,
"quadratische gleichungen"
:
"Quadratische summary text"
,
},
):
catalog
=
socratic_oranisator
.
build_subsection_catalog
()
self
.
assertEqual
(
catalog
,
[
{
"subsection_key"
:
"mengen"
,
"label"
:
"Mengen"
,
"refs"
:
[[
1
,
1
,
1
]],
"summary"
:
"Mengen summary text"
,
},
{
"subsection_key"
:
"quadratische gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"refs"
:
[[
1
,
3
,
3
]],
"summary"
:
"Quadratische summary text"
,
},
],
)
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_subsection_map"
,
...
@@ -172,6 +212,28 @@ class TaskApiSocraticTest(unittest.TestCase):
...
@@ -172,6 +212,28 @@ class TaskApiSocraticTest(unittest.TestCase):
self
.
assertEqual
(
body
[
"orchestrator"
],
"socratic"
)
self
.
assertEqual
(
body
[
"orchestrator"
],
"socratic"
)
self
.
assertEqual
(
body
[
"subsections"
][
0
][
"subsection_key"
],
"quadratische gleichungen"
)
self
.
assertEqual
(
body
[
"subsections"
][
0
][
"subsection_key"
],
"quadratische gleichungen"
)
def
test_list_socratic_subsections_returns_catalog
(
self
)
->
None
:
subsections
=
[
{
"subsection_key"
:
"quadratische gleichungen"
,
"label"
:
"Quadratische Gleichungen"
,
"refs"
:
[[
1
,
3
,
3
]],
"summary"
:
"Quadratische summary text"
,
}
]
with
patch
(
"app.api.tasks.config.get_orchestrator"
,
return_value
=
"socratic"
),
patch
(
"app.api.tasks.socratic_oranisator.build_subsection_catalog"
,
return_value
=
subsections
,
):
response
=
self
.
client
.
get
(
"/api/tasks/socratic-subsections"
)
self
.
assertEqual
(
response
.
status_code
,
200
)
body
=
response
.
json
()
self
.
assertEqual
(
body
[
"orchestrator"
],
"socratic"
)
self
.
assertTrue
(
body
[
"enabled"
])
self
.
assertEqual
(
body
[
"subsections"
],
subsections
)
def
test_select_subsection_endpoint_returns_selected_key
(
self
)
->
None
:
def
test_select_subsection_endpoint_returns_selected_key
(
self
)
->
None
:
sheet
:
dict
[
str
,
object
]
=
{}
sheet
:
dict
[
str
,
object
]
=
{}
...
...
math-tutor/frontend/src/api/taskApi.ts
View file @
18be0686
...
@@ -27,6 +27,12 @@ export type TasksResponse = {
...
@@ -27,6 +27,12 @@ export type TasksResponse = {
subsections
:
SubsectionOption
[];
subsections
:
SubsectionOption
[];
};
};
export
type
SocraticResponse
=
{
orchestrator
:
string
;
enabled
:
boolean
;
subsections
:
SubsectionOption
[];
};
export
type
SelectedTaskRef
=
{
export
type
SelectedTaskRef
=
{
fileId
:
string
;
fileId
:
string
;
taskId
:
string
;
taskId
:
string
;
...
@@ -55,6 +61,14 @@ export async function fetchTasks(): Promise<TasksResponse> {
...
@@ -55,6 +61,14 @@ export async function fetchTasks(): Promise<TasksResponse> {
return
response
.
json
();
return
response
.
json
();
}
}
export
async
function
fetchSocraticSubsections
():
Promise
<
SocraticResponse
>
{
const
response
=
await
fetch
(
"
/api/tasks/socratic-subsections
"
);
if
(
!
response
.
ok
)
{
throw
new
Error
(
`Socratic subsections failed:
${
response
.
status
}
`
);
}
return
response
.
json
();
}
export
async
function
selectTask
(
input
:
{
export
async
function
selectTask
(
input
:
{
draft
:
string
;
draft
:
string
;
fileId
:
string
;
fileId
:
string
;
...
...
math-tutor/frontend/src/state/tutorSession.tsx
View file @
18be0686
...
@@ -15,6 +15,7 @@ import {
...
@@ -15,6 +15,7 @@ import {
type
OrchestratorName
,
type
OrchestratorName
,
}
from
"
../api/orchestratorApi
"
;
}
from
"
../api/orchestratorApi
"
;
import
{
import
{
fetchSocraticSubsections
,
fetchTasks
,
fetchTasks
,
type
SelectedSubsectionRef
,
type
SelectedSubsectionRef
,
type
SelectedTaskRef
,
type
SelectedTaskRef
,
...
@@ -190,36 +191,30 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -190,36 +191,30 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
};
};
},
[
selectedSubsectionRef
,
subsections
]);
},
[
selectedSubsectionRef
,
subsections
]);
const
initTasks
=
useCallback
(
async
()
=>
{
const
loadSelectionData
=
useCallback
(
async
(
orchestrator
:
OrchestratorName
)
=>
{
setTasksError
(
null
);
setTasksError
(
null
);
setOrchestratorError
(
null
);
try
{
try
{
const
orchestratorPayload
=
await
fetchOrchestratorConfig
();
const
isSocratic
=
orchestrator
===
"
socratic
"
;
const
available
=
orchestratorPayload
.
available_orchestrators
.
length
let
files
:
TaskFile
[]
=
[];
?
orchestratorPayload
.
available_orchestrators
let
subsectionsPayload
:
SubsectionOption
[]
=
[];
:
getFallbackOrchestrators
();
setAvailableOrchestrators
(
available
);
if
(
isSocratic
)
{
setIsOrchestratorSelectable
(
true
);
const
payload
=
await
fetchSocraticSubsections
();
setSelectedOrchestratorState
((
prev
)
=>
subsectionsPayload
=
payload
.
subsections
||
[];
available
.
includes
(
prev
)
?
prev
:
orchestratorPayload
.
default_orchestrator
}
else
{
);
const
payload
=
await
fetchTasks
();
}
catch
(
error
)
{
files
=
payload
.
task_files
||
[];
setAvailableOrchestrators
(
getFallbackOrchestrators
());
subsectionsPayload
=
payload
.
subsections
||
[];
setSelectedOrchestratorState
(
"
qa
"
);
}
setIsOrchestratorSelectable
(
false
);
setOrchestratorError
(
t
(
"
failedLoadOrchestratorConfig
"
));
void
error
;
}
try
{
const
payload
=
await
fetchTasks
();
const
files
=
payload
.
task_files
||
[];
const
subsectionsPayload
=
payload
.
subsections
||
[];
const
selectableFiles
=
files
.
filter
((
file
)
=>
isSelectableTaskFile
(
file
));
const
selectableFiles
=
files
.
filter
((
file
)
=>
isSelectableTaskFile
(
file
));
setTaskFiles
(
files
);
setTaskFiles
(
files
);
setSubsections
(
subsectionsPayload
);
setSubsections
(
subsectionsPayload
);
setSelectedTaskRef
((
prev
)
=>
{
setSelectedTaskRef
((
prev
)
=>
{
if
(
isSocratic
)
{
return
null
;
}
if
(
prev
)
{
if
(
prev
)
{
const
file
=
selectableFiles
.
find
((
item
)
=>
item
.
file_id
===
prev
.
fileId
);
const
file
=
selectableFiles
.
find
((
item
)
=>
item
.
file_id
===
prev
.
fileId
);
if
(
file
&&
file
.
tasks
.
some
((
task
)
=>
task
.
task_id
===
prev
.
taskId
))
{
if
(
file
&&
file
.
tasks
.
some
((
task
)
=>
task
.
task_id
===
prev
.
taskId
))
{
...
@@ -238,8 +233,13 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -238,8 +233,13 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
return
{
fileId
:
firstFile
.
file_id
,
taskId
:
defaultTaskId
};
return
{
fileId
:
firstFile
.
file_id
,
taskId
:
defaultTaskId
};
});
});
setSelectedSubsectionRef
((
prev
)
=>
{
setSelectedSubsectionRef
((
prev
)
=>
{
if
(
!
isSocratic
)
{
return
null
;
}
if
(
prev
)
{
if
(
prev
)
{
const
option
=
subsectionsPayload
.
find
((
item
)
=>
item
.
subsection_key
===
prev
.
subsectionKey
);
const
option
=
subsectionsPayload
.
find
(
(
item
)
=>
item
.
subsection_key
===
prev
.
subsectionKey
);
if
(
option
)
{
if
(
option
)
{
return
prev
;
return
prev
;
}
}
...
@@ -258,15 +258,47 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
...
@@ -258,15 +258,47 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
setSelectedTaskRef
(
null
);
setSelectedTaskRef
(
null
);
setSelectedSubsectionRef
(
null
);
setSelectedSubsectionRef
(
null
);
void
error
;
void
error
;
}
},
[]);
const
initTasks
=
useCallback
(
async
()
=>
{
setOrchestratorError
(
null
);
try
{
const
orchestratorPayload
=
await
fetchOrchestratorConfig
();
const
available
=
orchestratorPayload
.
available_orchestrators
.
length
?
orchestratorPayload
.
available_orchestrators
:
getFallbackOrchestrators
();
const
nextOrchestrator
=
available
.
includes
(
selectedOrchestrator
)
?
selectedOrchestrator
:
orchestratorPayload
.
default_orchestrator
;
setAvailableOrchestrators
(
available
);
setIsOrchestratorSelectable
(
true
);
setSelectedOrchestratorState
(
nextOrchestrator
);
await
loadSelectionData
(
nextOrchestrator
);
}
catch
(
error
)
{
setAvailableOrchestrators
(
getFallbackOrchestrators
());
setSelectedOrchestratorState
(
"
qa
"
);
setIsOrchestratorSelectable
(
false
);
setOrchestratorError
(
t
(
"
failedLoadOrchestratorConfig
"
));
await
loadSelectionData
(
"
qa
"
);
void
error
;
}
finally
{
}
finally
{
setIsTasksInitialized
(
true
);
setIsTasksInitialized
(
true
);
}
}
},
[]);
},
[
loadSelectionData
,
selectedOrchestrator
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
void
initTasks
();
void
initTasks
();
},
[
initTasks
]);
},
[
initTasks
]);
useEffect
(()
=>
{
if
(
!
isTasksInitialized
)
{
return
;
}
void
loadSelectionData
(
selectedOrchestrator
);
},
[
isTasksInitialized
,
loadSelectionData
,
selectedOrchestrator
]);
const
setTaskFile
=
useCallback
(
const
setTaskFile
=
useCallback
(
(
fileId
:
string
)
=>
{
(
fileId
:
string
)
=>
{
if
(
!
fileId
)
{
if
(
!
fileId
)
{
...
...
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