Update Readme #20

Merged
meulilivio merged 1 commits from func_improvments into main 2026-05-27 08:00:08 +02:00

744
README.md
View File

@ -1,21 +1,21 @@
# AISE AI Code Editor — Technische Dokumentation
AI-Supported Lightweight Code Editor built with Streamlit (AISE501 Spring 2026)
Diese Datei enthält die ausführliche technische Dokumentation des Projekts. Für eine Kurzübersicht siehe `README.md`.
KI-unterstützter Lightweight Code Editor auf Basis von Streamlit (AISE501 Spring 2026).
---
## Inhaltsverzeichnis
1. [Projektstruktur](#projektstruktur)
2. [Frontend](#frontend)
3. [Backend Manager](#backend-manager)
4. [Backend Agent (MCP-System)](#backend-agent-mcp-system)
5. [MCP-Server-Konfiguration](#mcp-server-konfiguration)
6. [Tests](#tests)
7. [Setup](#setup)
8. [Architektur-Übersicht](#architektur-übersicht)
2. [Schnellstart](#schnellstart)
3. [Frontend](#frontend)
4. [Backend Manager](#backend-manager)
5. [Backend Agent (MCP-System)](#backend-agent-mcp-system)
6. [MCP-Server-Konfiguration](#mcp-server-konfiguration)
7. [Architektur-Übersicht](#architektur-übersicht)
8. [Wichtige Designentscheidungen](#wichtige-designentscheidungen)
9. [Tests](#tests)
10. [Umgebungsvariablen](#umgebungsvariablen)
---
@ -24,288 +24,52 @@ Diese Datei enthält die ausführliche technische Dokumentation des Projekts. F
```
AISE_AIAgent/
├── frontend/ # Streamlit UI-Komponenten
│ ├── app.py # Haupteinstiegspunkt der Streamlit-App
│ ├── state.py # Session-State-Verwaltung
│ ├── sidebar.py # Datei-Navigation (Sidebar)
│ ├── editor.py # Code-Editor-Pane
│ └── chat.py # Chat-Interface
│ ├── app.py # Einstiegspunkt der App; Seitenkonfiguration + Routing
│ ├── state.py # Zentrale Session-State-Initialisierung
│ ├── sidebar.py # Datei-Explorer + Navigations-Radio
│ ├── editor.py # Ace-Editor-Tabs + Ausführungs-Output
│ └── chat.py # Chat-Interface + Agent-Mode-UI
├── backend/ # Backend-Logik
│ ├── managers/ # Business-Logik für UI-Operationen
│ │ ├── file_manager.py # Datei-CRUD (lesen, schreiben, listen)
│ │ ├── chat_manager.py # AI-Chat-Verwaltung und -History
│ │ ├── system_prompter.py # System-Prompts und Kontext-Injektion
│ │ ├── search_manager.py # Web-Suche (DuckDuckGo)
│ │ ├── execution_engine.py # Code-Ausführung und Sandboxing
│ │ └── debug_logger.py # Logging, Fehlerbehandlung, Debug-Ausgaben
├── backend/
│ ├── managers/ # Business-Logik, direkt vom Frontend aufgerufen
│ │ ├── file_manager.py # Workspace-CRUD mit Path-Traversal-Schutz
│ │ ├── chat_manager.py # LLM-API-Wrapper + Sliding-Window-History
│ │ ├── system_prompter.py # Kontextbewusste System-Prompt-Generierung
│ │ ├── search_manager.py # DuckDuckGo-Websuche + Seitenabruf
│ │ ├── execution_engine.py # Subprocess-basierte Code-Ausführung (Python, LaTeX)
│ │ └── debug_logger.py # Rotierende Logdatei + Fehler-Aggregation
│ │
│ └── agent/ # Autonomes AI-Agent-System (MCP-basiert)
│ ├── coding_agent.py # Haupt-Agent-Loop (Plan-Act-Observe)
│ ├── mcp_server_adapter.py # MCP-Adapter: verbindet Agent mit MCP-Servern
│ ├── mcp_server_adapter_RAG.py # MCP-Adapter mit RAG-basierter Tool-Auswahl
│ ├── mcp_server_config.json # Konfiguration der MCP-Server (Startbefehle)
│ └── servers/ # MCP-Server-Implementierungen
│ ├── mcp_server_code_execution.py # Tool: Python-Code ausführen
│ ├── mcp_server_file_search.py # Tool: Dateien suchen und lesen
│ └── mcp_server_web_search.py # Tool: Web-Suche via DuckDuckGo
│ └── agent/ # Autonomes KI-Agenten-System (MCP-basiert)
│ ├── coding_agent.py # Plan→Aktion→Beobachten-Schleife
│ ├── mcp_server_adapter.py # Verbindet den Agenten mit MCP-Tool-Servern
│ ├── mcp_server_config.json # Welche MCP-Server gestartet werden (Pfade + Befehle)
│ └── servers/ # MCP-Server-Implementierungen (stdio-Transport)
│ ├── mcp_server_code_execution.py # Tool: Sandbox-Python-Ausführung + Linting
│ ├── mcp_server_file_search.py # Tool: Workspace-Datei lesen/schreiben/suchen
│ └── mcp_server_web_search.py # Tool: DuckDuckGo-Suche + Seitenabruf
├── tests/ # Unit-Tests (pytest)
│ ├── conftest.py # Globale Test-Fixtures und MCP-Mocks
│ ├── test_file_manager.py
├── tests/ # pytest-Unit-Tests
│ ├── conftest.py # Globaler MCP-Mock (keine echten Subprozesse in Tests)
│ ├── test_chat_manager.py
│ ├── test_execution_engine.py
│ ├── test_coding_agent.py
│ ├── test_debug_logger.py
│ ├── test_system_prompter.py
│ ├── test_execution_engine.py
│ ├── test_file_manager.py
│ ├── test_mcp_server_code_execution.py
│ ├── test_mcp_server_file_search.py
│ └── test_mcp_server_web_search.py
│ ├── test_mcp_server_web_search.py
│ ├── test_search_manager.py
│ └── test_system_prompter.py
├── workspace/ # Agent-Sandbox (isoliertes Arbeitsverzeichnis)
└── .env.example # Vorlage für Umgebungsvariablen
├── workspace/ # Sandbox-Verzeichnis für Agent- und Editor-Dateien
├── logs/ # Rotierende Logdateien (app.log, errors.log)
├── .env # Lokale Umgebungsvariablen (nicht eingecheckt)
└── .env.example # Vorlage für erforderliche Umgebungsvariablen
```
---
## Frontend
Das Frontend besteht aus Streamlit-Komponenten, die zusammen eine interaktive Code-Editor-Oberfläche bilden.
### `app.py`
Haupteinstiegspunkt der Applikation. Orchestriert das Layout und initialisiert alle UI-Komponenten (Sidebar, Editor, Chat).
### `state.py`
Zentralisierte Verwaltung des Streamlit Session-State. Stellt sicher, dass alle Komponenten denselben Zustand (geöffnete Datei, Chat-History, Agent-Status) teilen.
### `sidebar.py`
Datei-Browser und Projekt-Navigation. Erlaubt das Durchsuchen des Workspaces und das Öffnen von Dateien im Editor.
### `editor.py`
Code-Editor-Pane mit Syntax-Highlighting. Ermöglicht das Bearbeiten und Speichern von Code-Dateien direkt im Browser.
### `chat.py`
Chat-Interface für den KI-Assistenten. Zeigt die Konversations-History und ermöglicht Eingaben an das AI-Modell.
---
## Backend Manager
Die Manager-Klassen kapseln die Business-Logik und werden direkt vom Frontend aufgerufen.
### `file_manager.py`
Stellt CRUD-Operationen auf dem Workspace-Verzeichnis bereit:
- Dateien lesen, schreiben, umbenennen, löschen
- Verzeichnisstruktur auflisten
- Sichere Pfadvalidierung (verhindert Path-Traversal)
**Designentscheidung — `list_files()` vs. `get_file_tree()`:**
Die Projektspezifikation nennt `list_files()` als `FileManager`-Methode. Im vorliegenden Design wurde bewusst `get_file_tree()` implementiert, da das Frontend eine verschachtelte Baumstruktur benötigt (für den interaktiven File Explorer in der Sidebar). Eine flache Liste würde die Navigation nicht unterstützen. Für den Agent Mode übernimmt der MCP-Server `mcp_server_file_search.py` die Dateisuche — die Funktionalität ist damit im System vorhanden, nur architektonisch sauber getrennt.
### `chat_manager.py`
Verwaltet AI-Chat-Interaktionen:
- Aufbau und Verwaltung der Chat-History
- Senden von Nachrichten an das AI-Modell
- Formatierung von System- und User-Nachrichten
**Designentscheidung — Fehler-Output im normalen Chat:**
Laufzeitfehler und stderr-Output werden im normalen Chat bewusst **nicht automatisch** in den Chat-Kontext injiziert. Stattdessen gibt es den "Debug with AI"-Button im Editor, über den der User selbst entscheidet wann er die AI einschalten möchte. Dies verhindert, dass die Chat-History mit ungewollten Fehlermeldungen geflutet wird. Im Agent Mode wird dies anders gelöst: dort landet jeder Execution-Fehler automatisch als Observation im Plan-Act-Observe-Loop und der Agent replant ohne User-Eingriff.
### `system_prompter.py`
Generiert kontextreiche System-Prompts für den AI-Assistenten:
- Injektion von aktuellem Dateiinhalt als Kontext
- Steuerung des AI-Verhaltens (Coding-Assistent-Persona)
### `execution_engine.py`
Führt Python-Code sicher aus:
- Subprocess-basierte Code-Ausführung
- Timeout-Schutz und Output-Capture
- Fehler- und Exception-Handling
### `debug_logger.py`
Logging und Fehler-Tracking:
- Formatierte Log-Ausgaben für Debugging
- `format_debug_output(output)` formatiert den Execution-Output (`stdout`, `stderr`, `return_code`) in einen einheitlichen String für die UI-Anzeige und den AI-Chat-Kontext
**Designentscheidung — `log_error()` nicht implementiert:**
Die Projektspezifikation nennt `log_error()` als `DebugLogger`-Methode. Diese wurde bewusst nicht als separate Methode implementiert, da Python's eingebautes `logging`-Modul diese Funktionalität mit `logger.error()` bereits vollständig abdeckt. Im gesamten Projekt wird konsistent `logger = get_logger(__name__)` gefolgt von `logger.error(...)` verwendet — eine eigene Wrapper-Methode wäre toter Code ohne Mehrwert.
### `search_manager.py`
Web-Suche für den KI-Assistenten via DuckDuckGo:
- Nutzt die `ddgs`-Bibliothek (DuckDuckGo Search) für API-freie Websuche
- Gibt strukturierte Suchergebnisse zurück (Titel, URL, Snippet)
- Wird vom Chat-Manager aufgerufen, wenn der Assistent externe Dokumentation oder Code-Beispiele benötigt
- Keine API-Key-Konfiguration notwendig (da DuckDuckGo öffentlich zugänglich ist)
---
## Backend Agent (MCP-System)
Der Agent ist ein autonomes System, das komplexe Coding-Aufgaben selbstständig löst. Er kommuniziert mit externen Tool-Servern über das **Model Context Protocol (MCP)**.
### `coding_agent.py`
Implementiert den Plan-Act-Observe-Loop:
1. **Plan**: Das AI-Modell wählt das nächste Tool und Argumente
2. **Act**: Das Tool wird via MCP-Adapter aufgerufen (nach User-Bestätigung)
3. **Observe**: Das Ergebnis wird in die Message-History eingefügt
4. Der Loop wiederholt sich bis zur Fertigstellung oder einem `done`-Tool-Aufruf
Wichtige Klassen und Funktionen:
- `CodingAgent`: Haupt-Klasse mit `start_task()`, `propose_next_action()`, `approve()`, `reject()`
- `truncate_result()`: Kürzt lange Tool-Outputs bevor sie in die History gehen
- `trim_messages()`: Entfernt alte Turns aus der History wenn das Kontextfenster voll wird
- `_strip_code_fences()`: Bereinigt Markdown-Fences aus LLM-JSON-Antworten
Konstanten: `MAX_ITERATIONS`, `MAX_RESULT_LENGTH`, `MAX_HISTORY_CHARS`
### `mcp_server_adapter.py`
Verbindet den Coding-Agent mit den MCP-Tool-Servern:
- Liest `mcp_server_config.json` und startet die konfigurierten Server als Subprozesse
- Baut MCP-Sessions via `stdio_client` auf
- Registriert alle verfügbaren Tools aus allen Servern in einem zentralen `tool_registry`
- Delegiert Tool-Aufrufe an den richtigen Server via `call_tool()`
Hauptmethoden:
- `initialize_all_servers()`: Startet alle Server und baut Sessions auf
- `get_all_tools()`: Gibt alle registrierten Tool-Definitionen zurück
- `call_tool(tool_name, arguments)`: Führt ein Tool auf dem zuständigen Server aus
### `mcp_server_adapter_RAG.py`
Erweiterter MCP-Adapter mit semantischer Tool-Auswahl via Retrieval-Augmented Generation (RAG):
**Motivation**: Bei vielen MCP-Tools kann das LLM-Kontextfenster überfüllt werden, wenn alle Tool-Definitionen mitgesendet werden. Der RAG-Adapter löst dies durch semantische Vorauswahl.
**Funktionsweise**:
1. Beim Initialisieren werden alle Tool-Beschreibungen mit `SentenceTransformer('all-MiniLM-L6-v2')` in Embeddings umgewandelt
2. Bei jedem Agent-Schritt wird der aktuelle Task als Query kodiert
3. Cosine-Similarity zwischen Query- und Tool-Embeddings bestimmt die `top_k` relevantesten Tools
4. Nur diese Tools werden dem LLM als verfügbare Aktionen präsentiert
Hauptmethoden:
- `initialize_all_sessions()`: Startet Server, baut Sessions auf, erstellt Embedding-Index
- `get_relevant_tools(query, top_k=5)`: Gibt die `top_k` semantisch ähnlichsten Tools zurück
- `call_tool(tool_name, arguments)`: Findet den zuständigen Server und führt das Tool aus
- `shutdown_all_sessions()`: Schliesst alle offenen MCP-Sessions sauber
Abhängigkeit: `sentence-transformers`, `numpy`
**Hinweis**: Diese Klasse befindet sich noch in der Entwicklung (Work in Progress). Es gibt bekannte Bugs (z.B. Tippfehler `commanf` statt `command`, falsche Verwendung von `result.get()` vs. `result.tools`).
---
## MCP-Server-Konfiguration
### Format: `mcp_server_config.json`
Die Datei `backend/agent/mcp_server_config.json` definiert, welche MCP-Server der Adapter starten soll. Das Format ist ein JSON-Objekt, wobei jeder Key ein frei wählbarer Servername ist:
```json
{
"ServerName": {
"command": "py",
"args": ["servers/mcp_server_datei.py"],
"env": {
"API_KEY": "optional_key"
}
}
}
```
| Feld | Pflicht | Beschreibung |
|-----------|---------|--------------|
| `command` | Ja | Ausführbares Programm (z.B. `py`, `python3`, `node`) |
| `args` | Ja | Argumente als Array (Pfad zum Server-Script) |
| `env` | Nein | Umgebungsvariablen für den Serverprozess |
### Aktuelle Server
```json
{
"FileSearchServer": {
"command": "py",
"args": ["servers/mcp_server_file_search.py"]
},
"WebSearchServer": {
"command": "py",
"args": ["servers/mcp_server_web_search.py"],
"env": { "DDGS_API_KEY": "your_ddgs_api_key_here" }
},
"CodeExecutionServer": {
"command": "py",
"args": ["servers/mcp_server_code_execution.py"]
}
}
```
### Neuen MCP-Server hinzufügen
1. Neues Server-Script in `backend/agent/servers/` erstellen (MCP-konformes Python-Script)
2. Eintrag in `mcp_server_config.json` ergänzen:
```json
"MeinNeuerServer": {
"command": "py",
"args": ["servers/mcp_server_mein_tool.py"]
}
```
3. Der Adapter erkennt den neuen Server beim nächsten Start automatisch und registriert seine Tools
---
## Tests
### Tests ausführen
```bash
# Alle Tests ausführen
pytest tests/ -v
# Einzelnes Test-Modul ausführen
pytest tests/test_coding_agent.py -v
# Tests mit Kurzausgabe
pytest tests/
```
### Teststruktur
Die Tests liegen in `tests/` und folgen dem Muster `test_<modulname>.py`.
#### `conftest.py`
Globale Pytest-Konfiguration. Patcht den `MCPToolAdapter` auf `sys.modules`-Ebene, bevor irgendein Test-Modul importiert wird. Dadurch werden beim Import von `coding_agent` keine echten MCP-Subprozesse gestartet. Der Mock-Adapter liefert sofort leere Ergebnisse zurück.
#### Was wird getestet
| Test-Datei | Getestetes Modul | Schwerpunkt |
|---|---|---|
| `test_file_manager.py` | `backend/managers/file_manager.py` | Datei-CRUD, Pfad-Validierung |
| `test_chat_manager.py` | `backend/managers/chat_manager.py` | Chat-History, Nachrichtenformatierung |
| `test_execution_engine.py` | `backend/managers/execution_engine.py` | Code-Ausführung, Timeouts, Fehlerbehandlung |
| `test_system_prompter.py` | `backend/managers/system_prompter.py` | Prompt-Generierung, Kontext-Injektion |
| `test_debug_logger.py` | `backend/managers/debug_logger.py` | Log-Formatierung, Fehler-Aggregation |
| `test_coding_agent.py` | `backend/agent/coding_agent.py` | Agent-Loop, Tool-Dispatch, History-Trimming |
| `test_mcp_server_code_execution.py` | `backend/agent/servers/mcp_server_code_execution.py` | MCP Code-Execution-Tool |
| `test_mcp_server_file_search.py` | `backend/agent/servers/mcp_server_file_search.py` | MCP Datei-Such-Tool |
| `test_mcp_server_web_search.py` | `backend/agent/servers/mcp_server_web_search.py` | MCP Web-Such-Tool |
#### Testklassen in `test_coding_agent.py`
- **`TestTruncateResult`**: Prüft, dass lange Tool-Outputs korrekt gekürzt werden
- **`TestTrimMessages`**: Prüft, dass alte History-Turns entfernt werden wenn der Kontext zu gross wird; System-Message und Original-Task bleiben immer erhalten
- **`TestStripCodeFences`**: Prüft, dass Markdown-Codeblöcke aus LLM-Antworten entfernt werden
- **`TestCodingAgentInit`**: Prüft initialen Zustand und `start_task()`-Reset-Verhalten
- **`TestProposeNextAction`**: Prüft den API-Aufruf-Zyklus mit gemockter API; testet Fehler-Handling (JSON-Parse-Fehler, API-Exceptions, Max-Iterations)
- **`TestApprove`**: Prüft `approve()` mit gemocktem `dispatch_tool`; testet Tool-Ergebnis-Injektion und Error-Replan-Tagging
- **`TestReject`**: Prüft, dass `reject()` das Feedback korrekt in die History injiziert und kein Tool ausführt
#### Test-Konventionen
- MCP-Server werden in Tests **nicht** als echte Subprozesse gestartet (via `conftest.py`-Mock)
- Streamlit-Aufrufe werden mit `patch("modul.st")` gemockt
- Dateisystem-Tests nutzen `tmp_path` (pytest-Fixture) für isolierte temporäre Verzeichnisse
- Async-Tests verwenden `@pytest.mark.asyncio` (benötigt `pytest-asyncio`)
---
## Setup
## Schnellstart
### 1. Repository klonen
@ -320,7 +84,7 @@ cd AISE1_Project
# Windows
.\.venv\Scripts\Activate.ps1
# macOS/Linux
# macOS / Linux
source .venv/bin/activate
```
@ -334,10 +98,10 @@ pip install -r requirements.txt
```bash
cp .env.example .env
# .env mit API-Keys befüllen
# .env öffnen und HOST, PORT, API_KEY, MODEL eintragen
```
### 5. Applikation starten
### 5. App starten
```bash
streamlit run frontend/app.py
@ -349,6 +113,281 @@ streamlit run frontend/app.py
pytest tests/ -v
```
**Optionale Abhängigkeit:** Die LaTeX-Unterstützung (`.tex`-Ausführung) erfordert `pdflatex`
im System-`PATH`. Ohne `pdflatex` funktioniert der Editor weiterhin; beim Ausführen einer
`.tex`-Datei erscheint ein `FileNotFoundError` im stderr-Panel.
---
## Frontend
Alle Frontend-Module sind reine Streamlit-Komponenten. Sie enthalten keine Business-Logik,
sondern delegieren alles an die Backend-Manager.
### `app.py`
Einstiegspunkt der Applikation. Ruft `init_state()` auf Modul-Ebene auf (vor `main()`),
damit alle Session-State-Schlüssel existieren, bevor ein Widget gerendert wird. Delegiert
an `render_sidebar()`, `render_editor()` und `render_chat()` basierend auf dem Navigations-Radio.
### `state.py`
Zentrale Quelle aller `st.session_state`-Schlüsselnamen und ihrer Standardwerte.
Alle Schlüssel nutzen `if key not in st.session_state`-Guards, damit bei Streamlit-Reruns
keine bestehenden Werte überschrieben werden. Aktuell verwaltete Schlüssel:
| Schlüssel | Standard | Zweck |
|-----------|----------|-------|
| `last_selected` | `None` | Zuletzt angeklickter Baum-Knoten (verhindert erneutes Ausführen bei jedem Rerender) |
| `selected_folder` / `selected_folder_rel` | `None` | Aktuell markierter Ordner |
| `chat_manager` | `ChatManager()` | Live-ChatManager-Instanz |
| `open_files` | `[]` | Geordnete Liste absoluter Pfade als Editor-Tabs |
| `files_content` | `{}` | Pfad → aktueller Editor-Inhalt (kann von Disk abweichen) |
| `active_file` | `None` | Absoluter Pfad des aktiven Editor-Tabs |
| `exec_results` | `{}` | Pfad → letztes Ausführungsergebnis-Dict |
| `chat_history` | `[]` | Flache Liste von `{role, content}`-Dicts zur Anzeige |
| `agent_mode` | `False` | Ob die Agent-Mode-UI aktiv ist |
| `coding_agent` | `None` | Live-`CodingAgent`-Instanz während einer Aufgabe |
| `agent_status` | `"idle"` | `"idle"` / `"waiting_approval"` / `"done"` |
| `agent_log` | `[]` | Liste abgeschlossener Schritt-Einträge |
| `agent_pending_action` | `None` | Vorgeschlagene Aktion, die auf Benutzer-Genehmigung wartet |
| `search_results` | `[]` | Aktive Websuchergebnisse für Kontext-Injektion |
### `sidebar.py`
Rendert das Navigations-Radio und den Workspace-Datei-Explorer (basierend auf
`streamlit-arborist` für einen interaktiven Baum). Datei-Klicks öffnen einen neuen
Editor-Tab; Ordner-Klicks zeigen eine Aktionsleiste mit «Datei hinzufügen» / «Ordner
hinzufügen» / «Löschen». Ein Popover am unteren Rand ermöglicht das Erstellen und
Hochladen von Dateien (bis 1 MB) im Workspace-Wurzelverzeichnis.
### `editor.py`
Verwendet `streamlit-ace` für syntaxhervorgehobene Bearbeitung. Jede geöffnete Datei
erhält einen eigenen Tab via `st.tabs()`. Die aktive Datei steuert die Schaltflächen
«Code ausführen», «Herunterladen», «Schliessen», «Umbenennen» und «Löschen». Der Button
**Code ausführen** führt für Python-Dateien zuerst `ast.parse()` durch, um Syntaxfehler
vor dem Subprocess zu erkennen. Der Button **Mit KI debuggen** (nach einem fehlgeschlagenen
Lauf eingeblendet) formatiert die Fehlerausgabe und navigiert zur Chat-Ansicht mit einer
vorausgefüllten Debug-Nachricht.
### `chat.py`
Zwei sich gegenseitig ausschliessende Ansichten, umgeschaltet via `st.toggle("Agent Mode")`:
**Normaler Chat** (`render_normal_chat()`):
- Websuch-Panel oben — öffnet sich automatisch, wenn Suchergebnisse aktiv sind.
- System-Prompt wird vor jeder ausgehenden Nachricht neu generiert (`_set_system_prompt()`).
- Unterstützt Slash-Befehle `/search <Abfrage>` und `/search clear`.
- Einstellungs-Expander: Dateikontext-Toggle, Modell-Auswahl, Max-Token-Slider,
benutzerdefinierter System-Prompt.
- «Mit KI debuggen»-Nachrichten vom Editor werden über `pending_debug_message` im
Session-State weitergeleitet.
**Agent Mode** (`render_agent_mode()`):
- `idle` → Aufgabeneingabe + Start-Schaltfläche.
- `waiting_approval` → zeigt vorgeschlagenen Gedanken + Tool + Argumente; Benutzer kann
Genehmigen, Ablehnen (mit Feedback) oder Abbrechen.
- `done` → Erfolgsmeldung + Folgefrage-Eingabe zum Weiterführen der Aufgabe.
---
## Backend Manager
### `file_manager.py`
Alle öffentlichen Methoden lösen Pfade auf und prüfen, ob sie innerhalb von `workspace/`
bleiben, bevor sie das Dateisystem berühren (**Path-Traversal-Schutz**). Je nach Operation
werden relative oder absolute Pfade akzeptiert und zurückgegeben:
| Methode | Pfad-Typ | Hinweise |
|---------|----------|----------|
| `create_folder(relative_path, name)` | Workspace-relativ | Erstellt eine Ebene |
| `create_file(relative_path, name)` | Workspace-relativ | Standard: `.txt` |
| `read_file(absolute_path)` | Absolutes `Path`-Objekt | Vom Editor verwendet |
| `save_file(absolute_path, content)` | Absoluter String | Überschreibt vorhandenes |
| `rename_file(relative_path, new_name)` | Workspace-relativ | Erweiterung bleibt immer erhalten |
| `delete_file(relative_path)` | Workspace-relativ | |
| `delete_folder(relative_path)` | Workspace-relativ | Rekursiv via `shutil.rmtree` |
| `get_file_tree()` | — | Gibt verschachteltes Dict zurück; Verzeichnisse → Dict, Dateien → None |
Dateien werden in `get_file_tree()` standardmässig auf `CODE_EXTENSIONS` gefiltert.
### `chat_manager.py`
Kapselt einen OpenAI-kompatiblen REST-Endpunkt, konfiguriert via `.env`.
**Sliding-Window-History:** `_build_payload_messages()` sendet immer zuerst die
System-Nachricht (damit sie nie verworfen wird), gefolgt von den letzten
`max_history_messages` (20) Nicht-System-Nachrichten. Das begrenzt die Payload-Grösse,
ohne den System-Prompt zu verlieren.
**Fehlerbehandlung:** Verbindungs-Timeouts und HTTP-Fehler werden abgefangen, geloggt und
als Assistenten-Nachrichten in der History gespeichert (sodass die UI den Fehler inline
anzeigt).
**API-Key:** Falls `API_KEY` den Wert `"EMPTY"` hat oder fehlt, wird kein
`Authorization`-Header gesendet (unterstützt lokale/anonyme Endpunkte).
### `system_prompter.py`
Generiert kontextbewusste System-Prompts. Signatur:
```python
SystemPrompter.generate_prompt(
user_message="",
file_context=None, # {"name": str, "content": str}
search_context=None, # reserviert, noch nicht verdrahtet
task_type="default", # "debug" | "explain" | "optimize" | "default"
)
```
Der Aufgabentyp wird anhand von Schlüsselwörtern in der Benutzernachricht durch
`_detect_task_type()` in `chat.py` ermittelt. Der Dateiinhalt wird wörtlich in einen
XML-ähnlichen `<file>...<code>`-Block eingebettet und bei `MAX_FILE_CHARS` Zeichen
abgeschnitten. `_extract_relevant_context()` nutzt `ast.parse()`, um nur die spezifische
Funktion oder Klasse zurückzugeben, nach der der Benutzer fragt, anstatt die gesamte Datei.
### `execution_engine.py`
Führt Dateien in einem Subprocess mit `capture_output=True`, `text=True` und einem
`RUN_TIMEOUT` von 30 Sekunden aus. Aktuell unterstützt:
- `.py` — via `py`-Launcher (Windows) / `sys.executable`
- `.tex` — via `pdflatex -interaction=nonstopmode` (erfordert pdflatex im PATH)
Rückgabe: `{"stdout": str, "stderr": str, "rc": int}`.
### `search_manager.py`
DuckDuckGo-basierte Websuche und Seitenabruf für die Chat-Ansicht. SSRF-geschützt:
`_validate_url()` blockiert Nicht-HTTP(S)-Schemata, Loopback- und RFC-1918-private
IP-Bereiche. `fetch_page()` extrahiert lesbaren Text via BeautifulSoup, entfernt
`<script>`-, `<style>`-, `<nav>`- und `<footer>`-Tags und kürzt auf `MAX_PAGE_CHARS`.
### `debug_logger.py`
Richtet einen rotierenden Datei-Handler für `logs/app.log` (5 MB × 5 Backups) und eine
separate `logs/errors.log` für `ERROR`/`CRITICAL`-Einträge ein. Verwendung im Code:
```python
from backend.managers.debug_logger import get_logger
logger = get_logger(__name__) # Standard-Python-Logger
logger.info("Dienst gestartet")
logger.error("Etwas ist schiefgelaufen")
logger.exception("Unerwarteter Fehler") # loggt Stack-Trace
```
Zusätzliche Classmethods zur sitzungsweisen Fehler-Aggregation:
```python
DebugLogger.log_error("Nachricht") # loggt + hängt an _error_log-Liste an
DebugLogger.get_errors() # gibt Liste der Fehlermeldungen dieser Sitzung zurück
DebugLogger.clear_errors() # leert die In-Memory-Liste
DebugLogger.format_debug_output({ # formatiert Ausführungsergebnis für die KI
"return_code": 1,
"stdout": "...",
"stderr": "...",
})
```
---
## Backend Agent (MCP-System)
### `coding_agent.py`
Implementiert eine **Plan → Aktion → Beobachten**-Schleife, die schrittweise durch die
Streamlit-UI gesteuert wird. Das LLM antwortet immer mit einer strukturierten JSON-Aktion:
```json
{"thought": "...", "tool": "<tool_name>", "arguments": {"key": "value"}}
```
Wichtige Methoden:
| Methode | Beschreibung |
|---------|--------------|
| `start_task(task)` | Setzt den gesamten Zustand zurück, befüllt History mit System + Aufgabe |
| `propose_next_action()` | Ruft das LLM auf, parst JSON, speichert als `pending_action` |
| `approve()` | Führt das ausstehende Tool via `dispatch_tool()` aus, loggt Ergebnis |
| `reject(feedback)` | Injiziert Feedback + Replan-Tag; schlägt neue Aktion vor |
| `follow_up(question)` | Fügt nach «done» eine Folgefrage ein, setzt Schleife fort |
Hilfsfunktionen (Modul-Ebene):
| Funktion | Beschreibung |
|----------|--------------|
| `truncate_result(text)` | Begrenzt Tool-Output auf `MAX_RESULT_LENGTH` (10 000 Zeichen) |
| `trim_messages(msgs)` | Entfernt alte Turns, wenn History `MAX_HISTORY_CHARS` (80 000 Zeichen) überschreitet; System-Nachricht + Original-Aufgabe bleiben immer erhalten |
| `_strip_code_fences(text)` | Entfernt ` ```json `- / ` ``` `-Wrapper aus LLM-Antworten |
| `dispatch_tool(name, arguments)` | Leitet weiter an `MCPToolAdapter.call_tool()` |
| `get_tool_descriptions()` | Erstellt eine menschenlesbare Tool-Liste für den System-Prompt |
### `mcp_server_adapter.py`
Liest `mcp_server_config.json`, startet jeden Server als stdio-Subprocess (immer mit
`sys.executable`, unabhängig vom literalen Befehl in der Konfiguration) und registriert
alle Tools in einem flachen `tool_registry`. Verbindungen werden pro Aufruf geöffnet
(nicht dauerhaft gehalten), da Streamlits synchrones Rerun-Modell langlebige
async-Kontextmanager unpraktisch macht.
### MCP-Server (in `servers/`)
Alle drei Server sind FastMCP-Applikationen, die über stdio kommunizieren.
**`mcp_server_file_search.py`** — Workspace-Dateioperationen:
- `list_files()` — flache rekursive Auflistung
- `get_file_tree(dir_path)` — baumförmige Verzeichnisstruktur
- `search_files(query)` — Name- und Inhaltssuche (bis 30 Treffer)
- `read_file(path)` — Textdatei lesen
- `write_new_file(path, content)` — Erstellen (kein Überschreiben)
- `create_new_directory(path)` — Verzeichnis erstellen
**`mcp_server_web_search.py`** — Webzugriff:
- `web_search(query, max_results=5)` — DuckDuckGo-Suche
- `fetch_page(url)` — Abrufen + Text extrahieren (max. `MAX_PAGE_LENGTH` Zeichen)
- Beide Tools nutzen SSRF-Schutz (gleiche URL-Validierung wie `search_manager.py`)
**`mcp_server_code_execution.py`** — Sandbox-Python-Analyse:
- `check_code_safety(code)` — statische Analyse; blockiert gefährliche Imports/Builtins
- `analyse_structure(code)` — AST-basierte Strukturzusammenfassung
- `lint_code(code)` — pyflakes-Analyse
- `python_code_validation(code)` — Sicherheits- + Syntaxprüfung ohne Ausführung
- `run_python_sandboxed(code)` — Ausführung in einem Subprocess mit `PYTHONIOENCODING=utf-8`,
10 s Timeout, Output begrenzt auf `MAX_OUTPUT_LENGTH`
---
## MCP-Server-Konfiguration
### Format: `backend/agent/mcp_server_config.json`
```json
{
"Servername": {
"command": "py",
"args": ["servers/mcp_server_beispiel.py"]
}
}
```
| Feld | Pflicht | Beschreibung |
|------|---------|--------------|
| `command` | Ja | Ausführbare Datei (`py`, `python3`, `node`, …) — wird für Python immer durch `sys.executable` ersetzt |
| `args` | Ja | Argumente-Array — erstes Element ist der Server-Script-Pfad relativ zu `backend/agent/` |
| `env` | Nein | Zusätzliche Umgebungsvariablen für den Serverprozess |
### Neuen MCP-Server hinzufügen
1. Neues FastMCP-Script in `backend/agent/servers/` erstellen:
```python
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("MeinServer")
@mcp.tool()
def mein_tool(param: str) -> str:
"""Tool-Beschreibung, die dem Agenten angezeigt wird."""
return f"Ergebnis: {param}"
if __name__ == "__main__":
mcp.run(transport="stdio")
```
2. Eintrag in `mcp_server_config.json` ergänzen:
```json
"MeinServer": {
"command": "py",
"args": ["servers/mcp_server_mein_tool.py"]
}
```
3. Der Adapter erkennt den neuen Server beim nächsten App-Start automatisch und
bindet seine Tools in den System-Prompt des Agenten ein.
---
## Architektur-Übersicht
@ -356,36 +395,135 @@ pytest tests/ -v
```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (Streamlit) │
│ app.py → sidebar.py / editor.py / chat.py
│ state.py (Session-State)
│ app.py ──► sidebar.py / editor.py / chat.py
state.py (alle Session-State-Schlüssel) │
└──────────────┬──────────────────────────────────────────────────┘
direkte Python-Aufrufe
┌─────────────────────────────────────────────────────────────────┐
│ Backend Manager │
│ FileManager / ChatManager / SystemPrompter /
│ SearchManager / ExecutionEngine / DebugLogger │
│ FileManager ChatManager SystemPrompter
│ SearchManager ExecutionEngine DebugLogger
└──────────────┬──────────────────────────────────────────────────┘
async-Aufrufe (via _run_async-Bridge)
┌─────────────────────────────────────────────────────────────────┐
│ Coding Agent (backend/agent/) │
│ coding_agent.py ←→ mcp_server_adapter.py │
│ │ │
│ mcp_server_config.json │
│ │ │
│ ┌───────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ Coding Agent │
│ coding_agent.py ◄──► mcp_server_adapter.py │
│ │ stdio (Subprocess pro Aufruf) │
│ ┌────────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ mcp_server_file_search mcp_server_web mcp_server_code │
│ │
│ (Optional: mcp_server_adapter_RAG.py für semantische │
│ Tool-Auswahl via Sentence Transformers) │
└─────────────────────────────────────────────────────────────────┘
└──────────────┬──────────────────────────────────────────────────┘
│ lesen / schreiben
┌─────────────────────────────────────────────────────────────────┐
│ workspace/ │
│ Isoliertes Sandbox-Verzeichnis für Agent-Dateien │
│ workspace/ (isoliertes Sandbox-Verzeichnis) │
└─────────────────────────────────────────────────────────────────┘
```
**Async-Bridge:** Streamlit läuft synchron. Der `CodingAgent` verwendet async-Methoden
(da die MCP-Client-Bibliothek async ist). `_run_async(coro)` in `chat.py` erstellt pro
Aufruf eine neue Event-Loop (`asyncio.new_event_loop()`), was im Thread-Modell von
Streamlit sicher ist, da auf dem UI-Thread keine Loop läuft.
**MCP-Verbindungsmodell:** Der Adapter öffnet pro Tool-Aufruf eine neue stdio-Verbindung
statt eine persistente Session zu halten. Das vermeidet die Komplexität langlebiger
async-Kontextmanager über Streamlit-Reruns hinweg.
---
## Wichtige Designentscheidungen
### Warum `get_file_tree()` statt einer flachen Dateiliste?
Der interaktive Datei-Explorer in der Sidebar benötigt eine verschachtelte Struktur,
um das Baum-Widget aufzubauen. Eine flache Liste würde die clientseitige Rekonstruktion
von Eltern-Kind-Beziehungen erfordern. Der MCP-Server `mcp_server_file_search.py` stellt
sein eigenes `list_files()`-Tool für den Agenten bereit, wo eine flache Auflistung
für das LLM nützlicher ist.
### Warum wird der System-Prompt bei jeder Nachricht neu generiert?
Die im Editor aktive Datei kann sich zwischen Nachrichten ändern. Durch das Neuerstellen
des Prompts hat die KI immer den aktuellen Dateikontext. Der vorhandene System-Nachrichten-Eintrag
wird in-place aktualisiert (nicht angehängt), sodass die History stets genau eine
System-Nachricht enthält.
### Warum ist die Chat-History auf ein Sliding-Window begrenzt?
`ChatManager._build_payload_messages()` behält die System-Nachricht und die letzten 20 Turns.
Das verhindert, dass die Payload während langer Sitzungen das Kontextlimit des Modells
überschreitet, während der System-Prompt immer erhalten bleibt. Der Agent hat eine eigene,
separate Kürzungslogik (`trim_messages()`), die zusätzlich die ursprüngliche Aufgabenbeschreibung
bewahrt.
### Warum werden MCP-Tool-Verbindungen pro Aufruf geöffnet?
Streamlit führt das gesamte Script bei jeder Benutzerinteraktion erneut aus. Eine lebende
async-MCP-Session über Reruns hinweg zu erhalten würde entweder einen Hintergrund-Thread
oder eine persistente asyncio-Loop erfordern — beides erhöht Komplexität und Fehleranfälligkeit.
Verbindungen pro Aufruf sind einfacher und zuverlässiger, auf Kosten eines kleinen
Subprocess-Start-Overheads pro Tool-Aufruf.
### Warum wird Fehler-Output NICHT automatisch in den normalen Chat injiziert?
Das automatische Einschleusen jedes Laufzeitfehlers würde die Chat-History schnell mit
Rauschen überfluten. Stattdessen entscheidet der Benutzer selbst, wann er die KI über
den Button «Mit KI debuggen» im Editor einbezieht. Der Agent-Mode behandelt dies anders:
Tool-Fehler werden immer als Beobachtungen an das LLM zurückgegeben und lösen automatisches
Replanning aus.
### Warum setzt `run_python_sandboxed()` `PYTHONIOENCODING=utf-8`?
Unter Windows ist die Standard-Konsolen-Kodierung cp1252, die Unicode-Zeichen ausserhalb
des Latin-1-Bereichs (z. B. Emoji, CJK) nicht kodieren kann. Das Setzen von
`PYTHONIOENCODING=utf-8` in der Subprocess-Umgebung stellt sicher, dass `print()` für
beliebige Unicode-Inhalte korrekt funktioniert.
---
## Tests
### Tests ausführen
```bash
pytest tests/ -v # alle Tests
pytest tests/test_chat_manager.py -v # einzelne Datei
pytest tests/ -q # Kurzausgabe
```
### Test-Architektur
`conftest.py` patcht `MCPToolAdapter` auf `sys.modules`-Ebene **vor** dem Import eines
Testmoduls. Das verhindert, dass `coding_agent.py`'s Modul-Level-Aufruf
`asyncio.run(adapter.initialize_all_servers())` echte MCP-Subprozesse startet.
| Testdatei | Getestetes Modul | Wichtige Muster |
|-----------|-----------------|-----------------|
| `test_chat_manager.py` | `ChatManager` | `@patch("requests.post")` für HTTP |
| `test_coding_agent.py` | `CodingAgent` | `@pytest.mark.asyncio`, mock `_call_api` |
| `test_debug_logger.py` | `DebugLogger` | autouse-Fixture setzt `_error_log`-Klassenvariable zurück |
| `test_execution_engine.py` | `ExecutionEngine` | `@patch("subprocess.run")` |
| `test_file_manager.py` | `FileManager` | `tmp_path`-Fixture, mock `st` |
| `test_mcp_server_code_execution.py` | MCP-Code-Server | führt echten Python-Code aus |
| `test_mcp_server_file_search.py` | MCP-Datei-Server | `monkeypatch` tauscht `ALLOWED_DIR` |
| `test_mcp_server_web_search.py` | MCP-Web-Server | patcht `DDGS` im Server-Namespace |
| `test_search_manager.py` | `SearchManager` | mockt DDGS-Kontextmanager + requests |
| `test_system_prompter.py` | `SystemPrompter` | reine Funktion, kein Mocking nötig |
### Async-Tests
Die Tests in `test_coding_agent.py` verwenden `@pytest.mark.asyncio` aus `pytest-asyncio`.
Der `MCPToolAdapter` ist vollständig via `conftest.py` gemockt, sodass kein MCP-Subprocess
beteiligt ist.
---
## Umgebungsvariablen
`.env.example` nach `.env` kopieren und ausfüllen:
| Variable | Beschreibung | Beispiel |
|----------|--------------|---------|
| `HOST` | Hostname des LLM-API-Endpunkts | `localhost` |
| `PORT` | Port des LLM-API-Endpunkts | `8000` |
| `API_KEY` | Bearer-Token — `EMPTY` für offene Endpunkte verwenden | `sk-...` |
| `MODEL` | Modellname, der in API-Payloads gesendet wird | `mistral-7b` |
Die App funktioniert mit jedem OpenAI-kompatiblen API-Endpunkt (vLLM, Ollama mit
OpenAI-Shim, OpenAI selbst usw.).