Commit ef617e1d authored by Kantz's avatar Kantz
Browse files

Contex Sheet first draft

parent 927e6a37
......@@ -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"]
......
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}\nINPUT: {input}\nSYMBOLS: {symbols}\nSOLUTION: {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
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\nKontextblatt:\n"
+ context_sheet
+ "\n\nAntwortformat: 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"}
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}
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": sources, "tool_log": tool_log}
return {"reply": reply, "sources": sheet.get("sources", []), "tool_log": tool_log}
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 += "\nHistorie:\n" + history + "\n"
if context_sheet:
prompt += "\nKontextblatt:\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"],
},
......
......@@ -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);
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment