wiki/b-reference/platform-core/ai-agent.md

522 lines
40 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- status: canonical -->
<!-- lastReviewed: 2026-06-02 -->
<!-- verifiedAgainst: gateway (codebase audit 2026-04-07, post Automation Unification); platform-core/modules/features/teamsbot/service.py (Hybrid Agent Escalation 2026-04-24); Typed Action Architecture Phasen 1-5; featureDataAgent domain hints hook 2026-04-27; central parameterValidation + DatabaseQueryError 2026-04-28; OpenAI temperature contract for GPT-5.x / o-series 2026-04-28; Voice STT speechToText params note 2026-05-10; RAG Consent & Control Unification (Phases A-D) 2026-05-12; Zombie-Killer + Walker-Timeouts 2026-05-14; FeatureDataAgent Query-Repair-Loop + Ontology layer 2026-05-15; UDB DataSource Settings + configurable RAG-Limits 2026-05-17; Cost-Estimate Währung USD→CHF 2026-05-22; DataSource search-first + adapter scoping/pagination + inline metadata (Outlook/Gmail/SharePoint/OneDrive/Drive/Calendar/Contacts/ClickUp) 2026-06-02 (connectorMsft.py, connectorGoogle.py, connectorInfomaniak.py, connectorClickup.py, _dataSourceTools.py, routeFeatureWorkspace.py) -->
# 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, 1100) |
| `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=<short>, 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-<timestamp>.{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:/<folder>:/search(q=)` (Name + Inhalt) | angebundener Ordner | `@odata.nextLink` |
| Google Drive | `fullText contains` (Name + Inhalt) | `'<folderId>' 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 &mdash; 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.<key>` (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.