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
import app.config as config
from app.deterministic_services import session_store
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 (
get_default_orchestrator,
is_valid_orchestrator,
......@@ -205,7 +205,7 @@ def bootstrap_socratic(request: SocraticBootstrapRequest) -> SocraticBootstrapRe
context_store.save_sheet(sheet)
try:
reply = socratic_bootstrap_prompts.get_initial_message(request.subsection_key)
reply = socratic_oranisator.get_initial_message(request.subsection_key)
except ValueError as exc:
logger.exception("Socratic bootstrap prompt lookup failed")
raise HTTPException(status_code=500, detail=str(exc)) from exc
......
......@@ -6,7 +6,7 @@ from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
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()
......@@ -41,6 +41,11 @@ class TasksResponse(BaseModel):
subsections: List[SubsectionEntry] = Field(default_factory=list)
class SocraticResponse(BaseModel):
orchestrator: str
enabled: bool
subsections: List[SubsectionEntry] = Field(default_factory=list)
class SelectTaskRequest(BaseModel):
draft: str = Field(..., min_length=1)
file_id: str = Field(..., min_length=1)
......@@ -81,7 +86,16 @@ def list_tasks() -> TasksResponse:
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)
def select_task(request: SelectTaskRequest) -> SelectTaskResponse:
......
......@@ -49,3 +49,36 @@ def get_initial_message(subsection_key: str, path: Path = PROMPTS_PATH) -> str:
if not message:
raise ValueError(f"empty socratic bootstrap prompt for subsection '{normalized_key}'")
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
from fastapi.testclient import TestClient
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):
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:
with patch(
"app.deterministic_services.task_catalog.load_subsection_map",
......@@ -172,6 +212,28 @@ class TaskApiSocraticTest(unittest.TestCase):
self.assertEqual(body["orchestrator"], "socratic")
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:
sheet: dict[str, object] = {}
......
......@@ -27,6 +27,12 @@ export type TasksResponse = {
subsections: SubsectionOption[];
};
export type SocraticResponse = {
orchestrator: string;
enabled: boolean;
subsections: SubsectionOption[];
};
export type SelectedTaskRef = {
fileId: string;
taskId: string;
......@@ -55,6 +61,14 @@ export async function fetchTasks(): Promise<TasksResponse> {
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: {
draft: string;
fileId: string;
......
......@@ -15,6 +15,7 @@ import {
type OrchestratorName,
} from "../api/orchestratorApi";
import {
fetchSocraticSubsections,
fetchTasks,
type SelectedSubsectionRef,
type SelectedTaskRef,
......@@ -190,36 +191,30 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
};
}, [selectedSubsectionRef, subsections]);
const initTasks = useCallback(async () => {
const loadSelectionData = useCallback(async (orchestrator: OrchestratorName) => {
setTasksError(null);
setOrchestratorError(null);
try {
const orchestratorPayload = await fetchOrchestratorConfig();
const available = orchestratorPayload.available_orchestrators.length
? orchestratorPayload.available_orchestrators
: getFallbackOrchestrators();
setAvailableOrchestrators(available);
setIsOrchestratorSelectable(true);
setSelectedOrchestratorState((prev) =>
available.includes(prev) ? prev : orchestratorPayload.default_orchestrator
);
} catch (error) {
setAvailableOrchestrators(getFallbackOrchestrators());
setSelectedOrchestratorState("qa");
setIsOrchestratorSelectable(false);
setOrchestratorError(t("failedLoadOrchestratorConfig"));
void error;
}
const isSocratic = orchestrator === "socratic";
let files: TaskFile[] = [];
let subsectionsPayload: SubsectionOption[] = [];
if (isSocratic) {
const payload = await fetchSocraticSubsections();
subsectionsPayload = payload.subsections || [];
} else {
const payload = await fetchTasks();
files = payload.task_files || [];
subsectionsPayload = payload.subsections || [];
}
try {
const payload = await fetchTasks();
const files = payload.task_files || [];
const subsectionsPayload = payload.subsections || [];
const selectableFiles = files.filter((file) => isSelectableTaskFile(file));
setTaskFiles(files);
setSubsections(subsectionsPayload);
setSelectedTaskRef((prev) => {
if (isSocratic) {
return null;
}
if (prev) {
const file = selectableFiles.find((item) => item.file_id === prev.fileId);
if (file && file.tasks.some((task) => task.task_id === prev.taskId)) {
......@@ -238,8 +233,13 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
return { fileId: firstFile.file_id, taskId: defaultTaskId };
});
setSelectedSubsectionRef((prev) => {
if (!isSocratic) {
return null;
}
if (prev) {
const option = subsectionsPayload.find((item) => item.subsection_key === prev.subsectionKey);
const option = subsectionsPayload.find(
(item) => item.subsection_key === prev.subsectionKey
);
if (option) {
return prev;
}
......@@ -258,15 +258,47 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
setSelectedTaskRef(null);
setSelectedSubsectionRef(null);
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 {
setIsTasksInitialized(true);
}
}, []);
}, [loadSelectionData, selectedOrchestrator]);
useEffect(() => {
void initTasks();
}, [initTasks]);
useEffect(() => {
if (!isTasksInitialized) {
return;
}
void loadSelectionData(selectedOrchestrator);
}, [isTasksInitialized, loadSelectionData, selectedOrchestrator]);
const setTaskFile = useCallback(
(fileId: string) => {
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