search improvments, chat, improvments
This commit is contained in:
parent
33dd93fbbf
commit
b83cd2a790
351
README.md
351
README.md
@ -1,7 +1,24 @@
|
|||||||
# AISE AI Code Editor
|
# AISE AI Code Editor — Technische Dokumentation
|
||||||
|
|
||||||
AI-Supported Lightweight Code Editor built with Streamlit (AISE501 Spring 2026)
|
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`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Projektstruktur
|
## Projektstruktur
|
||||||
|
|
||||||
```
|
```
|
||||||
@ -45,48 +62,248 @@ AISE_AIAgent/
|
|||||||
│ └── test_mcp_server_web_search.py
|
│ └── test_mcp_server_web_search.py
|
||||||
│
|
│
|
||||||
├── workspace/ # Agent-Sandbox (isoliertes Arbeitsverzeichnis)
|
├── workspace/ # Agent-Sandbox (isoliertes Arbeitsverzeichnis)
|
||||||
├── run_agent.py # CLI-Einstiegspunkt für den Coding-Agent
|
|
||||||
└── .env.example # Vorlage für Umgebungsvariablen
|
└── .env.example # Vorlage für Umgebungsvariablen
|
||||||
```
|
```
|
||||||
|
|
||||||
## Komponenten
|
---
|
||||||
|
|
||||||
### Frontend (`frontend/`)
|
## Frontend
|
||||||
- **app.py**: Streamlit-Applikation, Layout-Orchestrierung
|
|
||||||
- **state.py**: Zentralisierte Session-State-Verwaltung
|
|
||||||
- **sidebar.py**: Datei-Browser und Projekt-Navigation
|
|
||||||
- **editor.py**: Code-Editor mit Syntax-Highlighting
|
|
||||||
- **chat.py**: AI-Assistent Chat-Interface
|
|
||||||
|
|
||||||
### Backend Manager (`backend/managers/`)
|
Das Frontend besteht aus Streamlit-Komponenten, die zusammen eine interaktive Code-Editor-Oberfläche bilden.
|
||||||
Werden direkt vom Frontend für UI-Operationen genutzt:
|
|
||||||
- **file_manager.py**: CRUD-Operationen auf Projektdateien (`get_file_tree()` liefert die verschachtelte Baumstruktur für den File Explorer; eine flache `list_files()` wurde bewusst nicht implementiert, da das Frontend die Baumstruktur benötigt — für den Agent Mode übernimmt `mcp_server_file_search.py` die Dateisuche)
|
|
||||||
- **chat_manager.py**: Chat-History, Nachrichten-Verwaltung (Fehler aus der Code-Ausführung werden im normalen Chat bewusst per "Debug with AI"-Button manuell an den Chat übergeben — der User entscheidet selbst wann die AI eingeschaltet wird; im Agent Mode geschieht dies automatisch über den Plan-Act-Observe-Loop)
|
|
||||||
- **system_prompter.py**: System-Prompt-Generierung und Datei-Kontext
|
|
||||||
- **execution_engine.py**: Sichere Code-Ausführung mit Output-Capture
|
|
||||||
- **debug_logger.py**: Fehler-Tracking und Log-Formatierung (`format_debug_output()` formatiert Execution-Output für UI und AI-Chat; `log_error()` wurde bewusst nicht als separate Methode implementiert — Python's Standard-`logging`-Modul mit `logger.error()` deckt diese Funktionalität bereits vollständig ab und wird konsequent im gesamten Code verwendet)
|
|
||||||
- **search_manager.py**: Web-Suche via DuckDuckGo (`ddgs`-Bibliothek)
|
|
||||||
|
|
||||||
### Backend Agent (`backend/agent/`)
|
### `app.py`
|
||||||
Autonomes AI-Agent-System für komplexe Coding-Aufgaben:
|
Haupteinstiegspunkt der Applikation. Orchestriert das Layout und initialisiert alle UI-Komponenten (Sidebar, Editor, Chat).
|
||||||
- **coding_agent.py**: Agent-Loop (Plan → Act → Observe → Wiederholen)
|
|
||||||
- **mcp_server_adapter.py**: Verbindet den Agent mit MCP-Servern via Konfigurationsdatei
|
|
||||||
- **mcp_server_adapter_RAG.py**: Erweiterter Adapter mit semantischer Tool-Auswahl (RAG)
|
|
||||||
- **mcp_server_config.json**: Definiert welche MCP-Server gestartet werden und mit welchen Argumenten
|
|
||||||
- **servers/**: Die eigentlichen MCP-Tool-Server (Code-Ausführung, Datei-Suche, Web-Suche)
|
|
||||||
|
|
||||||
### Workspace (`workspace/`)
|
### `state.py`
|
||||||
- Sandbox-Verzeichnis, in dem der Agent Dateien erstellt und ausführt
|
Zentralisierte Verwaltung des Streamlit Session-State. Stellt sicher, dass alle Komponenten denselben Zustand (geöffnete Datei, Chat-History, Agent-Status) teilen.
|
||||||
- Verhindert, dass der Agent auf Dateien ausserhalb dieses Verzeichnisses zugreift
|
|
||||||
|
|
||||||
## Features
|
### `sidebar.py`
|
||||||
|
Datei-Browser und Projekt-Navigation. Erlaubt das Durchsuchen des Workspaces und das Öffnen von Dateien im Editor.
|
||||||
|
|
||||||
- **Datei-Verwaltung**: Dateien im Workspace durchsuchen und bearbeiten
|
### `editor.py`
|
||||||
- **Chat-Interface**: KI-gestützter Code-Assistent
|
Code-Editor-Pane mit Syntax-Highlighting. Ermöglicht das Bearbeiten und Speichern von Code-Dateien direkt im Browser.
|
||||||
- **Code-Ausführung**: Python-Code sicher ausführen mit Debug-Output
|
|
||||||
- **Web-Suche**: Dokumentation und Beispiele via DuckDuckGo abrufen
|
### `chat.py`
|
||||||
- **Autonomer Agent**: MCP-basierter Coding-Agent mit Plan-Act-Observe-Loop
|
Chat-Interface für den KI-Assistenten. Zeigt die Konversations-History und ermöglicht Eingaben an das AI-Modell.
|
||||||
- **RAG Tool-Auswahl**: Semantische Tool-Selektion via Sentence Transformers
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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
|
## Setup
|
||||||
|
|
||||||
@ -132,37 +349,43 @@ streamlit run frontend/app.py
|
|||||||
pytest tests/ -v
|
pytest tests/ -v
|
||||||
```
|
```
|
||||||
|
|
||||||
## Konfiguration: `mcp_server_config.json`
|
---
|
||||||
|
|
||||||
Die Datei `backend/agent/mcp_server_config.json` definiert, welche MCP-Server der Agent starten soll. Jeder Eintrag enthält den Servernamen, den Startbefehl (`command`) und optionale Argumente (`args`) sowie Umgebungsvariablen (`env`):
|
## Architektur-Übersicht
|
||||||
|
|
||||||
```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_key_here" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Architektur
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Frontend (Streamlit) ──► Backend Manager ──► AI API
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
│
|
│ Frontend (Streamlit) │
|
||||||
└──► Coding Agent ──► MCP-Adapter ──► MCP-Server
|
│ app.py → sidebar.py / editor.py / chat.py │
|
||||||
(Code / File / Web)
|
│ │ │
|
||||||
```
|
│ state.py (Session-State) │
|
||||||
|
└──────────────┬──────────────────────────────────────────────────┘
|
||||||
## Entwicklung
|
│
|
||||||
|
▼
|
||||||
```bash
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
git add .
|
│ Backend Manager │
|
||||||
git commit -m "Deine Nachricht"
|
│ FileManager / ChatManager / SystemPrompter / │
|
||||||
git push origin main
|
│ SearchManager / ExecutionEngine / DebugLogger │
|
||||||
|
└──────────────┬──────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Coding Agent (backend/agent/) │
|
||||||
|
│ coding_agent.py ←→ mcp_server_adapter.py │
|
||||||
|
│ │ │
|
||||||
|
│ mcp_server_config.json │
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────────────┼───────────────┐ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ mcp_server_file_search mcp_server_web mcp_server_code │
|
||||||
|
│ │
|
||||||
|
│ (Optional: mcp_server_adapter_RAG.py für semantische │
|
||||||
|
│ Tool-Auswahl via Sentence Transformers) │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ workspace/ │
|
||||||
|
│ Isoliertes Sandbox-Verzeichnis für Agent-Dateien │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|||||||
392
READMEnew.md
392
READMEnew.md
@ -1,392 +0,0 @@
|
|||||||
# 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`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Projektstruktur
|
|
||||||
|
|
||||||
```
|
|
||||||
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
|
|
||||||
│
|
|
||||||
├── 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
|
|
||||||
│ │
|
|
||||||
│ └── 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
|
|
||||||
│
|
|
||||||
├── tests/ # Unit-Tests (pytest)
|
|
||||||
│ ├── conftest.py # Globale Test-Fixtures und MCP-Mocks
|
|
||||||
│ ├── test_file_manager.py
|
|
||||||
│ ├── test_chat_manager.py
|
|
||||||
│ ├── test_execution_engine.py
|
|
||||||
│ ├── test_coding_agent.py
|
|
||||||
│ ├── test_debug_logger.py
|
|
||||||
│ ├── test_system_prompter.py
|
|
||||||
│ ├── test_mcp_server_code_execution.py
|
|
||||||
│ ├── test_mcp_server_file_search.py
|
|
||||||
│ └── test_mcp_server_web_search.py
|
|
||||||
│
|
|
||||||
├── workspace/ # Agent-Sandbox (isoliertes Arbeitsverzeichnis)
|
|
||||||
├── run_agent.py # CLI-Einstiegspunkt für den Coding-Agent
|
|
||||||
└── .env.example # Vorlage für 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
|
|
||||||
|
|
||||||
### 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 mit API-Keys befüllen
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Applikation starten
|
|
||||||
|
|
||||||
```bash
|
|
||||||
streamlit run frontend/app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. Tests ausführen
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pytest tests/ -v
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Architektur-Übersicht
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Frontend (Streamlit) │
|
|
||||||
│ app.py → sidebar.py / editor.py / chat.py │
|
|
||||||
│ │ │
|
|
||||||
│ state.py (Session-State) │
|
|
||||||
└──────────────┬──────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Backend Manager │
|
|
||||||
│ FileManager / ChatManager / SystemPrompter / │
|
|
||||||
│ SearchManager / ExecutionEngine / DebugLogger │
|
|
||||||
└──────────────┬──────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ Coding Agent (backend/agent/) │
|
|
||||||
│ coding_agent.py ←→ mcp_server_adapter.py │
|
|
||||||
│ │ │
|
|
||||||
│ mcp_server_config.json │
|
|
||||||
│ │ │
|
|
||||||
│ ┌───────────────┼───────────────┐ │
|
|
||||||
│ ▼ ▼ ▼ │
|
|
||||||
│ mcp_server_file_search mcp_server_web mcp_server_code │
|
|
||||||
│ │
|
|
||||||
│ (Optional: mcp_server_adapter_RAG.py für semantische │
|
|
||||||
│ Tool-Auswahl via Sentence Transformers) │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
│
|
|
||||||
▼
|
|
||||||
┌─────────────────────────────────────────────────────────────────┐
|
|
||||||
│ workspace/ │
|
|
||||||
│ Isoliertes Sandbox-Verzeichnis für Agent-Dateien │
|
|
||||||
└─────────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
@ -32,6 +32,21 @@ class ChatManager:
|
|||||||
# Chat history stored in memory
|
# Chat history stored in memory
|
||||||
self.chat_history = []
|
self.chat_history = []
|
||||||
|
|
||||||
|
# Maximum number of non-system messages sent to the API.
|
||||||
|
# The system prompt is always included on top regardless of this limit.
|
||||||
|
self.max_history_messages = 20
|
||||||
|
|
||||||
|
def _build_payload_messages(self) -> list:
|
||||||
|
"""Return the messages to send to the API.
|
||||||
|
|
||||||
|
Always puts the system prompt first, then the most recent
|
||||||
|
max_history_messages non-system messages. This guarantees the system
|
||||||
|
prompt is never dropped even in long conversations.
|
||||||
|
"""
|
||||||
|
system = [m for m in self.chat_history if m["role"] == "system"]
|
||||||
|
others = [m for m in self.chat_history if m["role"] != "system"]
|
||||||
|
return system + others[-self.max_history_messages:]
|
||||||
|
|
||||||
def add_message(self, role: str, content: str) -> None:
|
def add_message(self, role: str, content: str) -> None:
|
||||||
"""Append a single message to the conversation history."""
|
"""Append a single message to the conversation history."""
|
||||||
self.chat_history.append({"role": role, "content": content})
|
self.chat_history.append({"role": role, "content": content})
|
||||||
@ -66,10 +81,10 @@ class ChatManager:
|
|||||||
if self.api_key and self.api_key != "EMPTY":
|
if self.api_key and self.api_key != "EMPTY":
|
||||||
headers["Authorization"] = f"Bearer {self.api_key}"
|
headers["Authorization"] = f"Bearer {self.api_key}"
|
||||||
|
|
||||||
# Full history is sent so the model has multi-turn conversation context
|
# System prompt + most recent messages — system prompt is always preserved.
|
||||||
payload = {
|
payload = {
|
||||||
"model": self.model,
|
"model": self.model,
|
||||||
"messages": self.chat_history,
|
"messages": self._build_payload_messages(),
|
||||||
"temperature": 0.7,
|
"temperature": 0.7,
|
||||||
"max_tokens": 2000,
|
"max_tokens": 2000,
|
||||||
"stream": False,
|
"stream": False,
|
||||||
|
|||||||
@ -14,6 +14,13 @@ logger = get_logger(__name__)
|
|||||||
WORKSPACE = Path("workspace")
|
WORKSPACE = Path("workspace")
|
||||||
WORKSPACE.mkdir(exist_ok=True)
|
WORKSPACE.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
# File extensions that are shown in the explorer when filter_extensions=True.
|
||||||
|
CODE_EXTENSIONS = {
|
||||||
|
".py", ".js", ".ts", ".html", ".css", ".json",
|
||||||
|
".yaml", ".yml", ".sh", ".md", ".txt", ".tex",
|
||||||
|
".c", ".cpp", ".java", ".rs", ".go",
|
||||||
|
}
|
||||||
|
|
||||||
class FileManager:
|
class FileManager:
|
||||||
"""Manages all file and folder operations inside the workspace directory.
|
"""Manages all file and folder operations inside the workspace directory.
|
||||||
|
|
||||||
@ -317,27 +324,33 @@ class FileManager:
|
|||||||
logger.exception("Error deleting folder %s: %s", relative_path, str(e))
|
logger.exception("Error deleting folder %s: %s", relative_path, str(e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_file_tree(self):
|
def get_file_tree(self, filter_extensions: bool = True) -> dict:
|
||||||
"""
|
"""Builds a nested dictionary representing the file tree.
|
||||||
Builds a nested dictionary representing the file tree starting from the base path.
|
|
||||||
Directories are represented as keys with dictionary values,
|
Directories are represented as keys with dictionary values,
|
||||||
and files are represented as keys with None
|
files as keys with None.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filter_extensions: When True (default), only files whose suffix is
|
||||||
|
in CODE_EXTENSIONS are included. Directories are always shown,
|
||||||
|
even when they are empty after filtering.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: A nested dictionary representing the file tree.
|
dict: A nested dictionary representing the file tree.
|
||||||
"""
|
"""
|
||||||
logger.info("Getting file tree ...")
|
logger.info("Getting file tree ...")
|
||||||
|
|
||||||
def build_tree(path: Path):
|
|
||||||
|
|
||||||
|
def build_tree(path: Path) -> dict:
|
||||||
tree = {}
|
tree = {}
|
||||||
|
|
||||||
for item in sorted(path.iterdir()):
|
for item in sorted(path.iterdir()):
|
||||||
if item.is_dir():
|
if item.is_dir():
|
||||||
tree[item.name] = build_tree(item) # recurse into sub-folders
|
tree[item.name] = build_tree(item)
|
||||||
else:
|
else:
|
||||||
tree[item.name] = None # leaf node for files
|
if filter_extensions and item.suffix not in CODE_EXTENSIONS:
|
||||||
|
continue
|
||||||
|
tree[item.name] = None
|
||||||
return tree
|
return tree
|
||||||
|
|
||||||
return build_tree(self.base_path)
|
return build_tree(self.base_path)
|
||||||
|
|
||||||
def list_files(self, extensions: list[str] | None = None) -> list[Path]:
|
def list_files(self, extensions: list[str] | None = None) -> list[Path]:
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
@ -285,6 +286,22 @@ def render_agent_mode():
|
|||||||
|
|
||||||
# ── Normal Chat helpers ───────────────────────────────────────────────────────
|
# ── Normal Chat helpers ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _strip_search_context_from_history(chat_manager: ChatManager) -> None:
|
||||||
|
"""Remove <search_context> blocks from all user messages in the API history.
|
||||||
|
|
||||||
|
Called when the user clears search results so the AI no longer receives
|
||||||
|
the stale context in follow-up messages.
|
||||||
|
"""
|
||||||
|
for msg in chat_manager.chat_history:
|
||||||
|
if msg["role"] == "user" and "<search_context>" in msg["content"]:
|
||||||
|
msg["content"] = re.sub(
|
||||||
|
r"<search_context>.*?</search_context>\n\n",
|
||||||
|
"",
|
||||||
|
msg["content"],
|
||||||
|
flags=re.DOTALL,
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
def _detect_task_type(user_input: str) -> str:
|
def _detect_task_type(user_input: str) -> str:
|
||||||
"""Infer the task type from keywords in the user message."""
|
"""Infer the task type from keywords in the user message."""
|
||||||
lower = user_input.lower()
|
lower = user_input.lower()
|
||||||
@ -367,7 +384,7 @@ def _render_search_panel():
|
|||||||
search_results = st.session_state.get("search_results", [])
|
search_results = st.session_state.get("search_results", [])
|
||||||
label = f"🔍 Web Search ({len(search_results)} result{'s' if len(search_results) != 1 else ''} active)" if search_results else "🔍 Web Search"
|
label = f"🔍 Web Search ({len(search_results)} result{'s' if len(search_results) != 1 else ''} active)" if search_results else "🔍 Web Search"
|
||||||
|
|
||||||
with st.expander(label, expanded=False):
|
with st.expander(label, expanded=bool(search_results)):
|
||||||
col_input, col_btn = st.columns([5, 1])
|
col_input, col_btn = st.columns([5, 1])
|
||||||
with col_input:
|
with col_input:
|
||||||
query = st.text_input(
|
query = st.text_input(
|
||||||
@ -397,6 +414,9 @@ def _render_search_panel():
|
|||||||
st.divider()
|
st.divider()
|
||||||
if st.button("Clear search results", use_container_width=True):
|
if st.button("Clear search results", use_container_width=True):
|
||||||
st.session_state.search_results = []
|
st.session_state.search_results = []
|
||||||
|
cm = st.session_state.get("chat_manager")
|
||||||
|
if cm:
|
||||||
|
_strip_search_context_from_history(cm)
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
|
|
||||||
@ -412,6 +432,9 @@ def render_normal_chat():
|
|||||||
logger.info("Chat mode")
|
logger.info("Chat mode")
|
||||||
chat_manager: ChatManager = st.session_state.chat_manager
|
chat_manager: ChatManager = st.session_state.chat_manager
|
||||||
|
|
||||||
|
# Search panel always rendered at the top — expands automatically when results are active.
|
||||||
|
_render_search_panel()
|
||||||
|
|
||||||
# Apply model/token overrides from the Settings panel before any API call.
|
# Apply model/token overrides from the Settings panel before any API call.
|
||||||
if st.session_state.get("selected_model"):
|
if st.session_state.get("selected_model"):
|
||||||
chat_manager.model = st.session_state.selected_model
|
chat_manager.model = st.session_state.selected_model
|
||||||
@ -470,11 +493,8 @@ def render_normal_chat():
|
|||||||
|
|
||||||
if results:
|
if results:
|
||||||
st.session_state.search_results = results
|
st.session_state.search_results = results
|
||||||
summary = f"Found {len(results)} result(s) for **{arg}**. They are now in context for this chat session.\n\n"
|
response_text = f"🔍 Found {len(results)} result(s) for **'{arg}'**. Results are shown in the search panel above."
|
||||||
for i, r in enumerate(results, 1):
|
st.markdown(response_text)
|
||||||
summary += f"**{i}. [{r['title']}]({r['url']})** \n{r['snippet']}\n\n"
|
|
||||||
st.markdown(summary)
|
|
||||||
response_text = summary
|
|
||||||
else:
|
else:
|
||||||
msg = f'No results found for "{arg}".'
|
msg = f'No results found for "{arg}".'
|
||||||
st.warning(msg)
|
st.warning(msg)
|
||||||
|
|||||||
@ -146,131 +146,139 @@ def run_active_file():
|
|||||||
st.session_state.exec_results[active_file] = result
|
st.session_state.exec_results[active_file] = result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def render_editor():
|
class FileViewer:
|
||||||
"""Render the full Code Editor view with tabs, Ace editor, and run output."""
|
"""UI component that renders the code editor tabs, Ace editor, and run output."""
|
||||||
st.subheader("Code Editor")
|
|
||||||
|
|
||||||
if not st.session_state.open_files:
|
def __init__(self):
|
||||||
st.info("Please select a file to edit.")
|
self.fm = FileManager()
|
||||||
return
|
|
||||||
|
|
||||||
fm = FileManager()
|
def render(self):
|
||||||
|
"""Render the full Code Editor view with tabs, Ace editor, and run output."""
|
||||||
|
st.subheader("Code Editor")
|
||||||
|
|
||||||
# ── Tab bar via st.tabs() ─────────────────────────────────────────────────
|
if not st.session_state.open_files:
|
||||||
# Build one tab per open file, named by the file's basename.
|
st.info("Please select a file to edit.")
|
||||||
tab_names = [Path(f).name for f in st.session_state.open_files]
|
return
|
||||||
tabs = st.tabs(tab_names)
|
|
||||||
|
|
||||||
# Tab-Sprung via JavaScript — pop() verhindert Loop bei jedem Rerun.
|
# ── Tab bar via st.tabs() ─────────────────────────────────────────────
|
||||||
# Wenn _jump_to_tab gesetzt ist, klickt das Script den richtigen Tab an.
|
tab_names = [Path(f).name for f in st.session_state.open_files]
|
||||||
jump_target = st.session_state.pop("_jump_to_tab", None)
|
tabs = st.tabs(tab_names)
|
||||||
if jump_target and jump_target in st.session_state.open_files:
|
|
||||||
idx = st.session_state.open_files.index(jump_target)
|
|
||||||
st.components.v1.html(
|
|
||||||
f"""<script>
|
|
||||||
(function() {{
|
|
||||||
setTimeout(function() {{
|
|
||||||
const tabs = window.parent.document
|
|
||||||
.querySelectorAll('button[data-baseweb="tab"]');
|
|
||||||
if (tabs[{idx}]) tabs[{idx}].click();
|
|
||||||
}}, 100);
|
|
||||||
}})();
|
|
||||||
</script>""",
|
|
||||||
height=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
for idx, file_path in enumerate(st.session_state.open_files):
|
# Tab-Sprung via JavaScript — pop() verhindert Loop bei jedem Rerun.
|
||||||
with tabs[idx]:
|
jump_target = st.session_state.pop("_jump_to_tab", None)
|
||||||
# Load file content from disk on first open; afterwards use the cached version.
|
if jump_target and jump_target in st.session_state.open_files:
|
||||||
if file_path not in st.session_state.files_content:
|
idx = st.session_state.open_files.index(jump_target)
|
||||||
st.session_state.files_content[file_path] = fm.read_file(Path(file_path))
|
st.components.v1.html(
|
||||||
|
f"""<script>
|
||||||
file_language = LANG_MAP.get(Path(file_path).suffix, "text")
|
(function() {{
|
||||||
|
setTimeout(function() {{
|
||||||
# Ace editor widget — auto_update sends content to Python on each keystroke.
|
const tabs = window.parent.document
|
||||||
code = st_ace.st_ace(
|
.querySelectorAll('button[data-baseweb="tab"]');
|
||||||
value=st.session_state.files_content[file_path],
|
if (tabs[{idx}]) tabs[{idx}].click();
|
||||||
language=file_language,
|
}}, 100);
|
||||||
theme="monokai",
|
}})();
|
||||||
key=f"code_editor_{file_path}",
|
</script>""",
|
||||||
auto_update=True,
|
height=0,
|
||||||
height=400,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Keep the in-memory cache in sync with what the editor currently shows.
|
for idx, file_path in enumerate(st.session_state.open_files):
|
||||||
# REVIEW: redundant round-trip — st_ace returns the same value that was passed as
|
with tabs[idx]:
|
||||||
# `value=` unless the user edited the content; comparing and re-assigning on every
|
# Load file content from disk on first open; use cached version afterwards.
|
||||||
# rerun is a no-op most of the time and adds overhead.
|
if file_path not in st.session_state.files_content:
|
||||||
if code != st.session_state.files_content[file_path]:
|
st.session_state.files_content[file_path] = self.fm.read_file(Path(file_path))
|
||||||
st.session_state.files_content[file_path] = code
|
|
||||||
|
|
||||||
cols = st.columns([1, 1, 1, 1])
|
file_language = LANG_MAP.get(Path(file_path).suffix, "text")
|
||||||
with cols[0]:
|
|
||||||
if st.button("Save Changes", key=f"save_{file_path}"):
|
|
||||||
if fm.save_file(file_path, code):
|
|
||||||
st.success("File saved successfully!")
|
|
||||||
|
|
||||||
with cols[1]:
|
code = st_ace.st_ace(
|
||||||
if st.button("Close File", key=f"close_{file_path}"):
|
value=st.session_state.files_content[file_path],
|
||||||
st.session_state.open_files.remove(file_path)
|
language=file_language,
|
||||||
st.session_state.files_content.pop(file_path, None)
|
theme="monokai",
|
||||||
# Switch active_file to the next available tab.
|
key=f"code_editor_{file_path}",
|
||||||
st.session_state.active_file = (
|
auto_update=True,
|
||||||
st.session_state.open_files[0]
|
height=400,
|
||||||
if st.session_state.open_files else None
|
)
|
||||||
|
|
||||||
|
if code != st.session_state.files_content[file_path]:
|
||||||
|
st.session_state.files_content[file_path] = code
|
||||||
|
|
||||||
|
cols = st.columns([1, 1, 1, 1, 1])
|
||||||
|
with cols[0]:
|
||||||
|
if st.button("Save Changes", key=f"save_{file_path}"):
|
||||||
|
if self.fm.save_file(file_path, code):
|
||||||
|
st.success("File saved successfully!")
|
||||||
|
|
||||||
|
with cols[1]:
|
||||||
|
st.download_button(
|
||||||
|
label="⬇ Download",
|
||||||
|
data=st.session_state.files_content.get(file_path, ""),
|
||||||
|
file_name=Path(file_path).name,
|
||||||
|
mime="text/plain",
|
||||||
|
key=f"download_{file_path}",
|
||||||
)
|
)
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
with cols[2]:
|
with cols[2]:
|
||||||
if st.button("Rename File", key=f"rename_{file_path}"):
|
if st.button("Close File", key=f"close_{file_path}"):
|
||||||
_rename_dialog(file_path)
|
st.session_state.open_files.remove(file_path)
|
||||||
|
st.session_state.files_content.pop(file_path, None)
|
||||||
with cols[3]:
|
st.session_state.active_file = (
|
||||||
if st.button("Delete File", key=f"delete_{file_path}"):
|
st.session_state.open_files[0]
|
||||||
_delete_dialog(file_path)
|
if st.session_state.open_files else None
|
||||||
|
|
||||||
# ── Run + Output ──────────────────────────────────────────────────
|
|
||||||
if st.button("▶ Run Code", key=f"run_code_{file_path}", type="primary"):
|
|
||||||
run_active_file()
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
result = st.session_state.get("exec_results", {}).get(file_path)
|
|
||||||
if result:
|
|
||||||
st.subheader("Execution Output")
|
|
||||||
|
|
||||||
if result.get("ast_error"):
|
|
||||||
st.warning("⚠️ Syntax Error detected before execution — code was not run.")
|
|
||||||
elif result["return_code"] == 0:
|
|
||||||
st.success(f"✅ Exit code: 0")
|
|
||||||
else:
|
|
||||||
st.error(f"❌ Exit code: {result['return_code']}")
|
|
||||||
|
|
||||||
# Debug with AI — only shown when there is an error or stderr output.
|
|
||||||
if result["return_code"] != 0 or result.get("stderr"):
|
|
||||||
if st.button("🐛 Debug with AI", key=f"debug_with_ai_{file_path}", type="primary"):
|
|
||||||
file_name = Path(file_path).name
|
|
||||||
code_content = st.session_state.files_content.get(file_path, "")
|
|
||||||
lang = LANG_MAP.get(Path(file_path).suffix, "python")
|
|
||||||
formatted_output = DebugLogger.format_debug_output(result)
|
|
||||||
debug_message = (
|
|
||||||
f"I got an error while running **{file_name}**:\n\n"
|
|
||||||
f"```\n{formatted_output}\n```\n\n"
|
|
||||||
f"**Here is the code:**\n```{lang}\n{code_content}\n```\n\n"
|
|
||||||
f"Can you help me fix this?"
|
|
||||||
)
|
)
|
||||||
st.session_state.pending_debug_message = debug_message
|
|
||||||
st.session_state["_navigate_to_chat"] = True
|
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
if result.get("stdout"):
|
with cols[3]:
|
||||||
st.text_area("Standard Output", value=result["stdout"], height=200,
|
if st.button("Rename File", key=f"rename_{file_path}"):
|
||||||
disabled=True, key=f"run_stdout_{file_path}")
|
_rename_dialog(file_path)
|
||||||
if result.get("stderr"):
|
|
||||||
st.text_area("Standard Error", value=result["stderr"], height=200,
|
|
||||||
disabled=True, key=f"run_stderr_{file_path}")
|
|
||||||
if not result.get("stdout") and not result.get("stderr"):
|
|
||||||
st.info("No output produced by the code execution.")
|
|
||||||
|
|
||||||
|
with cols[4]:
|
||||||
|
if st.button("Delete File", key=f"delete_{file_path}"):
|
||||||
|
_delete_dialog(file_path)
|
||||||
|
|
||||||
|
# ── Run + Output ──────────────────────────────────────────────
|
||||||
|
if st.button("▶ Run Code", key=f"run_code_{file_path}", type="primary"):
|
||||||
|
run_active_file()
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
result = st.session_state.get("exec_results", {}).get(file_path)
|
||||||
|
if result:
|
||||||
|
st.subheader("Execution Output")
|
||||||
|
|
||||||
|
if result.get("ast_error"):
|
||||||
|
st.warning("⚠️ Syntax Error detected before execution — code was not run.")
|
||||||
|
elif result["return_code"] == 0:
|
||||||
|
st.success("✅ Exit code: 0")
|
||||||
|
else:
|
||||||
|
st.error(f"❌ Exit code: {result['return_code']}")
|
||||||
|
|
||||||
|
if result["return_code"] != 0 or result.get("stderr"):
|
||||||
|
if st.button("🐛 Debug with AI", key=f"debug_with_ai_{file_path}", type="primary"):
|
||||||
|
file_name = Path(file_path).name
|
||||||
|
code_content = st.session_state.files_content.get(file_path, "")
|
||||||
|
lang = LANG_MAP.get(Path(file_path).suffix, "python")
|
||||||
|
formatted_output = DebugLogger.format_debug_output(result)
|
||||||
|
debug_message = (
|
||||||
|
f"I got an error while running **{file_name}**:\n\n"
|
||||||
|
f"```\n{formatted_output}\n```\n\n"
|
||||||
|
f"**Here is the code:**\n```{lang}\n{code_content}\n```\n\n"
|
||||||
|
f"Can you help me fix this?"
|
||||||
|
)
|
||||||
|
st.session_state.pending_debug_message = debug_message
|
||||||
|
st.session_state["_navigate_to_chat"] = True
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
if result.get("stdout"):
|
||||||
|
st.text_area("Standard Output", value=result["stdout"], height=200,
|
||||||
|
disabled=True, key=f"run_stdout_{file_path}")
|
||||||
|
if result.get("stderr"):
|
||||||
|
st.text_area("Standard Error", value=result["stderr"], height=200,
|
||||||
|
disabled=True, key=f"run_stderr_{file_path}")
|
||||||
|
if not result.get("stdout") and not result.get("stderr"):
|
||||||
|
st.info("No output produced by the code execution.")
|
||||||
|
|
||||||
|
|
||||||
|
def render_editor():
|
||||||
|
"""Entry point for app.py — delegates to FileViewer."""
|
||||||
|
FileViewer().render()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@ -390,6 +390,22 @@ def render_sidebar():
|
|||||||
if st.button("Add Folder", key="btn_add_folder", use_container_width=True):
|
if st.button("Add Folder", key="btn_add_folder", use_container_width=True):
|
||||||
_add_folder_dialog("")
|
_add_folder_dialog("")
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
uploaded = st.file_uploader(
|
||||||
|
"Upload File",
|
||||||
|
type=["py", "js", "html", "css", "json", "yaml", "txt", "md"],
|
||||||
|
key="sidebar_file_upload",
|
||||||
|
)
|
||||||
|
if uploaded is not None:
|
||||||
|
if uploaded.size > 1_000_000:
|
||||||
|
st.error("File is too large (max 1 MB).")
|
||||||
|
else:
|
||||||
|
content = uploaded.getvalue().decode("utf-8", errors="replace")
|
||||||
|
dest = str(fm.base_path / uploaded.name)
|
||||||
|
if fm.save_file(dest, content):
|
||||||
|
st.success(f"'{uploaded.name}' uploaded successfully.")
|
||||||
|
|
||||||
# REVIEW: bare `return` at end of void function — no-op; can be removed.
|
# REVIEW: bare `return` at end of void function — no-op; can be removed.
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user