Compare commits
No commits in common. "main" and "finetuning" have entirely different histories.
main
...
finetuning
10
.env.example
10
.env.example
@ -3,9 +3,11 @@
|
|||||||
# 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=
|
HOST=silicon.fhgr.ch
|
||||||
PORT=
|
PORT=7080
|
||||||
API_KEY=EMPTY
|
API_KEY=EMPTY
|
||||||
MODEL=
|
MODEL=qwen3.5-35b-a3b
|
||||||
|
|
||||||
|
|
||||||
|
# Optional: Add more configuration variables as needed
|
||||||
|
# DEBUG=False
|
||||||
|
# LOG_LEVEL=INFO
|
||||||
|
|||||||
156
README.md
156
README.md
@ -4,15 +4,6 @@ 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)
|
||||||
@ -80,124 +71,52 @@ AISE_AIAgent/
|
|||||||
|
|
||||||
## Schnellstart
|
## Schnellstart
|
||||||
|
|
||||||
> Die folgenden Schritte funktionieren auf **Windows** und **macOS** — abweichende Befehle sind jeweils mit dem Betriebssystem gekennzeichnet.
|
### 1. Repository klonen
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Schritt 1 — Voraussetzungen prüfen
|
|
||||||
|
|
||||||
**Python 3.10 oder neuer** muss installiert sein.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Windows (PowerShell)
|
git clone https://gitea.fhgr.ch/meulilivio/AISE1_Project.git
|
||||||
python --version
|
cd AISE1_Project
|
||||||
|
|
||||||
# macOS (Terminal)
|
|
||||||
python3 --version
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Falls Python nicht installiert ist:
|
### 2. Virtuelle Umgebung aktivieren
|
||||||
- **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
|
||||||
git clone https://gitea.fhgr.ch/meulilivio/AISE1_Project_Irina_Livio.git
|
# Windows
|
||||||
cd AISE1_Project_Irina_Livio
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 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
|
.\.venv\Scripts\Activate.ps1
|
||||||
```
|
|
||||||
|
|
||||||
> Falls PowerShell die Ausführung blockiert, einmalig folgenden Befehl ausführen und danach erneut versuchen:
|
# macOS / Linux
|
||||||
> ```powershell
|
|
||||||
> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
|
|
||||||
> ```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# macOS (Terminal)
|
|
||||||
source .venv/bin/activate
|
source .venv/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
Nach erfolgreicher Aktivierung erscheint `(.venv)` am Anfang der Eingabezeile.
|
### 3. Abhängigkeiten installieren
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Schritt 5 — Abhängigkeiten installieren
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Dieser Schritt lädt alle benötigten Pakete herunter (~2–5 Minuten je nach Internetverbindung). Er muss nur einmal ausgeführt werden.
|
### 4. Umgebungsvariablen konfigurieren
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 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
|
||||||
```
|
```
|
||||||
|
|
||||||
Danach die Datei `.env` in einem Texteditor öffnen und die Werte eintragen welche per Mail mitgeteilt wurden (HOST, PORT, API_KEY, MODEL).
|
### 5. App starten
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Schritt 7 — App starten
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Windows & macOS
|
|
||||||
streamlit run frontend/app.py
|
streamlit run frontend/app.py
|
||||||
```
|
```
|
||||||
|
|
||||||
Streamlit öffnet die App automatisch im Standard-Browser unter `http://localhost:8501`.
|
### 6. Tests ausführen
|
||||||
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
|
||||||
@ -252,12 +171,10 @@ 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()`):
|
||||||
- Kompakte 4-spaltige Toolbar direkt über dem Chat-Input:
|
- Websuch-Panel oben — öffnet sich automatisch, wenn Suchergebnisse aktiv sind.
|
||||||
`[● 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`.
|
||||||
- Settings-Popover: Dateikontext-Toggle, Modell-Auswahl, Max-Token-Slider,
|
- Einstellungs-Expander: 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.
|
||||||
@ -311,9 +228,7 @@ 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, # list[{"title", "url", "snippet"}] — in system_prompter.py implementiert,
|
search_context=None, # reserviert, noch nicht verdrahtet
|
||||||
# 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"
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@ -326,7 +241,8 @@ 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 `sys.executable` (plattformübergreifend; zeigt auf den aktuell aktiven Python-Interpreter)
|
- `.py` — via `py`-Launcher (Windows) / `sys.executable`
|
||||||
|
- `.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}`.
|
||||||
|
|
||||||
@ -381,7 +297,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 in die History; `propose_next_action()` wird danach separat aufgerufen |
|
| `reject(feedback)` | Injiziert Feedback + Replan-Tag; schlägt neue Aktion vor |
|
||||||
| `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):
|
||||||
@ -392,7 +308,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()` |
|
||||||
| `build_all_tool_description()` | Erstellt eine menschenlesbare Tool-Liste für den System-Prompt |
|
| `get_tool_descriptions()` | 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
|
||||||
@ -419,11 +335,12 @@ 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`,
|
||||||
15 s Timeout, Output begrenzt auf `MAX_OUTPUT_LENGTH`
|
10 s Timeout, Output begrenzt auf `MAX_OUTPUT_LENGTH`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -610,28 +527,3 @@ 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,7 +167,6 @@ 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,7 +6,6 @@ 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
|
||||||
@ -41,7 +40,15 @@ 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 = [sys.executable, active_file.name]
|
cmd = ["py", 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,38 +9,26 @@ 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
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
0
backend/utils/__init__.py
Normal file
0
backend/utils/__init__.py
Normal file
0
backend/utils/server_utils.py
Normal file
0
backend/utils/server_utils.py
Normal file
@ -38,13 +38,15 @@ def main():
|
|||||||
st.markdown(
|
st.markdown(
|
||||||
"""
|
"""
|
||||||
<style>
|
<style>
|
||||||
.block-container { padding-top: 4rem; }
|
.block-container { padding-top: 1rem; }
|
||||||
[data-testid="stSidebarContent"] { padding-top: 0rem; }
|
[data-testid="stSidebarContent"] { padding-top: 1rem; }
|
||||||
</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,12 +85,6 @@ 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."""
|
||||||
@ -194,15 +188,14 @@ 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"])
|
||||||
with st.expander("➡️ Result", expanded=False):
|
result_text = step.get("result", "")
|
||||||
result_text = step.get("result", "")
|
# Colour the result based on whether the tool succeeded or failed.
|
||||||
# Colour the result based on whether the tool succeeded or failed.
|
if result_text.startswith("ERROR") or result_text.startswith("SYNTAX ERROR"):
|
||||||
if result_text.startswith("ERROR") or result_text.startswith("SYNTAX ERROR"):
|
st.error(result_text)
|
||||||
st.error(result_text)
|
elif result_text.startswith("OK") or result_text.startswith("DONE"):
|
||||||
elif result_text.startswith("OK") or result_text.startswith("DONE"):
|
st.success(result_text)
|
||||||
st.success(result_text)
|
else:
|
||||||
else:
|
st.code(result_text, language=None)
|
||||||
st.code(result_text, language=None)
|
|
||||||
|
|
||||||
# ── Idle: task input ──────────────────────────────────────────────────────
|
# ── Idle: task input ──────────────────────────────────────────────────────
|
||||||
if agent_status == "idle":
|
if agent_status == "idle":
|
||||||
@ -232,10 +225,7 @@ def render_agent_mode():
|
|||||||
if args:
|
if args:
|
||||||
_render_arguments(args)
|
_render_arguments(args)
|
||||||
|
|
||||||
if "agent_reject_feedback" not in st.session_state:
|
feedback = st.text_input(
|
||||||
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...",
|
||||||
@ -248,11 +238,10 @@ def render_agent_mode():
|
|||||||
_approve_action()
|
_approve_action()
|
||||||
st.rerun()
|
st.rerun()
|
||||||
with col2:
|
with col2:
|
||||||
st.button(
|
if st.button("Reject", use_container_width=True):
|
||||||
"Reject",
|
with st.spinner("Agent is replanning..."):
|
||||||
use_container_width=True,
|
_reject_action(feedback)
|
||||||
on_click=_handle_reject,
|
st.rerun()
|
||||||
)
|
|
||||||
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()
|
||||||
@ -379,7 +368,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 toolbar.
|
"""Render the collapsible web search panel above the chat history.
|
||||||
|
|
||||||
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.
|
||||||
@ -435,6 +424,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
|
||||||
@ -464,58 +456,7 @@ def render_normal_chat():
|
|||||||
with st.chat_message(message["role"]):
|
with st.chat_message(message["role"]):
|
||||||
st.markdown(message["content"])
|
st.markdown(message["content"])
|
||||||
|
|
||||||
# ── Toolbar — directly above the sticky chat input ────────────────────────
|
# Chat input — Enter to send, no extra button needed.
|
||||||
# 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:
|
||||||
@ -594,6 +535,42 @@ 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,12 +13,13 @@ 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", ".js": "javascript",
|
".py": "python", ".tex": "latex", ".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")
|
||||||
@ -208,7 +209,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,6 +110,106 @@ 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()):
|
||||||
@ -174,10 +274,10 @@ def render_filetree_arborist(tree):
|
|||||||
selected = tree_view(
|
selected = tree_view(
|
||||||
data=data,
|
data=data,
|
||||||
icons={"open": "📂", "closed": "📁"},
|
icons={"open": "📂", "closed": "📁"},
|
||||||
height=350,
|
height=400,
|
||||||
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
|
||||||
@ -269,6 +369,17 @@ 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).
|
||||||
@ -296,5 +407,6 @@ 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()
|
||||||
|
|||||||
BIN
project_exercise.pdf
Normal file
BIN
project_exercise.pdf
Normal file
Binary file not shown.
@ -1,5 +1,5 @@
|
|||||||
# Core Framework
|
# Core Framework
|
||||||
streamlit==1.57.0
|
streamlit>=1.28.0
|
||||||
streamlit_arborist>=0.1.0
|
streamlit_arborist>=0.1.0
|
||||||
|
|
||||||
# AI/LLM Integration
|
# AI/LLM Integration
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import sys
|
|
||||||
import pytest
|
import pytest
|
||||||
import subprocess
|
import subprocess
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
@ -62,6 +61,26 @@ 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
|
||||||
@ -308,13 +327,34 @@ def test_run_unicode_filename(mock_run, engine, tmp_path):
|
|||||||
assert result["rc"] == 0
|
assert result["rc"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
# 17. .py nutzt sys.executable als Interpreter
|
# 16. .tex nutzt pdflatex
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|
||||||
@patch("subprocess.run")
|
@patch("subprocess.run")
|
||||||
def test_python_uses_sys_executable(mock_run, engine, tmp_path):
|
def test_tex_uses_pdflatex(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)")
|
||||||
|
|
||||||
@ -328,7 +368,7 @@ def test_python_uses_sys_executable(mock_run, engine, tmp_path):
|
|||||||
|
|
||||||
args, _ = mock_run.call_args
|
args, _ = mock_run.call_args
|
||||||
|
|
||||||
assert args[0][0] == sys.executable
|
assert args[0][0] == "py"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------
|
# ---------------------------------------------------------
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user