# AI Agent & Knowledge Store ## Überblick Der **AI-Agent** ist der Service `serviceAgent` (`AgentService`) im **ServiceCenter**. Er orchestriert einen **ReAct-Loop** mit nativer Function Calling-Unterstützung: pro Runde ruft das Modell `serviceAi` auf; bei Tool-Calls werden Handler über die **Tool Registry** ausgeführt und Ergebnisse wieder in den Konversationskontext gegeben. Er ist die zentrale Schicht für den **Workspace** (Chat, Streaming, Tools, Dateien, externe Quellen). Darunter liegt **`serviceAi`** als Low-Level-Gateway (Billing-Preflight, Provider-/Modellwahl, Neutralisierung vor Modellaufruf). **`serviceKnowledge`** liefert **RAG**: Indexierung extrahierter Inhalte, semantische Suche (pgvector) und **`buildAgentContext`** für kontextuelle Injektion vor jeder Agent-Runde. Der **AI-Core** (`platform-core/modules/aicore/`) kapselt Provider-Plugins und die Modellwahl; der Agent nutzt ihn indirekt über `serviceAi` (u. a. `OperationTypeEnum.AGENT`, Embeddings für den Knowledge Store). --- ## Agent Core ### ServiceCenter-Integration | Registry-Key | Klasse | `objectKey` | Dependencies (laut `registry.py`) | |--------------|--------|-------------|-----------------------------------| | `agent` | `AgentService` | `service.agent` | `ai`, `chat`, `utils`, `extraction`, `billing`, `streaming`, `knowledge` | | `knowledge` | `KnowledgeService` | `service.knowledge` | `ai` | Pro Request propagiert der **ServiceCenterContext** u. a. `userId`, `mandateId`, `featureInstanceId` und optional `requireNeutralization` (siehe Invarianten). ### Ablauf (Ist-Code) - **Einstieg:** `AgentService.runAgent` (`mainServiceAgent.py`) — baut die Tool Registry, optional Anreicherung des Prompts mit Datei-Metadaten/`FileContentIndex`, startet `runAgentLoop` (`agentLoop.py`). - **Loop:** `runAgentLoop` — `ConversationManager` + Systemprompt; **pro Runde:** optional RAG via `buildRagContextFn`, Budget-Check, **Progressive Summarization** bei Bedarf (`ConversationManager`, Modell-Operation `DATA_ANALYSE`), dann `AiCallRequest` mit `OperationTypeEnum.AGENT`, `messages`, `tools`. - **Tool Registry:** `registerCoreTools` in `coreTools/registerCore.py` delegiert an domänenspezifische Module (`_workspaceTools`, `_connectionTools`, `_dataSourceTools`, `_documentTools`, `_mediaTools`, `_featureSubAgentTools`, `_crossWorkflowTools`); **`ActionToolAdapter`** registriert alle Workflow-Actions mit `dynamicMode=True` als zusätzliche Tools (`method_action` → intern `executeAction`). - **Parallele Ausführung:** In `_executeToolCalls` werden als **`readOnly=True`** markierte Tool-Calls parallel (`asyncio.gather`), schreibende/übrige sequentiell — vermeidet Race Conditions bei zustandsändernden Tools. - **Budget:** `AgentConfig.maxCostCHF` — vor jeder Runde Abgleich mit Workflow-Kosten; bei Überschreitung Status `budgetExceeded` und abschliessender Fortschritts-Text (`FINAL`). - **Rundenlimit:** `AgentConfig.maxRounds` (Default 25); bei Erreichen während `running` → Status `maxRoundsReached` und Fortschritts-Zusammenfassung. - **Degradation:** Schlägt RAG-Injektion fehl, wird nur geloggt (**non-blocking**); der Agent läuft ohne diesen Kontext weiter. Fehlgeschlagene RoundMemory-Persistenz ebenfalls non-blocking. - **Streaming:** Optional `aiCallStreamFn` — Chunks als `CHUNK`-Events; Abschluss u. a. `AGENT_SUMMARY` mit Kosten- und Round-Metriken; Trace kann über Knowledge-Entities persistiert werden (`_persistTrace`). ### Konfiguration (`datamodelAgent.AgentConfig`, Ist) | Feld | Bedeutung | |------|-----------| | `maxRounds` | Max. Schleifenrunden (Default 25, 1–100) | | `maxCostCHF` | Optionales Workflow-Budget in CHF | | `toolSet` | Aktives Tool-Set (Default `"core"`) | | `temperature` | Optional für den Agent-AI-Call | ### System-Prompt: temporaler Kontext (Ist) `buildSystemPrompt` (`conversationManager.py`) injiziert beim Bauen des System-Prompts einen **Datums-/Zeitblock** (`_buildTemporalContext`). Der Sub-Agent-Prompt in `featureDataAgent._buildSchemaContext` macht dasselbe. Ohne diesen Block halluzinieren LLMs das aktuelle Datum aus ihrem Training-Cutoff (typisch: »Juni 2025«), was bei relativen Zeit-Filtern (»letzten Monat«, »Q1«) sofort zu falschen SQL-Filtern und falschen Antworten führt. Datenfluss der Zeitzone: 1. **Browser**: `Intl.DateTimeFormat().resolvedOptions().timeZone` (z. B. `"Europe/Zurich"`). 2. **Frontend**: `ui-nyla/src/api.ts` Axios-Interceptor sendet den IANA-Namen als `X-User-Timezone`-Header. 3. **Gateway**: `_requestContextMiddleware` (`app.py`) liest den Header und schreibt ihn via `_setRequestTimezone` in eine `ContextVar`. 4. **Konsumenten**: `getRequestNow()` / `getRequestTimezone()` aus `modules/shared/timeUtils.py` liefern die Werte für den Prompt-Block (gleiche Pattern wie `_setLanguage` / `_getLanguage`). 5. **Fallback**: Header fehlt oder ist ungültig → `UTC`. Niemals server-seitige Hardcoded-TZ. Storage bleibt UTC (`getUtcTimestamp`, `getIsoTimestamp`). Nur **user-sichtbare** Now-Werte (Agent-Prompt, formatierte Display-Strings) gehen über `getRequestNow()`. ### Toolbox Registry **Datei:** `serviceCenter/services/serviceAgent/toolboxRegistry.py` Statt alle Tools flach zu exponieren, gruppiert die **Toolbox Registry** Tools in thematische Toolboxes. Der Agent startet schlank und kann Spezial-Toolboxes zur Laufzeit nachfordern. | Toolbox | Anzahl Tools | Default | Requires Connection | |---------|-------------|---------|-------------------| | `core` | 20 | Ja | -- | | `ai` | 9 | Ja | -- | | `datasources` | 8 | Ja | -- | | `email` | 5 | Nein | `microsoft` | | `sharepoint` | 3 | Nein | `microsoft` | | `clickup` | 3 | Nein | `clickup` | | `jira` | 3 | Nein | `jira` | | `workflow` | 9 | Nein | -- (featureCode: graphicalEditor) | | `trustee` | 1 | Nein | -- (featureCode: trustee) | **Aktivierung (`_activateToolboxes` in `mainServiceAgent.py`):** - Default-Toolboxes sind immer aktiv - Connection-abhaengige Toolboxes werden aktiviert, wenn der User eine passende Connection hat - Tools aus inaktiven Toolboxes werden aus der Registry entfernt - `workflow`-Tools werden explizit via `workflowTools.getWorkflowToolDefinitions` registriert **`requestToolbox` Meta-Tool:** - Erlaubt dem Agent, zur Laufzeit inaktive Toolboxes anzufordern - Schema: `{ toolboxId: enum[inactive IDs], reason: string }` - Handler in `mainServiceAgent._registerRequestToolbox()` - Nach erfolgreichem Aufruf refresht `agentLoop.py` die `toolDefinitions` fuer die naechste Runde - Nur Toolboxes, die aktuell nicht aktiv sind, erscheinen als Optionen ### RBAC (Architekturentscheid) Keine separate Tool-Level-RBAC: Zugriff wird über **Datenbank-RBAC** (z. B. File-Queries), **OAuth/Connections** bei externen Quellen und **`serviceAi`** (Billing, Provider-/Modell-RBAC) sowie **`can_access_service`** für `service.agent` durchgesetzt. --- ## FeatureDataAgent: Query-Repair-Loop + Ontologie (ab 2026-05) Der Feature Data Sub-Agent (`platform-core/modules/serviceCenter/services/serviceAgent/featureDataAgent.py`) ist die Spezialisten-Schicht hinter dem `queryFeatureInstance`-Tool. Er hat seinen eigenen ReAct-Loop und drei Tools (`browseTable`, `queryTable`, `aggregateTable`) gegen jede Feature-Datentabelle. Ab Mai 2026 verhindern zwei Schichten Halluzinationen deterministisch: ### 1. Pre-execute Validator (`queryValidator.py`) `QueryValidator` validiert die Argumente jedes Tool-Calls **vor** der DB. Bei Fehlschlag liefert der Tool-Handler `ToolResult(success=False, error=, errorDetails={code, field, suggestion, hint})` -- der LLM erhält strukturierte Reparatur-Hinweise im nächsten ReAct-Turn und kann gezielt umlenken. | `code` | Triggert bei | LLM-Reaktion | |-------------------------------|------------------------------------------------------------------|-----------------------------------------------| | `FIELD_NOT_FOUND` | Feldname existiert nicht im Pydantic-Modell | `browseTable` aufrufen, dann `suggestion` nehmen | | `OPERATOR_INCOMPATIBLE` | `LIKE` auf int-Feld, `>` auf string | Op wechseln oder typkorrekten Wert filtern | | `INVALID_AGGREGATE_TARGET` | `SUM`/`AVG` auf `*Balance`/`*Total` (Convention oder Ontologie) | `queryTable` mit Periode-Filter benutzen | | `ORDER_BY_INVALID` | `orderBy` zeigt auf unbekanntes Feld | Feldname korrigieren | | `TYPE_MISMATCH` | `SUM` auf nicht-numerischem Feld | Anderes Feld wählen | Verkabelung: `runFeatureDataAgent` ruft `_buildValidatorForFeature(featureCode)`, das den Validator automatisch mit der `OntologyDescriptor` des Features verbindet (wenn vorhanden). Die Ontologie ersetzt die Convention-Defaults: `NEVER_AGGREGATE`-Constraints aus der Ontologie haben Vorrang vor dem `*Balance`/`*Total`-Suffix-Check. ### 2. Ontologie-Layer (Phase 2, Trustee-Pilot) Statt freier `_AGENT_DOMAIN_HINTS`-Strings exportieren Features einen **strukturierten `OntologyDescriptor`** über `getAgentOntology() -> OntologyDescriptor` in ihrer `mainXxx.py`. Der Descriptor enthält: - **`entities`**: Semantische Konzepte (z. B. `BankAccount` spezialisiert `Account`, gebunden an `TrusteeDataAccount` mit `accountNumber LIKE '102%'`). Sub-Entitäten mit `parentEntity` machen Sub-Group-Filter explizit. - **`relations`**: FK-Beziehungen mit `Cardinality` (z. B. `JournalLine -> JournalEntry (MANY_TO_ONE via journalEntryId)`). - **`constraints`**: Maschinenlesbare Regeln (`NEVER_AGGREGATE`, `REQUIRES_FILTER_ON`, `PREFERRED_TABLE_FOR_INTENT`), die **gleichzeitig vom Validator und vom Prompt-Compiler konsumiert werden** -- Single Source of Truth. - **`canonicalPatterns`**: Tool-Call-Skelette für häufige Intents (`BANK_BALANCE_AT_DATE`, `JOURNAL_SUM_AT_ACCOUNT`, ...). Werden vom Compiler als worked examples in den Prompt geschrieben. `featureDataAgent._buildSchemaContext` prüft bei jedem Sub-Agent-Run, ob das Feature `getAgentOntology()` exponiert (`_loadFeatureOntologyBlock`). Wenn ja, wird der Block via `ontologyToPromptCompiler.compileOntologyToPrompt()` deterministisch gerendert und an den Schema-Prompt angehängt. Sonst Fallback auf den Legacy-Pfad `getAgentDomainHints()`. Eval-only Override: `POWERON_DISABLE_FEATURE_ONTOLOGY=1` erzwingt den Legacy-Pfad (siehe Eval-Harness). ### 3. Eval-Harness (Phase 1.5) `platform-core/tests/eval/runTrusteeBenchmark.py` ist ein standalone Runner (kein pytest), der jede Frage des Goldstandards (`tests/fixtures/trusteeBenchmark/questions.yaml`, derzeit 19) gegen einen `FakeFeatureDataProvider` mit synthetischen aber realistischen Trustee-Daten fährt. Drei Modi werden parallel gemessen: | Mode | Validator | Ontologie | Prompt-Block | |-----------|-----------|-----------|-----------------------------------------------| | baseline | aus | aus | `getAgentDomainHints()` (Legacy) | | phase1 | an | aus | `getAgentDomainHints()` (Legacy) + Validator | | phase2 | an | an | `compileOntologyToPrompt(...)` + Validator | Pro Frage werden gemessen: `patternOk` (richtige Tool-Wahl + Filter), `forbidOk` (vermiedene Anti-Pattern), `numericOk` (Antwort enthält die erwarteten Zahlen), `accuracyOk` (alle drei). Aggregiert: `repairConversionRate` (erfolgreiche Repairs / Validator-Rejects), `totalCostCHF`, `totalRounds`. Output: Markdown-Bericht + JSON in `local/notes/trustee-benchmark-.{md,json}`. **Letzte Messung (2026-05-15, 19 Fragen × 3 Modi = 57 echte LLM-Runs):** | Mode | Accuracy | Pattern compliance | Validator rejects | Rounds | Cost (CHF) | |-----------|----------|--------------------|-------------------|--------|------------| | baseline | 89.5 % | 89.5 % | 0 | 40 | 0.0841 | | phase1 | 94.7 % | 100 % | 2 | 41 | 0.0851 | | phase2 | 100 % | 100 % | 0 | 39 | 0.0975 | ### 4. Repair-Telemetrie (`AgentTrace`) `AgentTrace` (`datamodelAgent.py`) aggregiert pro Sub-Agent-Run drei neue Counter (gefüllt von `agentLoop._computeRepairCounters` am Ende des Loops): - `validationFailures`: Tool-Calls, die der Validator vor der DB abgelehnt hat. - `repairAttempts`: Wiederholungen desselben `toolName` in späteren Runden (nachgewiesener Repair-Versuch). - `successAfterRepair`: Repair-Attempts, die einen `validationFailureCode=None`-Result lieferten. Jeder `ToolCallLog` trägt den `validationFailureCode` (z. B. `INVALID_AGGREGATE_TARGET`) für Pro-Call-Analyse. Der `AGENT_SUMMARY`-Event am Loop-Ende exposed die aggregierten Counter ans Frontend / Persistierung. ## Agent Tools ### Prinzip Tools sind registrierte Handler mit JSON-Schema für Argumente, **`readOnly`-Flag** (Parallelisierung) und optional `toolSet`. Tool-Ausgaben sind Rohdaten für das Modell; **Neutralisierung vor dem LLM** erfolgt zentral in `serviceAi`, nicht in den Tools. Zusätzlich zu den unten genannten **Kern-Tools** existieren **dynamische Tools** aus dem Workflow-System: `ActionToolAdapter` liest `methodDiscovery.methods` und registriert jede Action mit `dynamicMode=True` unter einem zusammengesetzten Namen (`{methodShort}_{actionName}`); im Adapter sind diese derzeit **alle als nicht-readOnly** registriert. > **Tool-Generierung aus dem Catalog (Typed Action Architecture, 2026-04):** `ActionToolAdapter` leitet das JSON-Schema fuer jeden Tool-Aufruf direkt aus der **typisierten Action-Signatur** ab (Pflicht-Felder, Typen, Default-Werte, `FeatureInstanceRef`-Discriminator). Es gibt keine handgepflegte Heuristik mehr -- Editor, AI-Agent und Adapter konsumieren denselben Catalog (`/api/automation2/catalog`). Aenderungen an einer Action-Signatur schlagen automatisch auf das Tool-Schema durch; veraltete oder gedriftete Felder werden vom Snapshot-Test `test_staticNodesHaveNoDriftAgainstLiveMethods` blockiert (`_KNOWN_ADAPTER_DRIFTS = frozenset()`). Details: `wiki/c-work/3-validate/2026-04-typed-action-architecture.md`. **Toolbox-Zuordnung:** Kern-Tools sind den Toolboxes `core`, `ai` und `datasources` zugeordnet (siehe Toolbox Registry oben). Connection-abhaengige Tools (`email`, `sharepoint`, `clickup`, `jira`) werden nur aktiviert, wenn der User eine passende Connection hat. Workflow-Editing-Tools (`workflow` Toolbox) werden separat via `workflowTools.py` registriert. ### Kern-Tools (registriert via `registerCoreTools` → `coreTools/`; Stand 2026-04 inkl. UDM-Helfer) **Workspace / Dateien** | Tool | Kurzbeschreibung | |------|------------------| | `readFile` | Dateiinhalt lesen (optional zeilenweise) | | `listFiles` | Dateien filtern (Ordner, Tags, Suche) | | `searchInFileContent` | Suche im Dateiinhalt | | `writeFile` | Datei anlegen / anhängen / überschreiben | | `deleteFile` | Datei löschen | | `renameFile` | Umbenennen | | `moveFile` | Verschieben (Ordner) | | `tagFile` | Tags setzen | | `copyFile` | Unabhängige Kopie | | `replaceInFile` | Ersetzen im Text | **Ordner** | Tool | Kurzbeschreibung | |------|------------------| | `listFolders` | Ordner auflisten | | `createFolder` | Ordner anlegen | | `deleteFolder` | Ordner löschen (optional rekursiv) | | `renameFolder` | Ordner umbenennen | | `moveFolder` | Ordner verschieben | **Web & Sprache** | Tool | Kurzbeschreibung | |------|------------------| | `webSearch` | Websuche | | `readUrl` | Inhalt einer bekannten URL laden | | `translateText` | Übersetzung (Voice/Translation-Pipeline) | | `textToSpeech` | TTS | | `speechToText` | Transkription Audio-Datei (Gateway: `VoiceObjects.speechToText`; optionale Connector-Parameter `model`/`lightweight`/`audioFormat` — Agent-Tool nutzt Defaults) | | `detectLanguage` | Spracherkennung für Text | **Externe Datenquellen / Mail** | Tool | Kurzbeschreibung | |------|------------------| | `listConnections` | User-Connections auflisten | | `browseDataSource` | Verzeichnis/Ordner einer Quelle auflisten (oder neueste Items eines Mail-/Kalender-Ordners) | | `searchDataSource` | **Primäres Tool** für gezielte Abfragen — Query läuft server-seitig in der Quelle | | `downloadFromDataSource` | Download → FileItem (inkl. Vererbung `neutralize` auf Datei, siehe Invarianten) | | `uploadToExternal` | Upload zu externer Quelle | | `sendMail` | E-Mail senden | #### Search-first-Strategie & Adapter-Effizienz (ab 2026-06) Externe Quellen können gigabyte- bis terabytegross sein. Damit der Agent sie **nicht wie ein lokales Dateisystem** behandelt (alles browsen + alles herunterladen), gelten drei Schichten: 1. **Agent-Steuerung (Prompt + Tool-Beschreibungen):** `buildDataSourceContext` (`routeFeatureWorkspace.py`) injiziert eine **Search-first-Regel**. Die Tool-Beschreibungen in `_dataSourceTools.py` deklarieren `searchDataSource` als primär (inkl. Per-Service-Query-Syntax) und `browseDataSource` als reines Directory-Listing. So lädt der Agent gezielt statt massenhaft. 2. **Server-seitige Suche + Scoping/Pagination pro Adapter:** Jeder `ServiceAdapter.search` nutzt die native Such-API der Quelle und scopt auf den angebundenen Ordner/Label; grosse Ergebnismengen werden paginiert. | Service | Query-Syntax / Mechanik | Scoping | Pagination | |---------|-------------------------|---------|-----------| | Outlook Mail | Graph `$search` (KQL: `from:`, `subject:`); `browse` mit Datumsbereich → `$filter receivedDateTime` | Mail-Ordner | `$top` | | Gmail | `q=` (`from:`, `after:`/`before:`); Metadaten parallel aufgelöst (kein sequenzielles N+1) | `labelIds` | `maxResults` | | SharePoint / OneDrive | Graph `drive/root:/:/search(q=)` (Name + Inhalt) | angebundener Ordner | `@odata.nextLink` | | Google Drive | `fullText contains` (Name + Inhalt) | `'' in parents` | `pageToken` | | MSFT / Google Calendar | Datumsbereich → `calendarView` bzw. `timeMin`/`timeMax` | Kalender | `$top` / `maxResults` | | Infomaniak Calendar | Datumsbereich-Filter (auf Vendor-Limit <3 Monate geclampt) statt fixem 90-Tage-Fenster | Kalender | -- | | ClickUp | `searchTeamTasks` | Team | API-Page | 3. **Strukturierte Metadaten inline (`_dataSourceTools.py`-Formatter):** Damit der Agent ohne Download entscheiden/antworten kann, rendern die Tool-Outputs pro Service-Typ die relevanten Felder inline: | Typ | Inline-Felder | Hinweis im Output | |-----|---------------|-------------------| | Mail (`✉️`) | Datum, Absender | "nur Betreff — Download für Volltext" | | Kalender (`📅`) | Start, Ende, Ort | "keine Einzel-Events downloaden" | | Kontakte (`👤`) | E-Mail, Telefon, Firma | "vCard nur bei Bedarf downloaden" | | ClickUp-Tasks (`☑️`) | Status, Assignee, Fälligkeit | "Task-JSON nur für Detail downloaden" | Die Felder kommen aus `ExternalEntry.metadata`; der Formatter normalisiert die je Provider unterschiedlichen Keys (z. B. Kontakt-E-Mail: MSFT `emailAddresses`, Google `emails`, Infomaniak `email`). **Dokumente / Content-Objekte** | Tool | Kurzbeschreibung | |------|------------------| | `browseContainer` | Struktur-Index (Seiten, Abschnitte, …) | | `readContentObjects` | Gezielt Content-Objekte lesen | | `extractContainerItem` | Element aus Container extrahieren | | `summarizeContent` | KI-Zusammenfassung | | `getUdmStructure` | UDM-JSON: Überblick (Knoten, Struktur, Block-Zahlen); `udmJson` als stringifiziertes Objekt | | `walkUdmBlocks` | UDM traversieren: alle `ContentBlock`-Knoten mit Pfad und Kurz-Preview | | `filterUdmByType` | UDM: alle Blöcke mit gegebenem `contentType` (z. B. `table`, `image`) | | `describeImage` | Vision-Analyse | | `renderDocument` | Dokument rendern (PDF/DOCX/PPTX/XLSX/HTML); optionaler `style`-Parameter fuer Agent-Overrides; AI-enhanced Styling basierend auf Dokumentkontext; Smart Table Styling via `tableStyle` pro Tabelle. Details: `b-reference/platform-core/document-rendering.md` | | `generateImage` | Bildgenerierung | | `createChart` | Chart erzeugen | **Feature / Workflow** | Tool | Kurzbeschreibung | |------|------------------| | `queryFeatureInstance` | Abfrage anderer Feature-Instanz (setzt bei Bedarf `requireNeutralization` für Sub-Calls). Sub-Agent hat `browseTable`, `queryTable` und `aggregateTable` (SUM/COUNT/AVG/MIN/MAX mit GROUP BY und `filters`). DB-Connection-Pooling und Result-Caching (5 Min TTL). System-Prompt wird aus drei Schichten gebaut: (1) generischer Header mit Tabellen + Pydantic-Schema (`_buildSchemaContext`), (2) generische Regeln inkl. **kein SUM/AVG auf bereits aggregierten Saldo-/Total-Feldern** (`closingBalance`, `openingBalance`, `debitTotal`, `creditTotal`, …), (3) **Domain-Block**: bevorzugt aus `getAgentOntology() -> OntologyDescriptor` via `ontologyToPromptCompiler.compileOntologyToPrompt()` (deterministisch, single source of truth für Prompt + Validator); Fallback auf `getAgentDomainHints() -> str` für Features ohne Ontologie. Round-/Cost-Budget wird vom Parent-Agent geerbt (`AgentConfig.maxRounds` → Tool-Context → `runFeatureDataAgent`). **Pre-execute Validator** (`QueryValidator`, mit Ontologie verkabelt via `_buildValidatorForFeature`) fängt vier Halluzinations-Klassen deterministisch ab und gibt strukturierte Repair-Hints zurück: siehe Abschnitt »FeatureDataAgent: Query-Repair-Loop + Ontologie« unten. | | `listWorkflowHistory` | Workflow-Historie | | `readWorkflowMessages` | Nachrichten eines Workflows lesen | **Sicherheit / Ausführung** | Tool | Kurzbeschreibung | |------|------------------| | `neutralizeData` | Anonymisierter Text (non-destructive) | | `executeCode` | Sandboxed Code-Ausführung (siehe `sandboxExecutor.py`) | --- ## AI-Core (Provider-Abstraction) | Komponente | Datei / Rolle | |------------|----------------| | **Model Registry** | `aicoreModelRegistry.py` — registriert Provider-Plugins und Modelle | | **Model Selector** | `aicoreModelSelector.py` — Auswahl nach Operationstyp, Promptgrösse, Restriktionen, Ranking | ### Provider-Plugins (Dateien unter `platform-core/modules/aicore/`) | Plugin-Modul | Typische Rolle | |--------------|----------------| | `aicorePluginAnthropic.py` | Claude-Modelle | | `aicorePluginOpenai.py` | GPT, Embeddings, Bild — modellabhaengiges Payload-Tuning: GPT-5.x und o-Serie (o1/o3/o4) sind Reasoning-Modelle und akzeptieren weder `max_tokens` (-> immer `max_completion_tokens`) noch ein custom `temperature` (-> Feld bei diesen Modellen weggelassen, OpenAI erzwingt sonst HTTP 400 `unsupported_value`) | | `aicorePluginMistral.py` | Mistral Chat / Embed | | `aicorePluginPerplexity.py` | Sonar / Recherche | | `aicorePluginTavily.py` | Web-Suche | | `aicorePluginPrivateLlm.py` | Private LLM | | `aicorePluginInternal.py` | Interne Extraktion/Generierung/Rendering | ### Operation Types (`datamodelAi.OperationTypeEnum`, Auszug) u. a. `plan`, `dataAnalyse`, `dataGenerate`, `dataExtract`, `imageAnalyse`, `imageGenerate`, `neutralizationText`, `neutralizationImage`, `webSearch`, `webCrawl`, **`agent`**, **`embedding`**, `speechTeams`. Der Agent-Loop verwendet **`AGENT`** für Hauptrunden und **`DATA_ANALYSE`** für Summarization-Calls; Embeddings laufen über die Knowledge-Service-Pipeline (`callEmbedding`). --- ## Knowledge Store (RAG) ### Datenmodelle (`datamodelKnowledge.py`) | Modell | Zweck | |--------|--------| | **FileContentIndex** | Strukturindex pro Datei (ohne LLM): `structure`, `objectSummary`, `status`, Spiegelung von Mandat/Instanz über **`scope`** (`personal`, `featureInstance`, `mandate`, `global`), Neutralisierungs-Felder `isNeutralized`, `neutralizationStatus` | | **ContentChunk** | Persistente Chunks mit Embedding (`vector(1536)`), `data`, `contextRef`, `contentType` | | **RoundMemory** | Pro Runde: `file_ref`, Tool-Ergebnisse, Entscheidungen — mit Embedding für semantische Wiederverwendung trotz ConversationManager-Kürzung | | **WorkflowMemory** | Workflow-scoped Key-Value-Cache (Entities, Fakten, inkl. optional Embedding) | Zugriff über **`interfaceDbKnowledge`** (`FileContentIndex`, `ContentChunk`, RoundMemory, WorkflowMemory). ### Indexierung **`KnowledgeService.indexFile`** — nach Extraktion (Content-Objekte): übernimmt Scope aus **FileItem** als Single Source of Truth; bei `FileItem.neutralize=True` werden Inhalte vor dem Speichern neutralisiert; Chunking (u. a. `DEFAULT_CHUNK_TOKENS` / Zeichen-Heuristik), Embedding via AI-Service, Persistenz von Index + Chunks; optional Billing-Reconciliation für Mandats-Speicher. ### Semantische Suche & Kontext **`buildAgentContext`** — priorisierte Schichten (Ist-Code, vereinfacht): 1. **RoundMemory** `file_ref` („Known Files“) 2. **Instance/personal/mandate/global** — `semanticSearch` mit Query-Embedding (Limit/Score wie im Code) 3. **RoundMemory** semantisch (`semanticSearchRoundMemory`) 4. **Workflow-Entities** (`getWorkflowEntities`) 5. **Mandate-Scope** — geteilte Mandats-Dokumente („Shared Knowledge“) 6. Optional **Cross-Workflow-Hints** (`workflowHintItems`) Rückgabe: formatierter String für Injektion in den Agent-Systemkontext. **Wenn Embedding fehlschlägt**, liefert `buildAgentContext` einen **leeren String** (Agent arbeitet ohne diesen RAG-Block). Erweiterte Hilfen (z. B. **`readSection`**, Caching) für selektives Lesen sind im selben Service dokumentiert. ### RAG Consent & Control (ab 2026-05) Volle Architektur für transparente, datenzentrierte Steuerung der RAG-Indexierung durch den Benutzer. #### Prinzipien 1. **Datenquelle ist die Single Source of Truth** — `DataSource.ragIndexEnabled` steuert, ob ein Baum-Element indexiert wird. Vererbung entlang der Baumstruktur (wie `scope` und `neutralize`). 2. **Kein Wizard-Seiteneffekt ohne UI-Revoke** — Jede Einstellung die im AddConnectionWizard gesetzt wird, ist in der UDB (Unified Data Bar) sicht- und revertbar. 3. **Konsent-Entzug = sofortige Purge** — Deaktivierung von `ragIndexEnabled` oder `knowledgeIngestionEnabled` löscht zugehörige Chunks synchron. #### Datenmodell-Erweiterungen | Modell | Feld | Zweck | |--------|------|-------| | `DataSource` | `ragIndexEnabled: bool` | Pro Baum-Element: Auto-Indexierung an/aus | | `DataSource` | `lastIndexed: Optional[float]` | Zeitstempel des letzten erfolgreichen Index-Laufs | | `UserConnection` | `knowledgeIngestionEnabled: bool` | Globaler Konsent pro Verbindung | #### Walker-Architektur Jeder Walker (`subConnectorSync*.py`) iteriert über `ragIndexEnabled=True` DataSources: - Empfängt `dataSources: List[Dict]` und `progressCb: JobProgressCallback` - Prüft `progressCb.isCancelled()` periodisch für graceful Abort - Enthält `dataSourceId` in der Provenance jedes `IngestionJob` - Nutzt per-DataSource `neutralize`-Policy (aus `subPolicyResolver`) #### Job-Cancellation `mainBackgroundJobService.py` bietet: - `cancelJob(jobId)` — setzt Status auf `CANCELLED` - `cancelJobsByConnection(connectionId)` — bulk-cancel aller laufenden Jobs - `JobProgressCallback.isCancelled()` — kooperativer Check mit 3s-Cache #### Zombie-Job-Recovery & Walker-Timeouts (ab 2026-05-14) Drei-stufige Absicherung gegen hängende Bootstraps: 1. **Boot-Recovery:** `recoverInterruptedJobs()` markiert beim Worker-Start RUNNING-Jobs als ERROR (kein Auto-Requeue, sonst Endlosschleife). 2. **Live-Killer:** Cron `background_jobs.zombie_killer` ruft alle 5 min `killZombieJobs(maxAgeSeconds=1800)` auf — RUNNING-Jobs ohne Progress-Update >30 min werden als ERROR markiert (nutzt `startedAt` aus dem DB-Record). 3. **Walker-Timeouts** (`subWalkerHelpers.py`): jeder Walker wickelt seine drei Hot-Spots in `asyncio.wait_for`: | Helper | Timeout | Zweck | |--------|---------|-------| | `downloadWithTimeout` | 60s | Adapter-Download (SharePoint/Drive/kDrive/Outlook-Attachment) | | `extractWithTimeout` | 90s | `runExtraction` auf Worker-Thread (sync Extraktor blockiert sonst Event-Loop) | | `ingestWithTimeout` | 60s | `KnowledgeService.requestIngestion` (Embedding-API) | Vor jedem Item ruft der Walker `logItemStart(service, path, sizeBytes, mime)` — das letzte solche Log vor einem Hang oder Timeout benennt das verursachende Item exakt (Pfad, Grösse, MIME). Sync-Extraktion läuft via `asyncio.to_thread` auf einem Worker-Thread; `wait_for` schützt nur den Awaiter, der Thread selbst kann weiterlaufen, aber der Walker geht zum nächsten Item. #### API-Endpunkte | Methode | Pfad | Zweck | |---------|------|-------| | `PATCH` | `/api/datasources/{id}/rag-index` | Toggle `ragIndexEnabled` (Trigger mini-bootstrap oder Purge) | | `PATCH` | `/api/connections/{id}/knowledge-consent` | Globaler Konsent-Toggle | | `POST` | `/api/connections/{id}/knowledge-stop` | Alle laufenden Jobs stoppen | | `GET` | `/api/rag/inventory/me` | Persönliche RAG-Übersicht | | `GET` | `/api/rag/inventory/mandate` | Mandant-Aggregation (Admin) | | `GET` | `/api/rag/inventory/platform` | Plattform-Statistik (SysAdmin) | | `GET` | `/api/rag/inventory/jobs` | Aktive RAG-Jobs des Users | #### Frontend-Komponenten | Komponente | Ort | Funktion | |-----------|-----|----------| | UDB `SourcesTab` | 4. Toggle (🧠) | ragIndexEnabled pro Tree-Element | | `AddConnectionWizard` | Connector-aware Steps | MS Admin Consent + Infomaniak PAT integriert | | `RagInventoryPage` | Start > Nutzung > RAG | Globale Übersicht & Steuerung | | `RagRunningBadge` | MainLayout (fixed) | Floating-Badge bei aktiven Jobs | #### Vererbung (Policy Resolver) `subPolicyResolver.py` implementiert Longest-Prefix-Matching: - Eltern-DataSource mit explizitem `ragIndexEnabled` vererbt an Kind-Pfade - Gleiches Pattern wie `neutralize` und `scope` #### Konfigurierbare RAG-Limits (ab 2026-05-17) Walker-Limits (`maxBytes`, `maxFileSize`, `maxItems`, `maxDepth` für File-Walker; `maxTasks`, `maxWorkspaces`, `maxListsPerWorkspace` für ClickUp) sind nicht mehr in den Walker-Modulen hartkodiert, sondern aus zwei Quellen zusammengesetzt: 1. **Zentraler Default** — `modules/serviceCenter/services/serviceKnowledge/_ragLimits.py` (`FILES_LIMITS_DEFAULT`, `CLICKUP_LIMITS_DEFAULT`). Die alten `MAX_*_DEFAULT`-Konstanten in den Walkern sind dünne Aliase und bleiben für Rückwärtskompatibilität bestehen. 2. **DataSource-Override** — `DataSource.settings.ragLimits.` (oder `FeatureDataSource.settings`). JSONB-Spalte, optional, vollständig vom User editierbar. **Semantik** (kritisch, weil leicht zu missverstehen): - `_ragLimits.getStoredOverrides(ds, kind)` liefert NUR die explizit gesetzten Overrides → Walker mergen sie auf den **caller-supplied** `limits=`-Parameter (Test-Override gewinnt weiterhin). - `_ragLimits.getRagLimits(ds, kind)` mergt Overrides auf die globalen Defaults → API/Cost-Estimate-Pfad. - **Keine Override-Schicht, kein Resolver, keine Vererbung** für `ragLimits`. Was im Settings-Modal steht, ist exakt das, was der Walker liest. **Settings-API:** | Methode | Pfad | Zweck | |---------|------|-------| | `PATCH` | `/api/datasources/{id}/settings` | Partial-Update auf `DataSource.settings`/`FeatureDataSource.settings`. Nur Top-Level-Key `ragLimits` akzeptiert; unknown keys → 400. Audit-Log: `AuditCategory.PERMISSION/datasource_settings_changed`. Owner-only (Personal); für Mandate-Scope auch Mandate-Admin. | | `GET` | `/api/datasources/{id}/cost-estimate` | Indikative CHF-Schätzung für einen Voll-Sync mit den aktuellen Limits. Antwort: `{estimatedTokens, estimatedChf, basis: {kind, limits, assumptions, notes}, sourceId}`. Default-Heuristik: `text-embedding-3-small` @ `0.02 CHF / 1M Token` (Projekt-Konvention: Anbieter-Listenpreise werden direkt als CHF behandelt, siehe `calculatepriceCHF` in `aicorePluginOpenai.py`), `BYTES_PER_TOKEN=4`, `EXTRACTABLE_FRACTION=0.4`. Quelle: `_costEstimate.py`. | **UDB Settings-Modal** (`DataSourceSettingsModal.tsx`): einziges UI für DataSource-Settings, geöffnet via ⚙️-Icon pro Tree-Node im `SourcesTab`. Drei Sektionen: 1. **Connection** — `knowledgeIngestionEnabled` Master-Toggle (= `patchKnowledgeConsent`-Pfad). 2. **DataSource RAG-Limits** — Editierbare Felder; Bytes-Limits in MB im UI, in Bytes am Backend. 3. **Kostenschätzung** — Indikativ, nicht-verbindlich, ändert sich live nach `PATCH /settings`. Das gleiche Modal wird auf der `RagInventoryPage` aus dem Partial-Banner (`stoppedAtLimit`) via "Limit anpassen"-Button geöffnet → User hat direkten Pfad vom Symptom zur Behebung. --- ## Teamsbot-Integration (Hybrid-Routing, kein eigenes Toolset) Der Teamsbot ruft den **selben** `AgentService.runAgent` über das ServiceCenter auf — es gibt **kein** Teamsbot-spezifisches Toolset. Aufrufer ist `platform-core/modules/features/teamsbot/service.py::_runAgentForMeeting` mit `AgentConfig(maxRounds=5, maxCostCHF=0.10, toolSet="core", initialToolboxes=["core","web"], excludeActionTools=True)`. | Trigger | Pfad | Wer ruft `runAgent`? | |---|---|---| | Operator schickt **Director Prompt** (One-Shot oder Persistent) via Regie-Panel | `routeFeatureTeamsbot.submitDirectorPrompt` → `service.submitDirectorPrompt` → `asyncio.create_task(_processDirectorPrompt)` → `_runAgentForMeeting` | direkt, umgeht `SPEECH_TEAMS` | | `SPEECH_TEAMS` setzt **`needsAgent=true`** + `agentReason` (z. B. „recherchier das im Internet") | `service._analyzeAndRespond` erkennt `needsAgent`, ruft `_runAgentForMeeting` mit `taskBrief = agentReason` | Eskalation aus dem schnellen Pfad | **FINAL-Delivery:** Der `FINAL`-Event-Text wird im Teamsbot-Service über die bestehenden Kanäle (TTS + `sendChatMessage`) ins Meeting gespielt — der Agent „spricht" nicht selbst, sondern liefert nur den Text. Damit braucht es **kein** Teamsbot-spezifisches Tool wie `sendChat` oder `readAloud` im Agent. **Workflow-ID-Konvention:** `workflowId = f"teamsbot:{sessionId}"` — RoundMemory und RAG akkumulieren pro Meeting, getrennt von anderen Sessions. **Persistente Direktiven:** `service._buildPersistentDirectorContext` rendert aktive `persistent`-Direktiven als `OPERATOR_DIRECTIVES`-Block in den `SPEECH_TEAMS`-Kontext, damit sie auch ohne erneuten Director-Prompt-Aufruf wirken (z. B. „Antworte immer in Englisch"). Siehe [`b-reference/teams-bot/architecture.md`](../teams-bot/architecture.md) für die vollständige Hybrid-Architektur und Director-Prompt-Lifecycle. --- ## Schlüssel-Dateien | Datei | Rolle | |-------|--------| | `platform-core/modules/serviceCenter/registry.py` | Registrierung `agent`, `knowledge`, Dependencies, `objectKey` | | `platform-core/modules/serviceCenter/services/serviceAgent/mainServiceAgent.py` | `AgentService`, `runAgent`, Prompt-Enrichment, Registry-Orchestrierung | | `platform-core/modules/serviceCenter/services/serviceAgent/coreTools/registerCore.py` | Orchestrator: delegiert Tool-Registrierung an Domänen-Module | | `platform-core/modules/serviceCenter/services/serviceAgent/coreTools/_workspaceTools.py` | Dateien, Ordner, Web, Übersetzung | | `platform-core/modules/serviceCenter/services/serviceAgent/coreTools/_connectionTools.py` | Externe Connections, Upload, Mail | | `platform-core/modules/serviceCenter/services/serviceAgent/coreTools/_dataSourceTools.py` | DataSource Browse/Search/Download | | `platform-core/modules/serviceCenter/services/serviceAgent/coreTools/_documentTools.py` | Container, Content-Objects, Vision | | `platform-core/modules/serviceCenter/services/serviceAgent/coreTools/_mediaTools.py` | Rendering, TTS, STT, Bildgenerierung, Charts, Neutralize, Code | | `platform-core/modules/serviceCenter/services/serviceAgent/coreTools/_featureSubAgentTools.py` | Feature Data Sub-Agent (queryFeatureInstance) | | `platform-core/modules/serviceCenter/services/serviceAgent/coreTools/_crossWorkflowTools.py` | Workflow-Historie, Messages, `_CORE_ONLY_TOOLS`-Tagging | | `platform-core/modules/serviceCenter/services/serviceAgent/agentLoop.py` | ReAct-Loop, Budget, RAG-Injektion, Tool-Dispatch, Summaries | | `platform-core/modules/serviceCenter/services/serviceAgent/toolRegistry.py` | Registrierung, Dispatch, `readOnly`, Function-Calling-Format | | `platform-core/modules/serviceCenter/services/serviceAgent/conversationManager.py` | Kontextfenster, Summarization, Systemprompt | | `platform-core/modules/serviceCenter/services/serviceAgent/datamodelAgent.py` | `AgentConfig`, Events, Trace-Modelle | | `platform-core/modules/serviceCenter/services/serviceAgent/actionToolAdapter.py` | Workflow-Actions → Agent-Tools (Schema-Generierung; Param-Validierung erfolgt zentral im `ActionExecutor`) | | `platform-core/modules/workflows/processing/shared/parameterValidation.py` | Universelle Action-Parameter-Validierung + Coercion (Required-Enforcement, Ref-Schema → id-String, Primitive-Coercion); aufgerufen aus `ActionExecutor.executeAction` für **alle** Aufrufpfade (Agent, Workflow-Graph, REST) | | `platform-core/modules/connectors/connectorDbPostgre.py` | DB-Connector; Read-Methoden raisen `DatabaseQueryError` bei echten Query-Fehlern (Postgres-Adapt, UndefinedTable/Column, OperationalError, …); Empty Result Sets bleiben `[]`/`None` | | `platform-core/modules/serviceCenter/services/serviceAgent/toolboxRegistry.py` | Toolbox-Definitionen, `requestToolbox` Meta-Tool-Schema | | `platform-core/modules/serviceCenter/services/serviceAgent/workflowTools.py` | Workflow-Editing-Tools (readWorkflowGraph, addNode, ...) | | `platform-core/modules/serviceCenter/services/serviceAgent/sandboxExecutor.py` | `executeCode`-Sandbox | | `platform-core/modules/serviceCenter/services/serviceKnowledge/mainServiceKnowledge.py` | Index, `buildAgentContext`, Workflow-/Round-Memory-Helfer | | `platform-core/modules/interfaces/interfaceDbKnowledge.py` | DB-Zugriff Knowledge / RAG | | `platform-core/modules/datamodels/datamodelKnowledge.py` | FileContentIndex, ContentChunk, RoundMemory, WorkflowMemory | | `platform-core/modules/serviceCenter/services/serviceAi/mainServiceAi.py` | Zentrales Neutralisierungs-Gate vor LLM, Billing, Provider-Aufruf | | `platform-core/modules/aicore/aicoreModelRegistry.py` | Provider-/Modell-Registry | | `platform-core/modules/aicore/aicoreModelSelector.py` | Modellauswahl | | `platform-core/modules/aicore/aicorePlugin*.py` | Provider-Implementierungen | | `platform-core/modules/datamodels/datamodelAi.py` | `OperationTypeEnum`, `AiCallRequest`, Optionen | --- ## Regeln / Invarianten 1. **Neutralisierung vor dem Modell:** Alle an ein LLM gehenden Inhalte (Prompt, Kontext, Messages) werden ausschliesslich in **`serviceAi`** entsprechend Policy neutralisiert bzw. blockiert — nicht in den Agent-Tools. 2. **Tools liefern Rohdaten:** Kein zweites Neutralisieren in Tools; Ausnahme ist bewusste **Data-at-rest**-Neutralisierung beim **Indexieren** (`indexFile` bei `FileItem.neutralize=True`). 3. **DataSource → FileItem:** `_downloadFromDataSource` vererbt das **`neutralize`-Flag** der DataSource auf erzeugte Dateien; **`queryFeatureInstance`** kann Sub-Agent-Calls mit **`requireNeutralization=True`** auslösen, wenn die Feature-Datenquelle das vorsieht. 4. **Kein Tool-RBAC:** Autorisierung über Datenbank-, Connection- und Service-Schicht. 5. **Parallele Tools nur bei `readOnly=True`:** Schreibende Tools nicht parallel zueinander aus demselben Batch. 6. **RAG optional:** Embedding- oder DB-Fehler im Kontextpfad führen nicht zwingend zum Abbruch des Agent-Runs (RAG wird übersprungen/entfällt). 7. **Billing:** AI-Calls und eingebettete Pfade (Embeddings, Summaries) laufen über bestehende Billing-/Subscription-Checks in `serviceAi` bzw. aufrufenden Schichten. Detaillierte Neutralisierungs-Kette und Compliance: **`wiki/compliance/Neutralisierung.md`**. Architektur-Roadmap (Unified Workspace, ProviderConnector 1:n, Phasenplan): **`wiki/concepts/AI-Agent-Architecture-Konzept.md`** — als Zielbild lesen, nicht als Ist-Abbild aller Codepfade.