"""Was ein Tool während eines Runs braucht; Quellen legt der Server fest, nie das Modell (→ Konzept 8.5).""" import uuid from dataclasses import dataclass, field from typing import Any from app.core.config import Settings from app.core.crypto import Vault from app.core.errors import Invalid from app.data_sources import service as sources from app.data_sources.connectors.git_repo import GitRepo, RepoCache from app.data_sources.models import DataSource @dataclass class ToolContext: settings: Settings vault: Vault workspace_id: int # Nur getestete Quellen dieses Workspace sources: list[DataSource] cache: RepoCache # Festgehaltener Commit je Quelle: alle Tools eines Runs sehen denselben Stand snapshots: dict[int, str] = field(default_factory=dict) run_id: uuid.UUID | None = None # Wer den Run gestartet hat; beantragt Schreibaktionen user_id: int | None = None # Für den Workspace freigeschaltete MCP-Server: Kurzname -> Ziel (URL, Token, Timeout) mcp: dict[str, Any] = field(default_factory=dict) # Erfolgreich umgewandelte Uploads des Workspace (DuckDB-Tabellen) tables: list[Any] = field(default_factory=list) # Chat des Runs: Git-Tools lesen den Chat-Branch, commit_change schreibt darauf db: Any = None chat_session_id: int | None = None # Arten der Hugin-Tools, die festgeschriebene Pakete freischalten (hugin, hugin:skill) hugin: set[str] = field(default_factory=set) # Ausgangs-Branch je Quelle, wenn der Chat auf einem bestehenden Branch arbeitet (sonst Ziel-Branch) bases: dict[int, str] = field(default_factory=dict) # Abgeglichene Ordner, die dieser Run lesen darf (app.documents.service.for_run) documents: bool = False @property def kinds(self) -> set[str]: """Arten, deren Tools in diesem Run aufgerufen werden dürfen.""" return ({s.kind for s in self.sources} | {f"mcp:{name}" for name in self.mcp} | ({"uploads"} if self.tables else set()) | self.hugin | ({"docs"} if self.documents else set())) def source(self, kind: str, name: str | None) -> DataSource: candidates = [s for s in self.sources if s.kind == kind] if name: chosen = [s for s in candidates if s.name == name] if not chosen: known = ", ".join(s.name for s in candidates) or "keine" raise Invalid(f"Quelle {name!r} gibt es in diesem Workspace nicht. Verfügbar: {known}.") return chosen[0] if len(candidates) != 1: raise Invalid("Bitte die Quelle angeben: " + ", ".join(s.name for s in candidates)) return candidates[0] def by_id(self, source_id: int | None) -> DataSource: for source in self.sources: if source.id == source_id: return source raise Invalid("Die Quelle dieses Vorschlags ist nicht mehr verbunden oder nicht mehr getestet.") def repo_for(self, source: DataSource) -> GitRepo: return self.cache.repo(source.id, sources.connector(source, self.settings, self.vault)) def git(self, name: str | None) -> tuple[GitRepo, str]: """Repository und der Stand, den die Tools lesen: der Branch des Chats, falls die KI schon committet hat, sonst der zu Beginn des Runs festgehaltene Stand des Ziel-Branches.""" source = self.source("git", name) repo = self.repo_for(source) if source.id not in self.snapshots: self.snapshots[source.id] = repo.sync(self.bases.get(source.id)) if self.db is not None and self.chat_session_id is not None: from app.changes import service as changes head = changes.head(self.db, repo, self.chat_session_id, source.id, self.settings) if head: return repo, head return repo, self.snapshots[source.id]