Commit 18be0686 authored by Kantz's avatar Kantz
Browse files

socratesmodus zieht auswahl aus inital_prompts

parent 52b14c87
...@@ -6,7 +6,7 @@ from typing import List, Optional ...@@ -6,7 +6,7 @@ from typing import List, Optional
import app.config as config import app.config as config
from app.deterministic_services import session_store from app.deterministic_services import session_store
from app.deterministic_services import context_store, retrieval_store, task_catalog from app.deterministic_services import context_store, retrieval_store, task_catalog
from app.deterministic_services import socratic_bootstrap_prompts from app.deterministic_services import socratic_oranisator
from app.deterministic_services.orchestrators.registry import ( from app.deterministic_services.orchestrators.registry import (
get_default_orchestrator, get_default_orchestrator,
is_valid_orchestrator, is_valid_orchestrator,
...@@ -205,7 +205,7 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe ...@@ -205,7 +205,7 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe
context_store.save_sheet(sheet) context_store.save_sheet(sheet)
try: try:
reply = socratic_bootstrap_prompts.get_initial_message(request.subsection_key) reply = socratic_oranisator.get_initial_message(request.subsection_key)
except ValueError as exc: except ValueError as exc:
logger.exception("Socratic bootstrap prompt lookup failed") logger.exception("Socratic bootstrap prompt lookup failed")
raise HTTPException(status_code=500, detail=str(exc)) from exc raise HTTPException(status_code=500, detail=str(exc)) from exc
......
...@@ -6,7 +6,7 @@ from fastapi import APIRouter, HTTPException ...@@ -6,7 +6,7 @@ from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
import app.config as config import app.config as config
from app.deterministic_services import context_store, task_catalog from app.deterministic_services import context_store, task_catalog, socratic_oranisator
router = APIRouter() router = APIRouter()
...@@ -41,6 +41,11 @@ class TasksResponse(BaseModel): ...@@ -41,6 +41,11 @@ class TasksResponse(BaseModel):
subsections: List[SubsectionEntry] = Field(default_factory=list) subsections: List[SubsectionEntry] = Field(default_factory=list)
class SocraticResponse(BaseModel):
orchestrator: str
enabled: bool
subsections: List[SubsectionEntry] = Field(default_factory=list)
class SelectTaskRequest(BaseModel): class SelectTaskRequest(BaseModel):
draft: str = Field(..., min_length=1) draft: str = Field(..., min_length=1)
file_id: str = Field(..., min_length=1) file_id: str = Field(..., min_length=1)
...@@ -81,7 +86,16 @@ def list_tasks() -> TasksResponse: ...@@ -81,7 +86,16 @@ def list_tasks() -> TasksResponse:
subsections=subsections, subsections=subsections,
) )
# Eigentlich sollte die Context-Selection erst passieren wen das schon fest steht
@router.get("/api/tasks/socratic-subsections", response_model=SocraticResponse)
def list_socratic_subsections() -> SocraticResponse:
orchestrator = config.get_orchestrator()
subsections = socratic_oranisator.build_subsection_catalog()
return SocraticResponse(
orchestrator=orchestrator,
enabled=orchestrator == "socratic",
subsections=subsections,
)
@router.post("/api/tasks/select", response_model=SelectTaskResponse) @router.post("/api/tasks/select", response_model=SelectTaskResponse)
def select_task(request: SelectTaskRequest) -> SelectTaskResponse: def select_task(request: SelectTaskRequest) -> SelectTaskResponse:
......
...@@ -49,3 +49,36 @@ def get_initial_message(subsection_key: str, path: Path = PROMPTS_PATH) -> str: ...@@ -49,3 +49,36 @@ def get_initial_message(subsection_key: str, path: Path = PROMPTS_PATH) -> str:
if not message: if not message:
raise ValueError(f"empty socratic bootstrap prompt for subsection '{normalized_key}'") raise ValueError(f"empty socratic bootstrap prompt for subsection '{normalized_key}'")
return message return message
def build_subsection_catalog(path: Path = PROMPTS_PATH) -> list[dict[str, Any]]:
items = load_initial_prompt_items(path)
subsection_summaries = task_catalog.load_subsection_summaries()
response: list[dict[str, Any]] = []
for raw_key, item in items.items():
subsection_key = task_catalog._normalize_subsection_key(str(raw_key))
if not subsection_key:
continue
label_source = str(item.get("subsection") or subsection_key)
label = task_catalog._format_subsection_label(label_source)
ref = task_catalog._parse_subsection_ref(str(item.get("index") or ""))
refs = [[int(ref[0]), int(ref[1]), int(ref[2])]] if ref is not None else []
response.append(
{
"subsection_key": subsection_key,
"label": label or task_catalog._format_subsection_label(subsection_key),
"refs": refs,
"summary": subsection_summaries.get(subsection_key, ""),
}
)
response.sort(
key=lambda item: (
item["refs"][0] if item["refs"] else (9999, 9999, 9999),
item["subsection_key"],
)
)
return response
...@@ -14,10 +14,50 @@ from fastapi import FastAPI ...@@ -14,10 +14,50 @@ from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from app.api import chat, tasks from app.api import chat, tasks
from app.deterministic_services import session_store, task_catalog from app.deterministic_services import session_store, socratic_oranisator, task_catalog
class TaskCatalogSocraticTest(unittest.TestCase): class TaskCatalogSocraticTest(unittest.TestCase):
def test_socratic_build_subsection_catalog_returns_response_shape(self) -> None:
with patch(
"app.deterministic_services.socratic_oranisator.load_initial_prompt_items",
return_value={
"quadratische gleichungen": {
"subsection": "Quadratische Gleichungen",
"index": "1:3:3",
},
"mengen": {
"subsection": "Mengen",
"index": "1:1:1",
},
},
), patch(
"app.deterministic_services.socratic_oranisator.task_catalog.load_subsection_summaries",
return_value={
"mengen": "Mengen summary text",
"quadratische gleichungen": "Quadratische summary text",
},
):
catalog = socratic_oranisator.build_subsection_catalog()
self.assertEqual(
catalog,
[
{
"subsection_key": "mengen",
"label": "Mengen",
"refs": [[1, 1, 1]],
"summary": "Mengen summary text",
},
{
"subsection_key": "quadratische gleichungen",
"label": "Quadratische Gleichungen",
"refs": [[1, 3, 3]],
"summary": "Quadratische summary text",
},
],
)
def test_build_subsection_catalog_includes_summary(self) -> None: def test_build_subsection_catalog_includes_summary(self) -> None:
with patch( with patch(
"app.deterministic_services.task_catalog.load_subsection_map", "app.deterministic_services.task_catalog.load_subsection_map",
...@@ -172,6 +212,28 @@ class TaskApiSocraticTest(unittest.TestCase): ...@@ -172,6 +212,28 @@ class TaskApiSocraticTest(unittest.TestCase):
self.assertEqual(body["orchestrator"], "socratic") self.assertEqual(body["orchestrator"], "socratic")
self.assertEqual(body["subsections"][0]["subsection_key"], "quadratische gleichungen") self.assertEqual(body["subsections"][0]["subsection_key"], "quadratische gleichungen")
def test_list_socratic_subsections_returns_catalog(self) -> None:
subsections = [
{
"subsection_key": "quadratische gleichungen",
"label": "Quadratische Gleichungen",
"refs": [[1, 3, 3]],
"summary": "Quadratische summary text",
}
]
with patch("app.api.tasks.config.get_orchestrator", return_value="socratic"), patch(
"app.api.tasks.socratic_oranisator.build_subsection_catalog",
return_value=subsections,
):
response = self.client.get("/api/tasks/socratic-subsections")
self.assertEqual(response.status_code, 200)
body = response.json()
self.assertEqual(body["orchestrator"], "socratic")
self.assertTrue(body["enabled"])
self.assertEqual(body["subsections"], subsections)
def test_select_subsection_endpoint_returns_selected_key(self) -> None: def test_select_subsection_endpoint_returns_selected_key(self) -> None:
sheet: dict[str, object] = {} sheet: dict[str, object] = {}
......
...@@ -27,6 +27,12 @@ export type TasksResponse = { ...@@ -27,6 +27,12 @@ export type TasksResponse = {
subsections: SubsectionOption[]; subsections: SubsectionOption[];
}; };
export type SocraticResponse = {
orchestrator: string;
enabled: boolean;
subsections: SubsectionOption[];
};
export type SelectedTaskRef = { export type SelectedTaskRef = {
fileId: string; fileId: string;
taskId: string; taskId: string;
...@@ -55,6 +61,14 @@ export async function fetchTasks(): Promise<TasksResponse> { ...@@ -55,6 +61,14 @@ export async function fetchTasks(): Promise<TasksResponse> {
return response.json(); return response.json();
} }
export async function fetchSocraticSubsections(): Promise<SocraticResponse> {
const response = await fetch("/api/tasks/socratic-subsections");
if (!response.ok) {
throw new Error(`Socratic subsections failed: ${response.status}`);
}
return response.json();
}
export async function selectTask(input: { export async function selectTask(input: {
draft: string; draft: string;
fileId: string; fileId: string;
......
...@@ -15,6 +15,7 @@ import { ...@@ -15,6 +15,7 @@ import {
type OrchestratorName, type OrchestratorName,
} from "../api/orchestratorApi"; } from "../api/orchestratorApi";
import { import {
fetchSocraticSubsections,
fetchTasks, fetchTasks,
type SelectedSubsectionRef, type SelectedSubsectionRef,
type SelectedTaskRef, type SelectedTaskRef,
...@@ -190,36 +191,30 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -190,36 +191,30 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
}; };
}, [selectedSubsectionRef, subsections]); }, [selectedSubsectionRef, subsections]);
const initTasks = useCallback(async () => { const loadSelectionData = useCallback(async (orchestrator: OrchestratorName) => {
setTasksError(null); setTasksError(null);
setOrchestratorError(null);
try { try {
const orchestratorPayload = await fetchOrchestratorConfig(); const isSocratic = orchestrator === "socratic";
const available = orchestratorPayload.available_orchestrators.length let files: TaskFile[] = [];
? orchestratorPayload.available_orchestrators let subsectionsPayload: SubsectionOption[] = [];
: getFallbackOrchestrators();
setAvailableOrchestrators(available); if (isSocratic) {
setIsOrchestratorSelectable(true); const payload = await fetchSocraticSubsections();
setSelectedOrchestratorState((prev) => subsectionsPayload = payload.subsections || [];
available.includes(prev) ? prev : orchestratorPayload.default_orchestrator } else {
); const payload = await fetchTasks();
} catch (error) { files = payload.task_files || [];
setAvailableOrchestrators(getFallbackOrchestrators()); subsectionsPayload = payload.subsections || [];
setSelectedOrchestratorState("qa"); }
setIsOrchestratorSelectable(false);
setOrchestratorError(t("failedLoadOrchestratorConfig"));
void error;
}
try {
const payload = await fetchTasks();
const files = payload.task_files || [];
const subsectionsPayload = payload.subsections || [];
const selectableFiles = files.filter((file) => isSelectableTaskFile(file)); const selectableFiles = files.filter((file) => isSelectableTaskFile(file));
setTaskFiles(files); setTaskFiles(files);
setSubsections(subsectionsPayload); setSubsections(subsectionsPayload);
setSelectedTaskRef((prev) => { setSelectedTaskRef((prev) => {
if (isSocratic) {
return null;
}
if (prev) { if (prev) {
const file = selectableFiles.find((item) => item.file_id === prev.fileId); const file = selectableFiles.find((item) => item.file_id === prev.fileId);
if (file && file.tasks.some((task) => task.task_id === prev.taskId)) { if (file && file.tasks.some((task) => task.task_id === prev.taskId)) {
...@@ -238,8 +233,13 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -238,8 +233,13 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
return { fileId: firstFile.file_id, taskId: defaultTaskId }; return { fileId: firstFile.file_id, taskId: defaultTaskId };
}); });
setSelectedSubsectionRef((prev) => { setSelectedSubsectionRef((prev) => {
if (!isSocratic) {
return null;
}
if (prev) { if (prev) {
const option = subsectionsPayload.find((item) => item.subsection_key === prev.subsectionKey); const option = subsectionsPayload.find(
(item) => item.subsection_key === prev.subsectionKey
);
if (option) { if (option) {
return prev; return prev;
} }
...@@ -258,15 +258,47 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -258,15 +258,47 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
setSelectedTaskRef(null); setSelectedTaskRef(null);
setSelectedSubsectionRef(null); setSelectedSubsectionRef(null);
void error; void error;
}
}, []);
const initTasks = useCallback(async () => {
setOrchestratorError(null);
try {
const orchestratorPayload = await fetchOrchestratorConfig();
const available = orchestratorPayload.available_orchestrators.length
? orchestratorPayload.available_orchestrators
: getFallbackOrchestrators();
const nextOrchestrator = available.includes(selectedOrchestrator)
? selectedOrchestrator
: orchestratorPayload.default_orchestrator;
setAvailableOrchestrators(available);
setIsOrchestratorSelectable(true);
setSelectedOrchestratorState(nextOrchestrator);
await loadSelectionData(nextOrchestrator);
} catch (error) {
setAvailableOrchestrators(getFallbackOrchestrators());
setSelectedOrchestratorState("qa");
setIsOrchestratorSelectable(false);
setOrchestratorError(t("failedLoadOrchestratorConfig"));
await loadSelectionData("qa");
void error;
} finally { } finally {
setIsTasksInitialized(true); setIsTasksInitialized(true);
} }
}, []); }, [loadSelectionData, selectedOrchestrator]);
useEffect(() => { useEffect(() => {
void initTasks(); void initTasks();
}, [initTasks]); }, [initTasks]);
useEffect(() => {
if (!isTasksInitialized) {
return;
}
void loadSelectionData(selectedOrchestrator);
}, [isTasksInitialized, loadSelectionData, selectedOrchestrator]);
const setTaskFile = useCallback( const setTaskFile = useCallback(
(fileId: string) => { (fileId: string) => {
if (!fileId) { if (!fileId) {
......
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