Compare commits
21 Commits
finetuning
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e669a2234 | |||
| 5b32b4e6d0 | |||
|
|
a70efea25f | ||
|
|
c702675758 | ||
|
|
adfb780f7c | ||
|
|
8209136bed | ||
|
|
3dcea4ece2 | ||
|
|
db371b906c | ||
|
|
e5e06da35b | ||
|
|
9633def4de | ||
|
|
3e8bab87cb | ||
|
|
46b9c925dd | ||
| 6a7e90ab40 | |||
|
|
3216a9a5f6 | ||
|
|
ba40f6fc82 | ||
|
|
a8403eee92 | ||
|
|
ea05437154 | ||
|
|
da236ce14e | ||
| 33969a7b59 | |||
|
|
8327c54bdb | ||
| 9836c2981a |
10
.env.example
10
.env.example
@ -3,11 +3,9 @@
|
|||||||
# NOTE: Never commit .env with real secrets to version control!
|
# NOTE: Never commit .env with real secrets to version control!
|
||||||
|
|
||||||
# Silicon Server Configuration
|
# Silicon Server Configuration
|
||||||
HOST=silicon.fhgr.ch
|
HOST=
|
||||||
PORT=7080
|
PORT=
|
||||||
API_KEY=EMPTY
|
API_KEY=EMPTY
|
||||||
MODEL=qwen3.5-35b-a3b
|
MODEL=
|
||||||
|
|
||||||
|
|
||||||
# Optional: Add more configuration variables as needed
|
|
||||||
# DEBUG=False
|
|
||||||
# LOG_LEVEL=INFO
|
|
||||||
|
|||||||
158
README.md
158
README.md
@ -4,6 +4,15 @@ KI-unterstützter Lightweight Code Editor auf Basis von Streamlit (AISE501 Sprin
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Projektinformationen
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Modul** | AI in Software Engineering 1 (AISE501) |
|
||||||
|
| **Autoren** | Irina Rüegg & Livio Meuli |
|
||||||
|
| **Semester** | Spring 2026 |
|
||||||
|
---
|
||||||
|
|
||||||
## Inhaltsverzeichnis
|
## Inhaltsverzeichnis
|
||||||
|
|
||||||
1. [Projektstruktur](#projektstruktur)
|
1. [Projektstruktur](#projektstruktur)
|
||||||
@ -71,52 +80,124 @@ AISE_AIAgent/
|
|||||||
|
|
||||||
## Schnellstart
|
## Schnellstart
|
||||||
|
|
||||||
### 1. Repository klonen
|
> Die folgenden Schritte funktionieren auf **Windows** und **macOS** — abweichende Befehle sind jeweils mit dem Betriebssystem gekennzeichnet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Schritt 1 — Voraussetzungen prüfen
|
||||||
|
|
||||||
|
**Python 3.10 oder neuer** muss installiert sein.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://gitea.fhgr.ch/meulilivio/AISE1_Project.git
|
# Windows (PowerShell)
|
||||||
cd AISE1_Project
|
python --version
|
||||||
|
|
||||||
|
# macOS (Terminal)
|
||||||
|
python3 --version
|
||||||
```
|
```
|
||||||
|
|
||||||
### 2. Virtuelle Umgebung aktivieren
|
Falls Python nicht installiert ist:
|
||||||
|
- **Windows:** [python.org/downloads](https://www.python.org/downloads/) herunterladen und installieren. Bei der Installation „Add Python to PATH" aktivieren.
|
||||||
|
- **macOS:** [python.org/downloads](https://www.python.org/downloads/) herunterladen und installieren, **oder** via Homebrew: `brew install python3`
|
||||||
|
|
||||||
|
**Git** muss ebenfalls installiert sein:
|
||||||
|
```bash
|
||||||
|
git --version
|
||||||
|
```
|
||||||
|
Falls nicht vorhanden: [git-scm.com](https://git-scm.com/downloads) (Windows) bzw. `brew install git` (macOS).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Schritt 2 — Repository klonen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Windows
|
git clone https://gitea.fhgr.ch/meulilivio/AISE1_Project_Irina_Livio.git
|
||||||
.\.venv\Scripts\Activate.ps1
|
cd AISE1_Project_Irina_Livio
|
||||||
|
```
|
||||||
|
|
||||||
# macOS / Linux
|
---
|
||||||
|
|
||||||
|
### Schritt 3 — Virtuelle Umgebung erstellen
|
||||||
|
|
||||||
|
Eine virtuelle Umgebung isoliert die Projekt-Abhängigkeiten vom restlichen System. Sie muss einmalig erstellt werden.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows (PowerShell)
|
||||||
|
python -m venv .venv
|
||||||
|
|
||||||
|
# macOS (Terminal)
|
||||||
|
python3 -m venv .venv
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Schritt 4 — Virtuelle Umgebung aktivieren
|
||||||
|
|
||||||
|
Die Umgebung muss **jedes Mal neu aktiviert** werden, wenn ein neues Terminal geöffnet wird.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Windows (PowerShell)
|
||||||
|
.\.venv\Scripts\Activate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
> Falls PowerShell die Ausführung blockiert, einmalig folgenden Befehl ausführen und danach erneut versuchen:
|
||||||
|
> ```powershell
|
||||||
|
> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
> ```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS (Terminal)
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. Abhängigkeiten installieren
|
Nach erfolgreicher Aktivierung erscheint `(.venv)` am Anfang der Eingabezeile.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Schritt 5 — Abhängigkeiten installieren
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. Umgebungsvariablen konfigurieren
|
Dieser Schritt lädt alle benötigten Pakete herunter (~2–5 Minuten je nach Internetverbindung). Er muss nur einmal ausgeführt werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Schritt 6 — Umgebungsvariablen konfigurieren
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Windows (PowerShell)
|
||||||
|
copy .env.example .env
|
||||||
|
|
||||||
|
# macOS (Terminal)
|
||||||
cp .env.example .env
|
cp .env.example .env
|
||||||
# .env öffnen und HOST, PORT, API_KEY, MODEL eintragen
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 5. App starten
|
Danach die Datei `.env` in einem Texteditor öffnen und die Werte eintragen welche per Mail mitgeteilt wurden (HOST, PORT, API_KEY, MODEL).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Schritt 7 — App starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Windows & macOS
|
||||||
streamlit run frontend/app.py
|
streamlit run frontend/app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
### 6. Tests ausführen
|
Streamlit öffnet die App automatisch im Standard-Browser unter `http://localhost:8501`.
|
||||||
|
Falls der Browser nicht automatisch aufgeht, die URL manuell eingeben.
|
||||||
|
|
||||||
|
Zum **Beenden** der App im Terminal `Ctrl + C` drücken.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Schritt 8 — Tests ausführen (optional)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pytest tests/ -v
|
pytest tests/ -v
|
||||||
```
|
```
|
||||||
|
|
||||||
**Optionale Abhängigkeit:** Die LaTeX-Unterstützung (`.tex`-Ausführung) erfordert `pdflatex`
|
|
||||||
im System-`PATH`. Ohne `pdflatex` funktioniert der Editor weiterhin; beim Ausführen einer
|
|
||||||
`.tex`-Datei erscheint ein `FileNotFoundError` im stderr-Panel.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Frontend
|
## Frontend
|
||||||
@ -171,10 +252,12 @@ vorausgefüllten Debug-Nachricht.
|
|||||||
Zwei sich gegenseitig ausschliessende Ansichten, umgeschaltet via `st.toggle("Agent Mode")`:
|
Zwei sich gegenseitig ausschliessende Ansichten, umgeschaltet via `st.toggle("Agent Mode")`:
|
||||||
|
|
||||||
**Normaler Chat** (`render_normal_chat()`):
|
**Normaler Chat** (`render_normal_chat()`):
|
||||||
- Websuch-Panel oben — öffnet sich automatisch, wenn Suchergebnisse aktiv sind.
|
- Kompakte 4-spaltige Toolbar direkt über dem Chat-Input:
|
||||||
|
`[● Agent Mode]` `[🔍 Search]` `[🗑️ Clear]` `[⚙️ Settings]`
|
||||||
|
— Web-Suche und Einstellungen jeweils als `st.popover`, Clear öffnet einen Bestätigungs-Dialog.
|
||||||
- System-Prompt wird vor jeder ausgehenden Nachricht neu generiert (`_set_system_prompt()`).
|
- System-Prompt wird vor jeder ausgehenden Nachricht neu generiert (`_set_system_prompt()`).
|
||||||
- Unterstützt Slash-Befehle `/search <Abfrage>` und `/search clear`.
|
- Unterstützt Slash-Befehle `/search <Abfrage>` und `/search clear`.
|
||||||
- Einstellungs-Expander: Dateikontext-Toggle, Modell-Auswahl, Max-Token-Slider,
|
- Settings-Popover: Dateikontext-Toggle, Modell-Auswahl, Max-Token-Slider,
|
||||||
benutzerdefinierter System-Prompt.
|
benutzerdefinierter System-Prompt.
|
||||||
- «Mit KI debuggen»-Nachrichten vom Editor werden über `pending_debug_message` im
|
- «Mit KI debuggen»-Nachrichten vom Editor werden über `pending_debug_message` im
|
||||||
Session-State weitergeleitet.
|
Session-State weitergeleitet.
|
||||||
@ -228,7 +311,9 @@ Generiert kontextbewusste System-Prompts. Signatur:
|
|||||||
SystemPrompter.generate_prompt(
|
SystemPrompter.generate_prompt(
|
||||||
user_message="",
|
user_message="",
|
||||||
file_context=None, # {"name": str, "content": str}
|
file_context=None, # {"name": str, "content": str}
|
||||||
search_context=None, # reserviert, noch nicht verdrahtet
|
search_context=None, # list[{"title", "url", "snippet"}] — in system_prompter.py implementiert,
|
||||||
|
# aber in chat.py nicht verwendet: dort wird Search-Kontext direkt
|
||||||
|
# als <search_context>-Block vor die Nachricht eingefügt
|
||||||
task_type="default", # "debug" | "explain" | "optimize" | "default"
|
task_type="default", # "debug" | "explain" | "optimize" | "default"
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@ -241,8 +326,7 @@ Funktion oder Klasse zurückzugeben, nach der der Benutzer fragt, anstatt die ge
|
|||||||
### `execution_engine.py`
|
### `execution_engine.py`
|
||||||
Führt Dateien in einem Subprocess mit `capture_output=True`, `text=True` und einem
|
Führt Dateien in einem Subprocess mit `capture_output=True`, `text=True` und einem
|
||||||
`RUN_TIMEOUT` von 30 Sekunden aus. Aktuell unterstützt:
|
`RUN_TIMEOUT` von 30 Sekunden aus. Aktuell unterstützt:
|
||||||
- `.py` — via `py`-Launcher (Windows) / `sys.executable`
|
- `.py` — via `sys.executable` (plattformübergreifend; zeigt auf den aktuell aktiven Python-Interpreter)
|
||||||
- `.tex` — via `pdflatex -interaction=nonstopmode` (erfordert pdflatex im PATH)
|
|
||||||
|
|
||||||
Rückgabe: `{"stdout": str, "stderr": str, "rc": int}`.
|
Rückgabe: `{"stdout": str, "stderr": str, "rc": int}`.
|
||||||
|
|
||||||
@ -297,7 +381,7 @@ Wichtige Methoden:
|
|||||||
| `start_task(task)` | Setzt den gesamten Zustand zurück, befüllt History mit System + Aufgabe |
|
| `start_task(task)` | Setzt den gesamten Zustand zurück, befüllt History mit System + Aufgabe |
|
||||||
| `propose_next_action()` | Ruft das LLM auf, parst JSON, speichert als `pending_action` |
|
| `propose_next_action()` | Ruft das LLM auf, parst JSON, speichert als `pending_action` |
|
||||||
| `approve()` | Führt das ausstehende Tool via `dispatch_tool()` aus, loggt Ergebnis |
|
| `approve()` | Führt das ausstehende Tool via `dispatch_tool()` aus, loggt Ergebnis |
|
||||||
| `reject(feedback)` | Injiziert Feedback + Replan-Tag; schlägt neue Aktion vor |
|
| `reject(feedback)` | Injiziert Feedback + Replan-Tag in die History; `propose_next_action()` wird danach separat aufgerufen |
|
||||||
| `follow_up(question)` | Fügt nach «done» eine Folgefrage ein, setzt Schleife fort |
|
| `follow_up(question)` | Fügt nach «done» eine Folgefrage ein, setzt Schleife fort |
|
||||||
|
|
||||||
Hilfsfunktionen (Modul-Ebene):
|
Hilfsfunktionen (Modul-Ebene):
|
||||||
@ -308,7 +392,7 @@ Hilfsfunktionen (Modul-Ebene):
|
|||||||
| `trim_messages(msgs)` | Entfernt alte Turns, wenn History `MAX_HISTORY_CHARS` (80 000 Zeichen) überschreitet; System-Nachricht + Original-Aufgabe bleiben immer erhalten |
|
| `trim_messages(msgs)` | Entfernt alte Turns, wenn History `MAX_HISTORY_CHARS` (80 000 Zeichen) überschreitet; System-Nachricht + Original-Aufgabe bleiben immer erhalten |
|
||||||
| `_strip_code_fences(text)` | Entfernt ` ```json `- / ` ``` `-Wrapper aus LLM-Antworten |
|
| `_strip_code_fences(text)` | Entfernt ` ```json `- / ` ``` `-Wrapper aus LLM-Antworten |
|
||||||
| `dispatch_tool(name, arguments)` | Leitet weiter an `MCPToolAdapter.call_tool()` |
|
| `dispatch_tool(name, arguments)` | Leitet weiter an `MCPToolAdapter.call_tool()` |
|
||||||
| `get_tool_descriptions()` | Erstellt eine menschenlesbare Tool-Liste für den System-Prompt |
|
| `build_all_tool_description()` | Erstellt eine menschenlesbare Tool-Liste für den System-Prompt |
|
||||||
|
|
||||||
### `mcp_server_adapter.py`
|
### `mcp_server_adapter.py`
|
||||||
Liest `mcp_server_config.json`, startet jeden Server als stdio-Subprocess (immer mit
|
Liest `mcp_server_config.json`, startet jeden Server als stdio-Subprocess (immer mit
|
||||||
@ -335,12 +419,11 @@ Alle drei Server sind FastMCP-Applikationen, die über stdio kommunizieren.
|
|||||||
- Beide Tools nutzen SSRF-Schutz (gleiche URL-Validierung wie `search_manager.py`)
|
- Beide Tools nutzen SSRF-Schutz (gleiche URL-Validierung wie `search_manager.py`)
|
||||||
|
|
||||||
**`mcp_server_code_execution.py`** — Sandbox-Python-Analyse:
|
**`mcp_server_code_execution.py`** — Sandbox-Python-Analyse:
|
||||||
- `check_code_safety(code)` — statische Analyse; blockiert gefährliche Imports/Builtins
|
|
||||||
- `analyse_structure(code)` — AST-basierte Strukturzusammenfassung
|
- `analyse_structure(code)` — AST-basierte Strukturzusammenfassung
|
||||||
- `lint_code(code)` — pyflakes-Analyse
|
- `lint_code(code)` — pyflakes-Analyse
|
||||||
- `python_code_validation(code)` — Sicherheits- + Syntaxprüfung ohne Ausführung
|
- `python_code_validation(code)` — Sicherheits- + Syntaxprüfung ohne Ausführung
|
||||||
- `run_python_sandboxed(code)` — Ausführung in einem Subprocess mit `PYTHONIOENCODING=utf-8`,
|
- `run_python_sandboxed(code)` — Ausführung in einem Subprocess mit `PYTHONIOENCODING=utf-8`,
|
||||||
10 s Timeout, Output begrenzt auf `MAX_OUTPUT_LENGTH`
|
15 s Timeout, Output begrenzt auf `MAX_OUTPUT_LENGTH`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -527,3 +610,28 @@ beteiligt ist.
|
|||||||
|
|
||||||
Die App funktioniert mit jedem OpenAI-kompatiblen API-Endpunkt (vLLM, Ollama mit
|
Die App funktioniert mit jedem OpenAI-kompatiblen API-Endpunkt (vLLM, Ollama mit
|
||||||
OpenAI-Shim, OpenAI selbst usw.).
|
OpenAI-Shim, OpenAI selbst usw.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Einsatz von KI-Werkzeugen
|
||||||
|
|
||||||
|
Während der Entwicklung wurden KI-Assistenten (Claude, GitHub Copilot) als
|
||||||
|
Werkzeuge eingesetzt — vergleichbar mit der Nutzung von Dokumentation, Stack Overflow
|
||||||
|
oder einer IDE mit Autocomplete.
|
||||||
|
|
||||||
|
Konkret bedeutet das:
|
||||||
|
- **Eigenständige Konzeption und Architektur**: Die Gesamtarchitektur (Schichtentrennung
|
||||||
|
Frontend / Manager / Agent), die Designentscheidungen und die Aufteilung in Komponenten
|
||||||
|
wurden selbst erarbeitet und geplant.
|
||||||
|
- **Implementierung mit Unterstützung**: Boilerplate-Code, Docstrings und einzelne
|
||||||
|
Hilfsfunktionen wurden teils mit KI-Unterstützung geschrieben, verstanden und
|
||||||
|
anschliessend in das Projekt integriert.
|
||||||
|
- **MCP-Integration und Chat-Logik**: Für das Model Context Protocol und den
|
||||||
|
Chat-Assistenten haben wir uns an den Kursbeispielen des Dozenten orientiert und
|
||||||
|
diese als Ausgangsbasis adaptiert und erweitert.
|
||||||
|
- **Debugging und Refactoring**: KI wurde als Gesprächspartner genutzt, um Fehler zu
|
||||||
|
analysieren und Lösungsansätze zu diskutieren — die Entscheidungen wurden jedoch
|
||||||
|
eigenständig getroffen und umgesetzt.
|
||||||
|
|
||||||
|
Der gesamte Code wurde von uns gelesen, verstanden und bewusst eingesetzt.
|
||||||
|
Unkritisch übernommener oder nicht verstandener Code wurde nicht ins Projekt aufgenommen.
|
||||||
|
|||||||
@ -167,6 +167,7 @@ Example:
|
|||||||
- After validation passes, run it with run_python to verify correctness.
|
- After validation passes, run it with run_python to verify correctness.
|
||||||
- If an error occurs, analyse it and try to fix it (up to 3 retries).
|
- If an error occurs, analyse it and try to fix it (up to 3 retries).
|
||||||
- Stay within the workspace directory.
|
- Stay within the workspace directory.
|
||||||
|
- Never use emojis, umlauts (ä, ö, ü, Ä, Ö, Ü, ß), or any non-ASCII characters in string literals or print() calls — the execution environment uses cp1252 encoding which cannot handle them.
|
||||||
- When the task is fully complete, call the "done" tool.
|
- When the task is fully complete, call the "done" tool.
|
||||||
- If you receive a <human_message>, acknowledge it and adjust your plan.
|
- If you receive a <human_message>, acknowledge it and adjust your plan.
|
||||||
- If you receive a <replan> tag, revise your plan before choosing the next tool.
|
- If you receive a <replan> tag, revise your plan before choosing the next tool.
|
||||||
|
|||||||
@ -6,6 +6,7 @@ processes from blocking the UI indefinitely.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from backend.managers.debug_logger import get_logger
|
from backend.managers.debug_logger import get_logger
|
||||||
@ -40,15 +41,7 @@ class ExecutionEngine:
|
|||||||
|
|
||||||
# Build the shell command depending on file type
|
# Build the shell command depending on file type
|
||||||
if suffix == ".py":
|
if suffix == ".py":
|
||||||
cmd = ["py", active_file.name]
|
cmd = [sys.executable, active_file.name]
|
||||||
elif suffix == ".tex":
|
|
||||||
# pdflatex in non-interactive mode so it never waits for input
|
|
||||||
cmd = [
|
|
||||||
"pdflatex",
|
|
||||||
"-interaction=nonstopmode",
|
|
||||||
f"-output-directory={current_dir}",
|
|
||||||
active_file.name,
|
|
||||||
]
|
|
||||||
else:
|
else:
|
||||||
return {"stdout": "", "stderr": f"Unsupported file type: {suffix}", "rc": 1}
|
return {"stdout": "", "stderr": f"Unsupported file type: {suffix}", "rc": 1}
|
||||||
|
|
||||||
|
|||||||
@ -9,26 +9,38 @@ logger = get_logger(__name__)
|
|||||||
MAX_FILE_CHARS = 4000
|
MAX_FILE_CHARS = 4000
|
||||||
|
|
||||||
# Per-task base prompts — selected via the task_type parameter.
|
# Per-task base prompts — selected via the task_type parameter.
|
||||||
|
# Appended to every prompt — ensures generated code is safe to run on Windows
|
||||||
|
# where the console encoding is cp1252 and cannot handle emojis or non-ASCII chars.
|
||||||
|
_CODE_SAFETY_NOTE = (
|
||||||
|
" When writing or suggesting code, never use emojis, umlauts (ä, ö, ü, Ä, Ö, Ü, ß), "
|
||||||
|
"or any non-ASCII characters in string literals or print statements, "
|
||||||
|
"as the execution environment uses cp1252 encoding which cannot handle them."
|
||||||
|
)
|
||||||
|
|
||||||
_TASK_PROMPTS: dict[str, str] = {
|
_TASK_PROMPTS: dict[str, str] = {
|
||||||
"debug": (
|
"debug": (
|
||||||
"You are a debugging expert integrated into a lightweight code editor. "
|
"You are a debugging expert integrated into a lightweight code editor. "
|
||||||
"Focus on identifying and fixing errors. "
|
"Focus on identifying and fixing errors. "
|
||||||
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
|
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
|
||||||
|
+ _CODE_SAFETY_NOTE
|
||||||
),
|
),
|
||||||
"explain": (
|
"explain": (
|
||||||
"You are a code explainer integrated into a lightweight code editor. "
|
"You are a code explainer integrated into a lightweight code editor. "
|
||||||
"Use simple language and examples. "
|
"Use simple language and examples. "
|
||||||
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
|
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
|
||||||
|
+ _CODE_SAFETY_NOTE
|
||||||
),
|
),
|
||||||
"optimize": (
|
"optimize": (
|
||||||
"You are a code optimization expert integrated into a lightweight code editor. "
|
"You are a code optimization expert integrated into a lightweight code editor. "
|
||||||
"Focus on performance and readability. "
|
"Focus on performance and readability. "
|
||||||
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
|
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
|
||||||
|
+ _CODE_SAFETY_NOTE
|
||||||
),
|
),
|
||||||
"default": (
|
"default": (
|
||||||
"You are an expert code assistant integrated into a lightweight code editor. "
|
"You are an expert code assistant integrated into a lightweight code editor. "
|
||||||
"Help the user with code suggestions, debugging, explanations, and improvements. "
|
"Help the user with code suggestions, debugging, explanations, and improvements. "
|
||||||
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
|
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
|
||||||
|
+ _CODE_SAFETY_NOTE
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -38,15 +38,13 @@ def main():
|
|||||||
st.markdown(
|
st.markdown(
|
||||||
"""
|
"""
|
||||||
<style>
|
<style>
|
||||||
.block-container { padding-top: 1rem; }
|
.block-container { padding-top: 4rem; }
|
||||||
[data-testid="stSidebarContent"] { padding-top: 1rem; }
|
[data-testid="stSidebarContent"] { padding-top: 0rem; }
|
||||||
</style>
|
</style>
|
||||||
""",
|
""",
|
||||||
unsafe_allow_html=True,
|
unsafe_allow_html=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
st.title("Lightweight code editor")
|
|
||||||
|
|
||||||
render_sidebar()
|
render_sidebar()
|
||||||
|
|
||||||
# Switch between the two main views based on the sidebar radio button
|
# Switch between the two main views based on the sidebar radio button
|
||||||
|
|||||||
131
frontend/chat.py
131
frontend/chat.py
@ -85,6 +85,12 @@ def _reject_action(feedback: str):
|
|||||||
st.session_state.agent_pending_action = next_action
|
st.session_state.agent_pending_action = next_action
|
||||||
st.session_state.agent_status = "waiting_approval"
|
st.session_state.agent_status = "waiting_approval"
|
||||||
|
|
||||||
|
def _handle_reject():
|
||||||
|
feedback = st.session_state.agent_reject_feedback
|
||||||
|
with st.spinner("Agent is replanning..."):
|
||||||
|
_reject_action(feedback)
|
||||||
|
st.session_state.agent_reject_feedback = ""
|
||||||
|
|
||||||
|
|
||||||
def _followup_agent(question: str):
|
def _followup_agent(question: str):
|
||||||
"""Continue a finished task by injecting a follow-up question and resuming the loop."""
|
"""Continue a finished task by injecting a follow-up question and resuming the loop."""
|
||||||
@ -188,14 +194,15 @@ def render_agent_mode():
|
|||||||
st.caption(f"Thought: {step['thought']}")
|
st.caption(f"Thought: {step['thought']}")
|
||||||
if step.get("arguments"):
|
if step.get("arguments"):
|
||||||
_render_arguments(step["arguments"])
|
_render_arguments(step["arguments"])
|
||||||
result_text = step.get("result", "")
|
with st.expander("➡️ Result", expanded=False):
|
||||||
# Colour the result based on whether the tool succeeded or failed.
|
result_text = step.get("result", "")
|
||||||
if result_text.startswith("ERROR") or result_text.startswith("SYNTAX ERROR"):
|
# Colour the result based on whether the tool succeeded or failed.
|
||||||
st.error(result_text)
|
if result_text.startswith("ERROR") or result_text.startswith("SYNTAX ERROR"):
|
||||||
elif result_text.startswith("OK") or result_text.startswith("DONE"):
|
st.error(result_text)
|
||||||
st.success(result_text)
|
elif result_text.startswith("OK") or result_text.startswith("DONE"):
|
||||||
else:
|
st.success(result_text)
|
||||||
st.code(result_text, language=None)
|
else:
|
||||||
|
st.code(result_text, language=None)
|
||||||
|
|
||||||
# ── Idle: task input ──────────────────────────────────────────────────────
|
# ── Idle: task input ──────────────────────────────────────────────────────
|
||||||
if agent_status == "idle":
|
if agent_status == "idle":
|
||||||
@ -225,7 +232,10 @@ def render_agent_mode():
|
|||||||
if args:
|
if args:
|
||||||
_render_arguments(args)
|
_render_arguments(args)
|
||||||
|
|
||||||
feedback = st.text_input(
|
if "agent_reject_feedback" not in st.session_state:
|
||||||
|
st.session_state.agent_reject_feedback = ""
|
||||||
|
|
||||||
|
st.text_input(
|
||||||
"Rejection feedback (optional):",
|
"Rejection feedback (optional):",
|
||||||
key="agent_reject_feedback",
|
key="agent_reject_feedback",
|
||||||
placeholder="e.g. Use a different approach...",
|
placeholder="e.g. Use a different approach...",
|
||||||
@ -238,10 +248,11 @@ def render_agent_mode():
|
|||||||
_approve_action()
|
_approve_action()
|
||||||
st.rerun()
|
st.rerun()
|
||||||
with col2:
|
with col2:
|
||||||
if st.button("Reject", use_container_width=True):
|
st.button(
|
||||||
with st.spinner("Agent is replanning..."):
|
"Reject",
|
||||||
_reject_action(feedback)
|
use_container_width=True,
|
||||||
st.rerun()
|
on_click=_handle_reject,
|
||||||
|
)
|
||||||
with col3:
|
with col3:
|
||||||
if st.button("Abort Task", use_container_width=True):
|
if st.button("Abort Task", use_container_width=True):
|
||||||
_reset_agent()
|
_reset_agent()
|
||||||
@ -368,7 +379,7 @@ def _clear_chat_dialog():
|
|||||||
# ── Normal Chat ───────────────────────────────────────────────────────────────
|
# ── Normal Chat ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _render_search_panel():
|
def _render_search_panel():
|
||||||
"""Render the collapsible web search panel above the chat history.
|
"""Render the collapsible web search panel above the chat toolbar.
|
||||||
|
|
||||||
Stores results in session_state.search_results so they are automatically
|
Stores results in session_state.search_results so they are automatically
|
||||||
injected as context into the next message the user sends.
|
injected as context into the next message the user sends.
|
||||||
@ -424,9 +435,6 @@ 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
|
||||||
@ -456,7 +464,58 @@ def render_normal_chat():
|
|||||||
with st.chat_message(message["role"]):
|
with st.chat_message(message["role"]):
|
||||||
st.markdown(message["content"])
|
st.markdown(message["content"])
|
||||||
|
|
||||||
# Chat input — Enter to send, no extra button needed.
|
# ── Toolbar — directly above the sticky chat input ────────────────────────
|
||||||
|
# In Streamlit, st.chat_input is a fixed footer. Elements placed BEFORE it
|
||||||
|
# in code appear in the scrollable content area right above the input bar.
|
||||||
|
_render_search_panel()
|
||||||
|
|
||||||
|
col_clear, col_agent, col_settings = st.columns([1, 1, 1])
|
||||||
|
with col_clear:
|
||||||
|
if st.button("🗑️ Clear Chat", use_container_width=True):
|
||||||
|
_clear_chat_dialog()
|
||||||
|
with col_agent:
|
||||||
|
st.toggle("Agent Mode", key="agent_mode")
|
||||||
|
with col_settings:
|
||||||
|
with st.popover("⚙️ Settings", use_container_width=True):
|
||||||
|
current_file = st.session_state.get("active_file")
|
||||||
|
if current_file:
|
||||||
|
st.toggle(
|
||||||
|
f"Include current file as context: **{Path(current_file).name}**",
|
||||||
|
key="include_file_context",
|
||||||
|
value=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
st.toggle(
|
||||||
|
"Include current file as context",
|
||||||
|
key="include_file_context",
|
||||||
|
value=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
default_model = chat_manager.model or ""
|
||||||
|
model_options = [default_model] if default_model else []
|
||||||
|
st.selectbox("Model", model_options, key="selected_model")
|
||||||
|
|
||||||
|
st.slider(
|
||||||
|
"Max Response Tokens",
|
||||||
|
min_value=256,
|
||||||
|
max_value=8000,
|
||||||
|
value=chat_manager.max_tokens,
|
||||||
|
step=256,
|
||||||
|
key="chat_max_tokens",
|
||||||
|
)
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
st.text_area(
|
||||||
|
"Custom System Prompt (overrides default if set)",
|
||||||
|
key="custom_system_prompt",
|
||||||
|
height=120,
|
||||||
|
placeholder="Leave empty to use the default assistant prompt with optional file context.",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Chat input — sticky footer, always at the very bottom of the viewport.
|
||||||
# Supports /search <query> and /search clear as special commands.
|
# Supports /search <query> and /search clear as special commands.
|
||||||
user_input = st.chat_input("Type a message or /search <query>...")
|
user_input = st.chat_input("Type a message or /search <query>...")
|
||||||
if user_input:
|
if user_input:
|
||||||
@ -535,42 +594,6 @@ def render_normal_chat():
|
|||||||
st.session_state.chat_history.append({"role": "assistant", "content": ai_response})
|
st.session_state.chat_history.append({"role": "assistant", "content": ai_response})
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
# 5d — Clear Chat opens a confirmation dialog instead of deleting immediately.
|
|
||||||
if st.button("🗑️ Clear Chat"):
|
|
||||||
_clear_chat_dialog()
|
|
||||||
|
|
||||||
# Toggle to switch to Agent Mode (render_normal_chat and render_agent_mode are
|
|
||||||
# mutually exclusive, so the same key here causes no DuplicateWidgetID conflict).
|
|
||||||
st.toggle("Agent Mode", key="agent_mode")
|
|
||||||
|
|
||||||
# 5h — Settings expander: file context toggle, model, token limit, custom prompt.
|
|
||||||
with st.expander("⚙️ Settings", expanded=False):
|
|
||||||
st.toggle("Include current file as context", key="include_file_context", value=True)
|
|
||||||
|
|
||||||
st.divider()
|
|
||||||
|
|
||||||
default_model = chat_manager.model or ""
|
|
||||||
model_options = [default_model] if default_model else []
|
|
||||||
for m in ["claude-3-5-sonnet-20241022", "claude-3-haiku-20240307", "gpt-4o", "gpt-4o-mini"]:
|
|
||||||
if m not in model_options:
|
|
||||||
model_options.append(m)
|
|
||||||
st.selectbox("Model", model_options, key="selected_model")
|
|
||||||
|
|
||||||
st.slider(
|
|
||||||
"Max Response Tokens",
|
|
||||||
min_value=256, max_value=8000,
|
|
||||||
value=chat_manager.max_tokens,
|
|
||||||
step=256, key="chat_max_tokens",
|
|
||||||
)
|
|
||||||
|
|
||||||
st.divider()
|
|
||||||
|
|
||||||
st.text_area(
|
|
||||||
"Custom System Prompt (overrides default if set)",
|
|
||||||
key="custom_system_prompt",
|
|
||||||
height=120,
|
|
||||||
placeholder="Leave empty to use the default assistant prompt with optional file context.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ── Entry point ───────────────────────────────────────────────────────────────
|
# ── Entry point ───────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -13,13 +13,12 @@ logger = get_logger(__name__)
|
|||||||
|
|
||||||
# Maps file extensions to Ace editor language modes for syntax highlighting.
|
# Maps file extensions to Ace editor language modes for syntax highlighting.
|
||||||
LANG_MAP = {
|
LANG_MAP = {
|
||||||
".py": "python", ".tex": "latex", ".js": "javascript",
|
".py": "python", ".js": "javascript",
|
||||||
".html": "html", ".css": "css", ".sh": "bash",
|
".html": "html", ".css": "css", ".sh": "bash",
|
||||||
".json": "json", ".yaml": "yaml", ".yml": "yaml"
|
".json": "json", ".yaml": "yaml", ".yml": "yaml"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ── Modals ────────────────────────────────────────────────────────────────────
|
# ── Modals ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@st.dialog("Rename File")
|
@st.dialog("Rename File")
|
||||||
@ -209,7 +208,7 @@ class FileViewer:
|
|||||||
|
|
||||||
with cols[1]:
|
with cols[1]:
|
||||||
st.download_button(
|
st.download_button(
|
||||||
label="⬇ Download",
|
label="Download",
|
||||||
data=st.session_state.files_content.get(file_path, ""),
|
data=st.session_state.files_content.get(file_path, ""),
|
||||||
file_name=Path(file_path).name,
|
file_name=Path(file_path).name,
|
||||||
mime="text/plain",
|
mime="text/plain",
|
||||||
|
|||||||
@ -110,106 +110,6 @@ def _add_folder_dialog(parent_path: str = ""):
|
|||||||
if cancel:
|
if cancel:
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|
||||||
|
|
||||||
@st.dialog("Rename File")
|
|
||||||
def _rename_file_dialog(relative_file_path: str, file_name: str):
|
|
||||||
"""
|
|
||||||
Dialog to rename a file.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
relative_file_path (str): The current relative path (without base path) to the file to rename, including the file name.
|
|
||||||
file_name (str): The current name of the file, including the extension.
|
|
||||||
"""
|
|
||||||
st.write(f"Current name: **{file_name}**")
|
|
||||||
|
|
||||||
with st.form("rename_file_form"):
|
|
||||||
new_name = st.text_input(
|
|
||||||
"New name:",
|
|
||||||
value=Path(relative_file_path).stem
|
|
||||||
)
|
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
|
|
||||||
with col1:
|
|
||||||
submitted = st.form_submit_button(
|
|
||||||
"Confirm",
|
|
||||||
type="primary",
|
|
||||||
use_container_width=True
|
|
||||||
)
|
|
||||||
|
|
||||||
with col2:
|
|
||||||
cancel = st.form_submit_button(
|
|
||||||
"Cancel",
|
|
||||||
use_container_width=True
|
|
||||||
)
|
|
||||||
|
|
||||||
if submitted:
|
|
||||||
if not new_name.strip():
|
|
||||||
st.warning("Please enter a name.")
|
|
||||||
elif "/" in new_name or "\\" in new_name or "." in new_name:
|
|
||||||
st.warning("Name must not contain slashes.")
|
|
||||||
else:
|
|
||||||
if fm.rename_file(relative_file_path, new_name.strip()):
|
|
||||||
absolute_file_path = str(Path(fm.base_path / relative_file_path))
|
|
||||||
ext = Path(absolute_file_path).suffix
|
|
||||||
new_file_path = str(
|
|
||||||
Path(absolute_file_path).parent / (Path(new_name.strip()).stem + ext)
|
|
||||||
)
|
|
||||||
|
|
||||||
if absolute_file_path in st.session_state.open_files:
|
|
||||||
i = st.session_state.open_files.index(absolute_file_path)
|
|
||||||
st.session_state.open_files[i] = new_file_path
|
|
||||||
|
|
||||||
if absolute_file_path in st.session_state.files_content:
|
|
||||||
st.session_state.files_content[new_file_path] = \
|
|
||||||
st.session_state.files_content.pop(absolute_file_path)
|
|
||||||
|
|
||||||
if st.session_state.active_file == absolute_file_path:
|
|
||||||
st.session_state.active_file = new_file_path
|
|
||||||
|
|
||||||
st.rerun()
|
|
||||||
else:
|
|
||||||
st.error("Rename failed. Check that the file still exists.")
|
|
||||||
st.error(f"Attempted to rename: {relative_file_path} to {new_name.strip()}")
|
|
||||||
|
|
||||||
if cancel:
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
|
|
||||||
@st.dialog("Delete File")
|
|
||||||
def _delete_file_dialog(relative_file_path: str, file_name: str):
|
|
||||||
"""Confirmation dialog before permanently deleting a file.
|
|
||||||
|
|
||||||
After a successful delete the file is also removed from the editor's
|
|
||||||
open-files list and content cache so it cannot be saved back to disk.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
relative_file_path: Workspace-relative path to the file (used by FileManager).
|
|
||||||
file_name: Display name shown in the warning message.
|
|
||||||
"""
|
|
||||||
st.warning(f"Delete **{file_name}**? This cannot be undone.")
|
|
||||||
|
|
||||||
col1, col2 = st.columns(2)
|
|
||||||
with col1:
|
|
||||||
if st.button("Delete", type="primary", use_container_width=True):
|
|
||||||
if fm.delete_file(relative_file_path):
|
|
||||||
abs_file_path = str(Path(fm.base_path) / relative_file_path)
|
|
||||||
st.session_state.open_files.remove(abs_file_path)
|
|
||||||
st.session_state.files_content.pop(abs_file_path, None)
|
|
||||||
# Fall back to the first remaining open file, or None if all tabs are closed.
|
|
||||||
if st.session_state.active_file == abs_file_path:
|
|
||||||
st.session_state.active_file = (
|
|
||||||
st.session_state.open_files[0]
|
|
||||||
if st.session_state.open_files else None
|
|
||||||
)
|
|
||||||
st.rerun()
|
|
||||||
else:
|
|
||||||
st.error("Delete failed. Check that the file still exists.")
|
|
||||||
with col2:
|
|
||||||
if st.button("Cancel", use_container_width=True):
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
|
|
||||||
# ── File tree ─────────────────────────────────────────────────────────────────
|
# ── File tree ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def build_arborist_tree(tree, parent_path=Path()):
|
def build_arborist_tree(tree, parent_path=Path()):
|
||||||
@ -274,10 +174,10 @@ def render_filetree_arborist(tree):
|
|||||||
selected = tree_view(
|
selected = tree_view(
|
||||||
data=data,
|
data=data,
|
||||||
icons={"open": "📂", "closed": "📁"},
|
icons={"open": "📂", "closed": "📁"},
|
||||||
height=400,
|
height=350,
|
||||||
selection=active_selection,
|
selection=active_selection,
|
||||||
select_internal_nodes=True, # allow clicking folder names, not just files
|
select_internal_nodes=True, # allow clicking folder names, not just files
|
||||||
open_by_default=True
|
open_by_default=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
return selected
|
return selected
|
||||||
@ -369,17 +269,6 @@ def render_sidebar():
|
|||||||
_add_folder_dialog(folder_rel)
|
_add_folder_dialog(folder_rel)
|
||||||
if st.button("Delete Folder", key="btn_delete_folder", use_container_width=True):
|
if st.button("Delete Folder", key="btn_delete_folder", use_container_width=True):
|
||||||
_delete_folder_dialog(folder_rel, folder_name)
|
_delete_folder_dialog(folder_rel, folder_name)
|
||||||
|
|
||||||
if st.session_state.get("active_file"):
|
|
||||||
active_file_name = Path(st.session_state.active_file).name
|
|
||||||
file_rel = str(Path(st.session_state.active_file).relative_to(fm.base_path))
|
|
||||||
|
|
||||||
with st.container(border=True):
|
|
||||||
st.write(f"**File actions:** {active_file_name}")
|
|
||||||
if st.button("Rename File", key="btn_rename_file", use_container_width=True):
|
|
||||||
_rename_file_dialog(file_rel, active_file_name)
|
|
||||||
if st.button("Delete File", key="btn_delete_active_file", use_container_width=True):
|
|
||||||
_delete_file_dialog(file_rel, active_file_name)
|
|
||||||
|
|
||||||
with add_more:
|
with add_more:
|
||||||
# Popover for workspace-root actions (not tied to any selected folder).
|
# Popover for workspace-root actions (not tied to any selected folder).
|
||||||
@ -407,6 +296,5 @@ def render_sidebar():
|
|||||||
st.success(f"'{uploaded.name}' uploaded successfully.")
|
st.success(f"'{uploaded.name}' uploaded successfully.")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
render_sidebar()
|
render_sidebar()
|
||||||
|
|||||||
Binary file not shown.
@ -1,5 +1,5 @@
|
|||||||
# Core Framework
|
# Core Framework
|
||||||
streamlit>=1.28.0
|
streamlit==1.57.0
|
||||||
streamlit_arborist>=0.1.0
|
streamlit_arborist>=0.1.0
|
||||||
|
|
||||||
# AI/LLM Integration
|
# AI/LLM Integration
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import sys
|
||||||
import pytest
|
import pytest
|
||||||
import subprocess
|
import subprocess
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
@ -61,26 +62,6 @@ def test_run_python_file_error(mock_run, engine, tmp_path):
|
|||||||
assert "ZeroDivisionError" in result["stderr"]
|
assert "ZeroDivisionError" in result["stderr"]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# 3. LaTeX-Datei wird kompiliert
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
@patch("subprocess.run")
|
|
||||||
def test_run_tex_file_success(mock_run, engine, tmp_path):
|
|
||||||
file = tmp_path / "doc.tex"
|
|
||||||
file.write_text("\\documentclass{article}")
|
|
||||||
|
|
||||||
mock_run.return_value = Mock(
|
|
||||||
stdout="PDF created",
|
|
||||||
stderr="",
|
|
||||||
returncode=0
|
|
||||||
)
|
|
||||||
|
|
||||||
result = engine.run_code(file)
|
|
||||||
|
|
||||||
assert result["rc"] == 0
|
|
||||||
assert "PDF created" in result["stdout"]
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 4. Unsupported File Type
|
# 4. Unsupported File Type
|
||||||
@ -327,34 +308,13 @@ def test_run_unicode_filename(mock_run, engine, tmp_path):
|
|||||||
assert result["rc"] == 0
|
assert result["rc"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 16. .tex nutzt pdflatex
|
# 17. .py nutzt sys.executable als Interpreter
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
@patch("subprocess.run")
|
@patch("subprocess.run")
|
||||||
def test_tex_uses_pdflatex(mock_run, engine, tmp_path):
|
def test_python_uses_sys_executable(mock_run, engine, tmp_path):
|
||||||
file = tmp_path / "doc.tex"
|
|
||||||
file.write_text("x")
|
|
||||||
|
|
||||||
mock_run.return_value = Mock(
|
|
||||||
stdout="",
|
|
||||||
stderr="",
|
|
||||||
returncode=0
|
|
||||||
)
|
|
||||||
|
|
||||||
engine.run_code(file)
|
|
||||||
|
|
||||||
args, _ = mock_run.call_args
|
|
||||||
|
|
||||||
assert args[0][0] == "pdflatex"
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
# 17. .py nutzt py Interpreter
|
|
||||||
# ---------------------------------------------------------
|
|
||||||
|
|
||||||
@patch("subprocess.run")
|
|
||||||
def test_python_uses_py_interpreter(mock_run, engine, tmp_path):
|
|
||||||
file = tmp_path / "main.py"
|
file = tmp_path / "main.py"
|
||||||
file.write_text("print(1)")
|
file.write_text("print(1)")
|
||||||
|
|
||||||
@ -368,7 +328,7 @@ def test_python_uses_py_interpreter(mock_run, engine, tmp_path):
|
|||||||
|
|
||||||
args, _ = mock_run.call_args
|
args, _ = mock_run.call_args
|
||||||
|
|
||||||
assert args[0][0] == "py"
|
assert args[0][0] == sys.executable
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user