From 1c9b62aef9ff98dd4221fbe766053cde3a843ef6 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 15 Mar 2026 18:14:21 +0100
Subject: [PATCH] unified ai service concept
---
concepts/AI-Agent-Architecture-Konzept.md | 1464 +++++++++++++++++++++
concepts/AI-Agent-Architecture-Review.md | 146 ++
2 files changed, 1610 insertions(+)
create mode 100644 concepts/AI-Agent-Architecture-Konzept.md
create mode 100644 concepts/AI-Agent-Architecture-Review.md
diff --git a/concepts/AI-Agent-Architecture-Konzept.md b/concepts/AI-Agent-Architecture-Konzept.md
new file mode 100644
index 0000000..d7de1c1
--- /dev/null
+++ b/concepts/AI-Agent-Architecture-Konzept.md
@@ -0,0 +1,1464 @@
+# Konzept: AI Agent Architecture & Unified Workspace
+
+> Poweron Platform -- Zentrales AI Handling und Editor-UI
+> Status: Konzept / Version 2.0 / März 2026
+
+---
+
+## 1. Ausgangslage
+
+Die Plattform hat vier isolierte Systeme für AI-Interaktionen, jedes mit eigenen Schwächen:
+
+| System | Stärke | Schwäche |
+|--------|--------|----------|
+| **serviceAi** (Pipeline) | Modell-Fallback, Billing, Dokumentenextraktion | Rigide Pipeline (extract→structure→fill), N×M AI-Call-Explosion, kein Caching |
+| **Codeeditor** | ReAct-Loop mit Tool-Use, Agent-Modus | Nur read-only Tools, eigenes UI, nur Codeeditor-Scope |
+| **Chatbot** | LangGraph, SQL/Tavily-Integration | Kein File-Handling, keine Dokumentbearbeitung, eigenes UI |
+| **Chatplayground** | Workflow-Management | Kein Streaming, kein Voice, eigenes UI |
+
+Zusätzliche Defizite:
+- **Kein Wissenstransfer:** Jeder Workflow startet bei null, selbst wenn dasselbe Dokument schon extrahiert wurde
+- **File-System:** Flach ohne Ordner, Tags oder FeatureInstance-Zuordnung
+- **Keine externen Datenquellen** im Chat referenzierbar (SharePoint-Ordner, Drive, etc.)
+- **Grosse Dokumente:** 500-Seiten-PDFs werden blind vollständig extrahiert (500+ AI-Calls)
+- **Kein Container-Handling:** ZIP-Files, Ordner und verschachtelte Dokumente werden nicht unterstützt
+- **Keine generische Provider-Abstraktion:** SharePoint/Outlook je eigene Implementierung, obwohl beides MSFT-Services über eine Connection. Kein Google Drive, FTP etc.
+- **36 Workflow-Actions isoliert:** Bestehende Actions (SharePoint, Outlook, Jira, AI) nicht als Agent-Tools nutzbar
+
+---
+
+## 2. Zielarchitektur
+
+### 2.1 Kernidee: Agent statt Pipeline
+
+**Bisher:** Code entscheidet starr, was passiert:
+```
+extractContent() → generateStructure() → fillSections() → render()
+```
+
+**Neu:** AI entscheidet dynamisch, welche Tools sie braucht:
+```
+User Prompt + Files → Agent Loop → Tool Calls → Result
+```
+
+### 2.2 Gesamtübersicht
+
+```mermaid
+graph TB
+ subgraph ui["Unified Workspace UI"]
+ ChatPanel["Chat / Streaming"]
+ FilePanel["File Browser"]
+ DataPanel["Data Source Picker"]
+ KnowledgePanel["Knowledge Browser"]
+ end
+
+ subgraph agentSvc["serviceAgent"]
+ Loop["Agent Loop\nReAct + native function calling"]
+ TR["Tool Registry"]
+ CTX["Context Manager\nRAG Retrieval"]
+ end
+
+ subgraph knowledge["Knowledge Store"]
+ SharedL["Shared Layer\nmandateId"]
+ InstanceL["Instance Layer\nuserId + featureInstanceId"]
+ WorkflowL["Workflow Layer\nworkflowId"]
+ end
+
+ subgraph tools["Tools"]
+ FileT["File Tools"]
+ DocT["Document Tools"]
+ ConnT["Connection Tools"]
+ ExtT["External Tools"]
+ WfT["Workflow Tools"]
+ end
+
+ subgraph infra["Bestehende Infrastruktur"]
+ AiSvc["serviceAi\nModell-Fallback + Billing"]
+ ExtrSvc["serviceExtraction"]
+ Stream["serviceStreaming"]
+ Voice["serviceVoice"]
+ UCon["UserConnections"]
+ end
+
+ ui -->|"SSE Stream"| agentSvc
+ CTX -->|"RAG"| knowledge
+ DocT -->|"Cache"| knowledge
+ Loop --> TR
+ TR --> tools
+ Loop --> CTX
+ Loop --> AiSvc
+ Loop --> Stream
+ DocT --> ExtrSvc
+ ConnT --> UCon
+```
+
+---
+
+## 3. Agent Core (`serviceAgent`)
+
+### 3.1 Aufbau
+
+Neuer Service unter `modules/serviceCenter/services/serviceAgent/`:
+
+| Datei | Funktion |
+|-------|----------|
+| `mainServiceAgent.py` | Entry Point: `runAgent(prompt, fileIds, toolSet, config)` |
+| `agentLoop.py` | ReAct-Loop mit native function calling |
+| `toolRegistry.py` | Tool-Registrierung und Dispatch |
+| `conversationManager.py` | Context-Window-Management |
+| `datamodelAgent.py` | AgentState, ToolDefinition, ToolResult, AgentConfig |
+
+**ServiceCenter-Registry:**
+
+```python
+# In serviceCenter/registry.py
+IMPORTABLE_SERVICES = {
+ ...
+ "agent": {
+ "module": "modules.serviceCenter.services.serviceAgent.mainServiceAgent",
+ "class": "ServiceAgent",
+ "dependencies": ["ai", "chat", "extraction", "billing", "streaming"],
+ "objectKey": "service.agent"
+ }
+}
+```
+
+`serviceAgent` hat die meisten transitiven Dependencies (über `ai` → `chat`, `utils`, `extraction`, `billing`). Die bestehende DI-Kette im ServiceCenter löst dies automatisch auf.
+
+**AgentConfig:**
+
+```python
+class AgentConfig(BaseModel):
+ maxRounds: int = 25
+ maxCostCHF: Optional[float] = None # Workflow-Budget-Cap
+ entityCacheEnabled: bool = False # Opt-in Entity Extraction (Extra-AI-Call pro Round)
+ toolSet: str = "core" # Welches Tool-Set aktiv
+ operationType: str = "AGENT" # Für Modell-Auswahl
+```
+
+### 3.2 Agent Loop
+
+```python
+async def runAgent(prompt, fileIds, toolSet, config, progressCallback):
+ state = AgentState(maxRounds=config.maxRounds)
+ tools = toolRegistry.getTools(toolSet)
+ messages = [buildSystemPrompt(tools), {"role": "user", "content": prompt}]
+
+ while state.status == "running" and state.currentRound < state.maxRounds:
+ state.currentRound += 1
+
+ # Kosten-Budget prüfen
+ if config.maxCostCHF and await billingService.getWorkflowCost(workflowId) > config.maxCostCHF:
+ state.status = "budgetExceeded"
+ yield AgentEvent(type="final", content=_summarizeProgress(state))
+ break
+
+ response = await aiService.callAi(AiCallRequest(
+ messages=messages,
+ options=AiCallOptions(operationType=OperationTypeEnum.AGENT),
+ tools=tools
+ ))
+
+ toolCalls = response.toolCalls
+ if not toolCalls:
+ state.status = "completed"
+ yield AgentEvent(type="final", content=response.content)
+ break
+
+ # Tool-Ausführung: sequential default, parallel nur für readOnly-Tools
+ results = await _executeToolCalls(toolCalls, context)
+
+ messages.extend(formatToolResults(toolCalls, results))
+ yield AgentEvent(type="toolResults", data=results)
+
+ # Graceful Degradation bei maxRounds
+ if state.currentRound >= state.maxRounds and state.status == "running":
+ state.status = "maxRoundsReached"
+ yield AgentEvent(type="final", content=_summarizeProgress(state))
+
+
+async def _executeToolCalls(toolCalls, context):
+ """Sequential als Default. Parallel nur für readOnly-Tools (keine Race Conditions)."""
+ readOnlyCalls = [tc for tc in toolCalls if toolRegistry.isReadOnly(tc.name)]
+ writeCalls = [tc for tc in toolCalls if not toolRegistry.isReadOnly(tc.name)]
+
+ results = {}
+ if readOnlyCalls:
+ readResults = await asyncio.gather(*[
+ toolRegistry.dispatch(tc.name, tc.args, context) for tc in readOnlyCalls
+ ])
+ for tc, result in zip(readOnlyCalls, readResults):
+ results[tc.id] = result
+
+ for tc in writeCalls:
+ results[tc.id] = await toolRegistry.dispatch(tc.name, tc.args, context)
+
+ return [results[tc.id] for tc in toolCalls]
+```
+
+### 3.3 Design-Entscheidungen
+
+- **Native function calling** als Standard (OpenAI tool_choice Format) -- zuverlässiger als text-basiertes Parsing
+- **Text-basierter Fallback** für Modelle ohne function calling Support
+- Nutzt bestehende `callAi`-Infrastruktur (Modell-Fallback, Billing, Provider-Auswahl) -- damit greifen Provider-RBAC, Model-RBAC und Billing-Checks automatisch
+- `AiCallRequest` wird erweitert um `messages` und `tools` (abwärtskompatibel: bestehende `prompt`/`context`/`contentParts` bleiben)
+- **Tool-Ausführung sequential** als Default -- parallel nur für explizit als `readOnly=True` markierte Tools (verhindert Race Conditions bei State-modifizierenden Tools)
+- **Kosten-Budget** (`maxCostCHF`) pro Workflow -- Agent stoppt mit Fortschritts-Summary bei Überschreitung
+- **Graceful Degradation** bei `maxRounds` -- Agent liefert Summary statt stiller Abbruch
+
+### 3.4 Fehlerbehandlung
+
+| Fehlertyp | Strategie |
+|-----------|-----------|
+| **Tool-Fehler** (Exception in Tool) | Fehler als ToolResult an Agent zurückgeben -- Agent entscheidet über Retry oder alternativen Ansatz |
+| **Halluzinierte Tool-Namen** | Validierung gegen `toolRegistry.getTools()` -- ungültige Tools werden als Fehler-ToolResult zurückgegeben |
+| **Externe API-Fehler** (Timeout, 5xx) | Retry mit Exponential Backoff (max 3 Versuche), dann Fehler-ToolResult |
+| **AI-Call-Fehler** | Bestehende Fallback-Logik in `serviceAi` (Modell-Fallback, Provider-Switch) |
+| **maxRounds erreicht** | Graceful Degradation: Agent liefert Summary des bisherigen Fortschritts |
+| **Budget überschritten** | Agent stoppt, liefert Zusammenfassung, User wird informiert |
+| **Knowledge Store nicht verfügbar** | Agent arbeitet ohne RAG-Kontext weiter (degraded mode) |
+
+**Input-Validierung:** Tool-Argumente vom LLM sind unvalidiert. Jedes Tool validiert seine Args vor Weitergabe an darunterliegende Interfaces:
+- Path-Traversal-Schutz (`readFile`, `writeFile`): Pfade auf erlaubten Scope beschränken
+- SQL-Injection-Schutz (`sqlQuery`): Parametrisierte Queries
+- Container-Limits: `maxTotalExtractedSize`, `maxFileCount`, Symlink-Blockierung (ZIP-Bomb-Schutz)
+
+### 3.5 RBAC und Sicherheit
+
+**Design-Entscheidung: Keine Tool-Level-RBAC nötig.** Die Berechtigungen greifen auf Daten- und Service-Ebene unterhalb der Tools:
+
+| Tool | Absicherung unterhalb |
+|------|----------------------|
+| File-Tools (`readFile`, `writeFile`, `listFiles`) | `interfaceDbApp` → `getRecordsetWithRBAC()` mit WHERE nach `MY/GROUP/ALL` |
+| External-Tools (`externalBrowse`, `externalDownload`) | UserConnection OAuth-Token -- User sieht nur, was sein Account erlaubt |
+| AI-Calls (Agent-Loop) | `serviceAi` → `_checkBillingBeforeAiCall()` → Provider-RBAC + Model-RBAC |
+| Service-Zugriff | `can_access_service()` prüft RESOURCE-ObjectKey |
+
+**serviceAgent als IMPORTABLE_SERVICE:**
+
+`serviceAgent` wird in der ServiceCenter-Registry registriert mit `objectKey = "service.agent"`. Damit greift `can_access_service()` und der Zugang zum Agent ist per Rolle steuerbar.
+
+**Knowledge Store Shared Layer:**
+
+Dokumente mit `isShared=True` sind mandateweit sichtbar. Neue Tabellen (`FileContentIndex`, `ContentChunk`) brauchen Einträge in `TABLE_NAMESPACE` mit DATA-ObjectKeys, damit `getRecordsetWithRBAC()` greift. Promote in Shared Layer (`isShared=True`) erfordert Access-Level `ALL`.
+
+### 3.6 Tool Registry
+
+```python
+CORE_TOOLS = ["readFile", "writeFile", "listFiles", "searchFiles",
+ "extractContent", "summarizeContent", "webSearch"]
+```
+
+Jedes Tool wird mit `readOnly`-Flag registriert (steuert parallele vs. sequentielle Ausführung):
+```python
+toolRegistry.register("readFile", readFileTool, readOnly=True)
+toolRegistry.register("writeFile", writeFileTool, readOnly=False)
+toolRegistry.register("sqlQuery", sqlQueryTool, featureType="chatbot", readOnly=True)
+toolRegistry.register("sharePointUpload", spTool, featureType="trustee", readOnly=False)
+```
+
+---
+
+## 4. Knowledge Store (RAG)
+
+### 4.1 Kernprinzip
+
+Persistenter Wissensspeicher mit drei Ebenen. Ein File wird **einmal** extrahiert und steht danach allen Workflows zur Verfügung. Automatische Indexierung bei Upload.
+
+### 4.2 Drei-Ebenen-Architektur
+
+```mermaid
+graph TB
+ subgraph scope["Knowledge Store"]
+ direction TB
+
+ subgraph shared["Shared Layer -- mandateId"]
+ GD["Globale Firmen-Dokumente\nTemplates, Shared Knowledge"]
+ end
+
+ subgraph instance["Instance Layer -- userId + featureInstanceId"]
+ FI["User-Dokumente\nFile-Embeddings\nDocument Indexes"]
+ end
+
+ subgraph wf["Workflow Layer -- workflowId"]
+ WF["Conversation Summary\nEntity Cache\nTool Results"]
+ end
+
+ shared --> instance
+ instance --> wf
+ end
+```
+
+| Ebene | Scope | Inhalt | Lebensdauer |
+|-------|-------|--------|-------------|
+| **Shared** | mandateId | Globale Dokumente, Templates | Permanent |
+| **Instance** | userId + featureInstanceId | Extrahierte Dokumente, Embeddings | Permanent pro Workspace |
+| **Workflow** | workflowId | Conversation Summaries, Entities, Tool-Ergebnisse | Lebt mit dem Workflow |
+
+### 4.3 Automatische Indexierung bei Upload
+
+Jedes hochgeladene File durchläuft eine Background-Pipeline -- **komplett ohne AI**:
+
+```
+Upload → Container auflösen (rekursiv) → Typ-Extractor pro File → Content-Objekte → Chunking → Embedding → Knowledge Store
+```
+
+```python
+async def _onFileUploaded(fileId, userId, featureInstanceId):
+ fileData = await getFileData(fileId)
+ mimeType = _detectMimeType(fileData)
+
+ # 1. Container auflösen (ZIP/Folder → Files), dann pro File extrahieren
+ files = await _resolveToFiles(fileId, fileData, mimeType)
+
+ for file in files:
+ # 2. Typ-Extractor: Content-Objekte extrahieren (kein AI)
+ contentIndex = await _extractFileContents(file.id, file.data, file.mimeType)
+
+ # 3. Text-Content-Objekte chunken und embedden
+ textObjects = [o for o in contentIndex.objects if o.contentType == "text"]
+ chunks = _chunkForEmbedding(textObjects, chunkSize=512)
+ embeddings = await embeddingService.embed([c.data for c in chunks])
+
+ # 4. Im Knowledge Store persistieren (userId + featureInstanceId scoped)
+ for chunk, embedding in zip(chunks, embeddings):
+ await knowledgeStore.upsertContentChunk(ContentChunk(
+ contentObjectId=chunk.id, fileId=file.id,
+ userId=userId, featureInstanceId=featureInstanceId,
+ data=chunk.data, contextRef=chunk.contextRef,
+ embedding=embedding
+ ))
+
+ await knowledgeStore.updateFileStatus(fileId, "indexed")
+```
+
+Ergebnis: Sekunden bis Minuten nach Upload ist das File für alle Workflows RAG-ready. Nachfolgende Aktivitäten holen den Inhalt direkt aus dem Knowledge Store.
+
+### 4.4 Datenmodelle (PostgreSQL + pgvector)
+
+```python
+class FileContentIndex(BaseModel):
+ """Index der Content-Strukturen pro File. Lebt im Instance Layer.
+ Wird bei Extraktion erstellt (kein AI)."""
+ id: str # = fileId
+ userId: str
+ featureInstanceId: str
+ mandateId: str
+ isShared: bool = False # Shared Layer Sichtbarkeit
+ fileName: str
+ mimeType: str
+ containerPath: Optional[str] # Pfad im Container (z.B. "archiv.zip/folder/report.pdf")
+ totalObjects: int # Anzahl Content-Objekte
+ totalSize: int
+ structure: Dict[str, Any] # Strukturübersicht (Pages, Sections, etc.)
+ objectSummary: List[Dict] # Kompakte Übersicht pro Content-Objekt
+ extractedAt: float
+ status: str # pending, extracted, embedding, indexed, failed
+
+class ContentChunk(BaseModel):
+ """Persistierter Content-Chunk. Wiederverwendbar über Workflows.
+ Skalares Content-Objekt (oder Chunk davon) mit Embedding."""
+ id: str
+ contentObjectId: str # FK zum Content-Objekt
+ fileId: str # FK zum File
+ userId: str
+ featureInstanceId: str
+ contentType: str # text, image, videostream, audiostream, other
+ data: str # Inhalt (Text, Base64, URL)
+ contextRef: Dict[str, Any] # Kontext-Referenz (Seite, Position, Label)
+ summary: Optional[str] # AI-generierte Zusammenfassung (bei Bedarf)
+ metadata: Dict[str, Any] = {}
+ embedding: List[float] # pgvector (NOT NULL für text-Chunks)
+
+class WorkflowMemory(BaseModel):
+ """Workflow-spezifischer Cache."""
+ id: str
+ workflowId: str
+ userId: str
+ featureInstanceId: str
+ key: str # z.B. "entity:companyName"
+ value: str
+ source: str # extraction, tool, conversation, summary
+ createdAt: float
+ embedding: Optional[List[float]]
+```
+
+### 4.5 RAG-Retrieval: 3-Ebenen-Suche
+
+Vor jedem Agent-Round sucht der Context Manager über alle drei Ebenen:
+
+```python
+async def _buildAgentContext(self, currentPrompt, workflowId, userId,
+ featureInstanceId, mandateId, contextBudget):
+ # 1. Instance Layer: User-Dokumente (höchste Priorität)
+ instanceChunks = await knowledgeStore.semanticSearch(
+ query=currentPrompt, userId=userId,
+ featureInstanceId=featureInstanceId, limit=15, minScore=0.65
+ )
+ # 2. Shared Layer: Globale Firmen-Dokumente
+ sharedChunks = await knowledgeStore.semanticSearch(
+ query=currentPrompt, mandateId=mandateId,
+ isShared=True, limit=10, minScore=0.7
+ )
+ # 3. Workflow Layer: Aktueller Kontext
+ entities = await knowledgeStore.getEntities(workflowId)
+ summary = await knowledgeStore.getConversationSummary(workflowId)
+ toolResults = await knowledgeStore.getRecentToolResults(workflowId, limit=5)
+
+ context = ContextBuilder(budget=contextBudget)
+ context.add(priority=1, content=currentPrompt)
+ context.add(priority=2, content=instanceChunks)
+ context.add(priority=3, content=entities)
+ context.add(priority=4, content=sharedChunks)
+ context.add(priority=5, content=summary)
+ context.add(priority=6, content=toolResults)
+ return context.build()
+```
+
+### 4.6 Cross-Workflow Wissenstransfer
+
+```
+Workflow A (Montag):
+ User: "Analysiere Q4-Report.pdf"
+ → Extraktion + Embedding im Instance Layer (200 Chunks)
+ → Agent findet: Revenue = 12.5M CHF
+
+Workflow B (Dienstag):
+ User: "Vergleiche Q4-Report mit Budget.xlsx"
+ → Q4-Report: Bereits im Knowledge Store → 0 Extraktion
+ → Budget.xlsx: Automatisch indexiert nach Upload
+ → Semantische Suche "Q4 Revenue" → sofort relevanter Chunk
+```
+
+### 4.7 Progressive Summarization
+
+Statt alte Nachrichten abzuschneiden (`trim_messages`):
+
+```
+Round 1-3: Volle Nachrichten
+Round 4: Nachrichten 1-2 → Summary
+Round 7: Summary(1-4) + volle 5-7
+Round 15: Meta-Summary(1-10) + Summary(11-13) + volle 14-15
+```
+
+Der Agent verliert nie den Faden. Summaries werden im Workflow Layer persistiert. **Kosten:** Jede Summarization ist ein AI-Call (günstigeres Modell, z.B. `DATA_ANALYSE`). Bei einem 15-Round-Workflow: ~3 Summary-Calls. Diese Kosten fliessen ins Workflow-Budget (`maxCostCHF`) ein und erscheinen als eigener Typ in der `BillingTransaction`.
+
+### 4.8 Entity Cache
+
+Extraktion wichtiger Fakten nach Tool-Ergebnissen. **Opt-in per Config** (jeder Aufruf ist ein Extra-AI-Call):
+
+```python
+if config.entityCacheEnabled:
+ entities = await _extractEntities(response)
+ # {"company": "Acme AG", "revenueQ4": "12.5M CHF"}
+ for key, value in entities.items():
+ await knowledgeStore.upsertEntity(workflowId, key, value)
+```
+
+**Kosten-Kontrolle:** Entity-Extraktion ist standardmässig deaktiviert. Für häufig genutzte Entities (Firmennamen, Beträge, Daten) kann alternativ regelbasierte Extraktion (Regex/NER) ohne AI-Call eingesetzt werden.
+
+### 4.9 Embedding-Strategie
+
+| Aspekt | Entscheidung |
+|--------|-------------|
+| Embedding-Modell | OpenAI `text-embedding-3-small` (1536 dim); lokal: sentence-transformers |
+| Vector Store | pgvector auf bestehendem PostgreSQL |
+| Chunk-Grösse | 512 Tokens |
+| Index-Typ | IVFFlat (< 100k Chunks), HNSW (> 100k) |
+| Suche | Cosine Similarity, gefiltert nach userId + featureInstanceId |
+
+---
+
+## 5. Smart Document Handling
+
+### 5.1 Problem
+
+Ein 200MB PDF mit 500 Seiten: Aktuell werden alle 500 Seiten blind extrahiert, jede einzeln an AI gesendet → 500+ AI-Calls.
+
+### 5.2 Lösung: 3-Stufen-Modell
+
+```mermaid
+graph TD
+ Upload["Dokument Upload"]
+
+ subgraph stage1["Stufe 1: Structure Pre-Scan"]
+ TOC["TOC Extraction"]
+ Headings["Heading Detection"]
+ PageMap["Page Map"]
+ end
+
+ subgraph stage2["Stufe 2: Selective Extraction"]
+ OnDemand["Nur angeforderte Seiten"]
+ Cache["Knowledge Store Cache"]
+ end
+
+ subgraph stage3["Stufe 3: Deep Analysis"]
+ Vision["Vision AI"]
+ TableEx["Tabellen-Extraktion"]
+ OCR["OCR"]
+ end
+
+ Upload --> stage1
+ stage1 -->|"Agent entscheidet"| stage2
+ stage2 -->|"Bei Bedarf"| stage3
+```
+
+**Stufe 1 -- Structure Pre-Scan (Sekunden, kein AI):**
+Rein programmatisch mit PyMuPDF. Extrahiert TOC, Headings (Font-Size-Heuristik), Seitenstruktur, Bildpositionen. Ergebnis ist der `FileContentIndex` (siehe 5.4). Der Agent sieht sofort die Struktur eines 500-Seiten-PDFs.
+
+```python
+async def preScanDocument(fileId: str) -> FileContentIndex:
+ doc = fitz.open(stream=fileData, filetype="pdf")
+ toc = doc.get_toc()
+
+ pageMap = []
+ for i, page in enumerate(doc):
+ blocks = page.get_text("dict")["blocks"]
+ pageMap.append({
+ "pageIndex": i,
+ "headings": [h for h in blocks if _isHeading(h)],
+ "hasImages": any(b["type"] == 1 for b in blocks),
+ "textLength": len(page.get_text()),
+ "hasTable": _detectTableHeuristic(page)
+ })
+
+ sections = _buildSectionsFromTocOrHeadings(toc, pageMap)
+ return FileContentIndex(id=fileId, structure={"toc": toc, "sections": sections, "pageMap": pageMap})
+```
+
+**Stufe 2 -- On-Demand Extraction:**
+Agent liest nur die Seiten, die er braucht. Ergebnisse werden im Knowledge Store gecacht.
+
+```python
+async def readSection(fileId, sectionId):
+ cached = await knowledgeStore.getCachedSection(fileId, sectionId)
+ if cached:
+ return cached
+
+ section = _findSection(docIndex, sectionId)
+ parts = _extractPages(fileData, section["startPage"], section["endPage"])
+ await knowledgeStore.cacheExtraction(fileId, sectionId, parts)
+ return _formatSectionResult(parts)
+```
+
+**Stufe 3 -- Deep Analysis (nur bei explizitem Bedarf):**
+Vision AI für Bilder, Tabellen-Extraktion, OCR -- nur für einzelne angeforderte Elemente.
+
+### 5.3 Kostenvergleich
+
+| Szenario | Alt (Pipeline) | Neu (Smart) |
+|----------|----------------|-------------|
+| "Fasse Kapitel 3 zusammen" (8/500 Seiten) | 500 Seiten, 500+ AI-Calls | Pre-Scan + 8 Seiten + 1 AI-Call |
+| "Was steht auf Seite 47?" | 500 Seiten, 500+ AI-Calls | Pre-Scan + 1 Seite + 1 AI-Call |
+| Zweite Frage zum selben Dokument | Erneut 500 Seiten | Aus Knowledge Store, 0 Extraktion |
+| Gesamtanalyse | 500+ AI-Calls | ~21 AI-Calls (20 Section-Summaries + 1 Meta-Summary) |
+
+### 5.4 Container- und Content-Modell
+
+Das System unterscheidet klar zwischen **physischen Strukturen** (Container, Files) und **logischen Content-Objekten** (Text, Bild, Audio, Video). Die gesamte Extraktion bis zu den Content-Objekten erfolgt **ohne AI**.
+
+#### Physische Schicht: Container-Hierarchie
+
+Container sind verschachtelt (n,m : 0..i):
+
+```mermaid
+graph TD
+ ZIP["ZIP (Container)"]
+ FA["folder-a/ (Container)"]
+ FB["folder-b/ (Container)"]
+ PDF["report.pdf (File → Typ-Extractor)"]
+ IMG["photo.png (File → Typ-Extractor)"]
+ DOCX["vertrag.docx (File → Typ-Extractor)"]
+ CSV["data.csv (File → Typ-Extractor)"]
+
+ ZIP --> FA
+ ZIP --> FB
+ FA --> PDF
+ FA --> IMG
+ FB --> DOCX
+ FB --> CSV
+```
+
+| Ebene | Regel |
+|-------|-------|
+| **ZIP / TAR / GZ** | Container → enthält n Folders + m Files |
+| **Folder** | Container → enthält n Folders + m Files |
+| **File** | Hat einen bestimmten Typ → wird vom passenden Extractor verarbeitet |
+
+**File-Typen und ihre Container-Eigenschaft:**
+
+| File-Typ | Container? | Struktur |
+|----------|-----------|----------|
+| PDF | Ja | Seiten als Kontext-Referenz, jede Seite enthält Content-Objekte |
+| DOCX | Ja | Abschnitte/Paragraphen als Kontext-Referenz (Seiten nur als Referenz, nicht als Container) |
+| PPTX | Ja | Slides als Kontext-Referenz, jeder Slide enthält Content-Objekte |
+| XLSX | Ja | Sheets als Kontext-Referenz, jedes Sheet enthält Content-Objekte |
+| PNG/JPG/GIF | Nein | File selbst ist ein einzelnes Content-Objekt (image) |
+| MP4/WebM | Nein | File selbst ist ein einzelnes Content-Objekt (videostream) |
+| MP3/WAV | Nein | File selbst ist ein einzelnes Content-Objekt (audiostream) |
+| TXT/CSV/JSON/XML | Nein | File selbst ist ein einzelnes Content-Objekt (text) |
+
+#### Logische Schicht: Content-Objekte
+
+Jeder Extractor liefert **skalare Content-Objekte** mit Kontext-Referenz zum Ursprung im File. Es gibt genau fünf Content-Typen:
+
+| ContentType | Beschreibung | Beispiel |
+|-------------|-------------|----------|
+| `text` | Textinhalt (Fliesstext, Tabelle als Markdown, Code) | Paragraph auf Seite 3, Tabelle in Sheet "Q4" |
+| `image` | Einzelbild (als Base64 oder Referenz) | Bild auf Seite 5 unten links, Caption "Übersicht" |
+| `videostream` | Video-Inhalt | Eingebettetes Video in PPTX Slide 7 |
+| `audiostream` | Audio-Inhalt | Voice-Memo Attachment |
+| `other` | Nicht klassifizierbar (Binary, Formeln, etc.) | Eingebettetes OLE-Objekt |
+
+**Content-Objekt Datenmodell:**
+
+```python
+class ContentObject(BaseModel):
+ id: str
+ fileId: str # FK zum physischen File
+ contentType: str # text, image, videostream, audiostream, other
+ data: str # Inhalt (Text, Base64, URL)
+ contextRef: ContentContextRef # Kontext-Referenz zum Ursprung
+ metadata: Dict[str, Any] = {} # Extractor-spezifische Metadaten
+ sequence: int = 0 # Reihenfolge innerhalb des Kontexts
+
+class ContentContextRef(BaseModel):
+ """Referenz zum Kontext im Container/File."""
+ containerPath: str # z.B. "archiv.zip/folder-a/report.pdf"
+ location: str # z.B. "page:5/region:bottomLeft"
+ label: Optional[str] = None # z.B. "Abbildung 3: Übersicht"
+ pageIndex: Optional[int] = None # Seiten-Nummer (wenn relevant)
+ sectionId: Optional[str] = None # Abschnitt/Heading (wenn relevant)
+ sheetName: Optional[str] = None # Sheet-Name (XLSX)
+ slideIndex: Optional[int] = None # Slide-Nummer (PPTX)
+```
+
+#### Extraktions-Pipeline (komplett ohne AI)
+
+```mermaid
+graph LR
+ Input["ZIP / Folder / File"]
+
+ subgraph phase1 ["Phase 1: Container auflösen"]
+ Unpack["Entpacken\nrekursiv"]
+ FileList["File-Liste\nmit Typen"]
+ end
+
+ subgraph phase2 ["Phase 2: Content extrahieren"]
+ Ext["Typ-Extractor\npro File"]
+ CO["Content-Objekte\nmit ContextRef"]
+ end
+
+ subgraph phase3 ["Phase 3: Indexierung"]
+ Idx["ContentIndex\npro File"]
+ KS["Knowledge Store"]
+ end
+
+ Input --> phase1
+ phase1 --> phase2
+ phase2 --> phase3
+```
+
+```python
+async def _extractFileContents(fileId: str, fileData: bytes, mimeType: str) -> FileContentIndex:
+ """Extrahiert alle Content-Objekte eines Files. Kein AI-Call."""
+ extractor = extractorRegistry.resolve(mimeType)
+ contentObjects = await extractor.extract(fileData)
+
+ contentIndex = FileContentIndex(
+ fileId=fileId,
+ totalObjects=len(contentObjects),
+ structure=_buildStructureMap(contentObjects),
+ objects=contentObjects
+ )
+
+ await knowledgeStore.upsertContentIndex(contentIndex)
+ return contentIndex
+```
+
+**Rekursive Container-Auflösung:**
+
+```python
+MAX_TOTAL_EXTRACTED_SIZE = 500 * 1024 * 1024 # 500 MB
+MAX_FILE_COUNT = 10000
+
+async def _resolveContainerRecursive(containerData, containerPath="", depth=0, maxDepth=5,
+ _state=None):
+ """Löst Container rekursiv auf bis zu den Files. Kein AI-Call.
+ Schutz vor ZIP-Bombs: maxDepth, maxTotalExtractedSize, maxFileCount, Symlink-Blockierung."""
+ if _state is None:
+ _state = {"totalSize": 0, "fileCount": 0}
+ allFiles = []
+ entries = _listEntries(containerData, blockSymlinks=True)
+
+ for entry in entries:
+ entryPath = f"{containerPath}/{entry.name}" if containerPath else entry.name
+
+ if entry.isContainer: # Folder oder verschachteltes ZIP
+ childData = _extractEntry(containerData, entry)
+ childFiles = await _resolveContainerRecursive(
+ childData, entryPath, depth + 1, maxDepth
+ )
+ allFiles.extend(childFiles)
+ else:
+ _state["totalSize"] += entry.size
+ _state["fileCount"] += 1
+ if _state["totalSize"] > MAX_TOTAL_EXTRACTED_SIZE:
+ raise ContainerLimitError(f"Total extracted size exceeds {MAX_TOTAL_EXTRACTED_SIZE}")
+ if _state["fileCount"] > MAX_FILE_COUNT:
+ raise ContainerLimitError(f"File count exceeds {MAX_FILE_COUNT}")
+ allFiles.append(FileEntry(
+ path=entryPath,
+ data=_extractEntry(containerData, entry),
+ mimeType=_detectMimeType(entry.name),
+ size=entry.size
+ ))
+
+ return allFiles
+```
+
+#### File Content Index (Metadaten pro File)
+
+Jedes File erhält einen `FileContentIndex` als Metadaten (kanonische Definition siehe Abschnitt 4.4). Zusätzlich enthält jeder Index `ContentObjectSummary`-Einträge:
+
+```python
+class ContentObjectSummary(BaseModel):
+ """Kompakte Beschreibung eines Content-Objekts im Index."""
+ id: str
+ contentType: str # text, image, videostream, audiostream, other
+ contextRef: ContentContextRef
+ charCount: Optional[int] = None # Nur für text
+ dimensions: Optional[str] = None # Nur für image/video (z.B. "1920x1080")
+ duration: Optional[float] = None # Nur für audio/video (Sekunden)
+```
+
+Beispiel für einen PDF-ContentIndex:
+```
+report.pdf → FileContentIndex:
+ totalObjects: 47
+ structure: {pages: 12, images: 8, tables: 5, textBlocks: 34}
+ objectSummary:
+ - {id: "co-1", type: "text", contextRef: {page: 1, location: "header"}, charCount: 245}
+ - {id: "co-2", type: "text", contextRef: {page: 1, location: "body"}, charCount: 1830}
+ - {id: "co-3", type: "image", contextRef: {page: 1, location: "bottom", label: "Abb. 1"}, dimensions: "800x600"}
+ - ...
+```
+
+#### AI arbeitet auf skalaren Content-Objekten
+
+Nach der Extraktion hat der Agent nur noch skalare Content-Objekte vor sich. AI-Calls werden **gezielt** auf einzelne Objekte angewendet:
+
+```
+Agent: "Analysiere Kapitel 3 des Reports"
+ 1. browseContainer("report.pdf") → ContentIndex (47 Objekte, Struktur)
+ 2. Agent sieht: Kapitel 3 = Seiten 5-8, 12 Content-Objekte
+ 3. readContentObjects(fileId, filter={pageIndex: [5,6,7,8]}) → 12 skalare Objekte
+ 4. AI-Call mit den 12 Objekten als Kontext → Analyse
+```
+
+#### Container-Tools für den Agent
+
+| Tool | Funktion |
+|------|----------|
+| `browseContainer` | Zeigt ContentIndex / Baumstruktur eines Containers (ZIP, Folder, PDF, etc.) |
+| `readContentObjects` | Liest spezifische Content-Objekte nach Filter (Seite, Typ, Section) |
+| `extractContainerItem` | Extrahiert on demand ein Element, das noch nicht im Knowledge Store ist |
+
+**Prompt-Referenzen:** Im Unified UI können Container direkt referenziert werden:
+- `@archiv.zip` -- Agent sieht Baumstruktur, extrahiert gezielt
+- `@projektordner/` -- Agent browst Ordnerinhalt
+- `@report.pdf` -- Agent sieht ContentIndex mit allen Content-Objekten
+
+---
+
+## 6. Document Tools
+
+Tools für den Agent -- arbeiten auf der physischen (Container) und logischen (Content-Objekte) Schicht:
+
+**Container-Tools (physische Schicht):**
+
+| Tool | Funktion | Intern |
+|------|----------|--------|
+| `browseContainer` | Baumstruktur + ContentIndex eines Containers anzeigen | Container-Auflösung + FileContentIndex |
+| `extractContainerItem` | Spezifisches File aus Container on demand extrahieren | Rekursive Pipeline → Content-Objekte → Knowledge Store |
+
+**Content-Tools (logische Schicht, skalare Objekte):**
+
+| Tool | Funktion | Intern |
+|------|----------|--------|
+| `readContentObjects` | Content-Objekte nach Filter lesen (Seite, Typ, Section) | Knowledge Store / on-demand Extraktion |
+| `summarizeContent` | AI-basierte Zusammenfassung von Content-Objekten | serviceAi.callAi() mit Content-Objekten als Input |
+| `analyzeContent` | AI-basierte Analyse mit Prompt über Content-Objekte | serviceAi.callAi() |
+| `generateDocument` | DOCX/PDF/CSV erzeugen | Bestehende Renderer |
+| `editDocument` | Bestehendes Dokument ändern | Neue Logik |
+
+---
+
+## 7. File Management
+
+### 7.1 FileItem erweitern
+
+```python
+class FileItem(BaseModel):
+ # bestehend:
+ id, mandateId, featureInstanceId, fileName, mimeType, fileHash, fileSize, creationDate
+
+ # NEU:
+ tags: List[str] = []
+ folderId: Optional[str] = None
+ description: Optional[str] = ""
+ status: str = "active" # active, archived, processing
+```
+
+### 7.2 FileFolder (neu)
+
+```python
+class FileFolder(BaseModel):
+ id: str
+ parentId: Optional[str] = None
+ name: str
+ featureInstanceId: str
+ mandateId: str
+ createdBy: str
+ autoRule: Optional[str] = None # z.B. "tag:financial"
+```
+
+### 7.3 File Tools
+
+| Tool | Funktion |
+|------|----------|
+| `listFiles` | Auflisten mit Filter (tags, folder, mimeType, pattern) |
+| `readFile` | Inhalt lesen |
+| `writeFile` | Erstellen oder überschreiben |
+| `moveFile` | In Ordner verschieben |
+| `tagFile` | Tags hinzufügen/entfernen |
+| `searchFiles` | Volltextsuche über Inhalte |
+
+### 7.4 File-Referenzen im Prompt
+
+- `@filename.pdf` -- Autocomplete-Referenz
+- Drag-and-Drop Upload -- automatisch als File gespeichert + indexiert
+- Agent erhält `fileIds` und arbeitet via Tools
+
+---
+
+## 8. Data Containers (externe Datenquellen)
+
+### 8.1 Konzept
+
+User kann externe Datenquellen aus UserConnections (OAuth: Microsoft, Google) im Chat referenzieren. Konfiguration pro FeatureInstance.
+
+### 8.2 DataSource (neu)
+
+```python
+class DataSource(BaseModel):
+ id: str
+ connectionId: str # FK zu UserConnection
+ sourceType: str # sharepointFolder, googleDriveFolder, outlookFolder, ftpFolder
+ path: str # z.B. "/sites/MySite/Documents/Reports"
+ label: str
+ featureInstanceId: Optional[str]
+ autoSync: bool = False
+ lastSynced: Optional[float]
+```
+
+### 8.3 Provider-Connector-Architektur (1:n)
+
+**Kernprinzip:** Ein Connector ist pro **Provider** (Lieferant), nicht pro Service. Eine MSFT-Connection bedient SharePoint, Outlook, Teams etc. gleichzeitig. Die Beziehung ist 1 Provider : n Services.
+
+**IST-Zustand:** SharePoint und Outlook haben je eigene Services, Connection-Helper und Method-Actions, obwohl beide über dieselbe MSFT-Connection laufen. Es gibt keine generische Abstraktion. Google Drive, FTP etc. sind nicht implementiert.
+
+**Lösung: ProviderConnector + ServiceAdapter**
+
+```mermaid
+graph TB
+ AgentLoop["Agent Loop"]
+ GenTool["Generische Connection-Tools"]
+
+ subgraph providers ["ProviderConnector (1 pro Lieferant)"]
+ MSFT["MsftConnector\n1 Connection → n Services"]
+ Google["GoogleConnector\n1 Connection → n Services"]
+ FtpProv["FtpConnector\n1 Connection → 1 Service"]
+ end
+
+ subgraph msftServices ["MSFT Services (über 1 Token)"]
+ SP["SharePoint\nFiles, Sites"]
+ OL["Outlook\nMail, Calendar"]
+ Teams["Teams\nMessages, Channels"]
+ OneDrive["OneDrive\nFiles"]
+ end
+
+ subgraph googleServices ["Google Services (über 1 Token)"]
+ GDrive["Google Drive\nFiles"]
+ GMail["Gmail\nMail"]
+ end
+
+ AgentLoop --> GenTool
+ GenTool --> providers
+ MSFT --> msftServices
+ Google --> googleServices
+```
+
+```python
+class ProviderConnector(ABC):
+ """Ein Connector pro Provider. Verwaltet eine UserConnection + Token.
+ Bietet Zugriff auf n Services des Providers."""
+
+ def __init__(self, connection: UserConnection, accessToken: str):
+ self.connection = connection
+ self.accessToken = accessToken
+
+ @abstractmethod
+ def getAvailableServices(self) -> List[str]:
+ """Welche Services bietet dieser Provider?"""
+
+ @abstractmethod
+ def getServiceAdapter(self, service: str) -> "ServiceAdapter":
+ """Gibt den ServiceAdapter für einen bestimmten Service zurück."""
+
+
+class ServiceAdapter(ABC):
+ """Standardisierte Operationen pro Service eines Providers."""
+
+ @abstractmethod
+ async def browse(self, path: str, filter: str = None) -> List[ExternalEntry]: ...
+
+ @abstractmethod
+ async def download(self, path: str) -> bytes: ...
+
+ @abstractmethod
+ async def upload(self, path: str, data: bytes, fileName: str) -> ExternalEntry: ...
+
+ @abstractmethod
+ async def search(self, query: str, path: str = None) -> List[ExternalEntry]: ...
+```
+
+**Konkrete Provider und ihre Services:**
+
+| ProviderConnector | Authority | Services | Status |
+|-------------------|-----------|----------|--------|
+| `MsftConnector` | MSFT | `sharepoint` (Files, Sites), `outlook` (Mail, Calendar), `teams` (Messages), `onedrive` (Files) | Migration bestehender Services |
+| `GoogleConnector` | GOOGLE | `drive` (Files), `gmail` (Mail) | Neu |
+| `FtpConnector` | LOCAL | `files` (FTP/SFTP) | Neu |
+| `JiraConnector` | LOCAL | `tickets` (Issues, Boards) | Migration bestehender Jira-Actions |
+
+**ConnectorResolver:**
+
+```python
+class ConnectorResolver:
+ _providerRegistry = {
+ "msft": MsftConnector,
+ "google": GoogleConnector,
+ "local:ftp": FtpConnector,
+ "local:jira": JiraConnector,
+ }
+
+ async def resolve(self, connectionId: str) -> ProviderConnector:
+ """Löst connectionId → Provider-Connector mit frischem Token auf."""
+ connection = await _getUserConnection(connectionId)
+ token = await _getFreshToken(connectionId)
+ providerClass = self._providerRegistry[connection.authority]
+ return providerClass(connection, token.tokenAccess)
+
+ async def resolveService(self, connectionId: str, service: str) -> ServiceAdapter:
+ """Löst connectionId + service → konkreten ServiceAdapter auf."""
+ provider = await self.resolve(connectionId)
+ return provider.getServiceAdapter(service)
+```
+
+**Beispiel: MsftConnector (1 Connection → n Services):**
+
+```python
+class MsftConnector(ProviderConnector):
+
+ def getAvailableServices(self) -> List[str]:
+ return ["sharepoint", "outlook", "teams", "onedrive"]
+
+ def getServiceAdapter(self, service: str) -> ServiceAdapter:
+ adapters = {
+ "sharepoint": SharepointAdapter(self.accessToken),
+ "outlook": OutlookAdapter(self.accessToken),
+ "teams": TeamsAdapter(self.accessToken),
+ "onedrive": OneDriveAdapter(self.accessToken),
+ }
+ return adapters[service]
+```
+
+### 8.4 Connection Tools (generisch)
+
+Ein **generisches Tool-Set** -- der Agent gibt `connectionId` + `service` an, der Rest läuft über den Provider:
+
+| Tool | Funktion | Intern |
+|------|----------|--------|
+| `listConnections` | Verfügbare Connections + ihre Services anzeigen | UserConnections + `getAvailableServices()` |
+| `externalBrowse` | Ordner/Files einer externen Quelle listen | `resolveService(connId, service).browse(path)` |
+| `externalDownload` | Datei herunterladen → lokales File + Auto-Index | `adapter.download(path)` → `saveUploadedFile` → Knowledge Store |
+| `externalUpload` | Lokales File in externe Quelle hochladen | `adapter.upload(path, data)` |
+| `externalSearch` | In externer Quelle suchen | `adapter.search(query)` |
+| `sendMail` | E-Mail senden (Mail-spezifische Parameter) | `resolveService(connId, "outlook").send()` |
+
+Die bestehenden `MethodSharepoint`- und `MethodOutlook`-Actions werden schrittweise hinter `MsftConnector` → `SharepointAdapter` / `OutlookAdapter` migriert. Während der Migration fungiert der `ActionToolAdapter` (siehe Abschnitt 10.3) als Brücke.
+
+### 8.5 Ablauf
+
+```
+User: "Analysiere die letzten 3 Reports aus SharePoint/Reports"
+
+Agent → externalBrowse(connectionId="msft-123", service="sharepoint", path="/Reports", filter="*.pdf")
+Agent → externalDownload(connectionId="msft-123", service="sharepoint", path="/Reports/Q4-Report.pdf")
+ → File wird lokal gespeichert + automatisch im Knowledge Store indexiert
+Agent → readContentObjects(fileId="abc123", filter={contentType: "text"})
+ → Bereits indexiert, Content-Objekte aus Knowledge Store
+Agent → Antwortet mit Analyse
+```
+
+Gleicher Provider, anderer Service:
+```
+User: "Sende die Analyse per Mail an das Team"
+
+Agent → sendMail(connectionId="msft-123", to=["team@firma.ch"], subject="Q4 Analyse", body="...")
+ → Selbe MSFT-Connection, Service "outlook"
+```
+
+Anderer Provider, identisches Tool-Interface:
+```
+User: "Lade das Budget aus meinem Google Drive"
+
+Agent → externalBrowse(connectionId="google-456", service="drive", path="/Finanzen", filter="Budget*")
+Agent → externalDownload(connectionId="google-456", service="drive", path="/Finanzen/Budget-2026.xlsx")
+ → Identischer Flow: lokales File + Knowledge Store
+```
+
+---
+
+## 9. Unified Workspace UI
+
+### 9.1 Kernidee
+
+Der Codeeditor hat die beste UI-Architektur (SSE Streaming, Agent-Progress, File-Management). Dieses UI wird zur Grundlage für alle AI-Interaktionen. Chatbot und Playground gehen darin auf.
+
+### 9.2 Layout
+
+```mermaid
+graph LR
+ subgraph unified["Unified AI Workspace"]
+ subgraph left["Linke Sidebar"]
+ ConvList["Conversation List"]
+ FileTree["File Browser\nFolders + Tags"]
+ DataSrc["Data Sources\nSharePoint, Drive"]
+ end
+
+ subgraph center["Hauptbereich"]
+ ChatStream["Chat / Streaming\nMarkdown + Code + Edits\nTabellen + Charts"]
+ end
+
+ subgraph right["Rechte Sidebar"]
+ FilePreview["File Preview / Editor"]
+ ToolActivity["Tool Activity Log"]
+ end
+
+ subgraph bottom["Input-Bereich"]
+ PromptInput["Text + Voice"]
+ AttachBar["@file + Upload + DataSources"]
+ ActionBar["Send / Stop / Voice"]
+ end
+ end
+```
+
+### 9.3 UI-Komponenten
+
+**Linke Sidebar:**
+- **Conversation List:** Alle Workflows/Chats, sortiert nach Datum
+- **File Browser:** Lokale Files mit Ordnerstruktur und Tags, Upload per Drag-and-Drop
+- **Data Sources:** Konfigurierte externe Quellen (SharePoint, Drive) pro FeatureInstance
+
+**Hauptbereich:**
+- **Chat Stream:** SSE-basiertes Streaming mit Token-Chunks, Markdown-Rendering, Code-Blöcke, File-Edit-Proposals, Tabellen, Charts
+
+**Rechte Sidebar (kontextabhängig):**
+- **File Preview / Editor:** Vorschau oder Bearbeitung ausgewählter Files
+- **Tool Activity Log:** Zeigt in Echtzeit, welche Tools der Agent nutzt
+
+**Input-Bereich:**
+- Text-Eingabe mit `@file`-Autocomplete
+- Voice-Toggle (Mikrofon-Button)
+- Attachment-Bar mit aktiven Files und DataSources
+- Send / Stop / Voice-Buttons
+
+### 9.4 SSE Event-Typen
+
+Bestehend (vom Codeeditor übernommen):
+
+| Event | Funktion |
+|-------|----------|
+| `message` | Text-Nachricht (user/assistant) |
+| `status` | Progress-Label |
+| `fileEditProposal` | Datei-Änderungsvorschlag |
+| `fileVersion` | Akzeptierte Änderung |
+| `agentProgress` | Round/Tool-Call Fortschritt |
+| `agentSummary` | Abschluss-Statistik |
+| `complete` / `stopped` / `error` | Workflow-Ende |
+
+Neu:
+
+| Event | Funktion |
+|-------|----------|
+| `chunk` | Token-Streaming für Echtzeit-Textausgabe |
+| `toolCall` | Agent ruft Tool auf (für Activity Log) |
+| `toolResult` | Tool-Ergebnis (für Activity Log) |
+| `fileCreated` | Neues File erstellt |
+| `dataSourceAccess` | Zugriff auf externe Quelle |
+| `voiceResponse` | TTS Audio-Daten |
+
+### 9.5 Voice Integration
+
+- **STT:** Browser Web Speech API oder Whisper API. Mikrofon-Button im Input-Bereich. Transkribierter Text wird als Prompt gesendet.
+- **TTS:** Agent-Antworten optional vorlesen. Nutzt bestehende `interfaceVoiceObjects` oder Browser TTS.
+- **Voice-Toggle:** Input via Mikrofon, Output via TTS.
+
+```
+POST /api/workspace/{instanceId}/voice/transcribe → { "text": "..." }
+POST /api/workspace/{instanceId}/voice/synthesize → audio blob
+```
+
+### 9.6 Prompt Input Request
+
+```python
+class WorkspaceInputRequest(BaseModel):
+ prompt: str
+ fileIds: List[str] = []
+ uploadedFiles: List[UploadFile] = []
+ dataSourceIds: List[str] = []
+ voiceMode: bool = False
+ workflowId: Optional[str] = None
+ userLanguage: str = "en"
+```
+
+---
+
+## 10. Feature Integration
+
+### 10.1 Unified Workspace ersetzt drei Features
+
+Chatbot, Codeeditor und Playground migrieren auf `serviceAgent` + Unified UI. Die Unterscheidung erfolgt über die Tool-Konfiguration pro FeatureInstance:
+
+| Feature (bisher) | Tool-Set |
+|-------------------|----------|
+| Chatbot (SQL) | `core` + `sqlQuery` + `webSearch` |
+| Chatbot (Tavily) | `core` + `webSearch` |
+| Codeeditor | `core` + `readFile` + `writeFile` + `applyFileEdit` + `searchFiles` |
+| Playground | `core` + `webSearch` + `sendMail` |
+| Trustee | `core` + `connectionTools` + `documentAnalysis` |
+
+`core` = readFile, writeFile, listFiles, extractContent, summarizeContent, generateDocument
+
+### 10.2 serviceAi als Low-Level-Layer
+
+`serviceAi` bleibt bestehen für:
+- Modell-Auswahl und Fallback
+- Billing pro Call
+- Provider-Routing (OpenAI, Anthropic, PrivateLLM)
+- Native function calling Support
+
+Die Orchestrierungs-Logik (Structure Filling, Looping, JSON Repair) wandert in den Agent.
+
+### 10.3 Method/Action-Integration (bestehende Workflow-Actions als Tools)
+
+**IST-Zustand:** Das Workflow-System hat **7 Methods mit 36 Actions**, registriert via `methodDiscovery.py`. Jede Action hat `actionId`, `description`, `parameters` (Schema) und `execute` (async). 22 davon haben `dynamicMode=True` und werden bereits dynamisch von AI ausgewählt. Die Ausführung läuft über `actionExecutor.executeAction(method, action, params)`.
+
+| Kategorie | Methods | Actions | dynamicMode |
+|-----------|---------|---------|-------------|
+| **AI** | ai | process, webResearch, summarizeDocument, translateDocument, convertDocument, generateDocument, generateCode | Ja |
+| **External** | sharepoint, outlook | 9 SharePoint + 4 Outlook Actions | Ja |
+| **Domain** | context, trustee | getDocumentIndex, extractContent, neutralizeData, extractFromFiles, processDocuments, syncToAccounting | Teilweise |
+| **Integration** | jira, chatbot | 8 Jira + 1 Chatbot Actions | Nein/Teilweise |
+
+**Lösung: 3-Stufen-Migration**
+
+```mermaid
+graph LR
+ subgraph stufe1 ["Stufe 1: Automatisches Wrapping"]
+ Actions["Bestehende Actions\n36 Actions"]
+ Adapter["ActionToolAdapter"]
+ ToolReg["Tool Registry"]
+ end
+
+ subgraph stufe2 ["Stufe 2: Refactoring"]
+ NativeTools["Native Agent Tools"]
+ ConnTools["ProviderConnector Tools"]
+ end
+
+ subgraph stufe3 ["Stufe 3: Abloesung"]
+ AgentNative["Agent-native Logik\nersetzt AI-Actions"]
+ end
+
+ Actions --> Adapter
+ Adapter --> ToolReg
+ ToolReg --> NativeTools
+ NativeTools --> ConnTools
+ ConnTools --> AgentNative
+```
+
+**Stufe 1 -- Automatisches Wrapping (sofort, kein Rewrite):**
+
+Der `ActionToolAdapter` wandelt jede Action mit `dynamicMode=True` automatisch in ein Agent-Tool um. Kein Code-Rewrite nötig:
+
+```python
+class ActionToolAdapter:
+ """Wrapped bestehende Workflow-Actions als Agent-Tools."""
+
+ def _buildToolsFromActions(self) -> List[ToolDefinition]:
+ tools = []
+ for methodName, method in methods.items():
+ for actionName, action in method["actions"].items():
+ if not action.get("dynamicMode"):
+ continue
+ tools.append(ToolDefinition(
+ name=f"{methodName}.{actionName}",
+ description=action["description"],
+ parameters=_convertParameterSchema(action["parameters"])
+ ))
+ return tools
+
+ async def dispatch(self, toolName: str, args: Dict) -> ToolResult:
+ methodName, actionName = toolName.split(".", 1)
+ result = await actionExecutor.executeAction(methodName, actionName, args)
+ return ToolResult(
+ success=result.success,
+ data=_formatActionResult(result),
+ error=result.error
+ )
+```
+
+Vorteil: Alle 22 dynamischen Actions sofort als Agent-Tools verfügbar.
+
+**Stufe 2 -- Schrittweises Refactoring:**
+- **External-Actions** (SharePoint, Outlook, Jira) → hinter `ProviderConnector` + `ServiceAdapter` migrieren → generische `externalBrowse/Download/Upload/Search` Tools (siehe Abschnitt 8.3)
+- **Domain-Actions** (Trustee, Context) → bleiben als spezialisierte Tools, werden direkt in der `toolRegistry` registriert statt via Adapter
+
+**Stufe 3 -- AI-Actions ablösen:**
+- `ai.process`, `ai.generateDocument`, `ai.generateCode` etc. werden NICHT als Tools exponiert
+- Diese Fähigkeiten sind im Agent selbst (der Agent IST die AI) -- er nutzt direkt `extractContent`, `generateDocument`, `readSection` etc.
+- `subStructureFilling`, `subStructureGeneration`, `subAiCallLooping` entfallen
+
+**Kein kompletter Rewrite nötig:** Die bestehenden Methods/Actions bleiben initial funktionsfähig (Chatplayground, Automation nutzen sie weiter). Der Agent nutzt sie via Adapter. Schrittweise Migration ohne Breaking Changes.
+
+---
+
+## 11. Background Workflows
+
+### 11.1 Persistente Workflow-Queue
+
+Background-Workflows dürfen nicht auf `asyncio.create_task()` basieren (Server-Restart = Tasks verloren). Stattdessen eine **DB-basierte Queue** auf bestehendem PostgreSQL:
+
+```python
+class BackgroundJob(BaseModel):
+ """Persistierter Background-Job in PostgreSQL."""
+ id: str
+ workflowId: str
+ userId: str
+ featureInstanceId: str
+ status: str # queued, running, completed, failed, cancelled
+ description: str
+ subtasks: List[Dict[str, Any]]
+ maxCostCHF: Optional[float] # Billing-Cap
+ maxConcurrency: int = 1
+ retryCount: int = 0
+ maxRetries: int = 3
+ createdAt: float
+ startedAt: Optional[float]
+ completedAt: Optional[float]
+ error: Optional[str]
+ progress: float = 0.0
+ resultSummary: Optional[str]
+```
+
+**Worker-Prozess:** Ein Background-Worker pollt die Queue und führt Jobs aus:
+
+```python
+async def _processBackgroundQueue():
+ while True:
+ job = await backgroundQueue.dequeueNext()
+ if not job:
+ await asyncio.sleep(2)
+ continue
+
+ try:
+ await backgroundQueue.updateStatus(job.id, "running")
+ async for event in runAgent(job.subtasks, job.workflowId, ...):
+ await backgroundQueue.updateProgress(job.id, event)
+
+ # Billing-Cap prüfen
+ if job.maxCostCHF and await billingService.getWorkflowCost(job.workflowId) > job.maxCostCHF:
+ await backgroundQueue.updateStatus(job.id, "cancelled", error="budgetExceeded")
+ await _notifyUser(job.userId, f"Background-Job gestoppt: Budget überschritten")
+ break
+
+ await backgroundQueue.updateStatus(job.id, "completed")
+ except Exception as e:
+ if job.retryCount < job.maxRetries:
+ await backgroundQueue.requeueWithRetry(job.id)
+ else:
+ await backgroundQueue.updateStatus(job.id, "failed", error=str(e))
+ await _notifyUser(job.userId, f"Background-Job fehlgeschlagen: {e}")
+```
+
+### 11.2 Tools
+
+```python
+# Agent-Tool: Background-Job starten
+async def startBackground(description, subtasks, maxCostCHF=None):
+ job = await backgroundQueue.enqueue(BackgroundJob(
+ workflowId=workflowId, description=description,
+ subtasks=subtasks, maxCostCHF=maxCostCHF, ...
+ ))
+ return f"Background-Job {job.id} gestartet."
+
+# Agent-Tool: Job-Status abfragen
+async def checkBackgroundStatus(jobId):
+ job = await backgroundQueue.getJob(jobId)
+ return {"status": job.status, "progress": job.progress, "result": job.resultSummary}
+```
+
+### 11.3 Komplexe Ausgaben
+
+Eine Agent-Antwort kann mehrere Ausgabe-Typen kombinieren:
+- Text (Markdown) mit Token-Streaming
+- Generierte/bearbeitete Files als Attachments
+- Gestartete Hintergrund-Jobs mit Progress-Tracking
+- Tabellen, Charts
+- Voice-Ausgabe
+- Externe Aktionen (Mail, SharePoint-Upload)
+
+---
+
+## 12. Observability und Monitoring
+
+### 12.1 Agent-Tracing
+
+Jeder Agent-Workflow wird vollständig protokolliert. Ein `AgentTrace` aggregiert alle Rounds, Tool-Calls und Kosten:
+
+```python
+class AgentTrace(BaseModel):
+ """Vollständiges Protokoll eines Agent-Workflows."""
+ workflowId: str
+ userId: str
+ featureInstanceId: str
+ startedAt: float
+ completedAt: Optional[float]
+ status: str # running, completed, maxRoundsReached, budgetExceeded, error
+ totalRounds: int
+ totalToolCalls: int
+ totalCostCHF: float
+ abortReason: Optional[str]
+ rounds: List[AgentRoundLog]
+
+class AgentRoundLog(BaseModel):
+ """Log eines einzelnen Agent-Rounds."""
+ roundNumber: int
+ aiModel: str # Welches Modell wurde genutzt
+ inputTokens: int
+ outputTokens: int
+ costCHF: float
+ toolCalls: List[ToolCallLog] # Welche Tools mit welchen Args/Results
+ durationMs: int
+
+class ToolCallLog(BaseModel):
+ """Log eines einzelnen Tool-Calls."""
+ toolName: str
+ args: Dict[str, Any]
+ success: bool
+ durationMs: int
+ error: Optional[str]
+```
+
+### 12.2 Kosten-Transparenz
+
+Die bestehende `BillingTransaction` erfasst einzelne AI-Calls. Zusätzlich wird pro Workflow eine **Kosten-Aggregation** geführt:
+
+| Posten | Quelle | Tracking |
+|--------|--------|----------|
+| Agent-Rounds (AI-Calls) | `serviceAi.callAi()` → `billingCallback` | Automatisch via bestehende Billing-Integration |
+| Entity Cache (Opt-in) | `_extractEntities()` → `serviceAi.callAi()` | Eigener `description`-Typ in BillingTransaction |
+| Progressive Summaries | Summary-Calls → `serviceAi.callAi()` | Eigener `description`-Typ in BillingTransaction |
+| Embeddings | `embeddingService.embed()` | Neuer Billing-Posten |
+
+### 12.3 Monitoring
+
+Metriken pro FeatureInstance / Mandate:
+- Agent-Workflows pro Zeitraum (Anzahl, Durchschnittsdauer, Kosten)
+- Tool-Nutzung (welche Tools, Erfolgsrate, Durchschnittsdauer)
+- Budget-Auslastung (Kosten vs. maxCostCHF)
+- Knowledge Store (Anzahl Files, Chunks, Embedding-Grösse)
+- Abbruchgründe (maxRounds, Budget, Fehler)
+
+---
+
+## 13. Umsetzungsplan
+
+```mermaid
+graph LR
+ P1["Phase 1\nAgent Core\n+ ActionToolAdapter"]
+ P2["Phase 2\nKnowledge Store"]
+ P3["Phase 3\nSmart Documents\n+ Container Handling"]
+ P4["Phase 4\nDocument Tools"]
+ P5["Phase 5\nFile Management"]
+ P6["Phase 6\nData Containers\n+ ProviderConnectors"]
+ P7["Phase 7\nUnified UI"]
+ P8["Phase 8\nFeature Migration\n+ Action Refactoring"]
+ P9["Phase 9\nBackground Workflows"]
+
+ P1 --> P2
+ P1 --> P5
+ P2 --> P3
+ P3 --> P4
+ P5 --> P6
+ P4 --> P8
+ P6 --> P8
+ P7 --> P8
+ P8 --> P9
+```
+
+| Phase | Inhalt | Abhängigkeit |
+|-------|--------|-------------|
+| **1** | Agent Core: Loop, ToolRegistry, ConversationManager, AiCallRequest erweitern. **ActionToolAdapter**: bestehende 22 dynamicMode-Actions automatisch als Agent-Tools wrappen. serviceAgent als IMPORTABLE_SERVICE registrieren | Grundlage |
+| **2** | Knowledge Store: pgvector, 3-Ebenen, Auto-Index bei Upload, RAG-Retrieval | Phase 1 |
+| **3** | Smart Documents: Pre-Scan, On-Demand Extraction, Progressive Summarization. **Container Handling**: ContainerExtractor (ZIP/TAR), FolderExtractor, rekursive Extraktion, Lazy Loading | Phase 2 |
+| **4** | Document Tools: extractContent, readSection, browseContainer, extractContainerItem mit Knowledge-Store-Integration | Phase 3 |
+| **5** | File Management: Tags, Folders, File Tools. Container-Referenzen im Prompt (@archiv.zip, @ordner/) | Phase 1 (parallel zu 2-4) |
+| **6** | Data Containers: DataSource, **ProviderConnector-Architektur** (1 Provider : n Services), ConnectorResolver, generische externalBrowse/Download/Upload/Search Tools. MsftConnector, GoogleConnector, FtpConnector | Phase 5 |
+| **7** | Unified UI: Codeeditor-Basis, Streaming, Voice, Panels | Parallel zu 2-6 |
+| **8** | Feature Integration: Chatbot + Codeeditor + Playground migrieren. **Action Refactoring**: External-Actions → ProviderConnector + ServiceAdapter, Domain-Actions → native Tools, AI-Actions entfallen (Agent ist die AI) | Phase 4 + 6 + 7 |
+| **9** | Background Workflows: DB-basierte Queue (BackgroundJob), Worker-Prozess, Retry, Billing-Cap, Observability (AgentTrace, Monitoring) | Phase 8 |
+
+---
+
+## 14. Was bleibt, was geht
+
+| Komponente | Status |
+|------------|--------|
+| `serviceAi.callAi()` + Modell-Fallback + Billing | Bleibt (Low-Level) |
+| `serviceExtraction.extractContent()` | Bleibt + erweitert (Pre-Scan, On-Demand, Container Handling) |
+| `aicoreModelSelector` + `aicoreModelRegistry` | Bleibt |
+| `serviceStreaming` / EventManager | Bleibt (zentral für UI) |
+| UserConnections + OAuth | Bleibt + erweitert (DataSource, ProviderConnector 1:n) |
+| `interfaceVoiceObjects` (STT/TTS) | Bleibt (Unified UI) |
+| SharePoint Graph API Service | Bleibt → wird zu `SharepointAdapter` hinter `MsftConnector` |
+| PostgreSQL | Bleibt + erweitert (pgvector, FileContentIndex, ContentChunk, WorkflowMemory) |
+| Workflow Methods/Actions (36 Actions) | Phase 1: ActionToolAdapter-Wrapping → Phase 8: schrittweise Migration |
+| `MethodSharepoint` (9 Actions) | Migriert → `MsftConnector` → `SharepointAdapter` + generische Connection Tools |
+| `MethodOutlook` (4 Actions) | Migriert → `MsftConnector` → `OutlookAdapter` + generische Connection Tools |
+| `MethodAi` (7 Actions: process, generate, etc.) | Abgelöst (Agent ist die AI, nutzt Document Tools direkt) |
+| `MethodJira` (8 Actions) | Migriert → `JiraConnector` + `JiraAdapter` |
+| `MethodTrustee`, `MethodContext` | Bleiben als spezialisierte Agent-Tools |
+| `actionExecutor` + `methodDiscovery` | Bleibt während Migration (Adapter), später optional |
+| `subStructureFilling` (Pipeline) | Abgelöst (Agent + Document Tools + Knowledge Store) |
+| `subStructureGeneration` | Abgelöst |
+| `subAiCallLooping` (JSON Repair) | Abgelöst (Agent iteriert natürlich) |
+| `trim_messages(strategy="last")` | Ersetzt durch Progressive Summarization |
+| Codeeditor `codeEditorProcessor` | Migriert zu serviceAgent |
+| Codeeditor UI (SSE, Events) | Basis für Unified UI |
+| Chatbot LangGraph + DatabaseCheckpointer | Abgelöst (serviceAgent + Knowledge Store) |
+| Chatplayground | Abgelöst (Unified UI) |
+| `datamodelFiles.py` FileItem | Erweitert (tags, folders, description) |
+| `BinaryExtractor` (ZIP Fallback) | Ersetzt durch `ContainerExtractor` (rekursive Extraktion) |
diff --git a/concepts/AI-Agent-Architecture-Review.md b/concepts/AI-Agent-Architecture-Review.md
new file mode 100644
index 0000000..2979766
--- /dev/null
+++ b/concepts/AI-Agent-Architecture-Review.md
@@ -0,0 +1,146 @@
+# Review: AI Agent Architecture & Unified Workspace
+
+> Konzept-Review | Version 2.0 | März 2026
+> Bezugsdokument: AI-Agent-Architecture-Konzept.md v2.0
+
+---
+
+## 1. Gesamturteil
+
+Version 2.0 des Konzepts hat **die Mehrheit der kritischen Punkte aus dem ersten Review adressiert**. Die grössten Lücken (Fehlerbehandlung, Kosten-Budget, RBAC-Klarstellung, ServiceCenter-Integration, Background Workflows, Observability) sind nun beschrieben. Das Konzept ist von einer Richtungsvorgabe zu einem **tragfähigen Architektur-Dokument** gereift.
+
+| Dimension | v1.3 | v2.0 | Kommentar |
+|---|---|---|---|
+| Tauglichkeit | Gut | **Gut** | Unverändert stark |
+| Durchdachtheit | Teilweise | **Gut** | Fehlerbehandlung, Kosten-Kontrolle, Observability ergänzt |
+| Logische Korrektheit | Mehrheitlich | **Korrekt** | Zahlen korrigiert, Datenmodell-Duplikat aufgelöst |
+| Strukturelle Nachvollziehbarkeit | Gut | **Gut** | ServiceCenter-Registry explizit, RBAC-Design-Entscheidung dokumentiert |
+| Wartbarkeit | Unklar | **Teilweise** | Observability ergänzt, Test-Strategie fehlt weiterhin |
+| Praktikabilität | Riskant | **Machbar** | Background Workflows nun DB-basiert, Budget-Cap vorhanden |
+
+---
+
+## 2. Adressierte Punkte aus dem ersten Review
+
+### 2.1 Vollständig adressiert
+
+| Punkt | Wo in v2.0 | Bewertung |
+|---|---|---|
+| **Faktische Zahlen** (7 Methods, 36 Actions, 22 dynamicMode) | Abschnitt 10.3 | Korrekt |
+| **FileContentIndex Doppeldefinition** | Abschnitt 5.4 verweist auf 4.4 als kanonisch | Gelöst |
+| **ServiceCenter-Registry** (module, class, dependencies, objectKey) | Abschnitt 3.1, expliziter Registry-Eintrag | Sauber, Dependencies `["ai", "chat", "extraction", "billing", "streaming"]` korrekt |
+| **RBAC Design-Entscheidung** | Abschnitt 3.5, Tabelle mit darunterliegender Absicherung pro Tool | Korrekt: keine Tool-Level-RBAC, Daten-/Service-Ebene reicht |
+| **Knowledge Store Shared Layer RBAC** | Abschnitt 3.5: `isShared=True` erfordert Access-Level `ALL`, neue Tabellen brauchen `TABLE_NAMESPACE` | Klar definiert |
+| **maxCostCHF Budget-Cap** | Abschnitt 3.1 (`AgentConfig`), 3.2 (Loop-Prüfung) | Korrekt integriert, nutzt `getWorkflowCost()` |
+| **Entity Cache Opt-in** | Abschnitt 3.1 (`entityCacheEnabled: bool = False`), 4.8 (Opt-in + Regex/NER-Alternative) | Sauber gelöst |
+| **Progressive Summarization Kosten** | Abschnitt 4.7: günstigeres Modell, fliesst ins Budget ein, eigener Typ in BillingTransaction | Transparent |
+| **Fehlerbehandlung** | Abschnitt 3.4: Tabelle mit 7 Fehlertypen + Strategien | Vollständig |
+| **Input-Validierung** | Abschnitt 3.4: Path-Traversal, SQL-Injection, Container-Limits | Konkret |
+| **Parallele Tool-Ausführung** | Abschnitt 3.2 (`_executeToolCalls`), 3.3, 3.6 (readOnly-Flag) | Korrekt: readOnly parallel, write sequential |
+| **ZIP-Bomb-Schutz** | Abschnitt 5.4: `MAX_TOTAL_EXTRACTED_SIZE=500MB`, `MAX_FILE_COUNT=10000`, Symlink-Blockierung | Konkrete Limits definiert |
+| **Background Workflows** | Abschnitt 11: DB-basierte Queue (`BackgroundJob`), Worker-Prozess, Retry, Billing-Cap, User-Benachrichtigung | Von "schwächster Teil" zu solide aufgewertet |
+| **Observability** | Neuer Abschnitt 12: `AgentTrace`, `AgentRoundLog`, `ToolCallLog`, Kosten-Transparenz, Monitoring-Metriken | Umfassend |
+| **AiCallRequest Abwärtskompatibilität** | Abschnitt 3.3: "bestehende `prompt`/`context`/`contentParts` bleiben" | Explizit erwähnt |
+| **Graceful Degradation bei maxRounds** | Abschnitt 3.2: Status `maxRoundsReached` + `_summarizeProgress()` | Sauber implementiert |
+
+### 2.2 Teilweise adressiert
+
+| Punkt | Status | Was fehlt |
+|---|---|---|
+| **Background Workflow Concurrency** | `maxConcurrency` als Feld auf `BackgroundJob` definiert (Abschnitt 11.1) | Worker-Code erzwingt das Limit nicht -- die Queue-Logik braucht einen Semaphore oder Worker-Pool-Begrenzung |
+| **Kosten-Transparenz im UI** | Billing-Posten sind definiert (Abschnitt 12.2), Monitoring-Metriken beschrieben (12.3) | Kein **UI-Mockup** für die Kosten-Anzeige im Workspace. Kein Kosten-Voranschlag vor Agent-Start |
+
+### 2.3 Noch offen
+
+| Punkt | Priorität | Kommentar |
+|---|---|---|
+| **Test-Strategie** | Mittel | Kein Wort zu Unit-Tests, Integration-Tests oder Evaluation-Frameworks. Agent-Systeme sind nicht-deterministisch -- wie wird Qualität sichergestellt? Empfehlung: deterministische Tool-Mock-Tests, Prompt-Regression-Suite, End-to-End-Tests mit fixierten LLM-Responses |
+| **LangGraph-Migration** | Niedrig | Chatbot mit LangGraph-Graph (`planner` → conditional routing → `agent_sql_plan` / `agent_tavily` / `agent_answer`) und custom `DatabaseCheckpointer`. Konzept sagt "Abgelöst" (Abschnitt 14), aber: Migration bestehender Conversations? Wie bildet der neue Agent den Graph-Router ab? LangGraph-Features (interrupt/resume, state snapshots) -- Ersatz? |
+| **Zeitplanung** | Mittel | 9 Phasen weiterhin ohne Aufwandschätzung oder Meilensteine |
+| **End-to-End-Sequenzdiagramm** | Niedrig | User → SSE → Agent → Tool → Billing → Response als Mermaid Sequence Diagram wäre wertvoll für Implementierer |
+| **Rollback-Kriterien** | Niedrig | Was passiert, wenn eine Phase scheitert? |
+| **Embedding-Re-Indexierung** | Niedrig | Strategie bei Modellwechsel (alle Chunks neu embedden) nicht beschrieben |
+
+---
+
+## 3. Neue Inhalte in v2.0 -- Qualitätsprüfung
+
+### 3.1 AgentConfig (Abschnitt 3.1) -- Gut
+
+```python
+class AgentConfig(BaseModel):
+ maxRounds: int = 25
+ maxCostCHF: Optional[float] = None
+ entityCacheEnabled: bool = False
+ toolSet: str = "core"
+ operationType: str = "AGENT"
+```
+
+Sinnvolle Defaults. `maxCostCHF = None` bedeutet kein Limit als Default -- das ist riskant für unbeaufsichtigte Workflows. Empfehlung: **Default-Budget pro FeatureInstance** konfigurierbar machen, damit Admins ein Sicherheitsnetz haben.
+
+### 3.2 Fehlerbehandlung (Abschnitt 3.4) -- Gut
+
+Die Fehler-Taxonomie mit 7 Typen und klaren Strategien ist solide. Besonders gut:
+- Tool-Fehler → Agent entscheidet (nicht blindes Retry)
+- Knowledge Store nicht verfügbar → degraded mode (kein Hard-Fail)
+- Externe APIs → Exponential Backoff (max 3)
+
+Ein Punkt fehlt: **Poison Messages**. Wenn ein bestimmter Prompt den Agent reliabel in eine Endlosschleife treibt (z.B. Tool A schlägt fehl → Agent ruft Tool A erneut auf → Endlosschleife), wird das nur durch `maxRounds` abgefangen. Ein spezifischer Schutz (z.B. max 3 identische Tool-Calls hintereinander → Abbruch) wäre robuster.
+
+### 3.3 RBAC-Abschnitt (3.5) -- Korrekt
+
+Die Tabelle mit der darunterliegenden Absicherung pro Tool-Kategorie ist präzise und deckt sich mit der tatsächlichen Codebasis. Die Design-Entscheidung "keine Tool-Level-RBAC" ist korrekt begründet und architektonisch konsistent.
+
+Kleiner Hinweis: Die Formulierung "Promote in Shared Layer (`isShared=True`) erfordert Access-Level `ALL`" ist sinnvoll. Es sollte dokumentiert werden, **auf welchem ObjectKey** dieses Access-Level geprüft wird (vermutlich `data.knowledge.FileContentIndex` oder ähnlich).
+
+### 3.4 Background Workflows (Abschnitt 11) -- Deutlich verbessert
+
+Von `asyncio.create_task()` zu DB-basierter Queue mit `BackgroundJob`-Model, Worker-Prozess, Retry und Billing-Cap. Das ist ein grosser Sprung.
+
+Offene Punkte:
+- **Worker-Lifecycle**: Wer startet den Worker? Ist er Teil des FastAPI-Lifespan (wie der bestehende `eventManager`)? Oder ein separater Prozess?
+- **Concurrency**: `maxConcurrency` auf dem Job-Model, aber der Worker pollt sequentiell (`dequeueNext` → process → next). Mehrere Worker-Tasks parallel zu starten fehlt.
+- **Dead-Letter-Queue**: Nach `maxRetries` wird der Job als `failed` markiert und der User benachrichtigt. Aber gibt es eine Möglichkeit, fehlgeschlagene Jobs zu inspizieren und manuell erneut zu starten?
+
+### 3.5 Observability (Abschnitt 12) -- Gut
+
+`AgentTrace` → `AgentRoundLog` → `ToolCallLog` ist eine saubere Hierarchie. Die Kosten-Transparenz-Tabelle (Abschnitt 12.2) mit eigenen `description`-Typen pro Billing-Posten ist praktisch.
+
+Die Monitoring-Metriken (12.3) decken die wichtigsten Dimensionen ab. Was fehlt: **Alerting**. Bei welchen Schwellwerten wird ein Admin benachrichtigt? (z.B. >50% Abbrüche wegen Budget, Knowledge-Store-Fehlerrate >5%, etc.)
+
+---
+
+## 4. Verbleibende Empfehlungen
+
+| Bereich | Empfehlung | Priorität |
+|---|---|---|
+| **Test-Strategie** | Neuer Abschnitt: deterministische Tool-Tests (Mock-LLM), Prompt-Regression, E2E-Tests | Mittel |
+| **Default-Budget** | `maxCostCHF` Default pro FeatureInstance konfigurierbar (Admin-Sicherheitsnetz) | Mittel |
+| **Poison-Message-Schutz** | Max N identische Tool-Calls hintereinander → Abbruch | Niedrig |
+| **Background Worker Lifecycle** | Klären: Teil von FastAPI-Lifespan oder separater Prozess? Concurrency-Enforcement | Mittel |
+| **LangGraph-Migrationsplan** | Detailkonzept für Phase 8: History-Migration, Router-Abbildung | Niedrig |
+| **Zeitplanung** | Aufwandschätzung pro Phase, Meilensteine | Mittel |
+| **Shared-Layer ObjectKey** | Dokumentieren, auf welchem DATA-ObjectKey der `ALL`-Check für `isShared=True` läuft | Niedrig |
+
+---
+
+## 5. Fazit
+
+Version 2.0 ist ein **umsetzungsreifes Architektur-Dokument**. Die kritischen Lücken der Vorgängerversion sind geschlossen:
+
+| Thema | v1.3 | v2.0 |
+|---|---|---|
+| Fehlerbehandlung | Fehlte komplett | 7 Fehlertypen mit Strategien |
+| Kosten-Kontrolle | Kein Budget-Cap | `maxCostCHF`, Entity-Cache Opt-in, Summary-Kosten transparent |
+| RBAC | Nicht erwähnt | Explizite Design-Entscheidung: keine Tool-RBAC, Daten-/Service-Ebene reicht |
+| ServiceCenter | Nicht beschrieben | Vollständiger Registry-Eintrag mit Dependencies |
+| Background Workflows | `asyncio.create_task()` | DB-basierte Queue, Retry, Billing-Cap |
+| Observability | Nicht erwähnt | AgentTrace, RoundLog, ToolCallLog, Monitoring-Metriken |
+| Parallele Tools | Blinde `asyncio.gather()` | readOnly-Flag, sequential default |
+| ZIP-Sicherheit | `maxDepth=5` ohne Limits | 500MB, 10000 Files, Symlink-Block |
+| Zahlen | 8/39/25 (falsch) | 7/36/22 (korrekt) |
+| FileContentIndex | Doppelt definiert | Kanonisch in 4.4, Referenz in 5.4 |
+
+Die **einzige relevante offene Lücke** ist die fehlende **Test-Strategie**. Für ein AI-Agent-System, das nicht-deterministisch arbeitet, ist das ein wichtiger Aspekt, der vor Phase 1 geklärt werden sollte -- mindestens als Grundsatz-Entscheidung (wie werden Agent-Workflows getestet?).
+
+Die Phasen 1-6 sind umsetzbar. Phase 7 (Unified UI) bleibt ein eigenständiges Grossprojekt. Phase 8 (LangGraph-Migration) braucht ein Detail-Konzept. Phase 9 ist nun solide fundiert.