Commit f339c3c3 authored by Kantz's avatar Kantz
Browse files

Merge branch 'dev' into 'main'

feedback mode in task modus überführt und altlasten entfernt

See merge request kantz/tutor_react!22
parents 3cc92d8d 7798ce2b
from __future__ import annotations
from app.deterministic_services import llm_client
SYSTEM_PROMPT = """
Du bist ein didaktischer Mathe-Tutor. Du gibst Feedback zu den Lösungen des Nutzers zur Gegebenen Frage.
Vergleiche das Ergebnis mit der korrekten Lösung.
Stelle Nachfragen wenn der Lösungsweg nicht vollständig ist.
Die Reihenfolge in mathematischen Ausdrücken ist irrelevant wenn sie äquivalent sind also a+b = b+a und a^bc = a^cb.
Zitiere 1 zu 1 aus der AKTUELLE Eingabe des Nutzers wenn du auf Fehler aufmerksam machst.
verwende die $ für mathematische Formeln, z.B. $a^2 + b^2 = c^2$
minf steht für minus unendlich.
Halte dich kurz und präzise.
Denke nach bevor du eine Ausgabe macht.
"""
def generate_hint(
query: str | None,
task: str,
hints: list[str],
solution: str,
history: list[dict] | None = None,
sources: str | None = None,
) -> str:
context_parts = [
f"Hier ist die zu lösende Aufgaben:{task}\n",
f"Hier ist eine korrekte Lösung als Referenz:{solution}\n",
f"Hier ist ein exemplarischer Lösungsweg:{hints}\n"
]
if sources:
context_parts.append(f"Kontext/Sources:\n{sources}")
messages = [{"role": "system", "content": SYSTEM_PROMPT}]
# Kompakter Kontext als eine Nachricht (kein langer Fließtext mit History mischen)
messages.append({"role": "user", "content": "\n\n".join(context_parts)})
# History als echte Turns (und ggf. begrenzen, siehe Punkt 2)
if history:
messages.extend(history)
# Aktuelle Frage als letzte Nachricht, fett hervorgehoben durch Struktur/Delimiters
messages.append({
"role": "user",
"content": f"AKTUELLE Eingabe des Studenten (höchste Priorität):\n{query}"
})
result = llm_client.chat(messages=messages)
return llm_client.get_message_content(result)
...@@ -3,17 +3,17 @@ from app.deterministic_services import llm_client ...@@ -3,17 +3,17 @@ from app.deterministic_services import llm_client
HINT_SYSTEM_PROMPT = """ HINT_SYSTEM_PROMPT = """
Du bist ein didaktischer Mathe-Tutor. Du beantwortest die mathematischen Fragen des Benutzers. Wenn ein Benutzer dir eine mathematische Aufgabe vorlegt, solltest du nicht direkt die Lösung nennen. Stattdessen sollst du den Benutzer zur Lösung der Aufgabe anleiten, indem Sie die sokratische Fragetechnik oder Scaffolding-Fragen verwenden, um den Schülern zu helfen, die Antwort selbst zu finden. Nutze dazu den exemplarischen Lösungsweg. Du kannst den Schüler beispielsweise zu einem bestimmten Teil der Frage befragen und ihn bitten, seine Gedanken dazu näher zu erläutern. Du sollst nach der Frage nicht selbst antworten, sondern den Nutzer zuerst antworten lassen. Stattdessen kannst du mögliche Richtungen aufzeigen und die Schüler bitten, weiter darüber nachzudenken, oder sie bitten, ihre Gedanken zu einem bestimmten Teil des Problems zu präzisieren und näher zu erläutern. Du bist ein didaktischer Mathe-Tutor. Du gibst Feedback zu den Lösungen des Nutzers zur Gegebenen Frage.
1) Antworte NUR mit einem kurzen Tipp (1 Satz), keine Beispiele, keine Herleitung, keine komplette Lösung. Vergleiche das Ergebnis mit der korrekten Lösung.
2) Beziehe dich PRIMÄR auf die 'AKTUELLE Eingabe'.
3) Wenn der Nutzer etwas Falsches sagt weiße ihn darauf hin. Stelle Nachfragen wenn der Lösungsweg nicht vollständig ist.
4) Die Übergebene Lösung ist IMMER korrekt.
5) Der Nutzer kenn die korrekte Lösung und den exemplarischer Lösungsweg nicht. Die Reihenfolge in mathematischen Ausdrücken ist irrelevant wenn sie äquivalent sind also a+b = b+a und a^bc = a^cb.
6) Die Reihenfolge in mathematischen Ausdrücken ist irrelevant wenn sie äquivalent sind also a+b = b+a und a^bc = a^cb.
7) Zitiere 1 zu 1 aus der AKTUELLE Eingabe des Nutzers wenn du auf Fehler aufmerksam machst. Zitiere 1 zu 1 aus der AKTUELLE Eingabe des Nutzers wenn du auf Fehler aufmerksam machst.
8) minf steht für minus unendlich.
verwende die $ für mathematische Formeln, z.B. $a^2 + b^2 = c^2$ verwende die $ für mathematische Formeln, z.B. $a^2 + b^2 = c^2$
minf steht für minus unendlich.
Halte dich kurz und präzise. Halte dich kurz und präzise.
Denke nach bevor du eine Ausgabe macht. Denke nach bevor du eine Ausgabe macht.
......
...@@ -94,7 +94,7 @@ def chat(request: ChatRequest) -> ChatResponse: ...@@ -94,7 +94,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", "socratic"}: if orchestrator_name in {"task", "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, socratic_ora ...@@ -10,7 +10,7 @@ from app.deterministic_services import context_store, task_catalog, socratic_ora
router = APIRouter() router = APIRouter()
TASK_ORCHESTRATORS = {"task", "feedback", "socratic"} TASK_ORCHESTRATORS = {"task", "socratic"}
class TaskItem(BaseModel): class TaskItem(BaseModel):
......
from __future__ import annotations
from app.LLM_services import feedback_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:
source_count = _retrieve_context_for_task(state, query_text)
if source_count > 0:
_ensure_context_task_fields(state, query_text)
task_text = context_store.context_store_new.get_task(state.sheet).strip()
retrieval_query = query_text
if task_text:
retrieval_query = f"Aufgabe:\n{task_text}\n\n{query_text}"
base.bootstrap_retrieval(state.sheet, retrieval_query, state.tool_log)
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:
store_new = context_store.context_store_new
history_turns = context_store.get_history_turns(state.sheet)
args = {
"query": state.last_user if not state.new_chat else None,
"task": store_new.get_task(state.sheet),
"hints": store_new.get_hints(state.sheet),
"solution": store_new.get_solution(state.sheet),
"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,
"new_generate_feedback",
args,
lambda: feedback_LLM.generate_hint(**args),
)
def run_chat(
messages: list[dict],
draft: str | None = None,
selected_task: dict | None = None,
selected_subsection: 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,
)
...@@ -4,20 +4,18 @@ from typing import Any ...@@ -4,20 +4,18 @@ from typing import Any
import app.config as config import app.config as config
from app.deterministic_services.orchestrators import ( from app.deterministic_services.orchestrators import (
orchestrator_feedback,
orchestrator_qa, orchestrator_qa,
orchestrator_socratic, orchestrator_socratic,
orchestrator_task, orchestrator_task,
orchestrator_tutor, orchestrator_tutor,
) )
AVAILABLE_ORCHESTRATORS: tuple[str, ...] = ("qa", "tutor", "task", "feedback", "socratic") AVAILABLE_ORCHESTRATORS: tuple[str, ...] = ("qa", "tutor", "task", "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,
"socratic": orchestrator_socratic, "socratic": orchestrator_socratic,
} }
......
export type OrchestratorName = "qa" | "tutor" | "task" | "feedback" | "socratic"; export type OrchestratorName = "qa" | "tutor" | "task" | "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", "socratic"]; const FALLBACK_ORCHESTRATORS: OrchestratorName[] = ["qa", "tutor", "task", "socratic"];
const normalizeOrchestrator = (value: string): OrchestratorName | null => { const normalizeOrchestrator = (value: string): OrchestratorName | null => {
if ( if (
value === "qa" || value === "qa" ||
value === "tutor" || value === "tutor" ||
value === "task" || value === "task" ||
value === "feedback" ||
value === "socratic" value === "socratic"
) { ) {
return value; return value;
......
...@@ -14,13 +14,11 @@ type ModeMeta = { ...@@ -14,13 +14,11 @@ type ModeMeta = {
| "orchestratorModeQaLabel" | "orchestratorModeQaLabel"
| "orchestratorModeTutorLabel" | "orchestratorModeTutorLabel"
| "orchestratorModeTaskLabel" | "orchestratorModeTaskLabel"
| "orchestratorModeFeedbackLabel"
| "orchestratorModeSocraticLabel"; | "orchestratorModeSocraticLabel";
descriptionKey: descriptionKey:
| "orchestratorModeQaDescription" | "orchestratorModeQaDescription"
| "orchestratorModeTutorDescription" | "orchestratorModeTutorDescription"
| "orchestratorModeTaskDescription" | "orchestratorModeTaskDescription"
| "orchestratorModeFeedbackDescription"
| "orchestratorModeSocraticDescription"; | "orchestratorModeSocraticDescription";
}; };
...@@ -37,10 +35,6 @@ const modeMetaMap: Record<OrchestratorName, ModeMeta> = { ...@@ -37,10 +35,6 @@ const modeMetaMap: Record<OrchestratorName, ModeMeta> = {
labelKey: "orchestratorModeTaskLabel", labelKey: "orchestratorModeTaskLabel",
descriptionKey: "orchestratorModeTaskDescription", descriptionKey: "orchestratorModeTaskDescription",
}, },
feedback: {
labelKey: "orchestratorModeFeedbackLabel",
descriptionKey: "orchestratorModeFeedbackDescription",
},
socratic: { socratic: {
labelKey: "orchestratorModeSocraticLabel", labelKey: "orchestratorModeSocraticLabel",
descriptionKey: "orchestratorModeSocraticDescription", descriptionKey: "orchestratorModeSocraticDescription",
......
...@@ -21,13 +21,11 @@ ...@@ -21,13 +21,11 @@
orchestratorModeQaLabel: "QA", orchestratorModeQaLabel: "QA",
orchestratorModeTutorLabel: "Tutor", orchestratorModeTutorLabel: "Tutor",
orchestratorModeTaskLabel: "Task", orchestratorModeTaskLabel: "Task",
orchestratorModeFeedbackLabel: "Feedback",
orchestratorModeSocraticLabel: "Socratic", 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",
orchestratorModeSocraticDescription: orchestratorModeSocraticDescription:
"Socratic guidance based on fixed subsection context for textbook exercises", "Socratic guidance based on fixed subsection context for textbook exercises",
directChildren: "Direct children", directChildren: "Direct children",
...@@ -122,13 +120,11 @@ ...@@ -122,13 +120,11 @@
orchestratorModeQaLabel: "QA", orchestratorModeQaLabel: "QA",
orchestratorModeTutorLabel: "Tutor", orchestratorModeTutorLabel: "Tutor",
orchestratorModeTaskLabel: "Task", orchestratorModeTaskLabel: "Task",
orchestratorModeFeedbackLabel: "Feedback",
orchestratorModeSocraticLabel: "Socratic", 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",
orchestratorModeSocraticDescription: orchestratorModeSocraticDescription:
"Sokratische Anleitung auf Basis fester Subsection-Kontexte für Aufgaben aus dem Lehrwerk", "Sokratische Anleitung auf Basis fester Subsection-Kontexte für Aufgaben aus dem Lehrwerk",
directChildren: "Direkte Quellen", directChildren: "Direkte Quellen",
......
...@@ -24,7 +24,6 @@ const normalizeOrchestrator = (value: string | null | undefined): OrchestratorNa ...@@ -24,7 +24,6 @@ const normalizeOrchestrator = (value: string | null | undefined): OrchestratorNa
value === "qa" || value === "qa" ||
value === "tutor" || value === "tutor" ||
value === "task" || value === "task" ||
value === "feedback" ||
value === "socratic" value === "socratic"
) { ) {
return value; return value;
...@@ -33,7 +32,7 @@ const normalizeOrchestrator = (value: string | null | undefined): OrchestratorNa ...@@ -33,7 +32,7 @@ const normalizeOrchestrator = (value: string | null | undefined): OrchestratorNa
}; };
const isTaskCoupledOrchestrator = (value: string | null | undefined): boolean => const isTaskCoupledOrchestrator = (value: string | null | undefined): boolean =>
value === "task" || value === "feedback"; value === "task";
type ArchivedChatSummary = { type ArchivedChatSummary = {
chat_id: string; chat_id: string;
...@@ -270,11 +269,7 @@ export default function ChatPage() { ...@@ -270,11 +269,7 @@ export default function ChatPage() {
processedDeepLinkRef.current = deepLinkTarget.key; processedDeepLinkRef.current = deepLinkTarget.key;
const targetRoute = getSelectionRouteForOrchestrator( const targetRoute = getSelectionRouteForOrchestrator(
searchParams.get("orchestrator") === "feedback" searchParams.get("orchestrator") === "socratic" ? "socratic" : "task"
? "feedback"
: searchParams.get("orchestrator") === "socratic"
? "socratic"
: "task"
); );
if ( if (
...@@ -314,11 +309,7 @@ export default function ChatPage() { ...@@ -314,11 +309,7 @@ export default function ChatPage() {
navigate(targetRoute, { replace: true }); navigate(targetRoute, { replace: true });
return; return;
} }
setSelectedOrchestrator( setSelectedOrchestrator("task");
searchParams.get("orchestrator") === "feedback"
? "feedback"
: "task"
);
setSubsectionRef(null); setSubsectionRef(null);
setTaskRef({ fileId: selectedFile.file_id, taskId: selectedTask.task_id }); setTaskRef({ fileId: selectedFile.file_id, taskId: selectedTask.task_id });
} }
...@@ -758,9 +749,8 @@ export default function ChatPage() { ...@@ -758,9 +749,8 @@ export default function ChatPage() {
clearDraft: true, clearDraft: true,
}); });
}; };
const handleFeedbackTip = async () => { const handleFeedbackTip = async () => {
if (selectedOrchestrator !== "feedback") { if (selectedOrchestrator !== "task") {
return; return;
} }
...@@ -771,7 +761,7 @@ export default function ChatPage() { ...@@ -771,7 +761,7 @@ export default function ChatPage() {
}; };
const handleRevealSolution = async () => { const handleRevealSolution = async () => {
if (selectedOrchestrator !== "feedback" || isSending) { if (selectedOrchestrator !== "task" || isSending) {
return; return;
} }
...@@ -1237,9 +1227,9 @@ export default function ChatPage() { ...@@ -1237,9 +1227,9 @@ export default function ChatPage() {
draft={draft} draft={draft}
onDraftChange={handleDraftChange} onDraftChange={handleDraftChange}
onSend={handleSend} onSend={handleSend}
onTip={selectedOrchestrator === "feedback" ? handleFeedbackTip : undefined} onTip={selectedOrchestrator === "task" ? handleFeedbackTip : undefined}
onRevealSolution={ onRevealSolution={
selectedOrchestrator === "feedback" ? handleRevealSolution : undefined selectedOrchestrator === "task" ? handleRevealSolution : undefined
} }
isSending={isSending} isSending={isSending}
onHistoryNavigate={handleHistoryNavigate} onHistoryNavigate={handleHistoryNavigate}
...@@ -1346,3 +1336,4 @@ export default function ChatPage() { ...@@ -1346,3 +1336,4 @@ export default function ChatPage() {
</div> </div>
); );
} }
...@@ -89,7 +89,7 @@ const isSelectableTaskFile = (file: TaskFile): boolean => ...@@ -89,7 +89,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 === "socratic"; value === "task" || value === "socratic";
const formatSubsectionRefs = (refs: [number, number, number][]): string => const formatSubsectionRefs = (refs: [number, number, number][]): string =>
refs.map((ref) => ref.join(":")).join(", "); refs.map((ref) => ref.join(":")).join(", ");
......
import type { OrchestratorName } from "../api/orchestratorApi"; import type { OrchestratorName } from "../api/orchestratorApi";
export const isTaskSelectionOrchestrator = (value: OrchestratorName): boolean => export const isTaskSelectionOrchestrator = (value: OrchestratorName): boolean =>
value === "task" || value === "feedback"; value === "task";
export const isSocraticOrchestrator = (value: OrchestratorName): boolean => export const isSocraticOrchestrator = (value: OrchestratorName): boolean =>
value === "socratic"; value === "socratic";
...@@ -10,7 +10,7 @@ export const getSelectionRouteForOrchestrator = (value: OrchestratorName): strin ...@@ -10,7 +10,7 @@ export const getSelectionRouteForOrchestrator = (value: OrchestratorName): strin
if (value === "socratic") { if (value === "socratic") {
return "/select-socratic"; return "/select-socratic";
} }
if (value === "task" || value === "feedback") { if (value === "task") {
return "/select-task"; return "/select-task";
} }
return "/chat"; return "/chat";
......
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