533 lines
25 KiB
Markdown
533 lines
25 KiB
Markdown
# 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.).
|