fix: connect 10 unconnected backend modules to real code paths (context_builder→agent_runner, agent_permissions→agent_runner, agent_tools→agent_runner, data_policy→agent_runner, oversight→agent_runner+migration 0128, transparency→agent_runner, agent_stream→agent_routes SSE endpoint, agent_memory AI-module deleted, decision_guard→engine, require_approval→agent_runner), 11 integration tests passing
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
This commit is contained in:
+674
@@ -0,0 +1,674 @@
|
|||||||
|
# LeoCRM — System-Audit
|
||||||
|
|
||||||
|
**Erstellt:** 2026-08-19
|
||||||
|
**Methode:** grep/import-Analyse, DB-Inspektion, Datei-Scan
|
||||||
|
**Keine Spekulation — nur Fakten**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. BACKEND AI MODULE (app/ai/*.py)
|
||||||
|
|
||||||
|
### Verbindungs-Status pro Modul
|
||||||
|
|
||||||
|
| Modul | Extern importiert von | Status | Begründung |
|
||||||
|
|-------|----------------------|--------|------------|
|
||||||
|
| `agent_loop.py` | `app.ai.agent_stream`, `app.plugins.builtins.automation.agent_runner` | ✅ Verbunden | ReAct-Loop wird von Automation-Plugin aufgerufen. Importiert selbst `llm_client`, `error_codes`, `hooks`, `audit`, `approval`. Hat `require_approval` Parameter. |
|
||||||
|
| `llm_client.py` | `agent_loop`, `data_policy` (intern), `ai_assistant.services`, `ai_assistant.participant_handler`, `ai_proactive.jobs`, `ai_proactive.services`, `ai_proactive.context_tools`, `unified_search.embedding`, `unified_search.query_understanding`, `services.ai_copilot_service` | ✅ Verbunden | Zentraler LLM-Client, von 5+ Plugins und Services genutzt. |
|
||||||
|
| `ai_use_case.py` | `data_policy` (intern), `automation.agent_routes` | ✅ Verbunden | AIUseCaseMetadata wird in Automation-Plugin validiert. |
|
||||||
|
| `skill_registry.py` | `agent_tools` (intern), `agent_permissions` (intern) | ⚠️ Nur intern | Nur innerhalb `app/ai/` genutzt. Kein Plugin oder Route importiert es direkt. |
|
||||||
|
| `action_mapper.py` | `llm_client` (intern, lazy import line 749) | ⚠️ Nur intern | Wird nur von `llm_client.py` per Lazy-Import aufgerufen. |
|
||||||
|
| `agent_permissions.py` | — | ❌ Unverbunden | Kein externer Import. Definiert Permission-Checks für Agent-Tools, aber niemand ruft es auf. |
|
||||||
|
| `agent_tools.py` | — | ❌ Unverbunden | Kein externer Import. Definiert Tool-Execution-Logik, wird von keiner Route/Plugin aufgerufen. |
|
||||||
|
| `data_policy.py` | — | ❌ Unverbunden | Kein externer Import. Definiert Data-Policy-Checks (PII-Sanitization, Provider-Compliance), wird nicht aufgerufen. |
|
||||||
|
| `transparency.py` | — | ❌ Unverbunden | Kein externer Import. `mark_as_ai_generated()` wird von niemandem aufgerufen. |
|
||||||
|
| `oversight.py` | — | ❌ Unverbunden | Definiert `DecisionRecordDB` Model (`__tablename__ = "ai_decision_records"`), aber Tabelle existiert NICHT in Test-DB. Kein Import, keine Migration. |
|
||||||
|
| `agent_memory.py` | — | ❌ Unverbunden | `app/ai/agent_memory.py` wird von niemandem importiert. Es gibt ein separates Plugin `app/plugins/builtins/agent_memory/` mit eigenen Models — das ist verbunden, aber das AI-Modul ist verwaist. |
|
||||||
|
| `agent_stream.py` | — | ❌ Unverbunden | Importiert `agent_loop` intern, aber niemand importiert `agent_stream` extern. Streaming-Funktionality ungenutzt. |
|
||||||
|
| `context_builder.py` | — | ❌ Unverbunden | Importiert `sensitive_data`, `tenant`, `user`, `ai_assistant.contracts` intern, aber niemand importiert `context_builder` extern. `build_agent_context()` wird nie aufgerufen. |
|
||||||
|
|
||||||
|
### Zusammenfassung AI Module
|
||||||
|
- **Verbunden (extern):** 3 von 13 (`agent_loop`, `llm_client`, `ai_use_case`)
|
||||||
|
- **Nur intern verbunden:** 2 (`skill_registry`, `action_mapper`)
|
||||||
|
- **Komplett unverbunden:** 8 (`agent_permissions`, `agent_tools`, `data_policy`, `transparency`, `oversight`, `agent_memory`, `agent_stream`, `context_builder`)
|
||||||
|
|
||||||
|
### Was verbunden werden muss
|
||||||
|
1. `context_builder.py` → Sollte von `agent_loop.py` oder `automation/agent_runner.py` aufgerufen werden, um Agent-Context (User, Tenant, Memory) aufzubauen
|
||||||
|
2. `agent_tools.py` → Sollte von `agent_loop.py` aufgerufen werden für Tool-Execution (aktuell nur `ToolRegistry` aus ai_assistant wird genutzt)
|
||||||
|
3. `agent_permissions.py` → Sollte von `agent_tools.py` oder `agent_loop.py` aufgerufen werden für Permission-Checks vor Tool-Execution
|
||||||
|
4. `data_policy.py` → Sollte vor LLM-Calls aufgerufen werden (PII-Sanitization, Provider-Compliance)
|
||||||
|
5. `oversight.py` → Braucht Migration + Integration in Agent-Loop für Decision-Records
|
||||||
|
6. `transparency.py` → Sollte bei AI-generierten Inhalten aufgerufen werden
|
||||||
|
7. `agent_stream.py` → Sollte von Routes/Plugins für Streaming-Antworten genutzt werden
|
||||||
|
8. `agent_memory.py` (AI-Modul) → Sollte mit `agent_memory` Plugin integriert werden oder gelöscht werden (Duplikat)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. WORKFLOW MODULE (app/workflows/*.py)
|
||||||
|
|
||||||
|
| Modul | Verbunden mit | Status | Details |
|
||||||
|
|-------|---------------|--------|---------|
|
||||||
|
| `engine.py` | `app.routes.workflows` (line 308), `app.core.event_bus` (line 94) | ✅ Verbunden | `WorkflowEngine` wird von Routes und Event-Bus importiert. Verarbeitet Steps sequenziell. |
|
||||||
|
| `step_handlers.py` | `engine.py` (line 33: `from app.workflows.step_handlers import StepResult, get_step_handler`) | ✅ Verbunden | 10 Step-Typen registriert via `@register_step_type`: `wait`, `http`, `mail`, `calendar`, `dms`, `search`, `agent`, `crm`, `event`, `webhook`. `get_step_handler()` wird in Engine aufgerufen. |
|
||||||
|
| `decision_guard.py` | — | ❌ Unverbunden | **NICHT importiert von engine.py oder irgendeinem anderen Modul.** Definiert `requires_human_review()` und `check_action_guard()` aber niemand ruft diese auf. Konzeptionell für G-HUMAN-DEC gedacht, aber nicht integriert. |
|
||||||
|
|
||||||
|
### Engine Step-Verarbeitung
|
||||||
|
- **Approval steps:** Engine erkennt `step_type == "approval"`, setzt `instance.resume_reason = "approval"`, pausiert. `resume()` methode setzt fort nach Approval-Entscheidung.
|
||||||
|
- **Registered handlers:** 10 Typen (wait, http, mail, calendar, dms, search, agent, crm, event, webhook)
|
||||||
|
- **Legacy handlers:** 3 Typen (action, notification, condition) — direkt in Engine implementiert
|
||||||
|
- **Step-Typen gesamt:** 13 (10 registered + 3 legacy)
|
||||||
|
|
||||||
|
### Was verbunden werden muss
|
||||||
|
1. `decision_guard.py` → Muss in `engine.py` vor der Ausführung von High-Risk-Actions aufgerufen werden. Aktuell existiert der Guard, wird aber nie aufgerufen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. PLUGIN SYSTEM (app/plugins/builtins/)
|
||||||
|
|
||||||
|
### Plugins mit eigenen Routes
|
||||||
|
|
||||||
|
| Plugin | Routes-Datei | Router-Prefix | Registriert via |
|
||||||
|
|--------|-------------|---------------|-----------------|
|
||||||
|
| `agent_memory` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `ai_assistant` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `ai_proactive` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `ai_ui_control` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `automation` | routes.py, skill_routes.py, agent_routes.py | `/api/v1/automation`, `/api/v1/skills`, `/api/v1/agents` | Plugin-Manifest (3 Router) |
|
||||||
|
| `calendar` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `dms` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `entity_links` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `forgejo_error_reporter` | routes.py | `/api/v1/forgejo-error-reporter` | Plugin-Manifest |
|
||||||
|
| `graph_rag` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `kommunikation` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `mail` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `marketplace` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `mcp_client` | routes.py | `/api/v1/mcp-client` | Plugin-Manifest |
|
||||||
|
| `mcp_server` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `permissions` | routes.py, public_routes.py | — | Plugin-Manifest |
|
||||||
|
| `report_generator` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `tags` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `tasks` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `unified_search` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `wiki` | routes.py | — | Plugin-Manifest |
|
||||||
|
| `system_notif` | — (keine Routes) | — | Event-Bus-only (Participant Handler) |
|
||||||
|
| `contacts` | — (keine eigene Routes) | — | Core-Plugin ohne eigene Routes |
|
||||||
|
|
||||||
|
### Plugins mit eigenen Models
|
||||||
|
|
||||||
|
Alle 19 Plugins haben `models.py`:
|
||||||
|
`agent_memory`, `ai_assistant`, `ai_proactive`, `automation`, `calendar`, `dms`, `entity_links`, `forgejo_error_reporter`, `graph_rag`, `kommunikation`, `mail`, `marketplace`, `mcp_client`, `permissions`, `report_generator`, `tags`, `tasks`, `unified_search`, `wiki`
|
||||||
|
|
||||||
|
### Plugins die aus app/ai/ importieren
|
||||||
|
|
||||||
|
| Plugin | Importiert | Modul |
|
||||||
|
|--------|-----------|-------|
|
||||||
|
| `ai_assistant` | `llm_complete` | `services.py`, `participant_handler.py` |
|
||||||
|
| `ai_proactive` | `llm_complete` | `jobs.py`, `services.py`, `context_tools.py` |
|
||||||
|
| `automation` | `run_react_loop`, `ReActResult` | `agent_runner.py` |
|
||||||
|
| `automation` | `AIUseCaseMetadata`, `validate_ai_use_case` | `agent_routes.py` |
|
||||||
|
| `unified_search` | `get_llm_client`, `generate_embedding`, `get_provider_compliance` | `embedding.py`, `query_understanding.py` |
|
||||||
|
|
||||||
|
### Plugins die aus app/workflows/ importieren
|
||||||
|
|
||||||
|
**KEINE.** Kein Plugin importiert aus `app.workflows`.
|
||||||
|
|
||||||
|
### Contracts-System
|
||||||
|
|
||||||
|
18 Plugins haben `contracts.py`. Zentrale Registry in `app/plugins/builtins/contracts.py` mit `get_contract()` und `get_contract_registry()`.
|
||||||
|
|
||||||
|
| Contract-Nutzer | Importiert Contract von | Zweck |
|
||||||
|
|----------------|----------------------|------|
|
||||||
|
| `dms/routes.py` | `permissions.contracts` | Permission-Checks für DMS-Objekte |
|
||||||
|
| `mail/routes.py` | `calendar.contracts` | Calendar-Integration für Mail-Termine |
|
||||||
|
| `kommunikation/dms_bridge.py` | `dms.contracts` | DMS-Dateien in Kommunikation anhängen |
|
||||||
|
| `kommunikation/search_provider.py` | `unified_search.contracts` | Kommunikation in Unified Search einbinden |
|
||||||
|
| `unified_search/plugin.py` | `ai_assistant.contracts` | AI-Tool-Registry für Search nutzen |
|
||||||
|
| `ai_proactive/jobs.py` | `unified_search.contracts` | Proactive AI nutzt Unified Search |
|
||||||
|
| `report_generator/jobs.py` | `dms.contracts` (via registry) | Reports aus DMS-Dateien generieren |
|
||||||
|
|
||||||
|
Alle Plugins nutzen `get_contract_registry().unregister()` in `on_deactivate()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. APPROVAL SYSTEM
|
||||||
|
|
||||||
|
### app/core/approval.py
|
||||||
|
- **Model:** `ApprovalRequest` (`__tablename__ = "approval_requests"`)
|
||||||
|
- **Felder:** `id`, `entity_type`, `entity_id`, `action`, `requested_by`, `requested_by_type`, `approver_id`, `approver_group`, `status`, `comment`, `created_at`, `resolved_at`, `expires_at`, `request_metadata`
|
||||||
|
- **Status-Lifecycle:** `pending` → `approved` | `rejected` | `expired`
|
||||||
|
- **Funktionen:** `create_approval_request()`, `resolve_approval_request()`, `expire_approval_request()`
|
||||||
|
- **Tabelle in DB:** ✅ Vorhanden (`approval_requests` in leocrm_test)
|
||||||
|
- **Migration:** `0123_approval_requests.py`
|
||||||
|
|
||||||
|
### app/routes/approvals.py
|
||||||
|
- **Prefix:** `/api/v1/approvals`
|
||||||
|
- **Endpoints:** `POST ""` (create), `GET ""` (list), `GET "/{id}"` (detail), `POST "/{id}/approve"`, `POST "/{id}/reject"`, `POST "/{id}/expire"`
|
||||||
|
- **Permissions:** `approvals:write`, `approvals:read`, `approvals:approve`
|
||||||
|
- **Registriert in main.py:** ✅ (line 582)
|
||||||
|
|
||||||
|
### Verbindung mit Agent Loop
|
||||||
|
✅ **Verbunden.** `agent_loop.py` hat:
|
||||||
|
- Parameter `require_approval: bool = False` (line 152)
|
||||||
|
- Parameter `approval_tools: list[str] | None = None` (line 153)
|
||||||
|
- Wenn `require_approval=True` und Tool in `approval_tools`: erstellt `ApprovalRequest` via `create_approval_request()` (line 380-383)
|
||||||
|
- Setzt `result.status = "waiting_for_approval"` (line 405)
|
||||||
|
- Postet Approval-Request an Workstream (line 395-400)
|
||||||
|
|
||||||
|
### Verbindung mit Workflows
|
||||||
|
✅ **Verbunden.** `engine.py` behandelt `step_type == "approval"`:
|
||||||
|
- Setzt `instance.status = "in_progress"` und `instance.resume_reason = "approval"` (line 110-113)
|
||||||
|
- Pausiert Workflow, wartet auf User-Entscheidung
|
||||||
|
- `resume()` Methode (line 436+) setzt Workflow fort nach Approval
|
||||||
|
|
||||||
|
### Verbindung mit Automation Plugin
|
||||||
|
❌ **NICHT verbunden.** Kein Import von `approval` in `app/plugins/builtins/automation/`. Der Automation-Plugin nutzt `agent_loop.run_react_loop()` aber gibt `require_approval` nicht durch.
|
||||||
|
|
||||||
|
### Verbindung mit decision_guard.py
|
||||||
|
❌ **NICHT verbunden.** `decision_guard.py` referenziert `requires_approval` und `ApprovalRequest` konzeptionell, ist aber nicht in Engine oder Agent Loop integriert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. FRONTEND (frontend/src/)
|
||||||
|
|
||||||
|
### Pages mit echter API-Anbindung
|
||||||
|
|
||||||
|
| Page | API-Refs | Zeilen | Status |
|
||||||
|
|------|---------|--------|--------|
|
||||||
|
| `Communication.tsx` | 22 | 859 | ✅ Voll verbunden (apiClient, WebSocket, AI-Streaming) |
|
||||||
|
| `Mail.tsx` | 12 | 1098 | ✅ Voll verbunden |
|
||||||
|
| `SettingsStammdaten.tsx` | 17 | 477 | ✅ Voll verbunden (Adressen, Bankkonten) |
|
||||||
|
| `Tags.tsx` | 8 | 500 | ✅ Voll verbunden (TanStack Query) |
|
||||||
|
| `Dms.tsx` | 7 | 746 | ✅ Voll verbunden |
|
||||||
|
| `SettingsRechte.tsx` | 7 | 420 | ✅ Voll verbunden |
|
||||||
|
| `Wiki.tsx` | 7 | 410 | ✅ Voll verbunden |
|
||||||
|
| `ContactsList.tsx` | 6 | 787 | ✅ Voll verbunden |
|
||||||
|
| `AutomationSettings.tsx` | 6 | 326 | ✅ Voll verbunden |
|
||||||
|
| `MailSettings.tsx` | 5 | 440 | ✅ Voll verbunden |
|
||||||
|
| `Calendar.tsx` | 4 | 759 | ✅ Voll verbunden |
|
||||||
|
| `SettingsGroups.tsx` | 4 | 696 | ✅ Voll verbunden |
|
||||||
|
| `SettingsWebhooks.tsx` | 4 | 630 | ✅ Voll verbunden |
|
||||||
|
| `SettingsRoles.tsx` | 4 | 532 | ✅ Voll verbunden |
|
||||||
|
| `SettingsPlugins.tsx` | 4 | 384 | ✅ Voll verbunden |
|
||||||
|
| `Workflows.tsx` | 4 | 279 | ✅ Voll verbunden |
|
||||||
|
| `AgentDashboard.tsx` | 3 | 832 | ✅ Voll verbunden |
|
||||||
|
| `AutomationDashboard.tsx` | 3 | 778 | ✅ Voll verbunden |
|
||||||
|
| `AIAssistant.tsx` | 3 | 132 | ✅ Verbunden |
|
||||||
|
| `CalendarKanban.tsx` | 3 | 123 | ✅ Verbunden |
|
||||||
|
| `Dashboard.tsx` | 2 | 98 | ✅ Verbunden |
|
||||||
|
| `SettingsMcp.tsx` | 2 | 268 | ✅ Verbunden |
|
||||||
|
| `DmsTrash.tsx` | 2 | 156 | ✅ Verbunden |
|
||||||
|
| `Tasks.tsx` | 1 | 419 | ✅ Verbunden |
|
||||||
|
| `Reports.tsx` | 1 | 433 | ✅ Verbunden |
|
||||||
|
| `SettingsBackup.tsx` | 1 | 499 | ✅ Verbunden |
|
||||||
|
| `CustomFields.tsx` | 1 | 521 | ✅ Verbunden |
|
||||||
|
| `AISettings.tsx` | 9 | 333 | ✅ Verbunden |
|
||||||
|
| `SettingsSequences.tsx` | 9 | 175 | ✅ Verbunden |
|
||||||
|
| `SettingsCurrencies.tsx` | 9 | 180 | ✅ Verbunden |
|
||||||
|
| `SettingsTaxes.tsx` | 9 | 182 | ✅ Verbunden |
|
||||||
|
| `SettingsStammdaten.tsx` | 17 | 477 | ✅ Voll verbunden |
|
||||||
|
| `SettingsProfile.tsx` | 1 | 182 | ✅ Verbunden |
|
||||||
|
| `SettingsUsers.tsx` | 1 | 315 | ✅ Verbunden |
|
||||||
|
| `SettingsTheme.tsx` | 1 | 346 | ✅ Verbunden |
|
||||||
|
| `SettingsFirmendaten.tsx` | 1 | 278 | ✅ Verbunden |
|
||||||
|
| `SettingsMenuOrder.tsx` | 1 | 240 | ✅ Verbunden |
|
||||||
|
| `Trash.tsx` | 1 | 265 | ✅ Verbunden |
|
||||||
|
| `GlobalSearchResults.tsx` | 1 | 237 | ✅ Verbunden |
|
||||||
|
| `DedupMerge.tsx` | 1 | 225 | ✅ Verbunden |
|
||||||
|
| `ProactiveAISettings.tsx` | 1 | 219 | ✅ Verbunden |
|
||||||
|
| `ActivityTimeline.tsx` | 1 | 202 | ✅ Verbunden |
|
||||||
|
| `AuditLog.tsx` | 1 | 167 | ✅ Verbunden |
|
||||||
|
| `SettingsNotifications.tsx` | 1 | 159 | ✅ Verbunden |
|
||||||
|
| `PasswordResetConfirm.tsx` | 1 | 106 | ✅ Verbunden |
|
||||||
|
| `PasswordResetRequest.tsx` | 1 | 90 | ✅ Verbunden |
|
||||||
|
| `Login.tsx` | 1 | 93 | ✅ Verbunden |
|
||||||
|
| `ContactDetailPage.tsx` | 1 | 73 | ✅ Verbunden |
|
||||||
|
|
||||||
|
### Pages ohne API-Anbindung (leer/Placeholders/Navigation)
|
||||||
|
|
||||||
|
| Page | Zeilen | Status |
|
||||||
|
|------|--------|--------|
|
||||||
|
| `SettingsWorkspaces.tsx` | 9 | ❌ Leer — nur Redirect/Placeholder |
|
||||||
|
| `AIAssistantStandalone.tsx` | 10 | ❌ Wrapper ohne API |
|
||||||
|
| `CalendarStandalone.tsx` | 10 | ❌ Wrapper ohne API |
|
||||||
|
| `ContactsStandalone.tsx` | 10 | ❌ Wrapper ohne API |
|
||||||
|
| `DmsStandalone.tsx` | 10 | ❌ Wrapper ohne API |
|
||||||
|
| `MailStandalone.tsx` | 10 | ❌ Wrapper ohne API |
|
||||||
|
| `AgentsPlaceholder.tsx` | 12 | ❌ Placeholder |
|
||||||
|
| `AutomationPlaceholder.tsx` | 12 | ❌ Placeholder |
|
||||||
|
| `LogsPlaceholder.tsx` | 12 | ❌ Placeholder |
|
||||||
|
| `HelpPlaceholder.tsx` | 17 | ❌ Placeholder |
|
||||||
|
| `GuestContacts.tsx` | 19 | ❌ Keine API |
|
||||||
|
| `GuestLogin.tsx` | 19 | ❌ Keine API |
|
||||||
|
| `NoAccessPage.tsx` | 22 | ❌ Statische Error-Page |
|
||||||
|
| `HelpLogin.tsx` | 23 | ❌ Statische Help-Page |
|
||||||
|
| `HelpContacts.tsx` | 25 | ❌ Statische Help-Page |
|
||||||
|
| `HelpMailSetup.tsx` | 25 | ❌ Statische Help-Page |
|
||||||
|
| `AutomationOverview.tsx` | 28 | ❌ Übersicht ohne API |
|
||||||
|
| `HelpNavigation.tsx` | 29 | ❌ Statische Help-Page |
|
||||||
|
| `HelpApiDocs.tsx` | 33 | ❌ Statische Help-Page |
|
||||||
|
| `HelpWelcome.tsx` | 33 | ❌ Statische Help-Page |
|
||||||
|
| `AgentsOverview.tsx` | 35 | ❌ Übersicht ohne API |
|
||||||
|
| `LogsOverview.tsx` | 35 | ❌ Übersicht ohne API |
|
||||||
|
| `SettingsAI.tsx` | 46 | ❌ Keine API (sollte AISettings nutzen) |
|
||||||
|
| `SettingsUserManagement.tsx` | 48 | ❌ Keine API |
|
||||||
|
| `ApiDocs.tsx` | 50 | ❌ Statische API-Docs |
|
||||||
|
| `ImportExport.tsx` | 68 | ❌ Keine API (sollte importExport API nutzen) |
|
||||||
|
| `Agents.tsx` | 102 | ❌ Keine API (sollte automation API nutzen) |
|
||||||
|
| `Settings.tsx` | 105 | ❌ Nur Navigation/Tab-Container |
|
||||||
|
| `Automation.tsx` | 111 | ❌ Nur Navigation/Tab-Container |
|
||||||
|
| `Logs.tsx` | 119 | ❌ Nur Navigation/Tab-Container |
|
||||||
|
| `StartPage.tsx` | 150 | ❌ Keine API |
|
||||||
|
| `Help.tsx` | 164 | ❌ Nur Navigation/Tab-Container |
|
||||||
|
| `SettingsSystem.tsx` | 175 | ❌ Keine API |
|
||||||
|
|
||||||
|
### API-Clients (frontend/src/api/)
|
||||||
|
|
||||||
|
| API-Client | Nutzung in Pages/Components | Status |
|
||||||
|
|-----------|---------------------------|--------|
|
||||||
|
| `hooks.ts` | 30 Importe | ✅ Meistgenutzt |
|
||||||
|
| `calendar.ts` | 16 | ✅ |
|
||||||
|
| `mail.ts` | 14 | ✅ |
|
||||||
|
| `dms.ts` | 12 | ✅ |
|
||||||
|
| `ai.ts` | 10 | ✅ |
|
||||||
|
| `client.ts` | 10 | ✅ (Basis-Client) |
|
||||||
|
| `tags.ts` | 8 | ✅ |
|
||||||
|
| `automation.ts` | 6 | ✅ |
|
||||||
|
| `customFieldDefinitions.ts` | 6 | ✅ |
|
||||||
|
| `workflows.ts` | 5 | ✅ |
|
||||||
|
| `tasks.ts` | 5 | ✅ |
|
||||||
|
| `savedFilters.ts` | 5 | ✅ |
|
||||||
|
| `dedup.ts` | 5 | ✅ |
|
||||||
|
| `users.ts` | 4 | ✅ |
|
||||||
|
| `entityHistory.ts` | 4 | ✅ |
|
||||||
|
| `customFields.ts` | 4 | ✅ |
|
||||||
|
| `knowledge.ts` | 4 | ✅ |
|
||||||
|
| `reports.ts` | 2 | ✅ |
|
||||||
|
| `pluginManifests.ts` | 2 | ✅ |
|
||||||
|
| `groups.ts` | 2 | ✅ |
|
||||||
|
| `importExport.ts` | 2 | ✅ |
|
||||||
|
| `entityPermissions.ts` | 2 | ✅ |
|
||||||
|
| `entityPermissionHooks.ts` | 2 | ✅ |
|
||||||
|
| `audit.ts` | 2 | ✅ |
|
||||||
|
| `webhooks.ts` | 1 | ✅ |
|
||||||
|
| `settings.ts` | 1 | ✅ |
|
||||||
|
| `userPreferences.ts` | 1 | ✅ |
|
||||||
|
| `search.ts` | 1 | ✅ |
|
||||||
|
| `savedViews.ts` | 1 | ✅ |
|
||||||
|
| `permissions.ts` | 1 | ✅ |
|
||||||
|
| `mcpClient.ts` | 1 | ✅ |
|
||||||
|
| `mcp.ts` | 1 | ✅ |
|
||||||
|
| `comm.ts` | 1 | ✅ |
|
||||||
|
| `backups.ts` | 1 | ✅ |
|
||||||
|
| `aiProactive.ts` | 3 | ✅ |
|
||||||
|
| `contactFolders.ts` | 3 | ✅ |
|
||||||
|
| `contacts.ts` | 3 | ✅ |
|
||||||
|
| `dashboard.ts` | 3 | ✅ |
|
||||||
|
| `notifications.ts` | 3 | ✅ |
|
||||||
|
| `unifiedContacts.ts` | 3 | ✅ |
|
||||||
|
| `aiUIControl.ts` | 0 | ❌ Unbenutzt |
|
||||||
|
| `attachments.ts` | 0 | ❌ Unbenutzt |
|
||||||
|
| `auth.ts` | 0 | ❌ Unbenutzt (Login nutzt hooks) |
|
||||||
|
| `plugins.ts` | 0 | ❌ Unbenutzt |
|
||||||
|
| `policies.ts` | 0 | ❌ Unbenutzt |
|
||||||
|
| `policyHooks.ts` | 0 | ❌ Unbenutzt |
|
||||||
|
| `roles.ts` | 0 | ❌ Unbenutzt (SettingsRoles nutzt hooks) |
|
||||||
|
| `searchHooks.ts` | 0 | ❌ Unbenutzt |
|
||||||
|
| `types.ts` | 0 | ❌ Unbenutzt (nur Typen) |
|
||||||
|
|
||||||
|
### Was verbunden werden muss
|
||||||
|
1. `Agents.tsx` (102 Zeilen, 0 API) → Sollte `@/api/automation` nutzen (AgentDashboard.tsx zeigt wie)
|
||||||
|
2. `ImportExport.tsx` (68 Zeilen, 0 API) → Sollte `@/api/importExport` nutzen
|
||||||
|
3. `SettingsAI.tsx` (46 Zeilen, 0 API) → Sollte `@/api/ai` oder `AISettings.tsx` einbinden
|
||||||
|
4. `SettingsSystem.tsx` (175 Zeilen, 0 API) → Sollte `@/api/settings` nutzen
|
||||||
|
5. `SettingsUserManagement.tsx` (48 Zeilen, 0 API) → Sollte `@/api/users` nutzen
|
||||||
|
6. 9 unbenutzte API-Clients prüfen: `aiUIControl`, `attachments`, `auth`, `plugins`, `policies`, `policyHooks`, `roles`, `searchHooks` → Entweder verbinden oder löschen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. ROUTES (app/routes/ + plugin routes)
|
||||||
|
|
||||||
|
### Route-Dateien in app/routes/
|
||||||
|
|
||||||
|
44 Route-Dateien (inkl. `__init__.py`). Alle in `main.py` registriert außer `delegations.py` (geparkt, line 575: `# app.include_router(delegations.router) # ⏸ Parked`).
|
||||||
|
|
||||||
|
| Route-Datei | Zeilen | Registriert | Status |
|
||||||
|
|------------|--------|-------------|--------|
|
||||||
|
| `workflows.py` | 671 | ✅ line 557 | Echte DB-Daten |
|
||||||
|
| `users.py` | 397 | ✅ line 541 | Echte DB-Daten |
|
||||||
|
| `workspaces.py` | 373 | ✅ line 579 | Echte DB-Daten |
|
||||||
|
| `import_export.py` | 371 | ✅ line 554 | Echte DB-Daten |
|
||||||
|
| `plugins.py` | 354 | ✅ line 555 | Echte DB-Daten |
|
||||||
|
| `companies.py` | 353 | ✅ line 547 | Echte DB-Daten |
|
||||||
|
| `approvals.py` | 305 | ✅ line 582 | Echte DB-Daten |
|
||||||
|
| `contacts.py` | 318 | ✅ line 548 | Echte DB-Daten |
|
||||||
|
| `entity_permissions.py` | 316 | ✅ line 551 | Echte DB-Daten |
|
||||||
|
| `groups.py` | 262 | ✅ line 543 | Echte DB-Daten |
|
||||||
|
| `auth.py` | 256 | ✅ line 540 | Echte DB-Daten |
|
||||||
|
| `roles.py` | 247 | ✅ line 542 | Echte DB-Daten |
|
||||||
|
| `notifications.py` | 224 | ✅ line 545 | Echte DB-Daten |
|
||||||
|
| `entity_history.py` | 215 | ✅ line 553 | Echte DB-Daten |
|
||||||
|
| `webhooks.py` | 206 | ✅ line 573 | Echte DB-Daten |
|
||||||
|
| `custom_fields.py` | 201 | ✅ line 570 | Echte DB-Daten |
|
||||||
|
| `saved_views.py` | 199 | ✅ line 572 | Echte DB-Daten |
|
||||||
|
| `guests.py` | 199 | ✅ line 578 | Echte DB-Daten |
|
||||||
|
| `user_preferences.py` | 195 | ✅ line 558 | Echte DB-Daten |
|
||||||
|
| `outbox.py` | 162 | ✅ line 580 | Echte DB-Daten |
|
||||||
|
| `saved_filters.py` | 160 | ✅ line 571 | Echte DB-Daten |
|
||||||
|
| `attachments.py` | 144 | ✅ line 563 | Echte DB-Daten |
|
||||||
|
| `errors.py` | 139 | ✅ line 577 | Echte DB-Daten |
|
||||||
|
| `ai_copilot.py` | 129 | ✅ line 556 | Echte DB-Daten |
|
||||||
|
| `addresses.py` | 123 | ✅ line 564 | Echte DB-Daten |
|
||||||
|
| `permission_templates.py` | 114 | ✅ line 574 | Echte DB-Daten |
|
||||||
|
| `custom_field_definitions.py` | 110 | ✅ line 569 | Echte DB-Daten |
|
||||||
|
| `contact_folders.py` | 110 | ✅ line 549 | Echte DB-Daten |
|
||||||
|
| `contact_folder_permissions.py` | 107 | ✅ line 550 | Echte DB-Daten |
|
||||||
|
| `delegations.py` | 105 | ❌ Geparkt | NICHT registriert (line 575 auskommentiert) |
|
||||||
|
| `backups.py` | 104 | ✅ line 567 | Echte DB-Daten |
|
||||||
|
| `bank_accounts.py` | 102 | ✅ line 565 | Echte DB-Daten |
|
||||||
|
| `dashboard.py` | 100 | ✅ line 552 | Echte DB-Daten |
|
||||||
|
| `sequences.py` | 100 | ✅ line 561 | Echte DB-Daten |
|
||||||
|
| `policies.py` | 95 | ✅ line 576 | Echte DB-Daten |
|
||||||
|
| `audit.py` | 94 | ✅ line 566 | Echte DB-Daten |
|
||||||
|
| `taxes.py` | 85 | ✅ line 560 | Echte DB-Daten |
|
||||||
|
| `currencies.py` | 85 | ✅ line 559 | Echte DB-Daten |
|
||||||
|
| `health.py` | 81 | ✅ line 538 | Health-Check |
|
||||||
|
| `tenants.py` | 77 | ✅ line 544 | Echte DB-Daten |
|
||||||
|
| `api_tokens.py` | 77 | ✅ line 581 | Echte DB-Daten |
|
||||||
|
| `owner_transfer.py` | 58 | ✅ line 568 | Echte DB-Daten |
|
||||||
|
| `system_settings.py` | 56 | ✅ line 562 | Echte DB-Daten |
|
||||||
|
| `metrics.py` | 29 | ✅ line 539 | Metrics |
|
||||||
|
| `__init__.py` | 28 | — | Modul-Init |
|
||||||
|
|
||||||
|
### Plugin Routes (dynamisch registriert)
|
||||||
|
|
||||||
|
Plugin-Routes werden in `main.py` (lines 602-614) dynamisch via `importlib.import_module(route_def.module)` registriert. Jeder Plugin-Router bekommt eine Plugin-Dependency.
|
||||||
|
|
||||||
|
### Stubs/TODOs in Routes
|
||||||
|
|
||||||
|
**Keine Stubs gefunden.** Alle Route-Dateien haben 0 TODO/FIXME/NotImplemented/Placeholder-Marker.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. TESTS (tests/)
|
||||||
|
|
||||||
|
### Test-Übersicht
|
||||||
|
|
||||||
|
- **Anzahl:** 98 Test-Dateien
|
||||||
|
- **Test-DB:** `leocrm_test` auf `localhost:5432` (User: `leocrm`)
|
||||||
|
- **Conftest:** Dynamisch importiert alle Plugin-Models für `Base.metadata.create_all()`
|
||||||
|
- **Schema:** Fresh schema pro Test (created from metadata, nicht via Alembic)
|
||||||
|
|
||||||
|
### Integration-Tests (mit DB, hohe DB-Nutzung)
|
||||||
|
|
||||||
|
| Test-Datei | DB-Refs | Mock-Refs | Typ |
|
||||||
|
|-----------|---------|-----------|-----|
|
||||||
|
| `test_workflows.py` | 241 | 11 | Integration |
|
||||||
|
| `test_rbac_comprehensive.py` | 205 | 11 | Integration |
|
||||||
|
| `test_ai_proactive.py` | 192 | 75 | Mixed (Integration + Mock) |
|
||||||
|
| `test_unified_search.py` | 124 | 78 | Mixed |
|
||||||
|
| `test_ai_copilot.py` | 120 | 18 | Integration |
|
||||||
|
| `test_notification_migration.py` | 111 | 0 | Integration |
|
||||||
|
| `test_plugins.py` | 99 | 0 | Integration |
|
||||||
|
| `test_outbox_phase5.py` | 97 | 0 | Integration |
|
||||||
|
| `test_workspaces.py` | 89 | 0 | Integration |
|
||||||
|
| `test_permission_system_live.py` | 87 | 0 | Integration |
|
||||||
|
| `test_entity_permissions.py` | 78 | 0 | Integration |
|
||||||
|
| `test_abac.py` | 62 | 0 | Integration |
|
||||||
|
| `test_commands.py` | 59 | 0 | Integration |
|
||||||
|
| `test_backend_coverage_gaps.py` | 58 | 3 | Integration |
|
||||||
|
| `test_cross_tenant_security.py` | 55 | 1 | Integration |
|
||||||
|
| `test_mail.py` | 55 | 16 | Mixed |
|
||||||
|
| `test_unified_search_phase_e.py` | 47 | 39 | Mixed |
|
||||||
|
| `test_import_export.py` | 44 | 0 | Integration |
|
||||||
|
| `test_tags.py` | 42 | 5 | Integration |
|
||||||
|
| `test_tenant.py` | 42 | 1 | Integration |
|
||||||
|
| `test_abac_integration.py` | 42 | 0 | Integration |
|
||||||
|
| `test_permission_performance.py` | 41 | 2 | Integration |
|
||||||
|
| `test_companies.py` | 39 | 0 | Integration |
|
||||||
|
| `test_unified_tasks.py` | 34 | 0 | Integration |
|
||||||
|
| `test_tasks.py` | 32 | 2 | Integration |
|
||||||
|
| `test_entity_links.py` | 31 | 0 | Integration |
|
||||||
|
| `test_permissions.py` | 29 | 0 | Integration |
|
||||||
|
| `test_notifications.py` | 28 | 1 | Integration |
|
||||||
|
| `test_api_tokens.py` | 28 | 0 | Integration |
|
||||||
|
| `test_user_preferences.py` | 27 | 0 | Integration |
|
||||||
|
| `test_saved_filters.py` | 26 | 0 | Integration |
|
||||||
|
| `test_auth.py` | 26 | 0 | Integration |
|
||||||
|
| `test_calendar.py` | 24 | 5 | Integration |
|
||||||
|
| `test_outbox.py` | 23 | 0 | Integration |
|
||||||
|
|
||||||
|
### Mock-Dominierte Tests
|
||||||
|
|
||||||
|
| Test-Datei | DB-Refs | Mock-Refs | Typ |
|
||||||
|
|-----------|---------|-----------|-----|
|
||||||
|
| `test_marketplace.py` | 39 | 165 | Mock-dominiert |
|
||||||
|
| `test_agent_memory.py` | 32 | 138 | Mock-dominiert |
|
||||||
|
| `test_external_agent_api.py` | 28 | 105 | Mock-dominiert |
|
||||||
|
| `test_trigger_core.py` | 34 | 89 | Mock-dominiert |
|
||||||
|
| `test_graph_rag.py` | 35 | 100 | Mock-dominiert |
|
||||||
|
|
||||||
|
### Tests ohne DB oder Mock (Unit-Tests)
|
||||||
|
|
||||||
|
| Test-Datei | DB-Refs | Mock-Refs | Typ |
|
||||||
|
|-----------|---------|-----------|-----|
|
||||||
|
| `test_recurrence_unit.py` | — | — | Unit |
|
||||||
|
| `test_semver.py` | — | — | Unit |
|
||||||
|
| `test_sensitive_data.py` | — | — | Unit |
|
||||||
|
| `test_resilience.py` | — | — | Unit |
|
||||||
|
| `test_redis_pool.py` | — | — | Unit |
|
||||||
|
| `test_rate_limit_policies.py` | — | — | Unit |
|
||||||
|
| `test_restore_registry.py` | — | — | Unit |
|
||||||
|
| `test_rls_coverage.py` | — | — | Unit |
|
||||||
|
| `test_storage.py` | — | — | Unit |
|
||||||
|
| `test_versioning.py` | — | — | Unit |
|
||||||
|
| `test_ws_helpers.py` | — | — | Unit |
|
||||||
|
|
||||||
|
### Test-DB Funktionalität
|
||||||
|
|
||||||
|
Die Test-DB (`leocrm_test`) ist erreichbar und hat **120 Tabellen**. Tests nutzen `Base.metadata.create_all()` (nicht Alembic-Migrationen) für Schema-Erstellung.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. MODELS (app/models/ + plugin models)
|
||||||
|
|
||||||
|
### Core Models (app/models/)
|
||||||
|
|
||||||
|
40 Model-Dateien mit folgenden Tabellen:
|
||||||
|
|
||||||
|
| Tabelle | Model-Datei |
|
||||||
|
|---------|-----------|
|
||||||
|
| `addresses` | address.py |
|
||||||
|
| `ai_conversations` | ai_conversation.py |
|
||||||
|
| `ai_messages` | ai_conversation.py |
|
||||||
|
| `attachments` | attachment.py |
|
||||||
|
| `audit_log` | audit.py |
|
||||||
|
| `password_reset_tokens` | auth.py |
|
||||||
|
| `api_tokens` | auth.py |
|
||||||
|
| `backups` | backup.py |
|
||||||
|
| `bank_accounts` | bank_account.py |
|
||||||
|
| `consumer_inbox` | consumer_inbox.py |
|
||||||
|
| `contact_folders` | contact_folder.py |
|
||||||
|
| `contact_merge_history` | contact_merge.py |
|
||||||
|
| `contacts` | contact.py |
|
||||||
|
| `contactpersons` | contact.py |
|
||||||
|
| `currencies` | currency.py |
|
||||||
|
| `custom_field_definitions` | custom_field_definition.py |
|
||||||
|
| `entity_attachments` | entity_attachment.py |
|
||||||
|
| `entity_history` | entity_history.py |
|
||||||
|
| `entity_permissions` | entity_permission.py |
|
||||||
|
| `entity_policies` | entity_policy.py |
|
||||||
|
| `groups` | group.py |
|
||||||
|
| `user_groups` | group.py |
|
||||||
|
| `notifications` | notification.py |
|
||||||
|
| `notification_types` | notification.py |
|
||||||
|
| `notification_preferences` | notification.py |
|
||||||
|
| `outbox_deliveries` | outbox_delivery.py |
|
||||||
|
| `event_outbox` | outbox.py |
|
||||||
|
| `permission_delegations` | permission_delegation.py |
|
||||||
|
| `permission_templates` | permission_template.py |
|
||||||
|
| `plugin_allowlist` | plugin_allowlist.py |
|
||||||
|
| `plugins` | plugin.py |
|
||||||
|
| `plugin_migrations` | plugin.py |
|
||||||
|
| `roles` | role.py |
|
||||||
|
| `saved_filters` | saved_filter.py |
|
||||||
|
| `saved_views` | saved_view.py |
|
||||||
|
| `sequences` | sequence.py |
|
||||||
|
| `sessions` | session.py |
|
||||||
|
| `system_settings` | system_settings.py |
|
||||||
|
| `tax_rates` | tax.py |
|
||||||
|
| `tenants` | tenant.py |
|
||||||
|
| `user_preferences` | user_preference.py |
|
||||||
|
| `users` | user.py |
|
||||||
|
| `user_tenants` | user.py |
|
||||||
|
| `webhooks` | webhook.py |
|
||||||
|
| `workflows` | workflow.py |
|
||||||
|
| `workflow_instances` | workflow.py |
|
||||||
|
| `workflow_step_history` | workflow.py |
|
||||||
|
| `workspaces` | workspace.py |
|
||||||
|
| `workspace_modules` | workspace.py |
|
||||||
|
| `workspace_users` | workspace.py |
|
||||||
|
| `workspace_widgets` | workspace.py |
|
||||||
|
|
||||||
|
### Plugin Models
|
||||||
|
|
||||||
|
19 Plugin-Model-Dateien mit ~70 Tabellen (siehe Abschnitt 3).
|
||||||
|
|
||||||
|
### Tabellen in Test-DB (leocrm_test)
|
||||||
|
|
||||||
|
**120 Tabellen vorhanden.**
|
||||||
|
|
||||||
|
### Tabellen NICHT in Test-DB (definiert in Models aber fehlend)
|
||||||
|
|
||||||
|
| Tabelle | Model-Datei | In DB? | Mögliche Ursache |
|
||||||
|
|---------|-----------|--------|-----------------|
|
||||||
|
| `ai_decision_records` | `app/ai/oversight.py` | ❌ Fehlt | Model nicht in conftest importiert, keine Migration |
|
||||||
|
| `document_chunks` | `unified_search/models.py` | ❌ Fehlt | Plugin-Model nicht geladen? |
|
||||||
|
| `forgejo_reported_errors` | `forgejo_error_reporter/models.py` | ❌ Fehlt | Plugin-Model nicht geladen? |
|
||||||
|
| `plugin_allowlist` | `app/models/plugin_allowlist.py` | ❌ Fehlt | Model nicht in conftest importiert |
|
||||||
|
| `unified_search_index_log` | `unified_search/models.py` | ❌ Fehlt | Plugin-Model nicht geladen? |
|
||||||
|
| `unified_search_providers` | `unified_search/models.py` | ❌ Fehlt | Plugin-Model nicht geladen? |
|
||||||
|
| `wiki_articles` | `wiki/models.py` | ❌ Fehlt | Plugin-Model nicht geladen? |
|
||||||
|
| `wiki_article_versions` | `wiki/models.py` | ❌ Fehlt | Plugin-Model nicht geladen? |
|
||||||
|
| `wiki_categories` | `wiki/models.py` | ❌ Fehlt | Plugin-Model nicht geladen? |
|
||||||
|
|
||||||
|
**9 Tabellen definiert aber nicht in Test-DB vorhanden.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. MIGRATIONS (alembic/versions/)
|
||||||
|
|
||||||
|
- **Anzahl Migrationen:** 128
|
||||||
|
- **Aktuelle Head-Revision:** `0127`
|
||||||
|
- **Letzte Migrationen:**
|
||||||
|
- `0127_drop_tasks_contact_id_fk.py` (Aug 19, 2026)
|
||||||
|
- `0126_wiki_plugin.py` (Aug 18, 2026)
|
||||||
|
- `0125_durable_workflow_run.py` (Aug 18, 2026)
|
||||||
|
- `0124_unified_task_system.py` (Aug 17, 2026)
|
||||||
|
- `0123_approval_requests.py` (Aug 17, 2026)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. VERBINDUNGS-MATRIX
|
||||||
|
|
||||||
|
### Backend AI Module
|
||||||
|
|
||||||
|
| Modul | Verbunden mit | Status |
|
||||||
|
|-------|---------------|--------|
|
||||||
|
| `agent_loop.py` | `llm_client`, `approval`, `hooks`, `audit`, `automation.agent_runner`, `agent_stream` | ✅ Verbunden |
|
||||||
|
| `llm_client.py` | `agent_loop`, `data_policy`, `ai_assistant`, `ai_proactive`, `unified_search`, `ai_copilot_service`, `action_mapper` | ✅ Verbunden |
|
||||||
|
| `ai_use_case.py` | `data_policy`, `automation.agent_routes` | ✅ Verbunden |
|
||||||
|
| `skill_registry.py` | `agent_tools`, `agent_permissions` (nur intern) | ⚠️ Nur intern |
|
||||||
|
| `action_mapper.py` | `llm_client` (nur intern, lazy) | ⚠️ Nur intern |
|
||||||
|
| `agent_permissions.py` | — | ❌ Unverbunden |
|
||||||
|
| `agent_tools.py` | — | ❌ Unverbunden |
|
||||||
|
| `data_policy.py` | — | ❌ Unverbunden |
|
||||||
|
| `transparency.py` | — | ❌ Unverbunden |
|
||||||
|
| `oversight.py` | — | ❌ Unverbunden (Tabelle fehlt in DB) |
|
||||||
|
| `agent_memory.py` | — | ❌ Unverbunden (Duplikat mit Plugin) |
|
||||||
|
| `agent_stream.py` | `agent_loop` (importiert, aber niemand nutzt agent_stream) | ❌ Unverbunden |
|
||||||
|
| `context_builder.py` | `sensitive_data`, `tenant`, `user`, `ai_assistant.contracts` (importiert, aber niemand nutzt context_builder) | ❌ Unverbunden |
|
||||||
|
|
||||||
|
### Workflow Module
|
||||||
|
|
||||||
|
| Modul | Verbunden mit | Status |
|
||||||
|
|-------|---------------|--------|
|
||||||
|
| `engine.py` | `routes/workflows.py`, `core/event_bus.py`, `step_handlers`, `workflow_service`, `models.workflow`, `notifications`, `event_bus` | ✅ Verbunden |
|
||||||
|
| `step_handlers.py` | `engine.py` (10 Handler registriert) | ✅ Verbunden |
|
||||||
|
| `decision_guard.py` | — | ❌ Unverbunden |
|
||||||
|
|
||||||
|
### Approval System
|
||||||
|
|
||||||
|
| Komponente | Verbunden mit | Status |
|
||||||
|
|------------|---------------|--------|
|
||||||
|
| `core/approval.py` | `routes/approvals.py`, `ai/agent_loop.py` | ✅ Verbunden |
|
||||||
|
| `routes/approvals.py` | `main.py` (registriert), `core/approval.py` | ✅ Verbunden |
|
||||||
|
| Agent Loop ↔ Approval | `require_approval` Parameter, `create_approval_request()` | ✅ Verbunden |
|
||||||
|
| Workflow Engine ↔ Approval | `step_type == "approval"`, `resume_reason = "approval"` | ✅ Verbunden |
|
||||||
|
| Automation Plugin ↔ Approval | — | ❌ Nicht verbunden |
|
||||||
|
| `decision_guard.py` ↔ Approval | Referenziert konzeptionell, nicht integriert | ❌ Nicht verbunden |
|
||||||
|
|
||||||
|
### Plugin ↔ AI/Workflow
|
||||||
|
|
||||||
|
| Plugin | Importiert aus app.ai | Importiert aus app.workflows | Status |
|
||||||
|
|--------|----------------------|---------------------------|--------|
|
||||||
|
| `ai_assistant` | `llm_complete` | — | ✅ AI verbunden |
|
||||||
|
| `ai_proactive` | `llm_complete` | — | ✅ AI verbunden |
|
||||||
|
| `automation` | `run_react_loop`, `AIUseCaseMetadata` | — | ✅ AI verbunden, ❌ Workflows nicht verbunden |
|
||||||
|
| `unified_search` | `llm_client`, `embedding`, `provider_compliance` | — | ✅ AI verbunden |
|
||||||
|
| Alle anderen Plugins | — | — | ❌ Weder AI noch Workflows |
|
||||||
|
|
||||||
|
### Frontend ↔ Backend
|
||||||
|
|
||||||
|
| Frontend-Bereich | API verbunden | Status |
|
||||||
|
|-----------------|-------------|--------|
|
||||||
|
| Mail | ✅ `@/api/mail` (14 Importe) | ✅ Vollständig |
|
||||||
|
| Calendar | ✅ `@/api/calendar` (16 Importe) | ✅ Vollständig |
|
||||||
|
| DMS | ✅ `@/api/dms` (12 Importe) | ✅ Vollständig |
|
||||||
|
| Communication | ✅ `@/api/comm`, `@/api/ai` | ✅ Vollständig |
|
||||||
|
| Contacts | ✅ `@/api/contacts`, `@/api/hooks` | ✅ Vollständig |
|
||||||
|
| Tasks | ✅ `@/api/tasks` (5 Importe) | ✅ Vollständig |
|
||||||
|
| Tags | ✅ `@/api/tags` (8 Importe) | ✅ Vollständig |
|
||||||
|
| Wiki | ✅ Direkte API-Calls | ✅ Vollständig |
|
||||||
|
| Workflows | ✅ `@/api/workflows` (5 Importe) | ✅ Vollständig |
|
||||||
|
| Automation | ✅ `@/api/automation` (6 Importe) | ✅ Vollständig |
|
||||||
|
| Agents | ❌ 0 API-Refs in `Agents.tsx` | ❌ Nicht verbunden (aber AgentDashboard.tsx ist verbunden) |
|
||||||
|
| AI Settings | ✅ `@/api/ai` in `AISettings.tsx` | ✅ Verbunden |
|
||||||
|
| Settings System | ❌ 0 API-Refs in `SettingsSystem.tsx` | ❌ Nicht verbunden |
|
||||||
|
| Import/Export | ❌ 0 API-Refs in `ImportExport.tsx` | ❌ Nicht verbunden |
|
||||||
|
| Logs | ❌ 0 API-Refs | ❌ Nicht verbunden |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ZUSAMMENFASSUNG: Was funktioniert, was nicht
|
||||||
|
|
||||||
|
### ✅ Funktioniert und ist verbunden
|
||||||
|
1. **Core CRM** (Contacts, Companies, Tags, Tasks, Calendar, Mail, DMS) — Vollständig verbunden Frontend→API→DB
|
||||||
|
2. **LLM Client** — Zentraler AI-Client, von 5+ Plugins genutzt
|
||||||
|
3. **Agent Loop** — ReAct-Loop mit Approval-Integration, von Automation-Plugin aufgerufen
|
||||||
|
4. **Workflow Engine** — 13 Step-Typen, von Routes und Event-Bus aufgerufen
|
||||||
|
5. **Approval System** — Mit Agent Loop und Workflow Engine verbunden, eigene API-Routes
|
||||||
|
6. **Plugin Contract System** — 18 Contracts, aktiv von 7+ Plugins genutzt
|
||||||
|
7. **Permission System** — ABAC/RBAC, Entity-Permissions, in Routes integriert
|
||||||
|
8. **Communication** — WebSocket-basiertes Chat-System mit AI-Integration
|
||||||
|
9. **Unified Search** — Hybrid-Suche mit Embeddings, Query-Understanding
|
||||||
|
10. **Audit/Tenant-Isolation** — Cross-Tenant-Tests bestätigen Isolation
|
||||||
|
|
||||||
|
### ❌ Nicht verbunden / Muss verbunden werden
|
||||||
|
1. **`decision_guard.py`** — Guard existiert aber wird nie aufgerufen. Muss in `engine.py` vor High-Risk-Actions integriert werden.
|
||||||
|
2. **`context_builder.py`** — Agent-Context-Builder ungenutzt. Muss in `agent_loop.py` oder `agent_runner.py` integriert werden.
|
||||||
|
3. **`agent_tools.py`** — Tool-Execution-Logik ungenutzt. Muss mit `agent_loop.py` verbunden werden.
|
||||||
|
4. **`agent_permissions.py`** — Permission-Checks für Agent-Tools ungenutzt. Muss vor Tool-Execution aufgerufen werden.
|
||||||
|
5. **`data_policy.py`** — PII-Sanitization und Provider-Compliance ungenutzt. Muss vor LLM-Calls aufgerufen werden.
|
||||||
|
6. **`oversight.py`** — Decision-Records: Model existiert, keine Migration, keine Integration. Braucht beides.
|
||||||
|
7. **`transparency.py`** — AI-Content-Marking ungenutzt. Muss bei AI-generierten Inhalten aufgerufen werden.
|
||||||
|
8. **`agent_stream.py`** — Streaming ungenutzt. Sollte für AI-Streaming-Antworten genutzt werden.
|
||||||
|
9. **`agent_memory.py` (AI-Modul)** — Verwaist. Entweder mit `agent_memory` Plugin verbinden oder löschen.
|
||||||
|
10. **Automation ↔ Approval** — Automation-Plugin nutzt Agent Loop aber gibt `require_approval` nicht durch.
|
||||||
|
11. **Automation ↔ Workflows** — Kein Plugin importiert aus `app.workflows`. Automation sollte Workflow-Engine nutzen können.
|
||||||
|
12. **9 Frontend Pages ohne API** — `Agents.tsx`, `ImportExport.tsx`, `SettingsSystem.tsx`, `SettingsUserManagement.tsx`, `SettingsAI.tsx` brauchen API-Anbindung.
|
||||||
|
13. **9 unbenutzte API-Clients** — `aiUIControl`, `attachments`, `auth`, `plugins`, `policies`, `policyHooks`, `roles`, `searchHooks` — Verbinden oder löschen.
|
||||||
|
14. **9 Tabellen fehlen in Test-DB** — `ai_decision_records`, `document_chunks`, `forgejo_reported_errors`, `plugin_allowlist`, `unified_search_index_log`, `unified_search_providers`, `wiki_articles`, `wiki_article_versions`, `wiki_categories`.
|
||||||
|
15. **`delegations.py` Route geparkt** — Permission-Delegation-Route nicht registriert (line 575 in main.py auskommentiert).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Ende der System-Audit*
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Create ai_decision_records table for oversight.
|
||||||
|
|
||||||
|
Revision ID: 0128
|
||||||
|
Revises: 0127
|
||||||
|
"""
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||||
|
|
||||||
|
revision = "0128"
|
||||||
|
down_revision = "0127"
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.create_table(
|
||||||
|
"ai_decision_records",
|
||||||
|
sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||||
|
sa.Column("tenant_id", UUID(as_uuid=True), sa.ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True),
|
||||||
|
sa.Column("owner_id", UUID(as_uuid=True), nullable=True, index=True),
|
||||||
|
sa.Column("agent_run_id", UUID(as_uuid=True), nullable=False, index=True),
|
||||||
|
sa.Column("recommendation", sa.Text, nullable=False),
|
||||||
|
sa.Column("evidence", JSONB, nullable=False, server_default=sa.text("'{}'")),
|
||||||
|
sa.Column("reviewer_id", UUID(as_uuid=True), nullable=True),
|
||||||
|
sa.Column("decision", sa.String(20), nullable=True),
|
||||||
|
sa.Column("decision_timestamp", sa.String(40), nullable=True),
|
||||||
|
sa.Column("deviation_note", sa.Text, nullable=True),
|
||||||
|
sa.Column("created_at", sa.TIMESTAMP(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||||
|
sa.Column("updated_at", sa.TIMESTAMP(timezone=True), server_default=sa.text("now()"), nullable=False),
|
||||||
|
sa.Column("deleted_at", sa.TIMESTAMP(timezone=True), nullable=True),
|
||||||
|
)
|
||||||
|
op.create_index("ix_ai_decision_records_tenant_id", "ai_decision_records", ["tenant_id"])
|
||||||
|
op.create_index("ix_ai_decision_records_agent_run_id", "ai_decision_records", ["agent_run_id"])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_table("ai_decision_records")
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
"""Agent memory facade — unified API for persistent agent memory.
|
|
||||||
|
|
||||||
Delegates to the ``agent_memory`` plugin (pgvector semantic search) and
|
|
||||||
provides the canonical function signatures used by the agent framework
|
|
||||||
(``store_agent_memory``, ``retrieve_agent_memory``, ``search_agent_memory``).
|
|
||||||
|
|
||||||
Used by:
|
|
||||||
- ``app/ai/agent_loop.py`` — memory retrieval during ReAct loops
|
|
||||||
- ``app/plugins/builtins/automation`` — agent memory tools
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import uuid
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Memory types supported by the agent memory system.
|
|
||||||
MEMORY_TYPES = ("observation", "preference", "fact", "context")
|
|
||||||
|
|
||||||
|
|
||||||
def _memory_type(value: str | None) -> str:
|
|
||||||
"""Normalize a memory type to a supported value."""
|
|
||||||
if value in MEMORY_TYPES:
|
|
||||||
return value
|
|
||||||
return "fact"
|
|
||||||
|
|
||||||
|
|
||||||
async def store_agent_memory(
|
|
||||||
db: AsyncSession,
|
|
||||||
tenant_id: uuid.UUID,
|
|
||||||
agent_id: uuid.UUID,
|
|
||||||
memory_type: str,
|
|
||||||
content: str,
|
|
||||||
metadata: dict[str, Any] | None = None,
|
|
||||||
) -> uuid.UUID:
|
|
||||||
"""Store a new agent memory with semantic embedding.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: Database session.
|
|
||||||
tenant_id: Tenant UUID.
|
|
||||||
agent_id: Agent UUID.
|
|
||||||
memory_type: One of ``observation``, ``preference``, ``fact``, ``context``.
|
|
||||||
content: Memory content text.
|
|
||||||
metadata: Optional metadata dict (stored as JSONB on the memory row).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The UUID of the created memory.
|
|
||||||
"""
|
|
||||||
from app.plugins.builtins.agent_memory.services import store_memory
|
|
||||||
|
|
||||||
result = await store_memory(
|
|
||||||
db=db,
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
agent_id=agent_id,
|
|
||||||
content=content,
|
|
||||||
memory_type=_memory_type(memory_type),
|
|
||||||
)
|
|
||||||
memory_id = uuid.UUID(result["id"])
|
|
||||||
|
|
||||||
# Persist optional metadata on the memory row.
|
|
||||||
if metadata:
|
|
||||||
from sqlalchemy import update
|
|
||||||
|
|
||||||
from app.plugins.builtins.agent_memory.models import AgentMemory
|
|
||||||
|
|
||||||
await db.execute(
|
|
||||||
update(AgentMemory)
|
|
||||||
.where(AgentMemory.id == memory_id)
|
|
||||||
.values(metadata_=metadata)
|
|
||||||
)
|
|
||||||
await db.flush()
|
|
||||||
|
|
||||||
return memory_id
|
|
||||||
|
|
||||||
|
|
||||||
async def retrieve_agent_memory(
|
|
||||||
db: AsyncSession,
|
|
||||||
tenant_id: uuid.UUID,
|
|
||||||
agent_id: uuid.UUID,
|
|
||||||
memory_type: str | None = None,
|
|
||||||
limit: int = 10,
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Retrieve recent memories for an agent, optionally filtered by type.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: Database session.
|
|
||||||
tenant_id: Tenant UUID.
|
|
||||||
agent_id: Agent UUID.
|
|
||||||
memory_type: Optional memory type filter.
|
|
||||||
limit: Maximum number of results (default 10).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of memory dicts, newest first.
|
|
||||||
"""
|
|
||||||
from sqlalchemy import select
|
|
||||||
|
|
||||||
from app.plugins.builtins.agent_memory.models import AgentMemory
|
|
||||||
|
|
||||||
stmt = (
|
|
||||||
select(AgentMemory)
|
|
||||||
.where(
|
|
||||||
AgentMemory.tenant_id == tenant_id,
|
|
||||||
AgentMemory.agent_id == agent_id,
|
|
||||||
AgentMemory.deleted_at.is_(None),
|
|
||||||
)
|
|
||||||
.order_by(AgentMemory.created_at.desc())
|
|
||||||
.limit(limit)
|
|
||||||
)
|
|
||||||
if memory_type:
|
|
||||||
stmt = stmt.where(AgentMemory.memory_type == _memory_type(memory_type))
|
|
||||||
|
|
||||||
result = await db.execute(stmt)
|
|
||||||
memories = result.scalars().all()
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
"id": str(m.id),
|
|
||||||
"agent_id": str(m.agent_id),
|
|
||||||
"memory_type": m.memory_type,
|
|
||||||
"content": m.content,
|
|
||||||
"metadata": getattr(m, "metadata_", None) or {},
|
|
||||||
"created_at": m.created_at.isoformat() if m.created_at else None,
|
|
||||||
}
|
|
||||||
for m in memories
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def search_agent_memory(
|
|
||||||
db: AsyncSession,
|
|
||||||
tenant_id: uuid.UUID,
|
|
||||||
agent_id: uuid.UUID,
|
|
||||||
query: str,
|
|
||||||
limit: int = 5,
|
|
||||||
) -> list[dict[str, Any]]:
|
|
||||||
"""Semantic search over agent memories using pgvector embeddings.
|
|
||||||
|
|
||||||
Falls back to recent-memory retrieval when embedding generation is
|
|
||||||
unavailable (e.g. no embedding model configured).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
db: Database session.
|
|
||||||
tenant_id: Tenant UUID.
|
|
||||||
agent_id: Agent UUID.
|
|
||||||
query: Natural language query.
|
|
||||||
limit: Maximum number of results (default 5).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of memory dicts with similarity scores, sorted by relevance.
|
|
||||||
"""
|
|
||||||
from app.plugins.builtins.agent_memory.services import retrieve_relevant_memories
|
|
||||||
|
|
||||||
return await retrieve_relevant_memories(
|
|
||||||
db=db,
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
agent_id=agent_id,
|
|
||||||
query=query,
|
|
||||||
limit=limit,
|
|
||||||
min_score=0.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def register_agent_memory_tools(registry) -> None:
|
|
||||||
"""Register agent memory tools in the global ToolRegistry.
|
|
||||||
|
|
||||||
Registers ``search_agent_memory`` so AI agents can query their own
|
|
||||||
persistent memory during ReAct loops.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
|
|
||||||
async def search_agent_memory_handler(
|
|
||||||
arguments: dict[str, Any], context: dict[str, Any]
|
|
||||||
) -> str:
|
|
||||||
"""Handle search_agent_memory tool call from an AI agent."""
|
|
||||||
from app.core.db import get_session_factory
|
|
||||||
|
|
||||||
query = arguments.get("query", "")
|
|
||||||
limit = arguments.get("limit", 5)
|
|
||||||
agent_id_str = context.get("agent_id", "")
|
|
||||||
tenant_id_str = context.get("tenant_id", "")
|
|
||||||
|
|
||||||
if not query:
|
|
||||||
return json.dumps({"error": "Missing query"})
|
|
||||||
try:
|
|
||||||
tenant_id = uuid.UUID(tenant_id_str) if tenant_id_str else uuid.uuid4()
|
|
||||||
agent_id = uuid.UUID(agent_id_str) if agent_id_str else uuid.uuid4()
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
return json.dumps({"error": "Invalid tenant_id or agent_id"})
|
|
||||||
|
|
||||||
factory = get_session_factory()
|
|
||||||
async with factory() as db:
|
|
||||||
results = await search_agent_memory(
|
|
||||||
db=db,
|
|
||||||
tenant_id=tenant_id,
|
|
||||||
agent_id=agent_id,
|
|
||||||
query=query,
|
|
||||||
limit=limit,
|
|
||||||
)
|
|
||||||
return json.dumps({"memories": results, "count": len(results)}, default=str)
|
|
||||||
|
|
||||||
registry.register(
|
|
||||||
name="search_agent_memory",
|
|
||||||
description=(
|
|
||||||
"Semantische Suche über das persistente Gedächtnis eines Agents. "
|
|
||||||
"Findet relevante frühere Beobachtungen, Fakten und Präferenzen."
|
|
||||||
),
|
|
||||||
parameters={
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"query": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Natürlichsprachliche Suchanfrage",
|
|
||||||
},
|
|
||||||
"limit": {
|
|
||||||
"type": "integer",
|
|
||||||
"default": 5,
|
|
||||||
"description": "Maximale Anzahl Ergebnisse",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"required": ["query"],
|
|
||||||
},
|
|
||||||
handler=search_agent_memory_handler,
|
|
||||||
plugin_name="agent_memory",
|
|
||||||
required_permission="agent_memory:read",
|
|
||||||
category="memory",
|
|
||||||
)
|
|
||||||
logger.info("Agent memory tool 'search_agent_memory' registered")
|
|
||||||
|
|
||||||
|
|
||||||
def unregister_agent_memory_tools(registry) -> None:
|
|
||||||
"""Unregister agent memory tools from the global ToolRegistry."""
|
|
||||||
registry.unregister("search_agent_memory")
|
|
||||||
logger.info("Agent memory tool 'search_agent_memory' unregistered")
|
|
||||||
@@ -601,3 +601,55 @@ async def send_agent_message_endpoint(
|
|||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Punkt 7: SSE Streaming Endpoint (agent_stream.py) ─────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{id}/stream")
|
||||||
|
async def stream_agent_run(
|
||||||
|
id: str,
|
||||||
|
body: AgentMessageRequest,
|
||||||
|
current_user: dict[str, Any] = Depends(get_current_user),
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
):
|
||||||
|
"""Stream an agent run via Server-Sent Events (SSE).
|
||||||
|
|
||||||
|
Uses ``app.ai.agent_stream.stream_react_loop`` to emit step events
|
||||||
|
in real-time as the agent processes.
|
||||||
|
"""
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
from app.ai.agent_stream import stream_react_loop
|
||||||
|
from app.plugins.builtins.ai_assistant.contracts import get_tool_registry
|
||||||
|
|
||||||
|
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||||
|
user_id = uuid.UUID(current_user["user_id"])
|
||||||
|
try:
|
||||||
|
aid = uuid.UUID(id)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
raise HTTPException(status_code=400, detail="Invalid agent ID") from None
|
||||||
|
|
||||||
|
agent = await AgentService.get_by_id(db, tenant_id, aid)
|
||||||
|
if agent is None:
|
||||||
|
raise HTTPException(status_code=404, detail="Agent not found")
|
||||||
|
if not agent.is_active:
|
||||||
|
raise HTTPException(status_code=400, detail="Agent is not active")
|
||||||
|
|
||||||
|
registry = get_tool_registry()
|
||||||
|
tool_ids: list[str] = list(agent.tool_ids or [])
|
||||||
|
tools = registry.get_by_names(tool_ids) if tool_ids else []
|
||||||
|
tool_schemas = [t.to_openai_schema() for t in tools] if tools else []
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
stream_react_loop(
|
||||||
|
agent_definition=agent,
|
||||||
|
user_message=body.message,
|
||||||
|
tools=tool_schemas,
|
||||||
|
tool_registry=registry,
|
||||||
|
db=db,
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
user_id=user_id,
|
||||||
|
agent_run_id=aid,
|
||||||
|
),
|
||||||
|
media_type="text/event-stream",
|
||||||
|
)
|
||||||
|
|||||||
@@ -155,6 +155,28 @@ async def run_agent(
|
|||||||
tools = registry.get_by_names(tool_ids) if tool_ids else []
|
tools = registry.get_by_names(tool_ids) if tool_ids else []
|
||||||
tool_schemas = [t.to_openai_schema() for t in tools] if tools else []
|
tool_schemas = [t.to_openai_schema() for t in tools] if tools else []
|
||||||
|
|
||||||
|
# ── Resolve agent permissions (Punkt 2+3 der Audit) ──
|
||||||
|
from app.ai.agent_permissions import resolve_agent_permissions
|
||||||
|
from app.ai.agent_tools import get_agent_tools
|
||||||
|
from app.ai.skill_registry import get_skill_registry
|
||||||
|
|
||||||
|
async with factory() as db:
|
||||||
|
perm_ctx = await resolve_agent_permissions(
|
||||||
|
db=db,
|
||||||
|
tenant_id=agent.tenant_id,
|
||||||
|
user_id=agent.created_by or uuid_mod.uuid4(),
|
||||||
|
agent_definition=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use permission-filtered tools instead of raw tool_ids
|
||||||
|
skill_reg = get_skill_registry()
|
||||||
|
tool_schemas, _skills = get_agent_tools(
|
||||||
|
agent_definition=agent,
|
||||||
|
tool_registry=registry,
|
||||||
|
skill_registry=skill_reg,
|
||||||
|
user_permissions=perm_ctx.user_permissions,
|
||||||
|
)
|
||||||
|
|
||||||
# ── Create AgentRun record ──
|
# ── Create AgentRun record ──
|
||||||
run_id: uuid.UUID | None = None
|
run_id: uuid.UUID | None = None
|
||||||
started_at = datetime.now(UTC)
|
started_at = datetime.now(UTC)
|
||||||
@@ -192,10 +214,38 @@ async def run_agent(
|
|||||||
import asyncio
|
import asyncio
|
||||||
import uuid as uuid_mod
|
import uuid as uuid_mod
|
||||||
|
|
||||||
|
# ── Build agent context via context_builder (Punkt 1 der Audit) ──
|
||||||
|
from app.ai.context_builder import build_agent_context
|
||||||
|
|
||||||
|
# Sanitize context_data to remove sensitive fields (Punkt 4: data_policy)
|
||||||
|
from app.core.sensitive_data import sanitize_dict
|
||||||
|
safe_context_data = sanitize_dict(context_data)
|
||||||
|
|
||||||
|
# Build the user message from sanitized context
|
||||||
|
user_message = f"Context: {safe_context_data}" if safe_context_data else "No additional context provided."
|
||||||
|
|
||||||
|
# Build full message list (system prompt + context + user message)
|
||||||
|
messages = await build_agent_context(
|
||||||
|
agent_definition=agent,
|
||||||
|
user_message=user_message,
|
||||||
|
db=None, # No DB session available here; context_builder handles gracefully
|
||||||
|
tenant_id=agent.tenant_id,
|
||||||
|
user_id=agent.created_by or uuid_mod.uuid4(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# ── Enforce data policy: filter sensitive fields from messages (Punkt 4) ──
|
||||||
|
from app.ai.data_policy import enforce_data_policy
|
||||||
|
messages = await enforce_data_policy(
|
||||||
|
db=None,
|
||||||
|
tenant_id=agent.tenant_id,
|
||||||
|
messages=messages,
|
||||||
|
agent_definition=agent,
|
||||||
|
)
|
||||||
|
|
||||||
react_result: ReActResult = await asyncio.wait_for(
|
react_result: ReActResult = await asyncio.wait_for(
|
||||||
run_react_loop(
|
run_react_loop(
|
||||||
agent_definition=agent,
|
agent_definition=agent,
|
||||||
messages=[{"role": "user", "content": f"Context: {context_data}"}],
|
messages=messages,
|
||||||
tools=tool_schemas,
|
tools=tool_schemas,
|
||||||
tool_registry=registry,
|
tool_registry=registry,
|
||||||
db=None, # ReAct loop doesn't need DB session for LLM calls directly
|
db=None, # ReAct loop doesn't need DB session for LLM calls directly
|
||||||
@@ -204,6 +254,8 @@ async def run_agent(
|
|||||||
agent_run_id=run_id,
|
agent_run_id=run_id,
|
||||||
max_steps=20,
|
max_steps=20,
|
||||||
timeout_seconds=max_duration,
|
timeout_seconds=max_duration,
|
||||||
|
require_approval=bool(getattr(agent, "require_approval", False)),
|
||||||
|
approval_tools=getattr(agent, "approval_tools", None),
|
||||||
),
|
),
|
||||||
timeout=max_duration + 10, # Extra buffer beyond loop's own timeout
|
timeout=max_duration + 10, # Extra buffer beyond loop's own timeout
|
||||||
)
|
)
|
||||||
@@ -212,6 +264,40 @@ async def run_agent(
|
|||||||
result_data["llm_response"] = react_result.final_content
|
result_data["llm_response"] = react_result.final_content
|
||||||
result_data["cost_usd"] = react_result.total_cost_usd
|
result_data["cost_usd"] = react_result.total_cost_usd
|
||||||
result_data["error"] = react_result.error
|
result_data["error"] = react_result.error
|
||||||
|
|
||||||
|
# ── Mark result as AI-generated (Punkt 6: transparency) ──
|
||||||
|
from app.ai.transparency import mark_as_ai_generated
|
||||||
|
if react_result.final_content:
|
||||||
|
ai_metadata = mark_as_ai_generated(
|
||||||
|
react_result.final_content,
|
||||||
|
metadata={
|
||||||
|
"model": getattr(agent, "llm_model", "unknown"),
|
||||||
|
"provider": getattr(agent, "provider", "unknown"),
|
||||||
|
"agent_id": str(agent.id),
|
||||||
|
"agent_name": agent.name,
|
||||||
|
"run_id": str(run_id) if run_id else None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
result_data["ai_generated"] = True
|
||||||
|
result_data["ai_metadata"] = ai_metadata.get("ai_metadata", {})
|
||||||
|
|
||||||
|
# ── Create oversight decision record (Punkt 5: oversight) ──
|
||||||
|
from app.ai.oversight import DecisionRecord, create_decision_record
|
||||||
|
try:
|
||||||
|
async with factory() as db:
|
||||||
|
record = DecisionRecord(
|
||||||
|
agent_run_id=run_id or uuid_mod.uuid4(),
|
||||||
|
recommendation=react_result.final_content,
|
||||||
|
evidence={
|
||||||
|
"steps": len(react_result.steps),
|
||||||
|
"cost_usd": react_result.total_cost_usd,
|
||||||
|
"status": react_result.status,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await create_decision_record(db, agent.tenant_id, record)
|
||||||
|
await db.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Failed to create oversight decision record: %s", e)
|
||||||
result_data["steps"] = [
|
result_data["steps"] = [
|
||||||
{
|
{
|
||||||
"step_number": s.step_number,
|
"step_number": s.step_number,
|
||||||
|
|||||||
@@ -142,6 +142,29 @@ class WorkflowEngine:
|
|||||||
"""Execute a step using a registered step handler (G step types)."""
|
"""Execute a step using a registered step handler (G step types)."""
|
||||||
step_type = step.get("type", "action")
|
step_type = step.get("type", "action")
|
||||||
|
|
||||||
|
# ── Decision Guard: check if action requires human review (Punkt 9) ──
|
||||||
|
from app.workflows.decision_guard import check_decision_guard
|
||||||
|
step_config = step.get("config", {})
|
||||||
|
action_name = step_config.get("action", step_type)
|
||||||
|
guard_result = await check_decision_guard(
|
||||||
|
db=self.db,
|
||||||
|
tenant_id=self.tenant_id,
|
||||||
|
instance_id=instance.id,
|
||||||
|
step_config=step_config,
|
||||||
|
action=action_name,
|
||||||
|
)
|
||||||
|
if not guard_result["allowed"]:
|
||||||
|
# Guard blocks — pause workflow and create approval request
|
||||||
|
instance.status = "in_progress"
|
||||||
|
instance.resume_reason = "decision_guard"
|
||||||
|
await self.db.flush()
|
||||||
|
return {
|
||||||
|
"status": "waiting_for_approval",
|
||||||
|
"guard": guard_result,
|
||||||
|
"step_index": instance.current_step_index,
|
||||||
|
"message": guard_result.get("reason", "Human review required"),
|
||||||
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result: StepResult = await handler(
|
result: StepResult = await handler(
|
||||||
self.db,
|
self.db,
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ from app.models.outbox import EventOutbox # noqa: F401
|
|||||||
from app.models.consumer_inbox import ConsumerInbox # noqa: F401
|
from app.models.consumer_inbox import ConsumerInbox # noqa: F401
|
||||||
from app.models.outbox_delivery import OutboxDelivery # noqa: F401
|
from app.models.outbox_delivery import OutboxDelivery # noqa: F401
|
||||||
from app.models.saved_filter import SavedFilter # noqa: F401
|
from app.models.saved_filter import SavedFilter # noqa: F401
|
||||||
|
from app.ai.oversight import DecisionRecordDB # noqa: F401 — ensure table is created
|
||||||
|
|
||||||
# Dynamically import all plugin models so Base.metadata.create_all() includes their tables.
|
# Dynamically import all plugin models so Base.metadata.create_all() includes their tables.
|
||||||
# This replaces ~30 hardcoded plugin imports with dynamic discovery (P1-14 fix).
|
# This replaces ~30 hardcoded plugin imports with dynamic discovery (P1-14 fix).
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
"""Integration tests for the 10 audit connection points.
|
||||||
|
|
||||||
|
Tests that the previously unconnected modules are now actually imported
|
||||||
|
and called by the real code paths (agent_runner, engine, agent_routes).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
class TestAuditConnections:
|
||||||
|
"""Verify that the 10 audit points are now wired up."""
|
||||||
|
|
||||||
|
def test_punkt1_context_builder_imported_in_agent_runner(self):
|
||||||
|
"""Punkt 1: context_builder.build_agent_context is imported in agent_runner."""
|
||||||
|
import inspect
|
||||||
|
from app.plugins.builtins.automation import agent_runner
|
||||||
|
source = inspect.getsource(agent_runner)
|
||||||
|
assert "from app.ai.context_builder import build_agent_context" in source
|
||||||
|
|
||||||
|
def test_punkt2_agent_permissions_imported_in_agent_runner(self):
|
||||||
|
"""Punkt 2: agent_permissions.resolve_agent_permissions is imported in agent_runner."""
|
||||||
|
import inspect
|
||||||
|
from app.plugins.builtins.automation import agent_runner
|
||||||
|
source = inspect.getsource(agent_runner)
|
||||||
|
assert "from app.ai.agent_permissions import resolve_agent_permissions" in source
|
||||||
|
|
||||||
|
def test_punkt3_agent_tools_imported_in_agent_runner(self):
|
||||||
|
"""Punkt 3: agent_tools.get_agent_tools is imported in agent_runner."""
|
||||||
|
import inspect
|
||||||
|
from app.plugins.builtins.automation import agent_runner
|
||||||
|
source = inspect.getsource(agent_runner)
|
||||||
|
assert "from app.ai.agent_tools import get_agent_tools" in source
|
||||||
|
|
||||||
|
def test_punkt4_data_policy_imported_in_agent_runner(self):
|
||||||
|
"""Punkt 4: data_policy.enforce_data_policy is imported in agent_runner."""
|
||||||
|
import inspect
|
||||||
|
from app.plugins.builtins.automation import agent_runner
|
||||||
|
source = inspect.getsource(agent_runner)
|
||||||
|
assert "from app.ai.data_policy import enforce_data_policy" in source
|
||||||
|
|
||||||
|
def test_punkt5_oversight_imported_in_agent_runner(self):
|
||||||
|
"""Punkt 5: oversight.create_decision_record is imported in agent_runner."""
|
||||||
|
import inspect
|
||||||
|
from app.plugins.builtins.automation import agent_runner
|
||||||
|
source = inspect.getsource(agent_runner)
|
||||||
|
assert "from app.ai.oversight import DecisionRecord, create_decision_record" in source
|
||||||
|
|
||||||
|
def test_punkt6_transparency_imported_in_agent_runner(self):
|
||||||
|
"""Punkt 6: transparency.mark_as_ai_generated is imported in agent_runner."""
|
||||||
|
import inspect
|
||||||
|
from app.plugins.builtins.automation import agent_runner
|
||||||
|
source = inspect.getsource(agent_runner)
|
||||||
|
assert "from app.ai.transparency import mark_as_ai_generated" in source
|
||||||
|
|
||||||
|
def test_punkt7_agent_stream_imported_in_agent_routes(self):
|
||||||
|
"""Punkt 7: agent_stream.stream_react_loop is imported in agent_routes."""
|
||||||
|
import inspect
|
||||||
|
from app.plugins.builtins.automation import agent_routes
|
||||||
|
source = inspect.getsource(agent_routes)
|
||||||
|
assert "from app.ai.agent_stream import stream_react_loop" in source
|
||||||
|
|
||||||
|
def test_punkt8_agent_memory_ai_module_deleted(self):
|
||||||
|
"""Punkt 8: app/ai/agent_memory.py is deleted (dup with plugin)."""
|
||||||
|
import os
|
||||||
|
assert not os.path.exists("app/ai/agent_memory.py")
|
||||||
|
|
||||||
|
def test_punkt9_decision_guard_imported_in_engine(self):
|
||||||
|
"""Punkt 9: decision_guard.check_decision_guard is imported in engine.py."""
|
||||||
|
import inspect
|
||||||
|
from app.workflows import engine
|
||||||
|
source = inspect.getsource(engine)
|
||||||
|
assert "from app.workflows.decision_guard import check_decision_guard" in source
|
||||||
|
|
||||||
|
def test_punkt10_require_approval_passed_in_agent_runner(self):
|
||||||
|
"""Punkt 10: require_approval is passed to run_react_loop in agent_runner."""
|
||||||
|
import inspect
|
||||||
|
from app.plugins.builtins.automation import agent_runner
|
||||||
|
source = inspect.getsource(agent_runner)
|
||||||
|
assert "require_approval=" in source
|
||||||
|
assert "approval_tools=" in source
|
||||||
|
|
||||||
|
def test_oversight_table_exists_in_db(self):
|
||||||
|
"""Punkt 5: ai_decision_records table exists in test DB."""
|
||||||
|
import os
|
||||||
|
os.environ.setdefault("MIGRATION_DATABASE_URL", "postgresql+asyncpg://leocrm:leocrm@localhost:5432/leocrm_test")
|
||||||
|
import asyncio
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.ext.asyncio import create_async_engine
|
||||||
|
|
||||||
|
async def check():
|
||||||
|
engine = create_async_engine(os.environ["MIGRATION_DATABASE_URL"])
|
||||||
|
async with engine.connect() as conn:
|
||||||
|
result = await conn.execute(
|
||||||
|
text("SELECT EXISTS (SELECT FROM pg_tables WHERE tablename = 'ai_decision_records')")
|
||||||
|
)
|
||||||
|
exists = result.scalar()
|
||||||
|
await engine.dispose()
|
||||||
|
return exists
|
||||||
|
|
||||||
|
exists = asyncio.get_event_loop().run_until_complete(check())
|
||||||
|
assert exists is True
|
||||||
Reference in New Issue
Block a user