diff --git a/README.md b/README.md index be4f596..44a32e2 100644 --- a/README.md +++ b/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) +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 ``` @@ -45,48 +62,248 @@ AISE_AIAgent/ │ └── 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 ``` -## Komponenten +--- -### 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 +## Frontend -### Backend Manager (`backend/managers/`) -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) +Das Frontend besteht aus Streamlit-Komponenten, die zusammen eine interaktive Code-Editor-Oberfläche bilden. -### Backend Agent (`backend/agent/`) -Autonomes AI-Agent-System für komplexe Coding-Aufgaben: -- **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) +### `app.py` +Haupteinstiegspunkt der Applikation. Orchestriert das Layout und initialisiert alle UI-Komponenten (Sidebar, Editor, Chat). -### Workspace (`workspace/`) -- Sandbox-Verzeichnis, in dem der Agent Dateien erstellt und ausführt -- Verhindert, dass der Agent auf Dateien ausserhalb dieses Verzeichnisses zugreift +### `state.py` +Zentralisierte Verwaltung des Streamlit Session-State. Stellt sicher, dass alle Komponenten denselben Zustand (geöffnete Datei, Chat-History, Agent-Status) teilen. -## 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 -- **Chat-Interface**: KI-gestützter Code-Assistent -- **Code-Ausführung**: Python-Code sicher ausführen mit Debug-Output -- **Web-Suche**: Dokumentation und Beispiele via DuckDuckGo abrufen -- **Autonomer Agent**: MCP-basierter Coding-Agent mit Plan-Act-Observe-Loop -- **RAG Tool-Auswahl**: Semantische Tool-Selektion via Sentence Transformers +### `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_.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 @@ -132,37 +349,43 @@ streamlit run frontend/app.py 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`): - -```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 +## Architektur-Übersicht ``` -Frontend (Streamlit) ──► Backend Manager ──► AI API - │ - └──► Coding Agent ──► MCP-Adapter ──► MCP-Server - (Code / File / Web) -``` - -## Entwicklung - -```bash -git add . -git commit -m "Deine Nachricht" -git push origin main +┌─────────────────────────────────────────────────────────────────┐ +│ 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 │ +└─────────────────────────────────────────────────────────────────┘ ``` diff --git a/READMEnew.md b/READMEnew.md deleted file mode 100644 index 4722fb9..0000000 --- a/READMEnew.md +++ /dev/null @@ -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_.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 │ -└─────────────────────────────────────────────────────────────────┘ -``` diff --git a/backend/managers/chat_manager.py b/backend/managers/chat_manager.py index 65a9114..be72c53 100644 --- a/backend/managers/chat_manager.py +++ b/backend/managers/chat_manager.py @@ -32,6 +32,21 @@ class ChatManager: # Chat history stored in memory 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: """Append a single message to the conversation history.""" self.chat_history.append({"role": role, "content": content}) @@ -66,10 +81,10 @@ class ChatManager: if self.api_key and self.api_key != "EMPTY": 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 = { "model": self.model, - "messages": self.chat_history, + "messages": self._build_payload_messages(), "temperature": 0.7, "max_tokens": 2000, "stream": False, diff --git a/backend/managers/file_manager.py b/backend/managers/file_manager.py index 0c87dd9..9fe7a77 100644 --- a/backend/managers/file_manager.py +++ b/backend/managers/file_manager.py @@ -14,6 +14,13 @@ logger = get_logger(__name__) WORKSPACE = Path("workspace") 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: """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)) return False - def get_file_tree(self): - """ - Builds a nested dictionary representing the file tree starting from the base path. - Directories are represented as keys with dictionary values, - and files are represented as keys with None + def get_file_tree(self, filter_extensions: bool = True) -> dict: + """Builds a nested dictionary representing the file tree. + + Directories are represented as keys with dictionary values, + 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: dict: A nested dictionary representing the file tree. """ logger.info("Getting file tree ...") - - def build_tree(path: Path): + def build_tree(path: Path) -> dict: tree = {} - for item in sorted(path.iterdir()): if item.is_dir(): - tree[item.name] = build_tree(item) # recurse into sub-folders + tree[item.name] = build_tree(item) 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 build_tree(self.base_path) def list_files(self, extensions: list[str] | None = None) -> list[Path]: diff --git a/frontend/chat.py b/frontend/chat.py index 3d4c32c..be6cf7a 100644 --- a/frontend/chat.py +++ b/frontend/chat.py @@ -2,6 +2,7 @@ import asyncio import json +import re from pathlib import Path import streamlit as st @@ -285,6 +286,22 @@ def render_agent_mode(): # ── Normal Chat helpers ─────────────────────────────────────────────────────── +def _strip_search_context_from_history(chat_manager: ChatManager) -> None: + """Remove 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 "" in msg["content"]: + msg["content"] = re.sub( + r".*?\n\n", + "", + msg["content"], + flags=re.DOTALL, + ).strip() + + def _detect_task_type(user_input: str) -> str: """Infer the task type from keywords in the user message.""" lower = user_input.lower() @@ -367,7 +384,7 @@ def _render_search_panel(): 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" - with st.expander(label, expanded=False): + with st.expander(label, expanded=bool(search_results)): col_input, col_btn = st.columns([5, 1]) with col_input: query = st.text_input( @@ -397,6 +414,9 @@ def _render_search_panel(): st.divider() if st.button("Clear search results", use_container_width=True): st.session_state.search_results = [] + cm = st.session_state.get("chat_manager") + if cm: + _strip_search_context_from_history(cm) st.rerun() @@ -412,6 +432,9 @@ def render_normal_chat(): logger.info("Chat mode") 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. if st.session_state.get("selected_model"): chat_manager.model = st.session_state.selected_model @@ -470,11 +493,8 @@ def render_normal_chat(): if 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" - for i, r in enumerate(results, 1): - summary += f"**{i}. [{r['title']}]({r['url']})** \n{r['snippet']}\n\n" - st.markdown(summary) - response_text = summary + response_text = f"🔍 Found {len(results)} result(s) for **'{arg}'**. Results are shown in the search panel above." + st.markdown(response_text) else: msg = f'No results found for "{arg}".' st.warning(msg) diff --git a/frontend/editor.py b/frontend/editor.py index ba1c13e..2264ae9 100644 --- a/frontend/editor.py +++ b/frontend/editor.py @@ -146,131 +146,139 @@ def run_active_file(): st.session_state.exec_results[active_file] = result return result -def render_editor(): - """Render the full Code Editor view with tabs, Ace editor, and run output.""" - st.subheader("Code Editor") +class FileViewer: + """UI component that renders the code editor tabs, Ace editor, and run output.""" - if not st.session_state.open_files: - st.info("Please select a file to edit.") - return + def __init__(self): + self.fm = FileManager() - 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() ───────────────────────────────────────────────── - # Build one tab per open file, named by the file's basename. - tab_names = [Path(f).name for f in st.session_state.open_files] - tabs = st.tabs(tab_names) + if not st.session_state.open_files: + st.info("Please select a file to edit.") + return - # Tab-Sprung via JavaScript — pop() verhindert Loop bei jedem Rerun. - # Wenn _jump_to_tab gesetzt ist, klickt das Script den richtigen Tab an. - jump_target = st.session_state.pop("_jump_to_tab", None) - 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"""""", - height=0, - ) + # ── Tab bar via st.tabs() ───────────────────────────────────────────── + tab_names = [Path(f).name for f in st.session_state.open_files] + tabs = st.tabs(tab_names) - for idx, file_path in enumerate(st.session_state.open_files): - with tabs[idx]: - # Load file content from disk on first open; afterwards use the cached version. - if file_path not in st.session_state.files_content: - st.session_state.files_content[file_path] = fm.read_file(Path(file_path)) - - file_language = LANG_MAP.get(Path(file_path).suffix, "text") - - # Ace editor widget — auto_update sends content to Python on each keystroke. - code = st_ace.st_ace( - value=st.session_state.files_content[file_path], - language=file_language, - theme="monokai", - key=f"code_editor_{file_path}", - auto_update=True, - height=400, + # Tab-Sprung via JavaScript — pop() verhindert Loop bei jedem Rerun. + jump_target = st.session_state.pop("_jump_to_tab", None) + 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"""""", + height=0, ) - # Keep the in-memory cache in sync with what the editor currently shows. - # REVIEW: redundant round-trip — st_ace returns the same value that was passed as - # `value=` unless the user edited the content; comparing and re-assigning on every - # rerun is a no-op most of the time and adds overhead. - if code != st.session_state.files_content[file_path]: - st.session_state.files_content[file_path] = code + for idx, file_path in enumerate(st.session_state.open_files): + with tabs[idx]: + # Load file content from disk on first open; use cached version afterwards. + if file_path not in st.session_state.files_content: + st.session_state.files_content[file_path] = self.fm.read_file(Path(file_path)) - cols = st.columns([1, 1, 1, 1]) - 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!") + file_language = LANG_MAP.get(Path(file_path).suffix, "text") - with cols[1]: - if st.button("Close File", key=f"close_{file_path}"): - st.session_state.open_files.remove(file_path) - st.session_state.files_content.pop(file_path, None) - # Switch active_file to the next available tab. - st.session_state.active_file = ( - st.session_state.open_files[0] - if st.session_state.open_files else None + code = st_ace.st_ace( + value=st.session_state.files_content[file_path], + language=file_language, + theme="monokai", + key=f"code_editor_{file_path}", + auto_update=True, + height=400, + ) + + 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]: - if st.button("Rename File", key=f"rename_{file_path}"): - _rename_dialog(file_path) - - with cols[3]: - 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(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?" + with cols[2]: + if st.button("Close File", key=f"close_{file_path}"): + st.session_state.open_files.remove(file_path) + st.session_state.files_content.pop(file_path, None) + st.session_state.active_file = ( + st.session_state.open_files[0] + if st.session_state.open_files else None ) - 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.") + with cols[3]: + if st.button("Rename File", key=f"rename_{file_path}"): + _rename_dialog(file_path) + 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__": diff --git a/frontend/sidebar.py b/frontend/sidebar.py index e265d61..94ed3a2 100644 --- a/frontend/sidebar.py +++ b/frontend/sidebar.py @@ -390,6 +390,22 @@ def render_sidebar(): if st.button("Add Folder", key="btn_add_folder", use_container_width=True): _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. return