2026-05-28 11:19:44 +02:00

533 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# AISE AI Code Editor — Technische Dokumentation
KI-unterstützter Lightweight Code Editor auf Basis von Streamlit (AISE501 Spring 2026).
---
## Inhaltsverzeichnis
1. [Projektstruktur](#projektstruktur)
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)
---
## Projektstruktur
```
AISE_AIAgent/
├── frontend/ # Streamlit UI-Komponenten
│ ├── 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/
│ ├── 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 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/ # pytest-Unit-Tests
│ ├── conftest.py # Globaler MCP-Mock (keine echten Subprozesse in Tests)
│ ├── test_chat_manager.py
│ ├── test_coding_agent.py
│ ├── test_debug_logger.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_search_manager.py
│ └── test_system_prompter.py
├── 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
```
---
## Schnellstart
### 1. Repository klonen
```bash
git clone https://gitea.fhgr.ch/meulilivio/AISE1_Project.git
cd AISE1_Project
```
### 2. Virtuelle Umgebung aktivieren
```bash
# Windows
.\.venv\Scripts\Activate.ps1
# macOS / Linux
source .venv/bin/activate
```
### 3. Abhängigkeiten installieren
```bash
pip install -r requirements.txt
```
### 4. Umgebungsvariablen konfigurieren
```bash
cp .env.example .env
# .env öffnen und HOST, PORT, API_KEY, MODEL eintragen
```
### 5. App starten
```bash
streamlit run frontend/app.py
```
### 6. Tests ausführen
```bash
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()`):
- Kompakte 4-spaltige Toolbar direkt über dem Chat-Input:
`[● Agent Mode]` `[🔍 Search]` `[🗑️ Clear]` `[⚙️ Settings]`
— Web-Suche und Einstellungen jeweils als `st.popover`, Clear öffnet einen Bestätigungs-Dialog.
- System-Prompt wird vor jeder ausgehenden Nachricht neu generiert (`_set_system_prompt()`).
- Unterstützt Slash-Befehle `/search <Abfrage>` und `/search clear`.
- Settings-Popover: 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, # list[{"title", "url", "snippet"}] — in system_prompter.py implementiert,
# aber in chat.py nicht verwendet: dort wird Search-Kontext direkt
# als <search_context>-Block vor die Nachricht eingefügt
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 `sys.executable` (plattformübergreifend; zeigt auf den aktuell aktiven Python-Interpreter)
- `.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 in die History; `propose_next_action()` wird danach separat aufgerufen |
| `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()` |
| `build_all_tool_description()` | 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:
- `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`,
15 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
```
┌─────────────────────────────────────────────────────────────────┐
│ Frontend (Streamlit) │
│ 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 │
└──────────────┬──────────────────────────────────────────────────┘
│ async-Aufrufe (via _run_async-Bridge)
┌─────────────────────────────────────────────────────────────────┐
│ Coding Agent │
│ coding_agent.py ◄──► mcp_server_adapter.py │
│ │ stdio (Subprocess pro Aufruf) │
│ ┌────────────────┼───────────────┐ │
│ ▼ ▼ ▼ │
│ mcp_server_file_search mcp_server_web mcp_server_code │
└──────────────┬──────────────────────────────────────────────────┘
│ lesen / schreiben
┌─────────────────────────────────────────────────────────────────┐
│ 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.).