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
ef617e1d
Commit
ef617e1d
authored
Jan 22, 2026
by
Kantz
Browse files
Contex Sheet first draft
parent
927e6a37
Changes
7
Hide whitespace changes
Inline
Side-by-side
math-tutor/backend/app/api/chat.py
View file @
ef617e1d
...
...
@@ -35,7 +35,8 @@ def chat(request: ChatRequest) -> ChatResponse:
try
:
result
=
orchestrator_engine
.
run_chat
(
[{
"role"
:
m
.
role
,
"content"
:
m
.
text
}
for
m
in
request
.
messages
]
[{
"role"
:
m
.
role
,
"content"
:
m
.
text
}
for
m
in
request
.
messages
],
draft
=
request
.
draft
,
)
reply
=
result
[
"reply"
]
sources
=
result
[
"sources"
]
...
...
math-tutor/backend/app/services/context_store.py
0 → 100644
View file @
ef617e1d
from
__future__
import
annotations
import
hashlib
import
json
import
os
from
datetime
import
datetime
from
threading
import
Lock
from
typing
import
Any
_CACHE
:
dict
[
str
,
dict
[
str
,
Any
]]
=
{}
_LOCK
=
Lock
()
_LOG_DIR
=
os
.
path
.
join
(
"logs"
,
"context_sheets"
)
def
_utc_now
()
->
str
:
return
datetime
.
utcnow
().
strftime
(
"%Y-%m-%dT%H:%M:%SZ"
)
def
is_new_chat
(
messages
:
list
[
dict
])
->
bool
:
return
not
any
(
m
.
get
(
"role"
)
==
"assistant"
for
m
in
messages
)
def
get_chat_id
(
messages
:
list
[
dict
],
draft
:
str
|
None
=
None
)
->
str
:
if
draft
:
return
f
"draft_
{
draft
}
"
first_user
=
next
((
m
for
m
in
messages
if
m
.
get
(
"role"
)
==
"user"
and
m
.
get
(
"content"
)),
None
)
if
not
first_user
:
return
"unknown"
digest
=
hashlib
.
sha1
(
first_user
[
"content"
].
encode
(
"utf-8"
)).
hexdigest
()
return
digest
[:
12
]
def
format_history
(
messages
:
list
[
dict
])
->
str
:
lines
=
[]
for
msg
in
messages
:
role
=
msg
.
get
(
"role"
,
"unknown"
)
content
=
msg
.
get
(
"content"
,
""
)
lines
.
append
(
f
"
{
role
}
:
{
content
}
"
)
return
"
\n
"
.
join
(
lines
)
def
init_sheet
(
chat_id
:
str
,
messages
:
list
[
dict
])
->
dict
[
str
,
Any
]:
timestamp
=
_utc_now
()
return
{
"chat_id"
:
chat_id
,
"created_at"
:
timestamp
,
"updated_at"
:
timestamp
,
"history"
:
messages
[:],
"retrieval_contexts"
:
[],
"math_solutions"
:
[],
"tool_outputs"
:
[],
"decisions"
:
[],
"sources"
:
[],
"initialized"
:
False
,
}
def
load_sheet
(
chat_id
:
str
)
->
dict
[
str
,
Any
]
|
None
:
with
_LOCK
:
if
chat_id
in
_CACHE
:
return
_CACHE
[
chat_id
]
latest_path
=
os
.
path
.
join
(
_LOG_DIR
,
f
"
{
chat_id
}
_latest.json"
)
if
os
.
path
.
exists
(
latest_path
):
with
open
(
latest_path
,
"r"
,
encoding
=
"utf-8"
)
as
f
:
sheet
=
json
.
load
(
f
)
with
_LOCK
:
_CACHE
[
chat_id
]
=
sheet
return
sheet
return
None
def
update_history
(
sheet
:
dict
[
str
,
Any
],
messages
:
list
[
dict
])
->
None
:
sheet
[
"history"
]
=
messages
[:]
sheet
[
"updated_at"
]
=
_utc_now
()
def
add_retrieval_context
(
sheet
:
dict
[
str
,
Any
],
query
:
str
,
context
:
str
,
sources
:
list
[
str
],
)
->
None
:
sheet
[
"retrieval_contexts"
].
append
(
{
"query"
:
query
,
"context"
:
context
,
"sources"
:
sources
}
)
sheet
[
"sources"
]
=
list
(
dict
.
fromkeys
(
sheet
[
"sources"
]
+
sources
))
sheet
[
"updated_at"
]
=
_utc_now
()
def
add_math_solution
(
sheet
:
dict
[
str
,
Any
],
task
:
str
,
input_text
:
str
,
symbols
:
list
[
str
]
|
None
,
solution
:
str
,
)
->
None
:
sheet
[
"math_solutions"
].
append
(
{
"task"
:
task
,
"input"
:
input_text
,
"symbols"
:
symbols
or
[],
"solution"
:
solution
,
}
)
sheet
[
"updated_at"
]
=
_utc_now
()
def
add_tool_output
(
sheet
:
dict
[
str
,
Any
],
entry
:
dict
[
str
,
Any
])
->
None
:
sheet
[
"tool_outputs"
].
append
(
entry
)
sheet
[
"updated_at"
]
=
_utc_now
()
def
add_decision
(
sheet
:
dict
[
str
,
Any
],
decision
:
dict
[
str
,
Any
])
->
None
:
entry
=
{
"timestamp"
:
_utc_now
(),
**
decision
}
sheet
[
"decisions"
].
append
(
entry
)
sheet
[
"updated_at"
]
=
_utc_now
()
def
latest_math_solution
(
sheet
:
dict
[
str
,
Any
])
->
str
:
if
not
sheet
[
"math_solutions"
]:
return
""
return
sheet
[
"math_solutions"
][
-
1
].
get
(
"solution"
,
""
)
def
format_sheet
(
sheet
:
dict
[
str
,
Any
])
->
str
:
parts
=
[]
history
=
format_history
(
sheet
.
get
(
"history"
,
[]))
parts
.
append
(
"HISTORY:
\n
"
+
(
history
or
"(leer)"
))
retrievals
=
sheet
.
get
(
"retrieval_contexts"
,
[])
if
retrievals
:
blocks
=
[]
for
item
in
retrievals
:
blocks
.
append
(
f
"QUERY:
{
item
.
get
(
'query'
,
''
)
}
\n
{
item
.
get
(
'context'
,
''
)
}
"
)
parts
.
append
(
"RETRIEVAL_CONTEXT:
\n
"
+
"
\n\n
"
.
join
(
blocks
))
else
:
parts
.
append
(
"RETRIEVAL_CONTEXT:
\n
(leer)"
)
math_solutions
=
sheet
.
get
(
"math_solutions"
,
[])
if
math_solutions
:
blocks
=
[]
for
item
in
math_solutions
:
blocks
.
append
(
"TASK: {task}
\n
INPUT: {input}
\n
SYMBOLS: {symbols}
\n
SOLUTION: {solution}"
.
format
(
task
=
item
.
get
(
"task"
,
""
),
input
=
item
.
get
(
"input"
,
""
),
symbols
=
","
.
join
(
item
.
get
(
"symbols"
,
[])),
solution
=
item
.
get
(
"solution"
,
""
),
)
)
parts
.
append
(
"MATH_SOLUTIONS:
\n
"
+
"
\n\n
"
.
join
(
blocks
))
else
:
parts
.
append
(
"MATH_SOLUTIONS:
\n
(leer)"
)
if
sheet
.
get
(
"tool_outputs"
):
parts
.
append
(
"TOOL_OUTPUTS:
\n
"
+
json
.
dumps
(
sheet
[
"tool_outputs"
],
ensure_ascii
=
True
))
else
:
parts
.
append
(
"TOOL_OUTPUTS:
\n
(leer)"
)
return
"
\n\n
"
.
join
(
parts
)
def
save_sheet
(
sheet
:
dict
[
str
,
Any
])
->
None
:
os
.
makedirs
(
_LOG_DIR
,
exist_ok
=
True
)
sheet
[
"updated_at"
]
=
_utc_now
()
timestamp
=
datetime
.
utcnow
().
strftime
(
"%Y%m%d_%H%M%S"
)
chat_id
=
sheet
.
get
(
"chat_id"
,
"unknown"
)
snapshot_path
=
os
.
path
.
join
(
_LOG_DIR
,
f
"
{
chat_id
}
_
{
timestamp
}
.json"
)
latest_path
=
os
.
path
.
join
(
_LOG_DIR
,
f
"
{
chat_id
}
_latest.json"
)
payload
=
json
.
dumps
(
sheet
,
ensure_ascii
=
True
,
indent
=
2
)
with
open
(
snapshot_path
,
"w"
,
encoding
=
"utf-8"
)
as
f
:
f
.
write
(
payload
)
with
open
(
latest_path
,
"w"
,
encoding
=
"utf-8"
)
as
f
:
f
.
write
(
payload
)
with
_LOCK
:
_CACHE
[
chat_id
]
=
sheet
math-tutor/backend/app/services/decision_tool.py
0 → 100644
View file @
ef617e1d
import
json
from
app.services
import
llm_client
CLASSIFIER_PROMPT
=
(
"Du bist ein Klassifikator. Entscheide, ob die vorhandenen Informationen "
"aus Historie und Kontextblatt ausreichen, um einen naechsten didaktisch "
"wertvollen Hinweis zu geben. Antworte ausschliesslich mit gueltigem JSON "
"im Format {
\"
needs_more_context
\"
: true/false,
\"
reason
\"
:
\"
...
\"
}."
)
def
needs_more_context
(
history
:
str
,
context_sheet
:
str
)
->
dict
:
prompt
=
(
"Historie:
\n
"
+
history
+
"
\n\n
Kontextblatt:
\n
"
+
context_sheet
+
"
\n\n
Antwortformat: JSON."
)
result
=
llm_client
.
chat
(
messages
=
[
{
"role"
:
"system"
,
"content"
:
CLASSIFIER_PROMPT
},
{
"role"
:
"user"
,
"content"
:
prompt
},
]
)
content
=
llm_client
.
get_message_content
(
result
)
try
:
payload
=
json
.
loads
(
content
)
return
{
"needs_more_context"
:
bool
(
payload
.
get
(
"needs_more_context"
)),
"reason"
:
payload
.
get
(
"reason"
,
""
),
}
except
json
.
JSONDecodeError
:
return
{
"needs_more_context"
:
True
,
"reason"
:
"classifier_parse_error"
}
math-tutor/backend/app/services/math_intent.py
0 → 100644
View file @
ef617e1d
import
json
from
app.services
import
llm_client
MATH_INTENT_PROMPT
=
(
"Du bist ein Parser fuer Matheaufgaben. Entscheide, ob sympy_solve genutzt "
"werden soll. Wenn ja, gib ein JSON-Objekt mit {
\"
use_math
\"
: true, "
"
\"
task
\"
:
\"
solve|simplify|diff|integrate
\"
,
\"
input
\"
:
\"
...
\"
, "
"
\"
symbols
\"
: [
\"
x
\"
, ...]} zurueck. Wenn nein, gib "
"{
\"
use_math
\"
: false} zurueck. Antworte nur mit JSON."
)
def
extract_math_request
(
user_text
:
str
)
->
dict
|
None
:
result
=
llm_client
.
chat
(
messages
=
[
{
"role"
:
"system"
,
"content"
:
MATH_INTENT_PROMPT
},
{
"role"
:
"user"
,
"content"
:
user_text
},
]
)
content
=
llm_client
.
get_message_content
(
result
)
try
:
payload
=
json
.
loads
(
content
)
except
json
.
JSONDecodeError
:
return
None
if
not
payload
.
get
(
"use_math"
):
return
None
task
=
payload
.
get
(
"task"
)
input_text
=
payload
.
get
(
"input"
)
symbols
=
payload
.
get
(
"symbols"
)
or
[]
if
not
task
or
not
input_text
:
return
None
return
{
"task"
:
task
,
"input"
:
input_text
,
"symbols"
:
symbols
}
math-tutor/backend/app/services/orchestrator_engine.py
View file @
ef617e1d
import
json
from
app.services
import
llm_client
,
retrieval_service
,
tool_registry
,
tool_logging
MAX_TOOL_STEPS
=
4
def
_normalize_args
(
raw_args
):
if
isinstance
(
raw_args
,
dict
)
and
"arguments"
in
raw_args
and
isinstance
(
raw_args
[
"arguments"
],
dict
):
return
raw_args
[
"arguments"
]
if
isinstance
(
raw_args
,
str
):
try
:
return
json
.
loads
(
raw_args
)
except
json
.
JSONDecodeError
:
return
{}
if
isinstance
(
raw_args
,
dict
):
return
raw_args
return
{}
def
_system_messages
(
context
:
str
)
->
list
[
dict
]:
return
[
{
"role"
:
"system"
,
"content"
:
retrieval_service
.
SYSTEM_PROMPT
},
{
"role"
:
"system"
,
"content"
:
context
},
{
"role"
:
"system"
,
"content"
:
(
"Nutze Tools, wenn sie relevant sind. "
"Wenn eine mathematische Aufgabe enthalten ist, rufe sympy_solve auf. "
"Wenn zusaetzlicher Kontext benoetigt wird, rufe retrieve_context auf. "
"Loese Aufgaben nicht manuell."
),
},
]
def
run_chat
(
messages
:
list
[
dict
])
->
dict
:
from
app.services
import
context_store
,
decision_tool
,
math_intent
,
retrieval_service
,
tool_logging
from
app.tools
import
hint_tool
,
math_tool
def
_append_tool_log
(
tool_log
:
list
[
dict
],
name
:
str
,
args
:
dict
,
response
:
object
)
->
None
:
tool_log
.
append
({
"name"
:
name
,
"arguments"
:
args
,
"response"
:
response
})
def
_bootstrap_context
(
sheet
:
dict
,
query_text
:
str
,
tool_log
:
list
[
dict
])
->
None
:
context
,
sources
=
retrieval_service
.
retrieve_context
(
query_text
=
query_text
)
context_store
.
add_retrieval_context
(
sheet
,
query_text
,
context
,
sources
)
context_store
.
add_tool_output
(
sheet
,
{
"name"
:
"retrieve_context"
,
"arguments"
:
{
"query"
:
query_text
},
"response"
:
context
},
)
_append_tool_log
(
tool_log
,
"retrieve_context"
,
{
"query"
:
query_text
},
{
"context"
:
context
,
"sources"
:
sources
})
math_request
=
math_intent
.
extract_math_request
(
query_text
)
if
math_request
:
solution
=
math_tool
.
sympy_solve
(
**
math_request
)
context_store
.
add_math_solution
(
sheet
,
math_request
[
"task"
],
math_request
[
"input"
],
math_request
.
get
(
"symbols"
),
solution
,
)
context_store
.
add_tool_output
(
sheet
,
{
"name"
:
"sympy_solve"
,
"arguments"
:
math_request
,
"response"
:
solution
},
)
_append_tool_log
(
tool_log
,
"sympy_solve"
,
math_request
,
solution
)
def
run_chat
(
messages
:
list
[
dict
],
draft
:
str
|
None
=
None
)
->
dict
:
if
not
messages
:
raise
ValueError
(
"messages required"
)
...
...
@@ -43,43 +40,38 @@ def run_chat(messages: list[dict]) -> dict:
if
not
last_user
:
raise
ValueError
(
"last user message required"
)
context
,
sources
=
retrieval_service
.
retrieve_context
(
query_text
=
last_user
.
get
(
"content"
,
""
))
chat_messages
=
_system_messages
(
context
)
+
messages
chat_id
=
context_store
.
get_chat_id
(
messages
,
draft
=
draft
)
new_chat
=
context_store
.
is_new_chat
(
messages
)
sheet
=
context_store
.
load_sheet
(
chat_id
)
if
new_chat
or
not
sheet
:
sheet
=
context_store
.
init_sheet
(
chat_id
,
messages
)
context_store
.
update_history
(
sheet
,
messages
)
tool_log
:
list
[
dict
]
=
[]
reply
=
""
for
_
in
range
(
MAX_TOOL_STEPS
):
result
=
llm_client
.
chat
(
chat_messages
,
tools
=
tool_registry
.
TOOL_SPECS
)
tool_calls
=
llm_client
.
get_tool_calls
(
result
)
if
not
tool_calls
:
reply
=
llm_client
.
get_message_content
(
result
)
break
for
tool_call
in
tool_calls
:
info
=
llm_client
.
normalize_tool_call
(
tool_call
)
name
=
info
.
get
(
"name"
)
args
=
_normalize_args
(
info
.
get
(
"arguments"
))
handler
=
tool_registry
.
TOOL_HANDLERS
.
get
(
name
)
if
not
handler
:
continue
tool_result
=
handler
(
**
args
)
tool_log
.
append
({
"name"
:
name
,
"arguments"
:
args
,
"response"
:
tool_result
})
if
isinstance
(
tool_result
,
dict
)
and
name
==
"retrieve_context"
:
context
=
tool_result
.
get
(
"context"
,
""
)
sources
=
tool_result
.
get
(
"sources"
,
sources
)
tool_content
=
context
else
:
tool_content
=
tool_result
if
not
isinstance
(
tool_content
,
str
):
tool_content
=
json
.
dumps
(
tool_content
,
ensure_ascii
=
True
)
chat_messages
.
append
({
"role"
:
"tool"
,
"name"
:
name
,
"content"
:
tool_content
})
if
new_chat
or
not
sheet
.
get
(
"initialized"
):
_bootstrap_context
(
sheet
,
last_user
.
get
(
"content"
,
""
),
tool_log
)
sheet
[
"initialized"
]
=
True
history_text
=
context_store
.
format_history
(
messages
)
sheet_text
=
context_store
.
format_sheet
(
sheet
)
decision
=
decision_tool
.
needs_more_context
(
history_text
,
sheet_text
)
context_store
.
add_decision
(
sheet
,
decision
)
if
decision
.
get
(
"needs_more_context"
):
_bootstrap_context
(
sheet
,
last_user
.
get
(
"content"
,
""
),
tool_log
)
sheet_text
=
context_store
.
format_sheet
(
sheet
)
reply
=
hint_tool
.
generate_hint
(
task
=
last_user
.
get
(
"content"
,
""
),
solution
=
context_store
.
latest_math_solution
(
sheet
),
history
=
history_text
,
context_sheet
=
sheet_text
,
)
if
not
reply
:
reply
=
"Dazu steht nichts im Material"
context_store
.
save_sheet
(
sheet
)
tool_logging
.
write_tool_log
(
tool_log
)
return
{
"reply"
:
reply
,
"sources"
:
s
ources
,
"tool_log"
:
tool_log
}
return
{
"reply"
:
reply
,
"sources"
:
s
heet
.
get
(
"sources"
,
[])
,
"tool_log"
:
tool_log
}
math-tutor/backend/app/tools/hint_tool.py
View file @
ef617e1d
from
app.services
import
llm_client
def
generate_hint
(
task
:
str
,
solution
:
str
,
history
:
str
|
None
=
None
)
->
str
:
def
generate_hint
(
task
:
str
,
solution
:
str
,
history
:
str
|
None
=
None
,
context_sheet
:
str
|
None
=
None
,
)
->
str
:
prompt
=
(
"Du bist ein didaktischer Tutor. "
"Gib einen naechsten hilfreichen Hinweis, aber keine komplette Loesung. "
...
...
@@ -15,6 +20,8 @@ def generate_hint(task: str, solution: str, history: str | None = None) -> str:
)
if
history
:
prompt
+=
"
\n
Historie:
\n
"
+
history
+
"
\n
"
if
context_sheet
:
prompt
+=
"
\n
Kontextblatt:
\n
"
+
context_sheet
+
"
\n
"
result
=
llm_client
.
chat
(
messages
=
[{
"role"
:
"user"
,
"content"
:
prompt
}],
...
...
@@ -42,6 +49,10 @@ TOOL_SPEC = {
"type"
:
"string"
,
"description"
:
"Optionaler Verlauf, kann leer sein"
,
},
"context_sheet"
:
{
"type"
:
"string"
,
"description"
:
"Optionales Kontextblatt mit Werkzeug- und Retrieval-Infos"
,
},
},
"required"
:
[
"task"
,
"solution"
],
},
...
...
math-tutor/frontend/src/pages/App.tsx
View file @
ef617e1d
...
...
@@ -6,13 +6,7 @@ import DocPanel from "../components/Retrieval/DocPanel";
import
type
{
ChatMessage
}
from
"
../components/Chat/MessageList
"
;
import
type
{
RetrievedDoc
}
from
"
../components/Retrieval/DocPanel
"
;
const
initialMessages
:
ChatMessage
[]
=
[
{
id
:
"
m2
"
,
role
:
"
assistant
"
,
text
:
"
How can I help you? $2+2=5$
"
,
},
];
const
initialMessages
:
ChatMessage
[]
=
[];
export
default
function
App
()
{
const
[
messages
,
setMessages
]
=
useState
<
ChatMessage
[]
>
(
initialMessages
);
...
...
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