Commit d8327473 authored by Kantz's avatar Kantz
Browse files

auräumaktion

parent d58cbed7
...@@ -15,7 +15,11 @@ cd math-tutor/frontend ...@@ -15,7 +15,11 @@ cd math-tutor/frontend
npm install npm install
npm run dev npm run dev
``` ```
Um es im Netzwerk verfügbar zu machen mit: npm run dev -- --host 0.0.0.0 To make it accessible over the network.
Add the frontend- and backend-adress in the .env file in the frontend- and backend-folder.
python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
npm run dev -- --host 0.0.0.0
## 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`:
...@@ -51,7 +55,7 @@ python -m scripts.retrieval_cli query --q "Was ist eine Teilmenge?" --k 8 --expa ...@@ -51,7 +55,7 @@ python -m scripts.retrieval_cli query --q "Was ist eine Teilmenge?" --k 8 --expa
## Configuration ## Configuration
System prompt (LLM): 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:
......
from __future__ import annotations from __future__ import annotations
from logging import config
from typing import List, Optional from typing import List, Optional
import logging import logging
...@@ -8,9 +9,12 @@ from fastapi import APIRouter, HTTPException, Path, Query ...@@ -8,9 +9,12 @@ from fastapi import APIRouter, HTTPException, Path, Query
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.deterministic_services import session_store from app.deterministic_services import session_store
from app.deterministic_services.orchestrators import orchestrator_tutor as orchestrator import app.config as config
from app.deterministic_services.orchestrators import orchestrator_QA
if config.get_orchestrator() == "tutor":
from app.deterministic_services.orchestrators import orchestrator_tutor as orchestrator
else:
from app.deterministic_services.orchestrators import orchestrator_QA as orchestrator
router = APIRouter() router = APIRouter()
......
...@@ -6,7 +6,6 @@ from fastapi import APIRouter, HTTPException ...@@ -6,7 +6,6 @@ from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from app.deterministic_services import context_store from app.deterministic_services import context_store
from app.deterministic_services import Source
router = APIRouter() router = APIRouter()
......
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
from typing import Any, Dict from typing import Any, Dict
import httpx import httpx
import psycopg import psycopg
from fastapi import APIRouter from fastapi import APIRouter
import app.config as config
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _check_ollama() -> dict: def _check_ollama() -> dict:
base_url = os.getenv("OLLAMA_URL", "http://localhost:11434").rstrip("/") base_url = config.get_ollama_settings().base_url.rstrip("/")
url = f"{base_url}/api/tags" url = f"{base_url}/api/tags"
try: try:
with httpx.Client(timeout=5.0) as client: with httpx.Client(timeout=5.0) as client:
...@@ -34,14 +35,13 @@ def _normalize_openai_models_url(base_url: str) -> str: ...@@ -34,14 +35,13 @@ def _normalize_openai_models_url(base_url: str) -> str:
def _check_openai() -> dict: def _check_openai() -> dict:
base_url = os.getenv("OPENAI_BASE_URL") settings = config.get_openai_base_settings()
api_key = os.getenv("OPENAI_API_KEY") if not settings:
if not base_url or not api_key:
return {"status": "missing_config"} return {"status": "missing_config"}
url = _normalize_openai_models_url(base_url) url = _normalize_openai_models_url(settings.base_url)
try: try:
headers = {"Authorization": f"Bearer {api_key}"} headers = {"Authorization": f"Bearer {settings.api_key}"}
with httpx.Client(timeout=5.0) as client: with httpx.Client(timeout=5.0) as client:
response = client.get(url, headers=headers) response = client.get(url, headers=headers)
if response.status_code in (401, 403): if response.status_code in (401, 403):
...@@ -53,8 +53,9 @@ def _check_openai() -> dict: ...@@ -53,8 +53,9 @@ def _check_openai() -> dict:
def _check_postgres() -> dict: def _check_postgres() -> dict:
pg_url = os.getenv("POSTGRES_URL") try:
if not pg_url: pg_url = config.get_postgres_url()
except ValueError:
return {"status": "missing_config"} return {"status": "missing_config"}
try: try:
with psycopg.connect(pg_url, connect_timeout=5) as conn: with psycopg.connect(pg_url, connect_timeout=5) as conn:
......
from __future__ import annotations
from __future__ import annotations
import os
from pathlib import Path
from typing import List, Optional
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel, Field
from app import config
from app.deterministic_services.embeddings import OpenAILikeEmbeddings
from app.deterministic_services import vector_store
router = APIRouter()
def _get_embedder() -> OpenAILikeEmbeddings:
settings = config.get_embedding_settings()
return OpenAILikeEmbeddings(
base_url=settings.base_url,
api_key=settings.api_key,
model=settings.model,
target_dim=settings.target_dim,
)
class InitDbRequest(BaseModel):
pg_url: Optional[str] = None
class IngestRequest(BaseModel):
pg_url: Optional[str] = None
base_dir: str = Field(default="markdown")
class QueryRequest(BaseModel):
pg_url: Optional[str] = None
query: str = Field(..., min_length=1)
k: int = 4
expand_links: bool = True
section_index: Optional[int] = None
subsection_index: Optional[int] = None
source_type_filter: Optional[List[str]] = None
neighbor_expand: int = 0
@router.post("/api/retrieval/init-db")
def init_db(request: InitDbRequest) -> dict:
pg_url = request.pg_url or os.getenv("POSTGRES_URL")
if not pg_url:
raise HTTPException(status_code=500, detail="Missing POSTGRES_URL")
vector_store.init_db(pg_url)
return {"status": "ok"}
@router.post("/api/retrieval/ingest")
def ingest(request: IngestRequest) -> dict:
pg_url = request.pg_url or os.getenv("POSTGRES_URL")
if not pg_url:
raise HTTPException(status_code=500, detail="Missing POSTGRES_URL")
base_dir = Path(request.base_dir)
if not base_dir.exists():
raise HTTPException(status_code=400, detail="base_dir does not exist")
embedder = _get_embedder()
docs = vector_store.load_docs(base_dir)
embeddings = vector_store.embed_documents(embedder, [doc.markdown for doc in docs])
upserted = vector_store.upsert_docs(pg_url, docs, embeddings)
return {"status": "ok", "upserted": upserted}
@router.post("/api/retrieval/query")
def query(request: QueryRequest) -> dict:
pg_url = request.pg_url or os.getenv("POSTGRES_URL")
if not pg_url:
raise HTTPException(status_code=500, detail="Missing POSTGRES_URL")
embedder = _get_embedder()
result = vector_store.retrieve(
pg_url=pg_url,
embedder=embedder,
query=request.query,
k=request.k,
section_index=request.section_index,
subsection_index=request.subsection_index,
source_type_filter=request.source_type_filter,
expand_links=request.expand_links,
neighbor_expand=request.neighbor_expand,
)
return result
@router.get("/api/retrieval/sections")
def sections(pg_url: Optional[str] = None) -> list[dict]:
url = pg_url or os.getenv("POSTGRES_URL")
if not url:
raise HTTPException(status_code=500, detail="Missing POSTGRES_URL")
return vector_store.list_sections(url)
@router.get("/api/retrieval/subsections")
def subsections(pg_url: Optional[str] = None, section_index: Optional[int] = None) -> list[dict]:
url = pg_url or os.getenv("POSTGRES_URL")
if not url:
raise HTTPException(status_code=500, detail="Missing POSTGRES_URL")
return vector_store.list_subsections(url, section_index)
...@@ -14,6 +14,10 @@ class EmbeddingSettings(BaseModel): ...@@ -14,6 +14,10 @@ class EmbeddingSettings(BaseModel):
model: str model: str
target_dim: int = 1024 target_dim: int = 1024
def get_orchestrator() -> str:
return os.getenv("ORCHESTRATOR", "qa").lower()
def get_embedding_settings() -> EmbeddingSettings: def get_embedding_settings() -> EmbeddingSettings:
embedding_type = os.getenv("EMBEDDING_TYPE", "openai-like") embedding_type = os.getenv("EMBEDDING_TYPE", "openai-like")
if embedding_type == "sentence-transformer": if embedding_type == "sentence-transformer":
...@@ -56,6 +60,11 @@ class OpenAIChatSettings: ...@@ -56,6 +60,11 @@ class OpenAIChatSettings:
timeout: float | None timeout: float | None
temperature: float | None temperature: float | None
@dataclass(frozen=True)
class OpenAIBaseSettings:
base_url: str
api_key: str
@dataclass(frozen=True) @dataclass(frozen=True)
class MathpixSettings: class MathpixSettings:
app_id: str app_id: str
...@@ -91,17 +100,26 @@ def get_ollama_settings() -> OllamaSettings: ...@@ -91,17 +100,26 @@ def get_ollama_settings() -> OllamaSettings:
temperature=_read_float(os.getenv("OLLAMA_TEMPERATURE")), temperature=_read_float(os.getenv("OLLAMA_TEMPERATURE")),
) )
def get_openai_base_settings() -> OpenAIBaseSettings | None:
base_url = os.getenv("OPENAI_BASE_URL")
api_key = os.getenv("OPENAI_API_KEY")
if not base_url or not api_key:
return None
return OpenAIBaseSettings(
base_url=base_url,
api_key=api_key,
)
def get_openai_chat_settings() -> OpenAIChatSettings | None: def get_openai_chat_settings() -> OpenAIChatSettings | None:
model = os.getenv("OPENAI_CHAT_MODEL") model = os.getenv("OPENAI_CHAT_MODEL")
if not model: if not model:
return None return None
base_url = os.getenv("OPENAI_BASE_URL") base_settings = get_openai_base_settings()
api_key = os.getenv("OPENAI_API_KEY") if not base_settings:
if not base_url or not api_key:
raise ValueError("Missing OPENAI_BASE_URL or OPENAI_API_KEY for chat") raise ValueError("Missing OPENAI_BASE_URL or OPENAI_API_KEY for chat")
return OpenAIChatSettings( return OpenAIChatSettings(
base_url=base_url, base_url=base_settings.base_url,
api_key=api_key, api_key=base_settings.api_key,
model=model, model=model,
timeout=_read_float(os.getenv("OPENAI_CHAT_TIMEOUT")), timeout=_read_float(os.getenv("OPENAI_CHAT_TIMEOUT")),
temperature=_read_float(os.getenv("OPENAI_CHAT_TEMPERATURE")), temperature=_read_float(os.getenv("OPENAI_CHAT_TEMPERATURE")),
......
from app.deterministic_services import context_store, retrieval_service, tool_logging
from app.deterministic_services import referenz_decoder
from app.LLM_services import qa_LLM
from typing import List from typing import List
from app.deterministic_services import context_store, tool_logging, vector_store, Source, referenz_decoder
from app.LLM_services import qa_LLM
from app.deterministic_services.embeddings import EmbeddingFactory
import app.config as config
def _is_new_chat(messages: list[dict]) -> bool: 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)
...@@ -13,7 +15,7 @@ def _append_tool_log(tool_log: list[dict], name: str, args: dict, response: obje ...@@ -13,7 +15,7 @@ def _append_tool_log(tool_log: list[dict], name: str, args: dict, response: obje
def _bootstrap_context(sheet: dict, query_text: str, tool_log: list[dict]) -> None: def _bootstrap_context(sheet: dict, query_text: str, tool_log: list[dict]) -> None:
# Erstelle einen neuen Retrieval-Block oder aktualisiere den bestehenden # Erstelle einen neuen Retrieval-Block oder aktualisiere den bestehenden
sources = retrieval_service.retrieve_context(query_text=query_text) sources = _retrieve_context(query_text=query_text)
retrievals = sheet.get("retrieval_contexts", []) retrievals = sheet.get("retrieval_contexts", [])
if retrievals: if retrievals:
...@@ -36,6 +38,17 @@ def _extract_user_messages(messages: list[dict]) -> list[str]: ...@@ -36,6 +38,17 @@ def _extract_user_messages(messages: list[dict]) -> list[str]:
user_contents.append(content) user_contents.append(content)
return user_contents return user_contents
def _retrieve_context(query_text: str, pg_url: str | None = None) -> List[Source]:
embedder = EmbeddingFactory.create(config.get_embedding_settings())
url = pg_url or config.get_postgres_url()
sources = vector_store.retrieve(
pg_url=url,
embedder=embedder,
query=query_text,
k=8,
expand_links=True,
)
return sources
def run_chat(messages: list[dict], draft: str | None = None) -> dict: def run_chat(messages: list[dict], draft: str | None = None) -> dict:
# Input-Fehlerbehandlung # Input-Fehlerbehandlung
......
from app.deterministic_services import context_store, retrieval_service, tool_logging from typing import List
from app.deterministic_services import referenz_decoder
from app.deterministic_services import context_store, tool_logging, vector_store, Source, referenz_decoder
from app.LLM_services import hint_LLM, decision_LLM, math_intent_LLM, solver_LLM from app.LLM_services import hint_LLM, decision_LLM, math_intent_LLM, solver_LLM
from app.deterministic_services.embeddings import EmbeddingFactory
import app.config as config
def _is_new_chat(messages: list[dict]) -> bool: def _is_new_chat(messages: list[dict]) -> bool:
...@@ -12,7 +16,7 @@ def _append_tool_log(tool_log: list[dict], name: str, args: dict, response: obje ...@@ -12,7 +16,7 @@ def _append_tool_log(tool_log: list[dict], name: str, args: dict, response: obje
def _bootstrap_context(sheet: dict, query_text: str, tool_log: list[dict]) -> None: def _bootstrap_context(sheet: dict, query_text: str, tool_log: list[dict]) -> None:
# Erstelle einen neuen Retrieval-Block oder aktualisiere den bestehenden # Erstelle einen neuen Retrieval-Block oder aktualisiere den bestehenden
sources = retrieval_service.retrieve_context(query_text=query_text) sources = _retrieve_context(query_text=query_text)
retrievals = sheet.get("retrieval_contexts", []) retrievals = sheet.get("retrieval_contexts", [])
if retrievals: if retrievals:
...@@ -40,6 +44,18 @@ def _extract_user_messages(messages: list[dict]) -> list[str]: ...@@ -40,6 +44,18 @@ def _extract_user_messages(messages: list[dict]) -> list[str]:
user_contents.append(content) user_contents.append(content)
return user_contents return user_contents
def _retrieve_context(query_text: str, pg_url: str | None = None) -> List[Source]:
embedder = EmbeddingFactory.create(config.get_embedding_settings())
url = pg_url or config.get_postgres_url()
sources = vector_store.retrieve(
pg_url=url,
embedder=embedder,
query=query_text,
k=8,
expand_links=True,
)
return sources
def run_chat(messages: list[dict], draft: str | None = None) -> dict: def run_chat(messages: list[dict], draft: str | None = None) -> dict:
# Input-Fehlerbehandlung # Input-Fehlerbehandlung
......
# app/deterministic_services/retrieval_service.py
from __future__ import annotations
from typing import List
from app.deterministic_services import Source
from app import config
from app.deterministic_services.embeddings import EmbeddingFactory
from app.deterministic_services import vector_store
def retrieve_context(query_text: str, pg_url: str | None = None) -> List[Source]:
embedder = EmbeddingFactory.create(config.get_embedding_settings())
url = pg_url or config.get_postgres_url()
sources = vector_store.retrieve(
pg_url=url,
embedder=embedder,
query=query_text,
k=8,
expand_links=True,
)
return sources
\ No newline at end of file
...@@ -2,7 +2,7 @@ from contextlib import asynccontextmanager ...@@ -2,7 +2,7 @@ from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from app.api import canvas, chat, health, retrieval, context from app.api import canvas, chat, health, context
from app.config import get_frontend_url from app.config import get_frontend_url
...@@ -24,6 +24,5 @@ app.add_middleware( ...@@ -24,6 +24,5 @@ app.add_middleware(
app.include_router(chat.router) app.include_router(chat.router)
app.include_router(canvas.router) app.include_router(canvas.router)
app.include_router(retrieval.router)
app.include_router(context.router) app.include_router(context.router)
app.include_router(health.router) app.include_router(health.router)
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