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
8e5526b8
Commit
8e5526b8
authored
Mar 31, 2026
by
Kantz
Browse files
start mit einer Bot nachricht
parent
3c67b18f
Changes
3
Show whitespace changes
Inline
Side-by-side
math-tutor/backend/app/api/chat.py
View file @
8e5526b8
...
...
@@ -3,7 +3,10 @@ from __future__ import annotations
import
logging
from
typing
import
List
,
Optional
import
app.config
as
config
from
app.deterministic_services
import
session_store
from
app.deterministic_services
import
context_store
,
retrieval_store
,
task_catalog
from
app.LLM_services
import
socratic_LLM
from
app.deterministic_services.orchestrators.registry
import
(
get_default_orchestrator
,
is_valid_orchestrator
,
...
...
@@ -66,6 +69,16 @@ class ChatArchiveDetail(BaseModel):
orchestrator
:
str
class
SocraticBootstrapRequest
(
BaseModel
):
draft
:
str
=
Field
(...,
min_length
=
1
)
subsection_key
:
str
=
Field
(...,
min_length
=
1
)
class
SocraticBootstrapResponse
(
BaseModel
):
reply
:
str
sources
:
List
[
dict
]
=
[]
@
router
.
post
(
"/api/chat"
,
response_model
=
ChatResponse
)
def
chat
(
request
:
ChatRequest
)
->
ChatResponse
:
if
not
request
.
messages
:
...
...
@@ -170,3 +183,41 @@ def archive_chat(request: ChatRequest) -> ChatArchiveResponse:
raise
HTTPException
(
status_code
=
502
,
detail
=
"chat archive failed"
)
from
exc
return
ChatArchiveResponse
(
status
=
"ok"
,
chat_id
=
chat_id
)
@
router
.
post
(
"/api/chat/bootstrap-socratic"
,
response_model
=
SocraticBootstrapResponse
)
def
bootstrap_socratic
(
request
:
SocraticBootstrapRequest
)
->
SocraticBootstrapResponse
:
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
,
[])
if
not
task_catalog
.
select_subsection_by_key
(
sheet
,
request
.
subsection_key
):
raise
HTTPException
(
status_code
=
404
,
detail
=
"subsection not found"
)
refs
=
task_catalog
.
get_selected_subsection_refs
(
sheet
)
sources
=
retrieval_store
.
retrieve_for_subsections
(
pg_url
=
config
.
get_postgres_url
(),
subsection_refs
=
refs
,
)
context_store
.
set_sources
(
sheet
,
sources
)
context_store
.
set_initialized
(
sheet
,
True
)
context_store
.
save_sheet
(
sheet
)
sources_text
=
"
\n
"
.
join
(
source
.
to_string
()
for
source
in
sources
)
reply
=
socratic_LLM
.
generate_dialog
(
query
=
"Was sind die Themen dieses Abschnitts? Frage mich, mit welchem ich mich zuerst beschäftigen möchte."
,
subsection_refs
=
refs
,
history
=
None
,
sources
=
sources_text
,
)
if
not
reply
:
reply
=
"Was sind die Themen dieses Abschnitts? Womit möchtest du anfangen?"
context_store
.
append_history_message
(
sheet
,
"assistant"
,
reply
)
context_store
.
save_sheet
(
sheet
)
return
SocraticBootstrapResponse
(
reply
=
reply
,
sources
=
[
source
.
model_dump
()
for
source
in
sources
],
)
math-tutor/backend/test/task_catalog_socratic_test.py
View file @
8e5526b8
...
...
@@ -13,7 +13,7 @@ os.environ.setdefault("POSTGRES_URL", "postgresql://localhost/test")
from
fastapi
import
FastAPI
from
fastapi.testclient
import
TestClient
from
app.api
import
tasks
from
app.api
import
chat
,
tasks
from
app.deterministic_services
import
session_store
,
task_catalog
...
...
@@ -152,6 +152,52 @@ class TaskApiSocraticTest(unittest.TestCase):
select_mock
.
assert_called_once_with
(
sheet
,
subsection_key
=
"quadratische_gleichungen"
)
class
ChatBootstrapSocraticTest
(
unittest
.
TestCase
):
def
setUp
(
self
)
->
None
:
app
=
FastAPI
()
app
.
include_router
(
chat
.
router
)
self
.
client
=
TestClient
(
app
)
def
test_bootstrap_socratic_loads_sources_and_writes_only_assistant_history
(
self
)
->
None
:
sheet
:
dict
[
str
,
object
]
=
{}
with
patch
(
"app.api.chat.context_store.get_chat_id"
,
return_value
=
"chat-1"
),
patch
(
"app.api.chat.context_store.load_sheet"
,
return_value
=
sheet
,
),
patch
(
"app.api.chat.context_store.context_store_new.init_sheet"
,
return_value
=
sheet
,
),
patch
(
"app.api.chat.context_store.save_sheet"
),
patch
(
"app.api.chat.context_store.set_sources"
),
patch
(
"app.api.chat.context_store.set_initialized"
),
patch
(
"app.api.chat.task_catalog.load_subsection_map"
,
return_value
=
{
"quadratische gleichungen"
:
(
1
,
3
,
3
)},
),
patch
(
"app.api.chat.retrieval_store.retrieve_for_subsections"
,
return_value
=
[],
)
as
retrieve_mock
,
patch
(
"app.api.chat.socratic_LLM.generate_dialog"
,
return_value
=
"Was sind die Themen dieses Abschnitts?"
,
):
response
=
self
.
client
.
post
(
"/api/chat/bootstrap-socratic"
,
json
=
{
"draft"
:
"chat-1"
,
"subsection_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"
,
subsection_refs
=
[(
1
,
3
,
3
)])
self
.
assertEqual
(
sheet
[
"selected_subsection_key"
],
"quadratische gleichungen"
)
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
:
record
=
{
...
...
math-tutor/frontend/src/pages/ChatPage.tsx
View file @
8e5526b8
...
...
@@ -116,6 +116,55 @@ const toRetrievedDoc = (source: ContextSource, index: number): RetrievedDoc => (
markdown
:
source
.
markdown
||
""
,
});
const
applyRetrievedSources
=
(
sources
:
ContextSource
[],
setters
:
{
setDirectChildren
:
(
value
:
RetrievedDoc
[])
=>
void
;
setTaskChildren
:
(
value
:
RetrievedDoc
[])
=>
void
;
setIndirectChildren
:
(
value
:
RetrievedDoc
[])
=>
void
;
setSubsections
:
(
value
:
RetrievedDoc
[])
=>
void
;
setSections
:
(
value
:
RetrievedDoc
[])
=>
void
;
}
)
=>
{
const
nextDirect
:
RetrievedDoc
[]
=
[];
const
nextTask
:
RetrievedDoc
[]
=
[];
const
nextIndirect
:
RetrievedDoc
[]
=
[];
const
nextSubsections
:
RetrievedDoc
[]
=
[];
const
nextSections
:
RetrievedDoc
[]
=
[];
sources
.
forEach
((
source
,
index
)
=>
{
const
doc
=
toRetrievedDoc
(
source
,
index
);
switch
(
source
.
retrieved_as
)
{
case
"
children_direct
"
:
nextDirect
.
push
(
doc
);
break
;
case
"
children_expanded
"
:
nextIndirect
.
push
(
doc
);
break
;
case
"
task_childs
"
:
nextTask
.
push
(
doc
);
break
;
case
"
subsections
"
:
nextSubsections
.
push
(
doc
);
break
;
case
"
sections
"
:
nextSections
.
push
(
doc
);
break
;
case
"
neighbors
"
:
nextIndirect
.
push
(
doc
);
break
;
default
:
nextIndirect
.
push
(
doc
);
}
});
setters
.
setDirectChildren
(
nextDirect
);
setters
.
setTaskChildren
(
nextTask
);
setters
.
setIndirectChildren
(
nextIndirect
);
setters
.
setSubsections
(
nextSubsections
);
setters
.
setSections
(
nextSections
);
};
export
default
function
ChatPage
()
{
const
showChatsButton
=
String
(
import
.
meta
.
env
.
VITE_SHOW_CHATS_BUTTON
??
"
true
"
)
.
trim
()
...
...
@@ -169,6 +218,7 @@ export default function ChatPage() {
message
:
string
;
}
|
null
>
(
null
);
const
processedDeepLinkRef
=
useRef
<
string
>
(
""
);
const
socraticBootstrapRef
=
useRef
<
string
>
(
""
);
const
deepLinkTarget
=
useMemo
(()
=>
{
const
orchestrator
=
String
(
searchParams
.
get
(
"
orchestrator
"
)
||
""
)
...
...
@@ -324,6 +374,87 @@ export default function ChatPage() {
unlockTask
,
]);
useEffect
(()
=>
{
if
(
!
isTasksInitialized
||
selectedOrchestrator
!==
"
socratic
"
||
!
selectedSubsectionRef
||
messages
.
length
>
0
)
{
return
;
}
const
bootstrapKey
=
`
${
chatSessionId
}
|
${
selectedSubsectionRef
.
subsectionKey
}
`
;
if
(
socraticBootstrapRef
.
current
===
bootstrapKey
)
{
return
;
}
let
cancelled
=
false
;
void
(
async
()
=>
{
try
{
const
response
=
await
fetch
(
`/api/chat/bootstrap-socratic`
,
{
method
:
"
POST
"
,
headers
:
{
"
Content-Type
"
:
"
application/json
"
},
body
:
JSON
.
stringify
({
draft
:
chatSessionId
,
subsection_key
:
selectedSubsectionRef
.
subsectionKey
,
}),
});
if
(
!
response
.
ok
)
{
throw
new
Error
(
`Bootstrap failed:
${
response
.
status
}
`
);
}
const
payload
:
{
reply
?:
string
;
sources
?:
ContextSource
[]
}
=
await
response
.
json
();
if
(
cancelled
)
{
return
;
}
const
reply
=
typeof
payload
.
reply
===
"
string
"
&&
payload
.
reply
.
trim
().
length
>
0
?
payload
.
reply
:
t
(
"
chatRequestFailed
"
);
setMessages
([
{
id
:
`m-
${
Date
.
now
()}
-assistant`
,
role
:
"
assistant
"
,
text
:
reply
,
},
]);
applyRetrievedSources
(
payload
.
sources
||
[],
{
setDirectChildren
,
setTaskChildren
,
setIndirectChildren
,
setSubsections
,
setSections
,
});
socraticBootstrapRef
.
current
=
bootstrapKey
;
}
catch
(
error
)
{
if
(
!
cancelled
)
{
setRetrievalError
(
t
(
"
retrievalFailed
"
));
setMessages
([
{
id
:
`m-
${
Date
.
now
()}
-assistant`
,
role
:
"
assistant
"
,
text
:
t
(
"
chatRequestFailed
"
),
},
]);
}
void
error
;
}
})();
return
()
=>
{
cancelled
=
true
;
};
},
[
applyRetrievedSources
,
chatSessionId
,
isTasksInitialized
,
messages
.
length
,
selectedOrchestrator
,
selectedSubsectionRef
,
]);
useEffect
(()
=>
{
if
(
!
isTasksInitialized
||
!
isTaskModeEnabled
)
{
return
;
...
...
@@ -582,43 +713,13 @@ export default function ChatPage() {
}
const
sources
:
ContextSource
[]
=
await
contextResponse
.
json
();
const
nextDirect
:
RetrievedDoc
[]
=
[];
const
nextTask
:
RetrievedDoc
[]
=
[];
const
nextIndirect
:
RetrievedDoc
[]
=
[];
const
nextSubsections
:
RetrievedDoc
[]
=
[];
const
nextSections
:
RetrievedDoc
[]
=
[];
sources
.
forEach
((
source
,
index
)
=>
{
const
doc
=
toRetrievedDoc
(
source
,
index
);
switch
(
source
.
retrieved_as
)
{
case
"
children_direct
"
:
nextDirect
.
push
(
doc
);
break
;
case
"
children_expanded
"
:
nextIndirect
.
push
(
doc
);
break
;
case
"
task_childs
"
:
nextTask
.
push
(
doc
);
break
;
case
"
subsections
"
:
nextSubsections
.
push
(
doc
);
break
;
case
"
sections
"
:
nextSections
.
push
(
doc
);
break
;
case
"
neighbors
"
:
nextIndirect
.
push
(
doc
);
break
;
default
:
nextIndirect
.
push
(
doc
);
}
applyRetrievedSources
(
sources
,
{
setDirectChildren
,
setTaskChildren
,
setIndirectChildren
,
setSubsections
,
setSections
,
});
setDirectChildren
(
nextDirect
);
setTaskChildren
(
nextTask
);
setIndirectChildren
(
nextIndirect
);
setSubsections
(
nextSubsections
);
setSections
(
nextSections
);
}
catch
(
error
)
{
setRetrievalError
(
t
(
"
retrievalFailed
"
));
void
error
;
...
...
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