Commit a4c0b261 authored by Kantz's avatar Kantz
Browse files

Feedback mit Thinking

parent 6c805864
from __future__ import annotations
from app.deterministic_services import llm_client
SYSTEM_PROMPT = """
Du bist ein didaktischer Mathe-Tutor. Du gibts 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.
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$
Halte dich kurz und präzise.
"""
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)
......@@ -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 == "task":
if orchestrator_name in {"task", "feedback"}:
result = orchestrator_impl.run_chat(
payload_messages,
draft=request.draft,
......
......@@ -10,6 +10,8 @@ from app.deterministic_services import context_store, task_catalog
router = APIRouter()
TASK_ORCHESTRATORS = {"task", "feedback"}
class TaskItem(BaseModel):
task_id: str
......@@ -46,7 +48,7 @@ class SelectTaskResponse(BaseModel):
@router.get("/api/tasks/config")
def get_task_config() -> dict[str, object]:
orchestrator = config.get_orchestrator()
return {"orchestrator": orchestrator, "enabled": orchestrator == "task"}
return {"orchestrator": orchestrator, "enabled": orchestrator in TASK_ORCHESTRATORS}
@router.get("/api/tasks", response_model=TasksResponse)
......@@ -55,7 +57,7 @@ def list_tasks() -> TasksResponse:
task_files = task_catalog.build_task_catalog()
return TasksResponse(
orchestrator=orchestrator,
enabled=orchestrator == "task",
enabled=orchestrator in TASK_ORCHESTRATORS,
task_files=task_files,
)
......
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)
print(f"_retrieve_context_for_task source_count={source_count}")
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,
) -> 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,
)
......@@ -4,17 +4,19 @@ from typing import Any
import app.config as config
from app.deterministic_services.orchestrators import (
orchestrator_feedback,
orchestrator_qa,
orchestrator_task,
orchestrator_tutor,
)
AVAILABLE_ORCHESTRATORS: tuple[str, ...] = ("qa", "tutor", "task")
AVAILABLE_ORCHESTRATORS: tuple[str, ...] = ("qa", "tutor", "task", "feedback")
_ORCHESTRATOR_MODULES: dict[str, Any] = {
"qa": orchestrator_qa,
"tutor": orchestrator_tutor,
"task": orchestrator_task,
"feedback": orchestrator_feedback,
}
......
export type OrchestratorName = "qa" | "tutor" | "task";
export type OrchestratorName = "qa" | "tutor" | "task" | "feedback";
export type OrchestratorConfigResponse = {
default_orchestrator: OrchestratorName;
available_orchestrators: OrchestratorName[];
};
const FALLBACK_ORCHESTRATORS: OrchestratorName[] = ["qa", "tutor", "task"];
const FALLBACK_ORCHESTRATORS: OrchestratorName[] = ["qa", "tutor", "task", "feedback"];
const normalizeOrchestrator = (value: string): OrchestratorName | null => {
if (value === "qa" || value === "tutor" || value === "task") {
if (value === "qa" || value === "tutor" || value === "task" || value === "feedback") {
return value;
}
return null;
......
......@@ -29,7 +29,10 @@ export default function MessageBubble({
}: MessageBubbleProps) {
const bubbleRef = useRef<HTMLDivElement | null>(null);
const [isRawView, setIsRawView] = useState(false);
const renderedText = useMemo(() => escapeAsterisksInsideMath(text), [text]);
const renderedText = useMemo(
() => escapeAsterisksInsideMath(text.replaceAll("</think>", "\n --- \n")),
[text]
);
useEffect(() => {
if (isRawView) {
......
......@@ -13,11 +13,13 @@ type ModeMeta = {
labelKey:
| "orchestratorModeQaLabel"
| "orchestratorModeTutorLabel"
| "orchestratorModeTaskLabel";
| "orchestratorModeTaskLabel"
| "orchestratorModeFeedbackLabel";
descriptionKey:
| "orchestratorModeQaDescription"
| "orchestratorModeTutorDescription"
| "orchestratorModeTaskDescription";
| "orchestratorModeTaskDescription"
| "orchestratorModeFeedbackDescription";
};
const modeMetaMap: Record<OrchestratorName, ModeMeta> = {
......@@ -33,6 +35,10 @@ const modeMetaMap: Record<OrchestratorName, ModeMeta> = {
labelKey: "orchestratorModeTaskLabel",
descriptionKey: "orchestratorModeTaskDescription",
},
feedback: {
labelKey: "orchestratorModeFeedbackLabel",
descriptionKey: "orchestratorModeFeedbackDescription",
},
};
type ModeDescriptionDisplay = "tooltip" | "helperText" | "optionSuffix";
......
......@@ -20,10 +20,12 @@
orchestratorModeQaLabel: "QA",
orchestratorModeTutorLabel: "Tutor",
orchestratorModeTaskLabel: "Task",
orchestratorModeFeedbackLabel: "Feedback",
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",
directChildren: "Direct children",
taskChildren: "Task sources",
indirectChildren: "Indirect children",
......@@ -95,10 +97,12 @@
orchestratorModeQaLabel: "QA",
orchestratorModeTutorLabel: "Tutor",
orchestratorModeTaskLabel: "Task",
orchestratorModeFeedbackLabel: "Feedback",
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",
directChildren: "Direkte Quellen",
taskChildren: "Aufgaben-Quellen",
indirectChildren: "Indirekte Quellen",
......
......@@ -17,12 +17,15 @@ import sumintLogo from "../../SuMINT-Logo.png";
const initialMessages: ChatMessage[] = [];
const normalizeOrchestrator = (value: string | null | undefined): OrchestratorName | null => {
if (value === "qa" || value === "tutor" || value === "task") {
if (value === "qa" || value === "tutor" || value === "task" || value === "feedback") {
return value;
}
return null;
};
const isTaskCoupledOrchestrator = (value: string | null | undefined): boolean =>
value === "task" || value === "feedback";
type ArchivedChatSummary = {
chat_id: string;
saved_at: string;
......@@ -163,7 +166,7 @@ export default function ChatPage() {
: rawTaskId;
const hasFileId = Boolean(fileId);
const hasTaskId = Boolean(taskId);
const isTaskOrchestrator = orchestrator === "task";
const isTaskOrchestrator = isTaskCoupledOrchestrator(orchestrator);
const hasAnyTaskParam = hasFileId || hasTaskId;
const hasRequiredParams = hasFileId && hasTaskId;
const key = `${orchestrator}|${fileId}|${taskId}`;
......@@ -208,7 +211,11 @@ export default function ChatPage() {
return;
}
setSelectedOrchestrator("task");
setSelectedOrchestrator(
deepLinkTarget.isTaskOrchestrator && searchParams.get("orchestrator") === "feedback"
? "feedback"
: "task"
);
setTaskRef({ fileId: selectedFile.file_id, taskId: selectedTask.task_id });
unlockTask();
......@@ -536,7 +543,7 @@ export default function ChatPage() {
: selectedOrchestrator;
setSelectedOrchestrator(nextOrchestrator);
if (nextOrchestrator === "task") {
if (isTaskCoupledOrchestrator(nextOrchestrator)) {
const restoredTask = payload.selected_task;
if (restoredTask?.file_id && restoredTask?.task_id) {
setTaskRef({
......@@ -671,7 +678,7 @@ export default function ChatPage() {
}
resetChatState();
switchOrchestrator(next);
navigate(next === "task" ? "/select-task" : "/chat");
navigate(isTaskCoupledOrchestrator(next) ? "/select-task" : "/chat");
};
const handleCanvasSave = async (dataUrl: string) => {
......
......@@ -8,6 +8,9 @@ import { useTutorSession } from "../state/tutorSession";
import "../styles/theme.css";
import sumintLogo from "../../SuMINT-Logo.png";
const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean =>
value === "task" || value === "feedback";
export default function TaskSelectionPage() {
const navigate = useNavigate();
const {
......@@ -86,7 +89,7 @@ export default function TaskSelectionPage() {
return;
}
switchOrchestrator(next);
navigate(next === "task" ? "/select-task" : "/chat", { replace: true });
navigate(isTaskCoupledOrchestrator(next) ? "/select-task" : "/chat", { replace: true });
};
if (!isTasksInitialized) {
......
......@@ -68,6 +68,9 @@ export const getDefaultTaskId = (tasks: Array<{ task_id: string }>): string =>
const isSelectableTaskFile = (file: TaskFile): boolean =>
Array.isArray(file.subsections) && file.subsections.length > 0;
const isTaskCoupledOrchestrator = (value: OrchestratorName): boolean =>
value === "task" || value === "feedback";
export function TutorSessionProvider({ children }: PropsWithChildren) {
const [chatSessionId, setChatSessionId] = useState<string>(() => createSessionId());
const [selectedOrchestrator, setSelectedOrchestratorState] =
......@@ -83,7 +86,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const [tasksError, setTasksError] = useState<string | null>(null);
const [taskLocked, setTaskLocked] = useState(false);
const isTaskModeEnabled = selectedOrchestrator === "task";
const isTaskModeEnabled = isTaskCoupledOrchestrator(selectedOrchestrator);
const selectedTask = useMemo<SelectedTask | null>(() => {
if (!selectedTaskRef) {
......@@ -228,7 +231,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
setSelectedOrchestratorState(value);
setChatSessionId(createSessionId());
setTaskLocked(false);
if (value !== "task") {
if (!isTaskCoupledOrchestrator(value)) {
setSelectedTaskRef(null);
}
}, []);
......
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