Commit 528a483a authored by Kantz's avatar Kantz
Browse files

V1 des Sokrates

parent fc2b7b7c
from __future__ import annotations
from typing import Iterable
from app.deterministic_services import llm_client
HINT_SYSTEM_PROMPT = """
Du bist ein didaktischer Mathe-Tutor.
Antworte auf Deutsch, kurz und präzise.
Nutze ausschließlich den bereitgestellten Kontext.
Stelle vor allem Verständnisfragen und leite den Schüler sokratisch.
Gib keine komplette Lösung aus.
Verwende für mathematische Formeln immer die $-Notation, z.B. $a^2 + b^2 = c^2$.
minf steht für minus unendlich.
"""
def _format_subsection_refs(subsection_refs: Iterable[tuple[int, int, int]] | None) -> str:
if not subsection_refs:
return "Keine Subsection-Referenzen übergeben."
formatted = [f"{chap}:{sec}:{sub}" for chap, sec, sub in subsection_refs]
return ", ".join(formatted)
def generate_dialog(
query: str | None,
subsection_refs: list[tuple[int, int, int]] | None = None,
history: list[dict] | None = None,
sources: str | None = None,
) -> str:
context_parts = [
f"Subsection-Referenzen:\n{_format_subsection_refs(subsection_refs)}",
]
if sources:
context_parts.append(f"Kontext/Sources:\n{sources}")
messages = [{"role": "system", "content": HINT_SYSTEM_PROMPT}]
messages.append({"role": "user", "content": "\n\n".join(context_parts)})
if history:
messages.extend(history)
if query:
messages.append(
{
"role": "user",
"content": (
"AKTUELLE Eingabe des Studenten (höchste Priorität):\n"
f"{query}\n\n"
"Reagiere ausschließlich auf diese Eingabe und bleibe sokratisch."
),
}
)
result = llm_client.chat(messages=messages)
return llm_client.get_message_content(result)
...@@ -75,7 +75,7 @@ def chat(request: ChatRequest) -> ChatResponse: ...@@ -75,7 +75,7 @@ def chat(request: ChatRequest) -> ChatResponse:
try: try:
payload_messages = [{"role": m.role, "content": m.text} for m in request.messages] payload_messages = [{"role": m.role, "content": m.text} for m in request.messages]
if orchestrator_name in {"task", "feedback"}: if orchestrator_name in {"task", "feedback", "socratic"}:
result = orchestrator_impl.run_chat( result = orchestrator_impl.run_chat(
payload_messages, payload_messages,
draft=request.draft, draft=request.draft,
......
...@@ -10,7 +10,7 @@ from app.deterministic_services import context_store, task_catalog ...@@ -10,7 +10,7 @@ from app.deterministic_services import context_store, task_catalog
router = APIRouter() router = APIRouter()
TASK_ORCHESTRATORS = {"task", "feedback"} TASK_ORCHESTRATORS = {"task", "feedback", "socratic"}
class TaskItem(BaseModel): class TaskItem(BaseModel):
......
from __future__ import annotations
from app.LLM_services import socratic_LLM
import app.config as config
from app.deterministic_services import (
context_store,
retrieval_store,
task_catalog,
)
from app.deterministic_services.orchestrators import orchestrator_base as base
def _ensure_context_task_fields(state: base.ChatState, query_text: str) -> tuple[str, str] | None:
store_new = context_store.context_store_new
has_task = bool(store_new.get_task(state.sheet))
has_hints = bool(store_new.get_hints(state.sheet))
has_solution = bool(store_new.get_solution(state.sheet))
if has_task and has_hints and has_solution:
selected = task_catalog.get_selected_task_ids(state.sheet)
if selected[0] and selected[1]:
was_selected = task_catalog.select_task_by_ids(
state.sheet,
selected[0],
selected[1],
)
if was_selected:
return selected[0], selected[1]
sources_text = "\n".join([source.to_string() for source in context_store.get_retrieval(state.sheet)])
selection = task_catalog.select_task_for_context(
state.sheet,
query_text=query_text,
sources_text=sources_text,
history=context_store.get_history_turns(state.sheet),
)
if not selection:
return None
task_file, task_entry = selection
selected_file_id = str(task_file.get("_file_id", ""))
selected_task_id = str(task_entry.get("id", "")).zfill(2)
base.append_tool_log(
state.tool_log,
"task_json_selected",
{"tasks_dir": str(task_catalog.TASKS_DIR)},
{
"file": task_file.get("_path", ""),
"file_id": selected_file_id,
"task_id": selected_task_id,
"hint_count": len(store_new.get_hints(state.sheet)),
"has_solution": bool(store_new.get_solution(state.sheet)),
},
)
return selected_file_id, selected_task_id
def _retrieve_context_for_task(state: base.ChatState, query_text: str) -> int:
refs = task_catalog.get_selected_task_subsection_refs(state.sheet)
if not refs:
return 0
def _retrieve() -> dict:
sources = retrieval_store.retrieve_for_subsections(
pg_url=config.get_postgres_url(),
subsection_refs=refs,
)
context_store.update_retrieval_context(state.sheet, sources)
return {
"subsection_refs": refs,
"source_count": len(sources),
}
result = base.log_timed_call(
state.tool_log,
"retrieve_context_with_task_subsections",
{
"query": query_text,
"subsection_refs": refs,
},
_retrieve,
)
return int(result.get("source_count", 0))
def _on_bootstrap(state: base.ChatState, query_text: str) -> None:
_ensure_context_task_fields(state, query_text)
_retrieve_context_for_task(state, query_text)
def _on_turn_logic(state: base.ChatState) -> None:
_ensure_context_task_fields(state, state.last_user)
def _on_build_reply(state: base.ChatState) -> str | None:
history_turns = context_store.get_history_turns(state.sheet)
subsection_refs = task_catalog.get_selected_task_subsection_refs(state.sheet)
args = {
"query": state.last_user,
"subsection_refs": subsection_refs,
"history": history_turns,
"sources": "\n".join([source.to_string() for source in context_store.get_retrieval(state.sheet)]),
}
return base.log_timed_call(
state.tool_log,
"socratic_generate_dialog",
args,
lambda: socratic_LLM.generate_dialog(**args),
)
def run_chat(
messages: list[dict],
draft: str | None = None,
selected_task: dict | None = None,
) -> dict:
def _apply_selected_task(state: base.ChatState) -> None:
if not selected_task:
return
selected_file_id = str(selected_task.get("file_id", "")).strip()
selected_task_id = str(selected_task.get("task_id", "")).strip()
if selected_file_id and selected_task_id:
task_catalog.select_task_by_ids(
state.sheet,
selected_file_id,
selected_task_id,
)
def on_bootstrap(state: base.ChatState, query_text: str) -> None:
_apply_selected_task(state)
_on_bootstrap(state, query_text)
def on_turn_logic(state: base.ChatState) -> None:
_apply_selected_task(state)
_on_turn_logic(state)
return base.run_chat_common(
messages=messages,
draft=draft,
on_bootstrap=on_bootstrap,
on_turn_logic=on_turn_logic,
on_build_reply=_on_build_reply,
init_sheet_fn=context_store.context_store_new.init_sheet,
)
...@@ -6,17 +6,19 @@ import app.config as config ...@@ -6,17 +6,19 @@ import app.config as config
from app.deterministic_services.orchestrators import ( from app.deterministic_services.orchestrators import (
orchestrator_feedback, orchestrator_feedback,
orchestrator_qa, orchestrator_qa,
orchestrator_socratic,
orchestrator_task, orchestrator_task,
orchestrator_tutor, orchestrator_tutor,
) )
AVAILABLE_ORCHESTRATORS: tuple[str, ...] = ("qa", "tutor", "task", "feedback") AVAILABLE_ORCHESTRATORS: tuple[str, ...] = ("qa", "tutor", "task", "feedback", "socratic")
_ORCHESTRATOR_MODULES: dict[str, Any] = { _ORCHESTRATOR_MODULES: dict[str, Any] = {
"qa": orchestrator_qa, "qa": orchestrator_qa,
"tutor": orchestrator_tutor, "tutor": orchestrator_tutor,
"task": orchestrator_task, "task": orchestrator_task,
"feedback": orchestrator_feedback, "feedback": orchestrator_feedback,
"socratic": orchestrator_socratic,
} }
......
export type OrchestratorName = "qa" | "tutor" | "task" | "feedback"; export type OrchestratorName = "qa" | "tutor" | "task" | "feedback" | "socratic";
export type OrchestratorConfigResponse = { export type OrchestratorConfigResponse = {
default_orchestrator: OrchestratorName; default_orchestrator: OrchestratorName;
available_orchestrators: OrchestratorName[]; available_orchestrators: OrchestratorName[];
}; };
const FALLBACK_ORCHESTRATORS: OrchestratorName[] = ["qa", "tutor", "task", "feedback"]; const FALLBACK_ORCHESTRATORS: OrchestratorName[] = ["qa", "tutor", "task", "feedback", "socratic"];
const normalizeOrchestrator = (value: string): OrchestratorName | null => { const normalizeOrchestrator = (value: string): OrchestratorName | null => {
if (value === "qa" || value === "tutor" || value === "task" || value === "feedback") { if (
value === "qa" ||
value === "tutor" ||
value === "task" ||
value === "feedback" ||
value === "socratic"
) {
return value; return value;
} }
return null; return null;
......
...@@ -14,12 +14,14 @@ type ModeMeta = { ...@@ -14,12 +14,14 @@ type ModeMeta = {
| "orchestratorModeQaLabel" | "orchestratorModeQaLabel"
| "orchestratorModeTutorLabel" | "orchestratorModeTutorLabel"
| "orchestratorModeTaskLabel" | "orchestratorModeTaskLabel"
| "orchestratorModeFeedbackLabel"; | "orchestratorModeFeedbackLabel"
| "orchestratorModeSocraticLabel";
descriptionKey: descriptionKey:
| "orchestratorModeQaDescription" | "orchestratorModeQaDescription"
| "orchestratorModeTutorDescription" | "orchestratorModeTutorDescription"
| "orchestratorModeTaskDescription" | "orchestratorModeTaskDescription"
| "orchestratorModeFeedbackDescription"; | "orchestratorModeFeedbackDescription"
| "orchestratorModeSocraticDescription";
}; };
const modeMetaMap: Record<OrchestratorName, ModeMeta> = { const modeMetaMap: Record<OrchestratorName, ModeMeta> = {
...@@ -39,6 +41,10 @@ const modeMetaMap: Record<OrchestratorName, ModeMeta> = { ...@@ -39,6 +41,10 @@ const modeMetaMap: Record<OrchestratorName, ModeMeta> = {
labelKey: "orchestratorModeFeedbackLabel", labelKey: "orchestratorModeFeedbackLabel",
descriptionKey: "orchestratorModeFeedbackDescription", descriptionKey: "orchestratorModeFeedbackDescription",
}, },
socratic: {
labelKey: "orchestratorModeSocraticLabel",
descriptionKey: "orchestratorModeSocraticDescription",
},
}; };
type ModeDescriptionDisplay = "tooltip" | "helperText" | "optionSuffix"; type ModeDescriptionDisplay = "tooltip" | "helperText" | "optionSuffix";
......
...@@ -22,11 +22,14 @@ ...@@ -22,11 +22,14 @@
orchestratorModeTutorLabel: "Tutor", orchestratorModeTutorLabel: "Tutor",
orchestratorModeTaskLabel: "Task", orchestratorModeTaskLabel: "Task",
orchestratorModeFeedbackLabel: "Feedback", orchestratorModeFeedbackLabel: "Feedback",
orchestratorModeSocraticLabel: "Socratic",
orchestratorModeDescriptionTitle: "Mode help", orchestratorModeDescriptionTitle: "Mode help",
orchestratorModeQaDescription: "Direct answers based on the script", orchestratorModeQaDescription: "Direct answers based on the script",
orchestratorModeTutorDescription: "Help with your own questions", orchestratorModeTutorDescription: "Help with your own questions",
orchestratorModeTaskDescription: "Help with textbook exercises", orchestratorModeTaskDescription: "Help with textbook exercises",
orchestratorModeFeedbackDescription: "Short feedback on your solution for textbook exercises", orchestratorModeFeedbackDescription: "Short feedback on your solution for textbook exercises",
orchestratorModeSocraticDescription:
"Socratic guidance based on fixed subsection context for textbook exercises",
directChildren: "Direct children", directChildren: "Direct children",
taskChildren: "Task sources", taskChildren: "Task sources",
indirectChildren: "Indirect children", indirectChildren: "Indirect children",
...@@ -106,11 +109,14 @@ ...@@ -106,11 +109,14 @@
orchestratorModeTutorLabel: "Tutor", orchestratorModeTutorLabel: "Tutor",
orchestratorModeTaskLabel: "Task", orchestratorModeTaskLabel: "Task",
orchestratorModeFeedbackLabel: "Feedback", orchestratorModeFeedbackLabel: "Feedback",
orchestratorModeSocraticLabel: "Socratic",
orchestratorModeDescriptionTitle: "Modus-Hilfe", orchestratorModeDescriptionTitle: "Modus-Hilfe",
orchestratorModeQaDescription: "Direkte Antworten basierend auf dem Skript", orchestratorModeQaDescription: "Direkte Antworten basierend auf dem Skript",
orchestratorModeTutorDescription: "Hilfe bei selbst gestellten Fragen", orchestratorModeTutorDescription: "Hilfe bei selbst gestellten Fragen",
orchestratorModeTaskDescription: "Hilfe bei Aufgaben aus dem Lehrwerk", orchestratorModeTaskDescription: "Hilfe bei Aufgaben aus dem Lehrwerk",
orchestratorModeFeedbackDescription: "Kurzes Feedback zu deiner Lösung bei Aufgaben aus dem Lehrwerk", orchestratorModeFeedbackDescription: "Kurzes Feedback zu deiner Lösung bei Aufgaben aus dem Lehrwerk",
orchestratorModeSocraticDescription:
"Sokratische Anleitung auf Basis fester Subsection-Kontexte für Aufgaben aus dem Lehrwerk",
directChildren: "Direkte Quellen", directChildren: "Direkte Quellen",
taskChildren: "Aufgaben-Quellen", taskChildren: "Aufgaben-Quellen",
indirectChildren: "Indirekte Quellen", indirectChildren: "Indirekte Quellen",
......
...@@ -17,14 +17,20 @@ import sumintLogo from "../../SuMINT-Logo.png"; ...@@ -17,14 +17,20 @@ import sumintLogo from "../../SuMINT-Logo.png";
const initialMessages: ChatMessage[] = []; const initialMessages: ChatMessage[] = [];
const normalizeOrchestrator = (value: string | null | undefined): OrchestratorName | null => { const normalizeOrchestrator = (value: string | null | undefined): OrchestratorName | null => {
if (value === "qa" || value === "tutor" || value === "task" || value === "feedback") { if (
value === "qa" ||
value === "tutor" ||
value === "task" ||
value === "feedback" ||
value === "socratic"
) {
return value; return value;
} }
return null; return null;
}; };
const isTaskCoupledOrchestrator = (value: string | null | undefined): boolean => const isTaskCoupledOrchestrator = (value: string | null | undefined): boolean =>
value === "task" || value === "feedback"; value === "task" || value === "feedback" || value === "socratic";
type ArchivedChatSummary = { type ArchivedChatSummary = {
chat_id: string; chat_id: string;
...@@ -214,9 +220,11 @@ export default function ChatPage() { ...@@ -214,9 +220,11 @@ export default function ChatPage() {
} }
setSelectedOrchestrator( setSelectedOrchestrator(
deepLinkTarget.isTaskOrchestrator && searchParams.get("orchestrator") === "feedback" searchParams.get("orchestrator") === "feedback"
? "feedback" ? "feedback"
: "task" : searchParams.get("orchestrator") === "socratic"
? "socratic"
: "task"
); );
setTaskRef({ fileId: selectedFile.file_id, taskId: selectedTask.task_id }); setTaskRef({ fileId: selectedFile.file_id, taskId: selectedTask.task_id });
unlockTask(); unlockTask();
......
...@@ -9,7 +9,7 @@ import "../styles/theme.css"; ...@@ -9,7 +9,7 @@ import "../styles/theme.css";
import sumintLogo from "../../SuMINT-Logo.png"; import sumintLogo from "../../SuMINT-Logo.png";
const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean => const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean =>
value === "task" || value === "feedback"; value === "task" || value === "feedback" || value === "socratic";
export default function TaskSelectionPage() { export default function TaskSelectionPage() {
const navigate = useNavigate(); const navigate = useNavigate();
......
...@@ -69,7 +69,7 @@ const isSelectableTaskFile = (file: TaskFile): boolean => ...@@ -69,7 +69,7 @@ const isSelectableTaskFile = (file: TaskFile): boolean =>
Array.isArray(file.subsections) && file.subsections.length > 0; Array.isArray(file.subsections) && file.subsections.length > 0;
const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean => const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean =>
value === "task" || value === "feedback"; value === "task" || value === "feedback" || value === "socratic";
export function TutorSessionProvider({ children }: PropsWithChildren) { export function TutorSessionProvider({ children }: PropsWithChildren) {
const [chatSessionId, setChatSessionId] = useState<string>(() => createSessionId()); const [chatSessionId, setChatSessionId] = useState<string>(() => createSessionId());
......
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