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:
try:
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(
payload_messages,
draft=request.draft,
......
......@@ -10,7 +10,7 @@ from app.deterministic_services import context_store, task_catalog
router = APIRouter()
TASK_ORCHESTRATORS = {"task", "feedback"}
TASK_ORCHESTRATORS = {"task", "feedback", "socratic"}
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
from app.deterministic_services.orchestrators import (
orchestrator_feedback,
orchestrator_qa,
orchestrator_socratic,
orchestrator_task,
orchestrator_tutor,
)
AVAILABLE_ORCHESTRATORS: tuple[str, ...] = ("qa", "tutor", "task", "feedback")
AVAILABLE_ORCHESTRATORS: tuple[str, ...] = ("qa", "tutor", "task", "feedback", "socratic")
_ORCHESTRATOR_MODULES: dict[str, Any] = {
"qa": orchestrator_qa,
"tutor": orchestrator_tutor,
"task": orchestrator_task,
"feedback": orchestrator_feedback,
"socratic": orchestrator_socratic,
}
......
export type OrchestratorName = "qa" | "tutor" | "task" | "feedback";
export type OrchestratorName = "qa" | "tutor" | "task" | "feedback" | "socratic";
export type OrchestratorConfigResponse = {
default_orchestrator: 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 => {
if (value === "qa" || value === "tutor" || value === "task" || value === "feedback") {
if (
value === "qa" ||
value === "tutor" ||
value === "task" ||
value === "feedback" ||
value === "socratic"
) {
return value;
}
return null;
......
......@@ -14,12 +14,14 @@ type ModeMeta = {
| "orchestratorModeQaLabel"
| "orchestratorModeTutorLabel"
| "orchestratorModeTaskLabel"
| "orchestratorModeFeedbackLabel";
| "orchestratorModeFeedbackLabel"
| "orchestratorModeSocraticLabel";
descriptionKey:
| "orchestratorModeQaDescription"
| "orchestratorModeTutorDescription"
| "orchestratorModeTaskDescription"
| "orchestratorModeFeedbackDescription";
| "orchestratorModeFeedbackDescription"
| "orchestratorModeSocraticDescription";
};
const modeMetaMap: Record<OrchestratorName, ModeMeta> = {
......@@ -39,6 +41,10 @@ const modeMetaMap: Record<OrchestratorName, ModeMeta> = {
labelKey: "orchestratorModeFeedbackLabel",
descriptionKey: "orchestratorModeFeedbackDescription",
},
socratic: {
labelKey: "orchestratorModeSocraticLabel",
descriptionKey: "orchestratorModeSocraticDescription",
},
};
type ModeDescriptionDisplay = "tooltip" | "helperText" | "optionSuffix";
......
......@@ -22,11 +22,14 @@
orchestratorModeTutorLabel: "Tutor",
orchestratorModeTaskLabel: "Task",
orchestratorModeFeedbackLabel: "Feedback",
orchestratorModeSocraticLabel: "Socratic",
orchestratorModeDescriptionTitle: "Mode help",
orchestratorModeQaDescription: "Direct answers based on the script",
orchestratorModeTutorDescription: "Help with your own questions",
orchestratorModeTaskDescription: "Help with 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",
taskChildren: "Task sources",
indirectChildren: "Indirect children",
......@@ -106,11 +109,14 @@
orchestratorModeTutorLabel: "Tutor",
orchestratorModeTaskLabel: "Task",
orchestratorModeFeedbackLabel: "Feedback",
orchestratorModeSocraticLabel: "Socratic",
orchestratorModeDescriptionTitle: "Modus-Hilfe",
orchestratorModeQaDescription: "Direkte Antworten basierend auf dem Skript",
orchestratorModeTutorDescription: "Hilfe bei selbst gestellten Fragen",
orchestratorModeTaskDescription: "Hilfe 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",
taskChildren: "Aufgaben-Quellen",
indirectChildren: "Indirekte Quellen",
......
......@@ -17,14 +17,20 @@ import sumintLogo from "../../SuMINT-Logo.png";
const initialMessages: ChatMessage[] = [];
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 null;
};
const isTaskCoupledOrchestrator = (value: string | null | undefined): boolean =>
value === "task" || value === "feedback";
value === "task" || value === "feedback" || value === "socratic";
type ArchivedChatSummary = {
chat_id: string;
......@@ -214,9 +220,11 @@ export default function ChatPage() {
}
setSelectedOrchestrator(
deepLinkTarget.isTaskOrchestrator && searchParams.get("orchestrator") === "feedback"
searchParams.get("orchestrator") === "feedback"
? "feedback"
: "task"
: searchParams.get("orchestrator") === "socratic"
? "socratic"
: "task"
);
setTaskRef({ fileId: selectedFile.file_id, taskId: selectedTask.task_id });
unlockTask();
......
......@@ -9,7 +9,7 @@ import "../styles/theme.css";
import sumintLogo from "../../SuMINT-Logo.png";
const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean =>
value === "task" || value === "feedback";
value === "task" || value === "feedback" || value === "socratic";
export default function TaskSelectionPage() {
const navigate = useNavigate();
......
......@@ -69,7 +69,7 @@ const isSelectableTaskFile = (file: TaskFile): boolean =>
Array.isArray(file.subsections) && file.subsections.length > 0;
const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean =>
value === "task" || value === "feedback";
value === "task" || value === "feedback" || value === "socratic";
export function TutorSessionProvider({ children }: PropsWithChildren) {
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