Commit 6cf9eb8c authored by Kantz's avatar Kantz
Browse files

simplifications

parent d9b6c146
...@@ -9,10 +9,11 @@ import app.config as config ...@@ -9,10 +9,11 @@ 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_oranisator from app.deterministic_services import socratic_oranisator
from app.deterministic_services.orchestrators.registry import ( from app.deterministic_services.orchestrators import (
get_default_orchestrator, orchestrator_qa,
is_valid_orchestrator, orchestrator_socratic,
resolve_orchestrator, orchestrator_task,
orchestrator_tutor,
) )
from fastapi import APIRouter, HTTPException, Path, Query from fastapi import APIRouter, HTTPException, Path, Query
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
...@@ -20,6 +21,13 @@ from pydantic import BaseModel, Field ...@@ -20,6 +21,13 @@ from pydantic import BaseModel, Field
router = APIRouter() router = APIRouter()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
DEFAULT_ORCHESTRATOR = "qa"
ORCHESTRATORS = {
"qa": orchestrator_qa,
"tutor": orchestrator_tutor,
"task": orchestrator_task,
"socratic": orchestrator_socratic,
}
class ChatMessage(BaseModel): class ChatMessage(BaseModel):
...@@ -87,12 +95,11 @@ def chat(request: ChatRequest) -> ChatResponse: ...@@ -87,12 +95,11 @@ def chat(request: ChatRequest) -> ChatResponse:
raise HTTPException(status_code=400, detail="messages required") raise HTTPException(status_code=400, detail="messages required")
requested_orchestrator = str(request.orchestrator or "").strip().lower() requested_orchestrator = str(request.orchestrator or "").strip().lower()
if requested_orchestrator and not is_valid_orchestrator(requested_orchestrator): if requested_orchestrator and requested_orchestrator not in ORCHESTRATORS:
raise HTTPException(status_code=422, detail="unsupported orchestrator") raise HTTPException(status_code=422, detail="unsupported orchestrator")
orchestrator_name, orchestrator_impl = resolve_orchestrator( orchestrator_name = requested_orchestrator or DEFAULT_ORCHESTRATOR
requested_orchestrator or None orchestrator_impl = ORCHESTRATORS[orchestrator_name]
)
try: try:
payload_messages = [{"role": m.role, "content": m.text} for m in request.messages] payload_messages = [{"role": m.role, "content": m.text} for m in request.messages]
...@@ -203,7 +210,7 @@ def get_archive(chat_id: str = Path(..., min_length=1)) -> ChatArchiveDetail: ...@@ -203,7 +210,7 @@ def get_archive(chat_id: str = Path(..., min_length=1)) -> ChatArchiveDetail:
history=[ChatMessage(role=item["role"], text=item["text"]) for item in record["history"]], history=[ChatMessage(role=item["role"], text=item["text"]) for item in record["history"]],
selected_task=selected_task, selected_task=selected_task,
selected_topic=selected_topic, selected_topic=selected_topic,
orchestrator=record.get("orchestrator") or get_default_orchestrator(), orchestrator=record.get("orchestrator") or DEFAULT_ORCHESTRATOR,
) )
...@@ -213,9 +220,9 @@ def archive_chat(request: ChatRequest) -> ChatArchiveResponse: ...@@ -213,9 +220,9 @@ def archive_chat(request: ChatRequest) -> ChatArchiveResponse:
return ChatArchiveResponse(status="skipped", chat_id="unknown") return ChatArchiveResponse(status="skipped", chat_id="unknown")
requested_orchestrator = str(request.orchestrator or "").strip().lower() requested_orchestrator = str(request.orchestrator or "").strip().lower()
if requested_orchestrator and not is_valid_orchestrator(requested_orchestrator): if requested_orchestrator and requested_orchestrator not in ORCHESTRATORS:
raise HTTPException(status_code=422, detail="unsupported orchestrator") raise HTTPException(status_code=422, detail="unsupported orchestrator")
archive_orchestrator = requested_orchestrator or get_default_orchestrator() archive_orchestrator = requested_orchestrator or DEFAULT_ORCHESTRATOR
try: try:
chat_id = session_store.archive_chat( chat_id = session_store.archive_chat(
......
# Package marker for services # Package marker for deterministic services.
from app.deterministic_services.retrieval_store import Source, SourceID
__all__ = ["Source", "SourceID"]
\ No newline at end of file
from app.deterministic_services.context_stores import context_store_base # Package marker for context store implementations.
from app.deterministic_services.context_stores import context_store_task
from app.deterministic_services.context_stores import context_store_open
__all__ = ["context_store_base", "context_store_open", "context_store_task"]
...@@ -11,7 +11,7 @@ from zoneinfo import ZoneInfo ...@@ -11,7 +11,7 @@ from zoneinfo import ZoneInfo
from threading import Lock from threading import Lock
from typing import Any from typing import Any
from app.deterministic_services import Source from app.deterministic_services.retrieval_store import Source
from app.deterministic_services.tool_logging import ( from app.deterministic_services.tool_logging import (
format_filename_timestamp, format_filename_timestamp,
format_log_timestamp, format_log_timestamp,
...@@ -36,12 +36,12 @@ def _resolve_latest_path(chat_id: str) -> str | None: ...@@ -36,12 +36,12 @@ def _resolve_latest_path(chat_id: str) -> str | None:
# timekeeping # timekeeping
# --- # ---
def _mez_now() -> str: def berlin_now_iso() -> str:
return datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y-%m-%dT%H:%M:%SZ") return datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y-%m-%dT%H:%M:%SZ")
def _touch(sheet: dict[str, Any]) -> None: def _touch(sheet: dict[str, Any]) -> None:
sheet["updated_at"] = _mez_now() sheet["updated_at"] = berlin_now_iso()
def touch_sheet(sheet: dict[str, Any]) -> None: def touch_sheet(sheet: dict[str, Any]) -> None:
...@@ -66,7 +66,7 @@ def get_chat_id(messages: list[dict], draft: str | None = None) -> str: ...@@ -66,7 +66,7 @@ def get_chat_id(messages: list[dict], draft: str | None = None) -> str:
def init_sheet_base(chat_id: str, messages: list[dict]) -> dict[str, Any]: def init_sheet_base(chat_id: str, messages: list[dict]) -> dict[str, Any]:
timestamp = _mez_now() timestamp = berlin_now_iso()
return { return {
"chat_id": chat_id, "chat_id": chat_id,
"created_at": timestamp, "created_at": timestamp,
...@@ -203,7 +203,7 @@ def get_decisions(sheet: dict[str, Any]) -> list[dict[str, Any]]: ...@@ -203,7 +203,7 @@ def get_decisions(sheet: dict[str, Any]) -> list[dict[str, Any]]:
def add_decision(sheet: dict[str, Any], decision: dict[str, Any]) -> None: def add_decision(sheet: dict[str, Any], decision: dict[str, Any]) -> None:
entry = {"timestamp": _mez_now(), **decision} entry = {"timestamp": berlin_now_iso(), **decision}
sheet.setdefault("decisions", []).append(entry) sheet.setdefault("decisions", []).append(entry)
_touch(sheet) _touch(sheet)
......
...@@ -7,7 +7,7 @@ from threading import Lock ...@@ -7,7 +7,7 @@ from threading import Lock
from typing import Any, Dict, Tuple from typing import Any, Dict, Tuple
import app.config as config import app.config as config
from app.deterministic_services.embeddings import BaseEmbeddings, EmbeddingFactory from app.deterministic_services.embeddings import BaseEmbeddings, create_embedder
_EMBEDDER_LOCK = Lock() _EMBEDDER_LOCK = Lock()
...@@ -35,7 +35,7 @@ def get_embedder() -> tuple[BaseEmbeddings, bool]: ...@@ -35,7 +35,7 @@ def get_embedder() -> tuple[BaseEmbeddings, bool]:
if _CACHED_EMBEDDER is not None and _CACHED_KEY == key: if _CACHED_EMBEDDER is not None and _CACHED_KEY == key:
return _CACHED_EMBEDDER, True return _CACHED_EMBEDDER, True
_CACHED_EMBEDDER = EmbeddingFactory.create(settings) _CACHED_EMBEDDER = create_embedder(settings)
_CACHED_KEY = key _CACHED_KEY = key
return _CACHED_EMBEDDER, False return _CACHED_EMBEDDER, False
......
# Embeddings-Implementation with Factory-Pattern
from __future__ import annotations from __future__ import annotations
import math import math
from typing import List, Optional, Union
from enum import Enum from enum import Enum
from typing import List, Optional, Union
from openai import OpenAI from openai import OpenAI
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from sentence_transformers import SentenceTransformer from sentence_transformers import SentenceTransformer
# -----------------------------
# configuration model
# -----------------------------
class EmbeddingType(str, Enum): class EmbeddingType(str, Enum):
OPENAI_LIKE = "openai-like" OPENAI_LIKE = "openai-like"
SENTENCE_TRANSFORMER = "sentence-transformer" SENTENCE_TRANSFORMER = "sentence-transformer"
class OpenAILikeConfig(BaseModel): class OpenAILikeConfig(BaseModel):
"""Konfiguration für OpenAI-ähnliche APIs.""" base_url: str = Field(..., description="Base URL of the embedding API.")
base_url: str = Field(..., api_key: str = Field(..., description="API key for the embedding API.")
description="Base URL der API (z. B. http://localhost:11434/v1)") model: str = Field(..., description="Embedding model name.")
api_key: str = Field(..., target_dim: int = Field(1024, description="Target embedding dimension.")
description="API-Key (z. B. 'ollama' für Ollama)")
model: str = Field(...,
description="Modellname (z. B. 'nomic-embed-text')")
target_dim: int = Field(1024, description="Ziel-Dimension der Embeddings")
class SentenceTransformerConfig(BaseModel): class SentenceTransformerConfig(BaseModel):
"""Konfiguration für lokale SentenceTransformer-Modelle.""" model: str = Field(..., description="SentenceTransformer model name.")
model: str = Field(..., target_dim: int = Field(384, description="Target embedding dimension.")
description="Name des SentenceTransformer-Modells (z. B. 'all-MiniLM-L6-v2')")
target_dim: int = Field(384, description="Ziel-Dimension der Embeddings")
class EmbeddingConfig(BaseModel): class EmbeddingConfig(BaseModel):
"""Gemeinsame Konfiguration für die Factory.""" embedding_type: EmbeddingType = Field(..., description="Embedding backend type.")
embedding_type: EmbeddingType = Field(...,
description="Typ der Embeddings")
config: Union[OpenAILikeConfig, SentenceTransformerConfig] = Field( config: Union[OpenAILikeConfig, SentenceTransformerConfig] = Field(
..., description="Spezifische Konfiguration") ..., description="Backend-specific embedding config."
)
# -----------------------------
# base class for embeddings
# -----------------------------
class BaseEmbeddings: class BaseEmbeddings:
"""
Basisklasse für Embedding-Generierung mit gemeinsamen Methoden.
"""
def __init__(self, target_dim: int = 384) -> None: def __init__(self, target_dim: int = 384) -> None:
self.target_dim = target_dim self.target_dim = target_dim
def _normalize(self, vec: List[float]) -> List[float]: def _normalize(self, vec: List[float]) -> List[float]:
"""Normalisiert einen Vektor auf L2-Norm."""
norm = math.sqrt(sum(x * x for x in vec)) norm = math.sqrt(sum(x * x for x in vec))
if norm == 0.0: if norm == 0.0:
return vec return vec
return [x / norm for x in vec] return [x / norm for x in vec]
def _truncate(self, vec: List[float]) -> List[float]: def _truncate(self, vec: List[float]) -> List[float]:
"""Trunziert oder füllt den Vektor auf die Ziel-Dimension."""
if len(vec) < self.target_dim: if len(vec) < self.target_dim:
raise ValueError( raise ValueError(f"Embedding dimension {len(vec)} < target {self.target_dim}")
f"Embedding dimension {len(vec)} < target {self.target_dim}")
if len(vec) > self.target_dim: if len(vec) > self.target_dim:
vec = vec[: self.target_dim] vec = vec[: self.target_dim]
return self._normalize(vec) return self._normalize(vec)
def embed_documents(self, texts: List[str]) -> List[List[float]]: def embed_documents(self, texts: List[str]) -> List[List[float]]:
"""Generiert Embeddings für eine Liste von Texten."""
return self._embed(texts) return self._embed(texts)
def embed_query(self, text: str) -> List[float]: def embed_query(self, text: str) -> List[float]:
"""Generiert ein Embedding für einen einzelnen Text."""
return self._embed(text)[0] return self._embed(text)[0]
def _embed(self, inputs: List[str] | str) -> List[List[float]]: def _embed(self, inputs: List[str] | str) -> List[List[float]]:
"""Abstrakte Methode – muss in Unterklassen implementiert werden.""" raise NotImplementedError
raise NotImplementedError("Subclass must implement _embed method.")
# -----------------------------
# subclass
# -----------------------------
class OpenAILikeEmbeddings(BaseEmbeddings): class OpenAILikeEmbeddings(BaseEmbeddings):
"""
Embeddings-Wrapper für OpenAI-ähnliche APIs (z. B. OpenAI, Ollama, TogetherAI).
"""
def __init__(self, config: OpenAILikeConfig) -> None: def __init__(self, config: OpenAILikeConfig) -> None:
super().__init__(target_dim=config.target_dim) super().__init__(target_dim=config.target_dim)
self.base_url = config.base_url.rstrip("/") self.base_url = config.base_url.rstrip("/")
...@@ -103,11 +68,7 @@ class OpenAILikeEmbeddings(BaseEmbeddings): ...@@ -103,11 +68,7 @@ class OpenAILikeEmbeddings(BaseEmbeddings):
self.model = config.model self.model = config.model
def _embed(self, inputs: List[str] | str) -> List[List[float]]: def _embed(self, inputs: List[str] | str) -> List[List[float]]:
"""Ruft die externe Embedding-API auf.""" client = OpenAI(api_key=self.api_key, base_url=self.base_url)
client = OpenAI(
api_key=self.api_key,
base_url=self.base_url,
)
response = client.embeddings.create( response = client.embeddings.create(
input=inputs, input=inputs,
model=self.model, model=self.model,
...@@ -118,16 +79,12 @@ class OpenAILikeEmbeddings(BaseEmbeddings): ...@@ -118,16 +79,12 @@ class OpenAILikeEmbeddings(BaseEmbeddings):
if not isinstance(data, list): if not isinstance(data, list):
raise ValueError("Embedding response missing 'data' list.") raise ValueError("Embedding response missing 'data' list.")
# Sortiere nach Index, falls nötig
data_sorted = sorted(data, key=_embedding_item_index)
embeddings: List[List[float]] = [] embeddings: List[List[float]] = []
for item in data_sorted: for item in sorted(data, key=_embedding_item_index):
emb = _embedding_item_vector(item) emb = _embedding_item_vector(item)
if not isinstance(emb, list): if not isinstance(emb, list):
raise ValueError("Embedding item missing 'embedding' list.") raise ValueError("Embedding item missing 'embedding' list.")
embeddings.append(self._truncate([float(x) for x in emb])) embeddings.append(self._truncate([float(x) for x in emb]))
return embeddings return embeddings
...@@ -144,10 +101,6 @@ def _embedding_item_vector(item: object) -> object: ...@@ -144,10 +101,6 @@ def _embedding_item_vector(item: object) -> object:
class SentenceTransformerEmbeddings(BaseEmbeddings): class SentenceTransformerEmbeddings(BaseEmbeddings):
"""
Embeddings-Wrapper für lokale SentenceTransformer Modelle.
"""
def __init__(self, config: SentenceTransformerConfig) -> None: def __init__(self, config: SentenceTransformerConfig) -> None:
super().__init__(target_dim=config.target_dim) super().__init__(target_dim=config.target_dim)
self.model_name = config.model self.model_name = config.model
...@@ -155,61 +108,31 @@ class SentenceTransformerEmbeddings(BaseEmbeddings): ...@@ -155,61 +108,31 @@ class SentenceTransformerEmbeddings(BaseEmbeddings):
@property @property
def model(self) -> SentenceTransformer: def model(self) -> SentenceTransformer:
"""Liefert das SentenceTransformer-Modell (lazy load)."""
if self._model is None: if self._model is None:
self._model = SentenceTransformer( self._model = SentenceTransformer(self.model_name, trust_remote_code=True)
self.model_name,
trust_remote_code=True,
)
self._model.max_seq_length = 512 self._model.max_seq_length = 512
return self._model return self._model
def embed_documents(self, texts: List[str]) -> List[List[float]]: def embed_documents(self, texts: List[str]) -> List[List[float]]:
"""Generiert Embeddings für eine Liste von Texten.""" vectors = self.model.encode(
passage_embeddings = self.model.encode(
sentences=texts, sentences=texts,
task="retrieval", task="retrieval",
prompt_name="document", prompt_name="document",
) )
return [self._truncate([float(x) for x in emb]) for emb in passage_embeddings] return [self._truncate([float(x) for x in emb]) for emb in vectors]
def embed_query(self, text: str) -> List[float]: def embed_query(self, text: str) -> List[float]:
"""Generiert ein Embedding für einen einzelnen Text.""" vectors = self.model.encode(
query_embeddings = self.model.encode(
sentences=[text], sentences=[text],
task="retrieval", task="retrieval",
prompt_name="query", prompt_name="query",
) )
return self._truncate([float(x) for x in query_embeddings[0]]) return self._truncate([float(x) for x in vectors[0]])
# ----------------------------- def create_embedder(config: EmbeddingConfig) -> BaseEmbeddings:
# Factory: creats embeddings based on config if config.embedding_type == EmbeddingType.OPENAI_LIKE:
# ----------------------------- return OpenAILikeEmbeddings(config=config)
if config.embedding_type == EmbeddingType.SENTENCE_TRANSFORMER:
class EmbeddingFactory: return SentenceTransformerEmbeddings(config=config)
""" raise ValueError(f"Unsupported embedding type: {config.embedding_type}")
Factory-Klasse zur dynamischen Erzeugung von Embeddings-Instanzen.
"""
@staticmethod
def create(config: EmbeddingConfig) -> BaseEmbeddings:
"""
Erzeugt eine Embeddings-Instanz basierend auf der Konfiguration.
Args:
config (EmbeddingConfig): Die Konfiguration mit Typ und Details.
Returns:
BaseEmbeddings: Instanz der passenden Embeddings-Klasse.
Raises:
ValueError: Wenn der Typ nicht unterstützt wird.
"""
if config.embedding_type == EmbeddingType.OPENAI_LIKE:
return OpenAILikeEmbeddings(config=config)
elif config.embedding_type == EmbeddingType.SENTENCE_TRANSFORMER:
return SentenceTransformerEmbeddings(config=config)
else:
raise ValueError(
f"Unsupported embedding type: {config.embedding_type}")
...@@ -2,13 +2,10 @@ from __future__ import annotations ...@@ -2,13 +2,10 @@ from __future__ import annotations
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime
from zoneinfo import ZoneInfo
from typing import Any, Callable, List, TypeVar from typing import Any, Callable, List, TypeVar
import app.config as config import app.config as config
from app.deterministic_services import ( from app.deterministic_services import (
Source,
context_store, context_store,
embedding_provider, embedding_provider,
referenz_decoder, referenz_decoder,
...@@ -16,6 +13,8 @@ from app.deterministic_services import ( ...@@ -16,6 +13,8 @@ from app.deterministic_services import (
tool_log_context, tool_log_context,
tool_logging, tool_logging,
) )
from app.deterministic_services.context_stores.context_store_base import berlin_now_iso
from app.deterministic_services.retrieval_store import Source
T = TypeVar("T") T = TypeVar("T")
...@@ -40,16 +39,12 @@ def is_new_chat(messages: list[dict]) -> bool: ...@@ -40,16 +39,12 @@ def is_new_chat(messages: list[dict]) -> bool:
# Timekeeping # Timekeeping
# --- # ---
def _utc_now_iso() -> str:
return datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y-%m-%dT%H:%M:%SZ")
def _start_timing() -> tuple[str, float]: def _start_timing() -> tuple[str, float]:
return _utc_now_iso(), time.perf_counter() return berlin_now_iso(), time.perf_counter()
def _finish_timing(started_perf: float) -> tuple[str, float]: def _finish_timing(started_perf: float) -> tuple[str, float]:
finished_at = _utc_now_iso() finished_at = berlin_now_iso()
duration_ms = round((time.perf_counter() - started_perf) * 1000, 2) duration_ms = round((time.perf_counter() - started_perf) * 1000, 2)
return finished_at, duration_ms return finished_at, duration_ms
......
from __future__ import annotations
from typing import Any
from app.deterministic_services.orchestrators import (
orchestrator_qa,
orchestrator_socratic,
orchestrator_task,
orchestrator_tutor,
)
AVAILABLE_ORCHESTRATORS: tuple[str, ...] = ("qa", "tutor", "task", "socratic")
DEFAULT_ORCHESTRATOR = "qa"
_ORCHESTRATOR_MODULES: dict[str, Any] = {
"qa": orchestrator_qa,
"tutor": orchestrator_tutor,
"task": orchestrator_task,
"socratic": orchestrator_socratic,
}
def get_default_orchestrator() -> str:
return DEFAULT_ORCHESTRATOR
def is_valid_orchestrator(value: str) -> bool:
return value in _ORCHESTRATOR_MODULES
def resolve_orchestrator(value: str | None = None) -> tuple[str, Any]:
if value:
normalized = value.strip().lower()
if normalized in _ORCHESTRATOR_MODULES:
return normalized, _ORCHESTRATOR_MODULES[normalized]
default_name = get_default_orchestrator()
return default_name, _ORCHESTRATOR_MODULES[default_name]
...@@ -13,14 +13,6 @@ from pydantic import BaseModel ...@@ -13,14 +13,6 @@ from pydantic import BaseModel
from app.deterministic_services.vector_store import EmbeddingLike, embed_query from app.deterministic_services.vector_store import EmbeddingLike, embed_query
def _load_subsection_store():
from app.deterministic_services import vector_store_subsection
return vector_store_subsection
class SourceID(BaseModel): class SourceID(BaseModel):
chapter_title: Optional[str] = None chapter_title: Optional[str] = None
section_title: Optional[str] = None section_title: Optional[str] = None
...@@ -29,16 +21,6 @@ class SourceID(BaseModel): ...@@ -29,16 +21,6 @@ class SourceID(BaseModel):
title: str title: str
doc_type: str doc_type: str
def to_dict(self) -> Dict[str, Any]:
return {
"chapter_title": self.chapter_title,
"section_title": self.section_title,
"subsection_title": self.subsection_title,
"subsubsection_title": self.subsubsection_title,
"title": self.title,
"doc_type": self.doc_type,
}
def to_string(self) -> str: def to_string(self) -> str:
string_rep = self.title string_rep = self.title
if self.subsubsection_title: if self.subsubsection_title:
...@@ -59,15 +41,6 @@ class Source(BaseModel): ...@@ -59,15 +41,6 @@ class Source(BaseModel):
score: float score: float
markdown: str markdown: str
def to_dict(self) -> Dict[str, Any]:
return {
"source_id": self.source_id.to_dict(),
"retrieved_as": self.retrieved_as,
"source_type": self.source_type,
"score": self.score,
"markdown": self.markdown,
}
def to_string(self) -> str: def to_string(self) -> str:
return ( return (
f"Source(source_id={self.source_id.to_string()},\n" f"Source(source_id={self.source_id.to_string()},\n"
...@@ -86,16 +59,6 @@ class Retrieved: ...@@ -86,16 +59,6 @@ class Retrieved:
metadata: Dict[str, Any] metadata: Dict[str, Any]
markdown: str markdown: str
def to_dict(self) -> Dict[str, Any]:
return {
"uid": self.uid,
"doc_type": self.doc_type,
"score": self.score,
"metadata": self.metadata,
"markdown": self.markdown,
}
def _row_to_retrieved(row: Dict[str, Any], source_type: Optional[str] = None) -> Retrieved: def _row_to_retrieved(row: Dict[str, Any], source_type: Optional[str] = None) -> Retrieved:
meta = { meta = {
"uid": row["uid"], "uid": row["uid"],
......
...@@ -5,22 +5,17 @@ from __future__ import annotations ...@@ -5,22 +5,17 @@ from __future__ import annotations
import json import json
import os import os
from collections import deque from collections import deque
from datetime import datetime
from threading import Lock from threading import Lock
from typing import Any from typing import Any
from zoneinfo import ZoneInfo
from app.deterministic_services import context_store from app.deterministic_services import context_store
from app.deterministic_services.context_stores.context_store_base import berlin_now_iso
_LOCK = Lock() _LOCK = Lock()
_LOG_DIR = os.path.join("logs", "chat_sessions") _LOG_DIR = os.path.join("logs", "chat_sessions")
_LOG_PATH = os.path.join(_LOG_DIR, "archive.jsonl") _LOG_PATH = os.path.join(_LOG_DIR, "archive.jsonl")
def _mez_now() -> str:
return datetime.now(ZoneInfo("Europe/Berlin")).strftime("%Y-%m-%dT%H:%M:%SZ")
def _extract_selected_task(sheet: dict[str, Any]) -> dict[str, str] | None: def _extract_selected_task(sheet: dict[str, Any]) -> dict[str, str] | None:
file_id = str(sheet.get("task_file_id", "")).strip() file_id = str(sheet.get("task_file_id", "")).strip()
task_id = str(sheet.get("task_id", "")).strip() task_id = str(sheet.get("task_id", "")).strip()
...@@ -51,7 +46,7 @@ def archive_chat( ...@@ -51,7 +46,7 @@ def archive_chat(
record = { record = {
"chat_id": chat_id, "chat_id": chat_id,
"saved_at": _mez_now(), "saved_at": berlin_now_iso(),
"orchestrator": str(orchestrator or "").strip().lower() or None, "orchestrator": str(orchestrator or "").strip().lower() or None,
"history": sheet.get("history", []), "history": sheet.get("history", []),
"context_sheet": context_store.format_sheet(sheet), "context_sheet": context_store.format_sheet(sheet),
......
...@@ -2,13 +2,11 @@ fastapi ...@@ -2,13 +2,11 @@ fastapi
uvicorn uvicorn
python-dotenv python-dotenv
mpxpy mpxpy
pillow
httpx httpx
ollama ollama
openai openai
mistralai mistralai
mcp mcp
sympy
psycopg[binary] psycopg[binary]
pgvector pgvector
pyyaml pyyaml
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from app.deterministic_services import retrieval_store, vector_store from app.deterministic_services import retrieval_store, vector_store
from app.deterministic_services.embeddings import BaseEmbeddings, EmbeddingFactory from app.deterministic_services.embeddings import BaseEmbeddings, create_embedder
import argparse import argparse
import os import os
...@@ -21,7 +21,7 @@ if str(ROOT_DIR) not in sys.path: ...@@ -21,7 +21,7 @@ if str(ROOT_DIR) not in sys.path:
def build_embedder() -> BaseEmbeddings: def build_embedder() -> BaseEmbeddings:
return EmbeddingFactory.create(config.get_embedding_settings()) return create_embedder(config.get_embedding_settings())
def cli_init_db(args: argparse.Namespace) -> None: def cli_init_db(args: argparse.Namespace) -> None:
......
...@@ -5,7 +5,7 @@ os.environ.setdefault("EMBEDDING_PROVIDER", "sentence-transformer") ...@@ -5,7 +5,7 @@ os.environ.setdefault("EMBEDDING_PROVIDER", "sentence-transformer")
os.environ.setdefault("EMBEDDING_TYPE", "sentence-transformer") os.environ.setdefault("EMBEDDING_TYPE", "sentence-transformer")
from app import config from app import config
from app.deterministic_services.embeddings import EmbeddingFactory from app.deterministic_services.embeddings import create_embedder
from app.deterministic_services import retrieval_store from app.deterministic_services import retrieval_store
...@@ -70,7 +70,7 @@ def main() -> None: ...@@ -70,7 +70,7 @@ def main() -> None:
args = parser.parse_args() args = parser.parse_args()
pg_url = args.pg or config.get_postgres_url() pg_url = args.pg or config.get_postgres_url()
embedder = EmbeddingFactory.create(config.get_embedding_settings()) embedder = create_embedder(config.get_embedding_settings())
subsection_refs = _parse_subsections( subsection_refs = _parse_subsections(
args.subsections, args.chapter_index, args.section_index args.subsections, args.chapter_index, args.section_index
......
...@@ -7,8 +7,9 @@ from unittest.mock import patch ...@@ -7,8 +7,9 @@ from unittest.mock import patch
os.environ.setdefault("EMBEDDING_PROVIDER", "sentence-transformer") os.environ.setdefault("EMBEDDING_PROVIDER", "sentence-transformer")
os.environ.setdefault("EMBEDDING_TYPE", "sentence-transformer") os.environ.setdefault("EMBEDDING_TYPE", "sentence-transformer")
from app.deterministic_services import Source as PackageSource, SourceID as PackageSourceID
from app.deterministic_services.retrieval_store import ( from app.deterministic_services.retrieval_store import (
Source as PackageSource,
SourceID as PackageSourceID,
Retrieved, Retrieved,
Source, Source,
SourceID, SourceID,
......
...@@ -12,7 +12,7 @@ import { ...@@ -12,7 +12,7 @@ import {
import { t } from "../i18n"; import { t } from "../i18n";
import { import {
DEFAULT_ORCHESTRATOR, DEFAULT_ORCHESTRATOR,
FALLBACK_ORCHESTRATORS, ORCHESTRATORS,
isTaskCoupledOrchestrator, isTaskCoupledOrchestrator,
type OrchestratorName, type OrchestratorName,
} from "../utils/orchestrator"; } from "../utils/orchestrator";
...@@ -102,7 +102,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -102,7 +102,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const [selectedOrchestrator, setSelectedOrchestratorState] = const [selectedOrchestrator, setSelectedOrchestratorState] =
useState<OrchestratorName>(DEFAULT_ORCHESTRATOR); useState<OrchestratorName>(DEFAULT_ORCHESTRATOR);
const [availableOrchestrators, setAvailableOrchestrators] = useState<OrchestratorName[]>( const [availableOrchestrators, setAvailableOrchestrators] = useState<OrchestratorName[]>(
() => [...FALLBACK_ORCHESTRATORS] () => [...ORCHESTRATORS]
); );
const [isTasksInitialized, setIsTasksInitialized] = useState(false); const [isTasksInitialized, setIsTasksInitialized] = useState(false);
const [taskFiles, setTaskFiles] = useState<TaskFile[]>([]); const [taskFiles, setTaskFiles] = useState<TaskFile[]>([]);
...@@ -313,7 +313,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -313,7 +313,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
const initTasks = useCallback(async () => { const initTasks = useCallback(async () => {
try { try {
const available = [...FALLBACK_ORCHESTRATORS]; const available = [...ORCHESTRATORS];
const nextOrchestrator = available.includes(selectedOrchestrator) const nextOrchestrator = available.includes(selectedOrchestrator)
? selectedOrchestrator ? selectedOrchestrator
: DEFAULT_ORCHESTRATOR; : DEFAULT_ORCHESTRATOR;
...@@ -322,7 +322,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) { ...@@ -322,7 +322,7 @@ export function TutorSessionProvider({ children }: PropsWithChildren) {
setSelectedOrchestratorState(nextOrchestrator); setSelectedOrchestratorState(nextOrchestrator);
await loadSelectionData(nextOrchestrator); await loadSelectionData(nextOrchestrator);
} catch (error) { } catch (error) {
setAvailableOrchestrators([...FALLBACK_ORCHESTRATORS]); setAvailableOrchestrators([...ORCHESTRATORS]);
setSelectedOrchestratorState(DEFAULT_ORCHESTRATOR); setSelectedOrchestratorState(DEFAULT_ORCHESTRATOR);
await loadSelectionData(DEFAULT_ORCHESTRATOR); await loadSelectionData(DEFAULT_ORCHESTRATOR);
void error; void error;
......
...@@ -4,8 +4,6 @@ export type OrchestratorName = (typeof ORCHESTRATORS)[number]; ...@@ -4,8 +4,6 @@ export type OrchestratorName = (typeof ORCHESTRATORS)[number];
export const DEFAULT_ORCHESTRATOR: OrchestratorName = "qa"; export const DEFAULT_ORCHESTRATOR: OrchestratorName = "qa";
export const FALLBACK_ORCHESTRATORS: OrchestratorName[] = [...ORCHESTRATORS];
export const normalizeOrchestrator = ( export const normalizeOrchestrator = (
value: string | null | undefined value: string | null | undefined
): OrchestratorName | null => { ): OrchestratorName | null => {
......
export {
getSelectionRouteForOrchestrator,
isSocraticOrchestrator,
isTaskSelectionOrchestrator,
type OrchestratorName,
} from "./orchestrator";
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