Commit aa1b2cd9 authored by Kantz's avatar Kantz
Browse files

Merge branch 'dev_external_task_selection' into 'main'

Dev external task selection

See merge request kantz/tutor_react!6
parents e31efbb9 a678fa77
.venv/ .venv/
venv/
.env .env
__pycache__/ __pycache__/
drawings/ drawings/
markdown/ sources/
logs/ logs/
\ No newline at end of file
## Run the app # DER Tutor
## Prequisits
- Postgres-Database [Database (pgvector) setup]
- sources folder with your markdown files structured with section, subsection and child
- OpenAI endpoint
- Ollama Instanz
if you want to use the task mode you need :
- task folder in sources with your tasks
- `_subsection_map.json`.
## Setup
Configurate the `backend/.env`-file based on the `backend/.env-example`. You can che following modes:
- QA : Direct answer based on the retrieval
- Tutor: Helps with tips on a task given by the user
- Task: A mode where you can select from the list in the taskfolder and get help with this Task.
Backend (Python): Backend (Python):
```powershell
``` powershell
cd math-tutor/backend cd math-tutor/backend
python -m venv .venv python -m venv .venv
.\.venv\Scripts\Activate.ps1 .\.venv\Scripts\Activate.ps1
pip install -r requirements.txt pip install -r requirements.txt
```
Frontend (Vite):
``` powershell
cd math-tutor/frontend
npm install
```
## Run the app
Backend (Python):
```powershell
cd math-tutor/backend
.\.venv\Scripts\Activate.ps1
python -m uvicorn app.main:app --reload python -m uvicorn app.main:app --reload
``` ```
Frontend (Vite): Frontend (Vite):
```powershell ```powershell
cd math-tutor/frontend cd math-tutor/frontend
npm install
npm run dev npm run dev
``` ```
To make it accessible over the network. To make it accessible over the network.
Add the frontend- and backend-adress in the .env file in the frontend- and backend-folder. Add the frontend- and backend-adress in the `backend/.env`-file in the frontend- and backend-folder. Use the following command to run the front- and backend.
```powershell
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
npm run dev -- --host 0.0.0.0 npm run dev -- --host 0.0.0.0
```
### Vite Proxy (Development)
The frontend now supports a dev proxy for API calls:
- Frontend requests `/api/...` to Vite
- Vite forwards `/api` to the backend target
Optional in `math-tutor/frontend/.env`:
```env
VITE_PROXY_TARGET="http://<BACKEND_HOST>:8000"
```
## Docker Compose (Production-style)
Template files:
- `math-tutor/docker/docker-compose.yml`
- `math-tutor/docker/nginx.conf`
- `math-tutor/backend/Dockerfile`
- `math-tutor/frontend/Dockerfile`
Run:
```powershell
cd math-tutor/docker
docker compose up --build -d
```
Open in browser:
- `http://<HOST>:80`
Notes:
- Nginx serves the built frontend and proxies `/api` to backend (`http://backend:8000`).
- Keep frontend API calls relative (`/api/...`) for this setup.
## Database (pgvector) setup ## Database (pgvector) setup
Ensure `POSTGRES_URL` and embedding env vars are in `backend/.env`: Ensure `POSTGRES_URL` and embedding env vars are in `backend/.env`:
```
``` env
POSTGRES_URL=postgresql://user:pass@host:5432/db POSTGRES_URL=postgresql://user:pass@host:5432/db
OPENAI_BASE_URL=... OPENAI_BASE_URL=...
OPENAI_API_KEY=... OPENAI_API_KEY=...
...@@ -33,6 +113,7 @@ OPENAI_EMBED_MODEL=... ...@@ -33,6 +113,7 @@ OPENAI_EMBED_MODEL=...
Create the Postgres database before running init. Create the Postgres database before running init.
Init DB schema: Init DB schema:
```powershell ```powershell
cd math-tutor/backend cd math-tutor/backend
.\.venv\Scripts\Activate.ps1 .\.venv\Scripts\Activate.ps1
...@@ -40,6 +121,7 @@ python -m scripts.retrieval_cli init-db ...@@ -40,6 +121,7 @@ python -m scripts.retrieval_cli init-db
``` ```
Ingest markdown docs (expects `markdown/sections`, `markdown/subsections`, `markdown/childs`): Ingest markdown docs (expects `markdown/sections`, `markdown/subsections`, `markdown/childs`):
```powershell ```powershell
cd math-tutor/backend cd math-tutor/backend
.\.venv\Scripts\Activate.ps1 .\.venv\Scripts\Activate.ps1
...@@ -47,6 +129,7 @@ python -m scripts.retrieval_cli ingest --base markdown --clear ...@@ -47,6 +129,7 @@ python -m scripts.retrieval_cli ingest --base markdown --clear
``` ```
Query via CLI: Query via CLI:
```powershell ```powershell
cd math-tutor/backend cd math-tutor/backend
.\.venv\Scripts\Activate.ps1 .\.venv\Scripts\Activate.ps1
...@@ -56,9 +139,11 @@ python -m scripts.retrieval_cli query --q "Was ist eine Teilmenge?" --k 8 --expa ...@@ -56,9 +139,11 @@ python -m scripts.retrieval_cli query --q "Was ist eine Teilmenge?" --k 8 --expa
## Configuration ## Configuration
Differend Orchestrators: Differend Orchestrators:
- Edit `math-tutor/backend/app/api/chat.py` and update `SYSTEM_PROMPT`. - Edit `math-tutor/backend/app/api/chat.py` and update `SYSTEM_PROMPT`.
Retrieval settings: Retrieval settings:
- Frontend request parameters live in `math-tutor/frontend/src/pages/App.tsx`: - Frontend request parameters live in `math-tutor/frontend/src/pages/App.tsx`:
- `k` - `k`
- `expand_links` - `expand_links`
...@@ -67,7 +152,17 @@ Retrieval settings: ...@@ -67,7 +152,17 @@ Retrieval settings:
- Core retrieval logic is in `math-tutor/backend/app/services/vector_store.py` (`retrieve`). - Core retrieval logic is in `math-tutor/backend/app/services/vector_store.py` (`retrieve`).
## Testing ## Testing
currently there a multiple test to test some components seperatly. Check the test files for specific calling. Here are some example calls.
```powershell
python -m test.hint_test --chat-id draft_session_mlgmxxzc_avmjfb python -m test.hint_test --chat-id draft_session_mlgmxxzc_avmjfb
python -m test.retrieval_store_test --query "Was ist eine Teilmenge?" --k 8 --expand python -m test.retrieval_store_test --query "Was ist eine Teilmenge?" --k 8 --expand
python -m test.retrieval_store_test --query "Was ist eine Teilmenge?" --k 3 --subsections 1:1:1
python -m test.math_intent_test --input "Integrate x^2" --input "Was ist 2+2?" python -m test.math_intent_test --input "Integrate x^2" --input "Was ist 2+2?"
python -m test.decision_test --chat-id draft_session_mlgmxxzc_avmjfb python -m test.decision_test --chat-id draft_session_mlgmxxzc_avmjfb
```
.venv
__pycache__
*.pyc
logs
test
FROM python:3.12-slim
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app ./app
COPY scripts ./scripts
COPY sources ./sources
EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
...@@ -28,6 +28,7 @@ WICHTIG: ...@@ -28,6 +28,7 @@ WICHTIG:
""" """
def needs_more_context(context_sheet: str) -> dict: def needs_more_context(context_sheet: str) -> dict:
return {"needs_more_context": False, "reason":"Deactiviert weil nicht Funktional"}
messages = [{"role": "system", "content": CLASSIFIER_SYSTEM}] messages = [{"role": "system", "content": CLASSIFIER_SYSTEM}]
messages.append( messages.append(
{ {
......
from app.deterministic_services import llm_client
HINT_SYSTEM_PROMPT = """
Du bist ein didaktischer Mathe-Tutor.
1) Antworte NUR mit EINEM Satz (max. 20 Wörter), der den Tipp enthält. Keine Beispiele, keine Herleitungen, keine Lösungen.
2) Beziehe dich IMMER auf die 'Aktuelle Frage'. Ignoriere ältere Fragen, außer sie sind DIREKT relevant (z. B. Definitionen).
3) Wenn der Nutzer etwas Falsches sagt, antworte: "Das ist noch nicht richtig. Prüfe [konkreter Aspekt]."
4) Falls eine 'Mathematische Lösung' (Formel/Algorithmus) existiert, gib NUR diese als Tipp an – selbst wenn der Nutzer nach Alternativen fragt.
5) Verwende IMMER die $-Notation für Formeln (z. B. $a^2 + b^2 = c^2$). Kein LaTeX außerhalb der $-Blöcke.
Antworte IMMER in dieser Form:
- "Das war richtig. [Tipp in einem Satz]."
- "Das ist noch nicht richtig. [Tipp in einem Satz]."
- "Gute Frage! [Tipp in einem Satz]."
"""
def generate_hint(
query: str,
task: str,
LLM_solution: str,
math_solution: str | None = None,
history: list[dict] | None = None,
sources: str | None = None,
) -> str:
context_parts = [
f"Aufgabe:\n{task}",
f"LLM-Lösung:\n{LLM_solution}",
]
if math_solution:
context_parts.append(f"Mathematische Lösung (maßgeblich):\n{math_solution}")
if sources:
context_parts.append(f"Kontext/Sources:\n{sources}")
messages = [{"role": "system", "content": HINT_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 FRAGE (höchste Priorität):\n{query}\n\nGib einen kurzen Tipp zum nächsten Schritt, der genau diese Frage adressiert."
})
result = llm_client.chat(messages=messages)
return llm_client.get_message_content(result)
\ No newline at end of file
from __future__ import annotations
from app.deterministic_services import llm_client from app.deterministic_services import llm_client
...@@ -6,7 +7,9 @@ Du bist ein didaktischer Mathe-Tutor." ...@@ -6,7 +7,9 @@ Du bist ein didaktischer Mathe-Tutor."
1) Antworte NUR mit einem kurzen Tipp (1-2 Sätze), keine Beispiele, keine Herleitung, keine komplette Lösung. 1) Antworte NUR mit einem kurzen Tipp (1-2 Sätze), keine Beispiele, keine Herleitung, keine komplette Lösung.
2) Beziehe dich PRIMÄR auf die 'Aktuelle Frage'. Ignoriere ältere Nebenfragen, außer sie sind nötig. 2) Beziehe dich PRIMÄR auf die 'Aktuelle Frage'. Ignoriere ältere Nebenfragen, außer sie sind nötig.
3) Wenn der Nutzer etwas Falsches sagt weiße ihn darauf hin gib aber keine Lösung an. 3) Wenn der Nutzer etwas Falsches sagt weiße ihn darauf hin gib aber keine Lösung an.
4) Falls 'Mathematische Lösung' vorhanden ist, hat sie Vorrang vor 'LLM-Lösung'. 4) Basiere deine Tipps auf den Hinweisen.
5) Die Übergebene Lösung ist IMMER korrekt.
6) Der Nutzer kenn die Lösung und die Hinweise nicht.
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$
...@@ -16,20 +19,18 @@ Das war richtig/das ist noch nicht richtig/gute Frage ...@@ -16,20 +19,18 @@ Das war richtig/das ist noch nicht richtig/gute Frage
""" """
def generate_hint( def generate_hint(
query: str, query: str | None,
task: str, task: str,
LLM_solution: str, hints: list[str],
math_solution: str | None = None, solution: str,
history: list[dict] | None = None, history: list[dict] | None = None,
sources: str | None = None, sources: str | None = None,
) -> str: ) -> str:
context_parts = [ context_parts = [
f"Aufgabe:\n{task}", f"Hier ist die Zu lösende Aufgaben:{task}\n",
f"LLM-Lösung:\n{LLM_solution}", f"Hier ist eine korrekte Lösung als referenz:{solution}\n",
f"Hier ist ein exemplarischer Lösungsweg:{hints}\n"
] ]
if math_solution:
context_parts.append(f"Mathematische Lösung (maßgeblich):\n{math_solution}")
if sources: if sources:
context_parts.append(f"Kontext/Sources:\n{sources}") context_parts.append(f"Kontext/Sources:\n{sources}")
...@@ -45,8 +46,8 @@ def generate_hint( ...@@ -45,8 +46,8 @@ def generate_hint(
# Aktuelle Frage als letzte Nachricht, fett hervorgehoben durch Struktur/Delimiters # Aktuelle Frage als letzte Nachricht, fett hervorgehoben durch Struktur/Delimiters
messages.append({ messages.append({
"role": "user", "role": "user",
"content": f"AKTUELLE FRAGE (höchste Priorität):\n{query}\n\nGib einen kurzen Tipp zum nächsten Schritt, der genau diese Frage adressiert." "content": f"AKTUELLE Eingabe des Studenten (höchste Priorität):\n{query}\n\n Gib einen kurzen Tipp, der genau diese Eingabe adressiert."
}) })
result = llm_client.chat(messages=messages) result = llm_client.chat(messages=messages)
return llm_client.get_message_content(result) return llm_client.get_message_content(result)
\ No newline at end of file
from __future__ import annotations from __future__ import annotations
from logging import config
from typing import List, Optional
import logging import logging
from typing import List, Optional
import app.config as config
from app.deterministic_services import session_store
from fastapi import APIRouter, HTTPException, Path, Query from fastapi import APIRouter, HTTPException, Path, Query
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.deterministic_services import session_store
import app.config as config
if config.get_orchestrator() == "tutor": if config.get_orchestrator() == "tutor":
from app.deterministic_services.orchestrators import orchestrator_tutor as orchestrator from app.deterministic_services.orchestrators import (
orchestrator_tutor as orchestrator,
)
elif config.get_orchestrator() == "task":
from app.deterministic_services.orchestrators import orchestrator_task as orchestrator
else: else:
from app.deterministic_services.orchestrators import orchestrator_qa as orchestrator from app.deterministic_services.orchestrators import orchestrator_qa as orchestrator
...@@ -29,6 +30,7 @@ class ChatMessage(BaseModel): ...@@ -29,6 +30,7 @@ class ChatMessage(BaseModel):
class ChatRequest(BaseModel): class ChatRequest(BaseModel):
messages: List[ChatMessage] messages: List[ChatMessage]
draft: Optional[str] = None draft: Optional[str] = None
selected_task: Optional[dict[str, str]] = None
class ChatResponse(BaseModel): class ChatResponse(BaseModel):
...@@ -48,10 +50,16 @@ class ChatArchiveSummary(BaseModel): ...@@ -48,10 +50,16 @@ class ChatArchiveSummary(BaseModel):
preview: str preview: str
class SelectedTaskRef(BaseModel):
file_id: str
task_id: str
class ChatArchiveDetail(BaseModel): class ChatArchiveDetail(BaseModel):
chat_id: str chat_id: str
saved_at: str saved_at: str
history: List[ChatMessage] history: List[ChatMessage]
selected_task: Optional[SelectedTaskRef] = None
@router.post("/api/chat", response_model=ChatResponse) @router.post("/api/chat", response_model=ChatResponse)
...@@ -60,18 +68,25 @@ def chat(request: ChatRequest) -> ChatResponse: ...@@ -60,18 +68,25 @@ def chat(request: ChatRequest) -> ChatResponse:
raise HTTPException(status_code=400, detail="messages required") raise HTTPException(status_code=400, detail="messages required")
try: try:
result = orchestrator.run_chat( payload_messages = [{"role": m.role, "content": m.text} for m in request.messages]
[{"role": m.role, "content": m.text} for m in request.messages], if config.get_orchestrator() == "task":
draft=request.draft, result = orchestrator.run_chat(
) payload_messages,
draft=request.draft,
selected_task=request.selected_task,
)
else:
result = orchestrator.run_chat(
payload_messages,
draft=request.draft,
)
reply = result["reply"] reply = result["reply"]
sources = result["sources"] sources = result["sources"]
except ValueError as exc: except ValueError as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc raise HTTPException(status_code=500, detail=str(exc)) from exc
except Exception as exc: except Exception as exc:
logger.exception("Chat request failed") logger.exception("Chat request failed")
raise HTTPException( raise HTTPException(status_code=502, detail="chat provider failed") from exc
status_code=502, detail="chat provider failed") from exc
return ChatResponse(reply=reply, sources=sources) return ChatResponse(reply=reply, sources=sources)
...@@ -82,8 +97,7 @@ def list_archives(limit: int = Query(20, ge=1, le=200)) -> List[ChatArchiveSumma ...@@ -82,8 +97,7 @@ def list_archives(limit: int = Query(20, ge=1, le=200)) -> List[ChatArchiveSumma
return session_store.list_archives(limit=limit) return session_store.list_archives(limit=limit)
except Exception as exc: except Exception as exc:
logger.exception("Chat archive list failed") logger.exception("Chat archive list failed")
raise HTTPException( raise HTTPException(status_code=502, detail="chat archive list failed") from exc
status_code=502, detail="chat archive list failed") from exc
@router.get("/api/chat/archive/{chat_id}", response_model=ChatArchiveDetail) @router.get("/api/chat/archive/{chat_id}", response_model=ChatArchiveDetail)
...@@ -92,17 +106,24 @@ def get_archive(chat_id: str = Path(..., min_length=1)) -> ChatArchiveDetail: ...@@ -92,17 +106,24 @@ def get_archive(chat_id: str = Path(..., min_length=1)) -> ChatArchiveDetail:
record = session_store.load_archive(chat_id) record = session_store.load_archive(chat_id)
except Exception as exc: except Exception as exc:
logger.exception("Chat archive load failed") logger.exception("Chat archive load failed")
raise HTTPException( raise HTTPException(status_code=502, detail="chat archive load failed") from exc
status_code=502, detail="chat archive load failed") from exc
if not record: if not record:
raise HTTPException(status_code=404, detail="chat archive not found") raise HTTPException(status_code=404, detail="chat archive not found")
selected_task_raw = record.get("selected_task")
selected_task: Optional[SelectedTaskRef] = None
if isinstance(selected_task_raw, dict):
file_id = str(selected_task_raw.get("file_id", "")).strip()
task_id = str(selected_task_raw.get("task_id", "")).strip()
if file_id and task_id:
selected_task = SelectedTaskRef(file_id=file_id, task_id=task_id)
return ChatArchiveDetail( return ChatArchiveDetail(
chat_id=record["chat_id"], chat_id=record["chat_id"],
saved_at=record.get("saved_at", ""), saved_at=record.get("saved_at", ""),
history=[ChatMessage(role=item["role"], text=item["text"]) history=[ChatMessage(role=item["role"], text=item["text"]) for item in record["history"]],
for item in record["history"]], selected_task=selected_task,
) )
...@@ -118,7 +139,6 @@ def archive_chat(request: ChatRequest) -> ChatArchiveResponse: ...@@ -118,7 +139,6 @@ def archive_chat(request: ChatRequest) -> ChatArchiveResponse:
) )
except Exception as exc: except Exception as exc:
logger.exception("Chat archive failed") logger.exception("Chat archive failed")
raise HTTPException( raise HTTPException(status_code=502, detail="chat archive failed") from exc
status_code=502, detail="chat archive failed") from exc
return ChatArchiveResponse(status="ok", chat_id=chat_id) return ChatArchiveResponse(status="ok", chat_id=chat_id)
from __future__ import annotations
from typing import List
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
import app.config as config
from app.deterministic_services import context_store, task_catalog
router = APIRouter()
class TaskItem(BaseModel):
task_id: str
statement: str
full_text: str
class TaskFile(BaseModel):
file_id: str
title: str
intro: str
tasks: List[TaskItem]
class TasksResponse(BaseModel):
orchestrator: str
enabled: bool
task_files: List[TaskFile]
class SelectTaskRequest(BaseModel):
draft: str = Field(..., min_length=1)
file_id: str = Field(..., min_length=1)
task_id: str = Field(..., min_length=1)
class SelectTaskResponse(BaseModel):
status: str
file_id: str
task_id: str
@router.get("/api/tasks/config")
def get_task_config() -> dict[str, object]:
orchestrator = config.get_orchestrator()
return {"orchestrator": orchestrator, "enabled": orchestrator == "task"}
@router.get("/api/tasks", response_model=TasksResponse)
def list_tasks() -> TasksResponse:
orchestrator = config.get_orchestrator()
task_files = task_catalog.build_task_catalog()
return TasksResponse(
orchestrator=orchestrator,
enabled=orchestrator == "task",
task_files=task_files,
)
# Eigentlich sollte die Context-Selection erst passieren wen das schon fest steht
@router.post("/api/tasks/select", response_model=SelectTaskResponse)
def select_task(request: SelectTaskRequest) -> SelectTaskResponse:
chat_id = context_store.get_chat_id([], draft=request.draft)
sheet = context_store.load_sheet(chat_id)
if not sheet:
sheet = context_store.context_store_new.init_sheet(chat_id, [])
updated = task_catalog.select_task_by_ids(
sheet,
file_id=request.file_id,
task_id=request.task_id,
)
if not updated:
raise HTTPException(status_code=404, detail="task not found")
context_store.save_sheet(sheet)
file_id, task_id = task_catalog.get_selected_task_ids(sheet)
return SelectTaskResponse(
status="ok",
file_id=file_id or request.file_id,
task_id=task_id or request.task_id,
)
from __future__ import annotations from __future__ import annotations
import hashlib # Shared/base API
import json from app.deterministic_services.context_stores.context_store_base import (
import os add_decision,
from datetime import datetime format_history,
from threading import Lock get_chat_id,
from typing import Any get_chat_id_value,
get_created_at,
from app.deterministic_services import Source get_decisions,
get_history,
get_history_turns,
# --------------------------------------------------------------------------------------------------- get_initialized,
# Basisfunktionen des Contex-Sheets get_retrieval,
# --------------------------------------------------------------------------------------------------- get_updated_at,
load_sheet,
_CACHE: dict[str, dict[str, Any]] = {} save_sheet,
_LOCK = Lock() set_chat_id,
_LOG_DIR = os.path.join("logs", "context_sheets") set_created_at,
set_decisions,
set_history,
def _utc_now() -> str: set_initialized,
return datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ") set_sources,
set_updated_at,
update_history,
def get_chat_id(messages: list[dict], draft: str | None = None) -> str: update_retrieval_context,
if draft: )
return f"draft_{draft}"
first_user = next((m for m in messages if m.get("role") # Old/open variant is the default for backwards compatibility.
== "user" and m.get("content")), None) from app.deterministic_services.context_stores.context_store_open import (
if not first_user: add_LLM_solution,
return "unknown" add_math_solution,
digest = hashlib.sha1(first_user["content"].encode("utf-8")).hexdigest() first_math_solution,
return digest[:12] format_sheet,
get_llm_solutions,
get_math_solutions,
def init_sheet(chat_id: str, messages: list[dict]) -> dict[str, Any]: get_task,
timestamp = _utc_now() init_sheet,
return { last_LLM_solution,
"chat_id": chat_id, set_llm_solutions,
"created_at": timestamp, set_math_solutions,
"updated_at": timestamp, )
"history": messages[:],
"sources": [], # Expose variant modules so callers can opt in explicitly.
"math_solutions": [], from app.deterministic_services.context_stores import context_store_new, context_store_open
"LLM_solutions": [],
"decisions": [], __all__ = [
"initialized": False, # shared/base
} "get_chat_id",
"load_sheet",
"save_sheet",
def load_sheet(chat_id: str) -> dict[str, Any] | None: "format_history",
with _LOCK: "update_history",
if chat_id in _CACHE: "get_history",
return _CACHE[chat_id] "get_history_turns",
"update_retrieval_context",
latest_path = os.path.join(_LOG_DIR, f"{chat_id}_latest.json") "get_retrieval",
if os.path.exists(latest_path): "add_decision",
with open(latest_path, "r", encoding="utf-8") as f: "set_chat_id",
sheet = json.load(f) "get_chat_id_value",
with _LOCK: "set_created_at",
_CACHE[chat_id] = sheet "get_created_at",
return sheet "set_updated_at",
return None "get_updated_at",
"set_history",
"set_decisions",
def save_sheet(sheet: dict[str, Any]) -> None: "get_decisions",
os.makedirs(_LOG_DIR, exist_ok=True) "set_initialized",
sheet["updated_at"] = _utc_now() "get_initialized",
"set_sources",
chat_id = sheet.get("chat_id", "unknown") # default/open variant
latest_path = os.path.join(_LOG_DIR, f"{chat_id}_latest.json") "init_sheet",
"format_sheet",
payload = json.dumps(sheet, ensure_ascii=True, indent=2) "set_math_solutions",
with open(latest_path, "w", encoding="utf-8") as f: "get_math_solutions",
f.write(payload) "add_math_solution",
"first_math_solution",
with _LOCK: "set_llm_solutions",
_CACHE[chat_id] = sheet "get_llm_solutions",
"add_LLM_solution",
"last_LLM_solution",
def format_sheet(sheet: dict[str, Any]) -> str: "get_task",
parts = [] # explicit variants
history = format_history(sheet.get("history", [])) "context_store_open",
parts.append("HISTORY:\n" + (history or "(leer)")) "context_store_new",
]
sources = sheet.get("sources", [])
if sources:
# Erstelle eine formatierte Liste der Quellen mit ihren Scores
source_blocks = []
for source in sources:
source_blocks.append(Source.model_validate(source).to_string())
source_info = "\n".join(source_blocks) if source_blocks else ""
parts.append(f"SOURCES:\n{source_info}")
else:
parts.append("SOURCES:\n(leer)")
math_solutions = sheet.get("math_solutions", [])
if math_solutions:
blocks = []
for item in math_solutions:
blocks.append(
"TASK: {task}\nINPUT: {input}\nSYMBOLS: {symbols}\nSOLUTION: {solution}".format(
task=item.get("task", ""),
input=item.get("input", ""),
symbols=",".join(item.get("symbols", [])),
solution=item.get("solution", ""),
)
)
parts.append("MATH_SOLUTIONS:\n" + "\n\n".join(blocks))
else:
parts.append("MATH_SOLUTIONS:\n(leer)")
return "\n\n".join(parts)
# ---------------------------------------------------------------------------------------------------
# History related
# ---------------------------------------------------------------------------------------------------
def update_history(sheet: dict[str, Any], messages: list[dict]) -> None:
sheet["history"] = messages[:]
sheet["updated_at"] = _utc_now()
def format_history(messages: list[dict]) -> str:
lines = []
for msg in messages:
role = msg.get("role", "unknown")
content = msg.get("content", "")
lines.append(f"{role}: {content}")
return "\n".join(lines)
def get_history(sheet: dict[str, Any]) -> str:
return format_history(sheet.get("history", []))
def get_history_turns(sheet: dict[str, Any]) -> list[dict]:
return sheet.get("history", [])
def get_task(sheet: dict[str, Any]) -> str:
history = sheet.get("history", [])
if history:
return history[-1].get("content", "")
return ""
# ---------------------------------------------------------------------------------------------------
# Retrieval related
# ---------------------------------------------------------------------------------------------------
def update_retrieval_context(
sheet: dict[str, Any],
sources: list[Source],
) -> None:
temp_sources = get_retrieval(sheet)
for source in sources:
temp_sources.append(source)
temp_sources = sorted(sources, key=lambda x: x.score, reverse=True)
temp_sources = temp_sources[:8]
for source in temp_sources:
sheet["sources"].append(source.model_dump())
sheet["updated_at"] = _utc_now()
def get_retrieval(sheet: dict[str, Any]) -> list[Source]:
sources = sheet.get("sources", [])
if not sources:
return []
return [Source.model_validate(source) for source in sources]
# ---------------------------------------------------------------------------------------------------
# Math-solution related
# ---------------------------------------------------------------------------------------------------
def add_math_solution(
sheet: dict[str, Any],
solution: str,
) -> None:
sheet["math_solutions"].append(
{
"solution": solution,
}
)
sheet["updated_at"] = _utc_now()
def first_math_solution(sheet: dict[str, Any]) -> str:
if not sheet["math_solutions"]:
return ""
return sheet["math_solutions"][0].get("solution", "")
# ---------------------------------------------------------------------------------------------------
# LLM-solution related
# ---------------------------------------------------------------------------------------------------
def add_LLM_solution(
sheet: dict[str, Any],
solution: str,
) -> None:
sheet["LLM_solutions"].append(
{
"solution": solution,
}
)
sheet["updated_at"] = _utc_now()
def last_LLM_solution(sheet: dict[str, Any]) -> str:
if not sheet["LLM_solutions"]:
return ""
return sheet["LLM_solutions"][-1].get("solution", "")
# ---------------------------------------------------------------------------------------------------
# decision related
# ---------------------------------------------------------------------------------------------------
def add_decision(sheet: dict[str, Any], decision: dict[str, Any]) -> None:
entry = {"timestamp": _utc_now(), **decision}
sheet["decisions"].append(entry)
sheet["updated_at"] = _utc_now()
from app.deterministic_services.context_stores import context_store_base
from app.deterministic_services.context_stores import context_store_new
from app.deterministic_services.context_stores import context_store_open
__all__ = ["context_store_base", "context_store_open", "context_store_new"]
from __future__ import annotations
import hashlib
import json
import os
from datetime import datetime
from threading import Lock
from typing import Any
from app.deterministic_services import Source
_CACHE: dict[str, dict[str, Any]] = {}
_LOCK = Lock()
_LOG_DIR = os.path.join("logs", "context_sheets")
def _utc_now() -> str:
return datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
def _touch(sheet: dict[str, Any]) -> None:
sheet["updated_at"] = _utc_now()
def touch_sheet(sheet: dict[str, Any]) -> None:
_touch(sheet)
def get_chat_id(messages: list[dict], draft: str | None = None) -> str:
if draft:
return f"draft_{draft}"
first_user = next(
(m for m in messages if m.get("role") == "user" and m.get("content")),
None,
)
if not first_user:
return "unknown"
digest = hashlib.sha1(first_user["content"].encode("utf-8")).hexdigest()
return digest[:12]
def init_sheet_base(chat_id: str, messages: list[dict]) -> dict[str, Any]:
timestamp = _utc_now()
return {
"chat_id": chat_id,
"created_at": timestamp,
"updated_at": timestamp,
"history": messages[:],
"sources": [],
"decisions": [],
"initialized": False,
}
def load_sheet(chat_id: str) -> dict[str, Any] | None:
with _LOCK:
if chat_id in _CACHE:
return _CACHE[chat_id]
latest_path = os.path.join(_LOG_DIR, f"{chat_id}_latest.json")
if os.path.exists(latest_path):
with open(latest_path, "r", encoding="utf-8") as file_handle:
sheet = json.load(file_handle)
with _LOCK:
_CACHE[chat_id] = sheet
return sheet
return None
def save_sheet(sheet: dict[str, Any]) -> None:
os.makedirs(_LOG_DIR, exist_ok=True)
_touch(sheet)
chat_id = sheet.get("chat_id", "unknown")
latest_path = os.path.join(_LOG_DIR, f"{chat_id}_latest.json")
payload = json.dumps(sheet, ensure_ascii=True, indent=2)
with open(latest_path, "w", encoding="utf-8") as file_handle:
file_handle.write(payload)
with _LOCK:
_CACHE[chat_id] = sheet
def format_history(messages: list[dict]) -> str:
lines = []
for msg in messages:
role = msg.get("role", "unknown")
content = msg.get("content", "")
lines.append(f"{role}: {content}")
return "\n".join(lines)
def format_sheet_base(sheet: dict[str, Any]) -> str:
parts = []
history = format_history(sheet.get("history", []))
parts.append("HISTORY:\n" + (history or "(leer)"))
sources = get_retrieval(sheet)
if sources:
parts.append("SOURCES:\n" + "\n".join([source.to_string() for source in sources]))
else:
parts.append("SOURCES:\n(leer)")
decisions = get_decisions(sheet)
if decisions:
parts.append("DECISIONS:\n" + json.dumps(decisions, ensure_ascii=True, indent=2))
else:
parts.append("DECISIONS:\n(leer)")
parts.append(f"INITIALIZED:\n{get_initialized(sheet)}")
return "\n\n".join(parts)
def set_chat_id(sheet: dict[str, Any], value: str) -> None:
sheet["chat_id"] = value
_touch(sheet)
def get_chat_id_value(sheet: dict[str, Any]) -> str:
return str(sheet.get("chat_id", ""))
def set_created_at(sheet: dict[str, Any], value: str) -> None:
sheet["created_at"] = value
_touch(sheet)
def get_created_at(sheet: dict[str, Any]) -> str:
return str(sheet.get("created_at", ""))
def set_updated_at(sheet: dict[str, Any], value: str) -> None:
sheet["updated_at"] = value
def get_updated_at(sheet: dict[str, Any]) -> str:
return str(sheet.get("updated_at", ""))
def set_history(sheet: dict[str, Any], messages: list[dict]) -> None:
sheet["history"] = messages[:]
_touch(sheet)
def update_history(sheet: dict[str, Any], messages: list[dict]) -> None:
set_history(sheet, messages)
def get_history(sheet: dict[str, Any]) -> str:
return format_history(sheet.get("history", []))
def get_history_turns(sheet: dict[str, Any]) -> list[dict]:
return sheet.get("history", [])
def set_decisions(sheet: dict[str, Any], decisions: list[dict[str, Any]]) -> None:
sheet["decisions"] = decisions[:]
_touch(sheet)
def get_decisions(sheet: dict[str, Any]) -> list[dict[str, Any]]:
return sheet.get("decisions", [])
def add_decision(sheet: dict[str, Any], decision: dict[str, Any]) -> None:
entry = {"timestamp": _utc_now(), **decision}
sheet.setdefault("decisions", []).append(entry)
_touch(sheet)
def set_initialized(sheet: dict[str, Any], value: bool) -> None:
sheet["initialized"] = bool(value)
_touch(sheet)
def get_initialized(sheet: dict[str, Any]) -> bool:
return bool(sheet.get("initialized", False))
def set_sources(sheet: dict[str, Any], sources: list[Source]) -> None:
sheet["sources"] = [source.model_dump() for source in sources]
_touch(sheet)
def get_retrieval(sheet: dict[str, Any]) -> list[Source]:
raw_sources = sheet.get("sources", [])
if not raw_sources:
return []
return [Source.model_validate(source) for source in raw_sources]
def update_retrieval_context(
sheet: dict[str, Any],
sources: list[Source],
) -> None:
merged = get_retrieval(sheet) + sources
merged_sorted = sorted(merged, key=lambda item: item.score, reverse=True)
set_sources(sheet, merged_sorted)
from __future__ import annotations
from typing import Any
from app.deterministic_services.context_stores.context_store_base import (
add_decision,
format_history,
format_sheet_base,
get_chat_id,
get_created_at,
get_decisions,
get_history,
get_history_turns,
get_initialized,
get_retrieval,
get_updated_at,
init_sheet_base,
load_sheet,
save_sheet,
set_chat_id,
set_created_at,
set_decisions,
set_history,
set_initialized,
set_sources,
set_updated_at,
touch_sheet,
update_history,
update_retrieval_context,
)
def init_sheet(chat_id: str, messages: list[dict]) -> dict[str, Any]:
sheet = init_sheet_base(chat_id, messages)
sheet["task"] = ""
sheet["hints"] = []
sheet["solution"] = ""
return sheet
def set_task(sheet: dict[str, Any], value: str) -> None:
sheet["task"] = value
touch_sheet(sheet)
def get_task(sheet: dict[str, Any]) -> str:
return str(sheet.get("task", ""))
def set_hints(sheet: dict[str, Any], hints: list[str]) -> None:
sheet["hints"] = hints[:]
touch_sheet(sheet)
def get_hints(sheet: dict[str, Any]) -> list[str]:
return sheet.get("hints", [])
def add_hint(sheet: dict[str, Any], hint: str) -> None:
sheet.setdefault("hints", []).append(hint)
touch_sheet(sheet)
def set_solution(sheet: dict[str, Any], value: str) -> None:
sheet["solution"] = value
touch_sheet(sheet)
def get_solution(sheet: dict[str, Any]) -> str:
return str(sheet.get("solution", ""))
def format_sheet(sheet: dict[str, Any]) -> str:
parts = [format_sheet_base(sheet)]
task = get_task(sheet)
parts.append("TASK:\n" + (task if task else "(leer)"))
hints = get_hints(sheet)
if hints:
parts.append("HINTS:\n" + "\n".join(hints))
else:
parts.append("HINTS:\n(leer)")
solution = get_solution(sheet)
parts.append("SOLUTION:\n" + (solution if solution else "(leer)"))
return "\n\n".join(parts)
from __future__ import annotations
from typing import Any
from app.deterministic_services.context_stores.context_store_base import (
format_history,
format_sheet_base,
get_chat_id,
get_decisions,
get_history,
get_history_turns,
get_initialized,
get_retrieval,
get_updated_at,
load_sheet,
save_sheet,
set_chat_id,
set_created_at,
set_decisions,
set_history,
set_initialized,
set_sources,
set_updated_at,
touch_sheet,
update_history,
update_retrieval_context,
add_decision,
init_sheet_base,
)
def init_sheet(chat_id: str, messages: list[dict]) -> dict[str, Any]:
sheet = init_sheet_base(chat_id, messages)
sheet["math_solutions"] = []
sheet["LLM_solutions"] = []
return sheet
def set_math_solutions(sheet: dict[str, Any], values: list[dict[str, Any]]) -> None:
sheet["math_solutions"] = values[:]
touch_sheet(sheet)
def get_math_solutions(sheet: dict[str, Any]) -> list[dict[str, Any]]:
return sheet.get("math_solutions", [])
def add_math_solution(
sheet: dict[str, Any],
solution: str,
) -> None:
sheet.setdefault("math_solutions", []).append({"solution": solution})
touch_sheet(sheet)
def first_math_solution(sheet: dict[str, Any]) -> str:
math_solutions = get_math_solutions(sheet)
if not math_solutions:
return ""
return math_solutions[0].get("solution", "")
def set_llm_solutions(sheet: dict[str, Any], values: list[dict[str, Any]]) -> None:
sheet["LLM_solutions"] = values[:]
touch_sheet(sheet)
def get_llm_solutions(sheet: dict[str, Any]) -> list[dict[str, Any]]:
return sheet.get("LLM_solutions", [])
def add_LLM_solution(
sheet: dict[str, Any],
solution: str,
) -> None:
sheet.setdefault("LLM_solutions", []).append({"solution": solution})
touch_sheet(sheet)
def last_LLM_solution(sheet: dict[str, Any]) -> str:
llm_solutions = get_llm_solutions(sheet)
if not llm_solutions:
return ""
return llm_solutions[-1].get("solution", "")
def get_task(sheet: dict[str, Any]) -> str:
history = get_history_turns(sheet)
if history:
return history[-1].get("content", "")
return ""
def format_sheet(sheet: dict[str, Any]) -> str:
parts = [format_sheet_base(sheet)]
math_solutions = get_math_solutions(sheet)
if math_solutions:
blocks = []
for item in math_solutions:
blocks.append(f"SOLUTION: {item.get('solution', '')}")
parts.append("MATH_SOLUTIONS:\n" + "\n\n".join(blocks))
else:
parts.append("MATH_SOLUTIONS:\n(leer)")
llm_solutions = get_llm_solutions(sheet)
if llm_solutions:
blocks = []
for item in llm_solutions:
blocks.append(f"SOLUTION: {item.get('solution', '')}")
parts.append("LLM_SOLUTIONS:\n" + "\n\n".join(blocks))
else:
parts.append("LLM_SOLUTIONS:\n(leer)")
return "\n\n".join(parts)
...@@ -34,6 +34,10 @@ def is_new_chat(messages: list[dict]) -> bool: ...@@ -34,6 +34,10 @@ def is_new_chat(messages: list[dict]) -> bool:
return not any(m.get("role") == "assistant" for m in messages) return not any(m.get("role") == "assistant" for m in messages)
# ---
# Timekeeping
# ---
def _utc_now_iso() -> str: def _utc_now_iso() -> str:
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
...@@ -48,26 +52,6 @@ def _finish_timing(started_perf: float) -> tuple[str, float]: ...@@ -48,26 +52,6 @@ def _finish_timing(started_perf: float) -> tuple[str, float]:
return finished_at, duration_ms return finished_at, duration_ms
def append_tool_log(
tool_log: list[dict],
name: str,
args: dict,
response: object,
*,
started_at: str | None = None,
finished_at: str | None = None,
duration_ms: float | None = None,
) -> None:
entry = {"name": name, "arguments": args, "response": response}
if started_at is not None:
entry["started_at"] = started_at
if finished_at is not None:
entry["finished_at"] = finished_at
if duration_ms is not None:
entry["duration_ms"] = duration_ms
tool_log.append(entry)
def log_timed_call( def log_timed_call(
tool_log: list[dict], tool_log: list[dict],
name: str, name: str,
...@@ -102,6 +86,33 @@ def log_timed_call( ...@@ -102,6 +86,33 @@ def log_timed_call(
) )
return response return response
# ---
# Toolcalling
# ---
def append_tool_log(
tool_log: list[dict],
name: str,
args: dict,
response: object,
*,
started_at: str | None = None,
finished_at: str | None = None,
duration_ms: float | None = None,
) -> None:
entry = {"name": name, "arguments": args, "response": response}
if started_at is not None:
entry["started_at"] = started_at
if finished_at is not None:
entry["finished_at"] = finished_at
if duration_ms is not None:
entry["duration_ms"] = duration_ms
tool_log.append(entry)
# ---
# LLM Nachricht entpacken
# ---
def extract_user_messages(messages: list[dict]) -> list[str]: def extract_user_messages(messages: list[dict]) -> list[str]:
user_contents: list[str] = [] user_contents: list[str] = []
...@@ -112,6 +123,9 @@ def extract_user_messages(messages: list[dict]) -> list[str]: ...@@ -112,6 +123,9 @@ def extract_user_messages(messages: list[dict]) -> list[str]:
user_contents.append(content) user_contents.append(content)
return user_contents return user_contents
# --
# Konext
# --
def retrieve_context( def retrieve_context(
query_text: str, pg_url: str | None = None query_text: str, pg_url: str | None = None
...@@ -158,6 +172,9 @@ def bootstrap_retrieval(sheet: dict, query_text: str, tool_log: list[dict]) -> N ...@@ -158,6 +172,9 @@ def bootstrap_retrieval(sheet: dict, query_text: str, tool_log: list[dict]) -> N
) )
context_store.update_retrieval_context(sheet, sources) context_store.update_retrieval_context(sheet, sources)
# --
# Chat Status
# --
def init_chat_state(messages: list[dict], draft: str | None = None) -> ChatState: def init_chat_state(messages: list[dict], draft: str | None = None) -> ChatState:
if not messages: if not messages:
...@@ -186,6 +203,10 @@ def init_chat_state(messages: list[dict], draft: str | None = None) -> ChatState ...@@ -186,6 +203,10 @@ def init_chat_state(messages: list[dict], draft: str | None = None) -> ChatState
) )
# --
# Ausgabe
# --
def finalize_response(state: ChatState, reply: str | None) -> dict: def finalize_response(state: ChatState, reply: str | None) -> dict:
if not reply: if not reply:
reply = "Dazu steht nichts im Material" reply = "Dazu steht nichts im Material"
...@@ -204,6 +225,10 @@ def finalize_response(state: ChatState, reply: str | None) -> dict: ...@@ -204,6 +225,10 @@ def finalize_response(state: ChatState, reply: str | None) -> dict:
return {"reply": reply, "sources": state.sheet.get("sources", []), "tool_log": state.tool_log} return {"reply": reply, "sources": state.sheet.get("sources", []), "tool_log": state.tool_log}
# --
# gesamtablauf einens Chats
# --
def run_chat_common( def run_chat_common(
messages: list[dict], messages: list[dict],
draft: str | None, draft: str | None,
......
...@@ -4,14 +4,17 @@ from app.LLM_services import qa_LLM ...@@ -4,14 +4,17 @@ from app.LLM_services import qa_LLM
from app.deterministic_services import context_store from app.deterministic_services import context_store
from app.deterministic_services.orchestrators import orchestrator_base as base from app.deterministic_services.orchestrators import orchestrator_base as base
# beim ersten Aufruf
def _on_bootstrap(state: base.ChatState, query_text: str) -> None: def _on_bootstrap(state: base.ChatState, query_text: str) -> None:
base.bootstrap_retrieval(state.sheet, query_text, state.tool_log) base.bootstrap_retrieval(state.sheet, query_text, state.tool_log)
# Sonstiges bei jedem Aufruf
def _on_turn_logic(state: base.ChatState) -> None: def _on_turn_logic(state: base.ChatState) -> None:
return None return None
# Antwort generieren
def _on_build_reply(state: base.ChatState) -> str | None: def _on_build_reply(state: base.ChatState) -> str | None:
args = { args = {
......
from __future__ import annotations
from app.LLM_services import task_hint_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) -> None:
refs = task_catalog.get_selected_task_subsection_refs(state.sheet)
if not refs:
return
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),
}
base.log_timed_call(
state.tool_log,
"retrieve_context_with_task_subsections",
{
"query": query_text,
"subsection_refs": refs,
},
_retrieve,
)
# beim ersten Aufruf
def _on_bootstrap(state: base.ChatState, query_text: str) -> None:
base.bootstrap_retrieval(state.sheet, query_text, state.tool_log)
_ensure_context_task_fields(state, query_text)
_retrieve_context_for_task(state, query_text)
# Sonstiges bei jedem Aufruf
def _on_turn_logic(state: base.ChatState) -> None:
_ensure_context_task_fields(state, state.last_user)
# Antwort generieren
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_hint",
args,
lambda: task_hint_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,
)
from __future__ import annotations from __future__ import annotations
from app.LLM_services import decision_LLM, hint_LLM, math_intent_LLM, solver_LLM from app.LLM_services import decision_LLM, open_hint_LLM, math_intent_LLM, solver_LLM
from app.deterministic_services import context_store from app.deterministic_services import context_store
from app.deterministic_services.orchestrators import orchestrator_base as base from app.deterministic_services.orchestrators import orchestrator_base as base
# beim ersten Aufruf
def _on_bootstrap(state: base.ChatState, query_text: str) -> None: def _on_bootstrap(state: base.ChatState, query_text: str) -> None:
base.bootstrap_retrieval(state.sheet, query_text, state.tool_log) base.bootstrap_retrieval(state.sheet, query_text, state.tool_log)
...@@ -16,6 +17,7 @@ def _on_bootstrap(state: base.ChatState, query_text: str) -> None: ...@@ -16,6 +17,7 @@ def _on_bootstrap(state: base.ChatState, query_text: str) -> None:
if math_solution: if math_solution:
context_store.add_math_solution(state.sheet, math_solution) context_store.add_math_solution(state.sheet, math_solution)
# Sonstiges bei jedem Aufruf
def _on_turn_logic(state: base.ChatState) -> None: def _on_turn_logic(state: base.ChatState) -> None:
sheet_text = context_store.format_sheet(state.sheet) sheet_text = context_store.format_sheet(state.sheet)
...@@ -42,6 +44,7 @@ def _on_turn_logic(state: base.ChatState) -> None: ...@@ -42,6 +44,7 @@ def _on_turn_logic(state: base.ChatState) -> None:
full_query = "\n".join(base.extract_user_messages(state.messages)) full_query = "\n".join(base.extract_user_messages(state.messages))
_on_bootstrap(state, full_query) _on_bootstrap(state, full_query)
# Antwort generieren
def _on_build_reply(state: base.ChatState) -> str | None: def _on_build_reply(state: base.ChatState) -> str | None:
history_turns = context_store.get_history_turns(state.sheet) history_turns = context_store.get_history_turns(state.sheet)
...@@ -57,7 +60,7 @@ def _on_build_reply(state: base.ChatState) -> str | None: ...@@ -57,7 +60,7 @@ def _on_build_reply(state: base.ChatState) -> str | None:
state.tool_log, state.tool_log,
"generate_hint", "generate_hint",
args, args,
lambda: hint_LLM.generate_hint(**args), lambda: open_hint_LLM.generate_hint(**args),
) )
......
...@@ -16,6 +16,7 @@ def retrieve( ...@@ -16,6 +16,7 @@ def retrieve(
embedder: EmbeddingLike, embedder: EmbeddingLike,
query: str, query: str,
k: int = 4, k: int = 4,
chapter_index: int | None = None,
section_index: int | None = None, section_index: int | None = None,
subsection_index: int | None = None, subsection_index: int | None = None,
source_type_filter: list[str] | None = None, source_type_filter: list[str] | None = None,
...@@ -28,6 +29,7 @@ def retrieve( ...@@ -28,6 +29,7 @@ def retrieve(
embedder=embedder, embedder=embedder,
query=query, query=query,
k=k, k=k,
chapter_index=chapter_index,
section_index=section_index, section_index=section_index,
subsection_index=subsection_index, subsection_index=subsection_index,
source_type_filter=source_type_filter, source_type_filter=source_type_filter,
...@@ -40,9 +42,63 @@ def retrieve( ...@@ -40,9 +42,63 @@ def retrieve(
embedder=embedder, embedder=embedder,
query=query, query=query,
k=k, k=k,
chapter_index=chapter_index,
section_index=section_index, section_index=section_index,
subsection_index=subsection_index, subsection_index=subsection_index,
source_type_filter=source_type_filter, source_type_filter=source_type_filter,
expand_links=expand_links, expand_links=expand_links,
neighbor_expand=neighbor_expand, neighbor_expand=neighbor_expand,
) )
def retrieve_with_subsections(
pg_url: str,
embedder: EmbeddingLike,
query: str,
subsection_refs: list[vector_store.SubsectionRef] | None = None,
k: int = 4,
chapter_index: int | None = None,
section_index: int | None = None,
subsection_index: int | None = None,
source_type_filter: list[str] | None = None,
expand_links: bool = True,
neighbor_expand: int = 0,
) -> List[Source]:
if _use_subsection_retrieval():
return vector_store_subsection.retrieve_with_subsections(
pg_url=pg_url,
embedder=embedder,
query=query,
subsection_refs=subsection_refs,
k=k,
chapter_index=chapter_index,
section_index=section_index,
subsection_index=subsection_index,
source_type_filter=source_type_filter,
expand_links=expand_links,
neighbor_expand=neighbor_expand,
)
return vector_store.retrieve_with_subsections(
pg_url=pg_url,
embedder=embedder,
query=query,
subsection_refs=subsection_refs,
k=k,
chapter_index=chapter_index,
section_index=section_index,
subsection_index=subsection_index,
source_type_filter=source_type_filter,
expand_links=expand_links,
neighbor_expand=neighbor_expand,
)
def retrieve_for_subsections(
pg_url: str,
subsection_refs: list[vector_store.SubsectionRef] | None = None,
) -> List[Source]:
return vector_store.load_children_for_subsections(
pg_url=pg_url,
subsection_refs=subsection_refs,
)
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