81 lines
3.8 KiB
Python
81 lines
3.8 KiB
Python
"""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]
|