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!
|
||||
|
||||
# Silicon Server Configuration
|
||||
HOST=
|
||||
PORT=
|
||||
HOST=silicon.fhgr.ch
|
||||
PORT=7080
|
||||
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
|
||||
|
||||
1. [Projektstruktur](#projektstruktur)
|
||||
@ -80,124 +71,52 @@ AISE_AIAgent/
|
||||
|
||||
## Schnellstart
|
||||
|
||||
> 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.
|
||||
### 1. Repository klonen
|
||||
|
||||
```bash
|
||||
# Windows (PowerShell)
|
||||
python --version
|
||||
|
||||
# macOS (Terminal)
|
||||
python3 --version
|
||||
git clone https://gitea.fhgr.ch/meulilivio/AISE1_Project.git
|
||||
cd AISE1_Project
|
||||
```
|
||||
|
||||
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
|
||||
### 2. Virtuelle Umgebung aktivieren
|
||||
|
||||
```bash
|
||||
git clone https://gitea.fhgr.ch/meulilivio/AISE1_Project_Irina_Livio.git
|
||||
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)
|
||||
# Windows
|
||||
.\.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)
|
||||
# macOS / Linux
|
||||
source .venv/bin/activate
|
||||
```
|
||||
|
||||
Nach erfolgreicher Aktivierung erscheint `(.venv)` am Anfang der Eingabezeile.
|
||||
|
||||
---
|
||||
|
||||
### Schritt 5 — Abhängigkeiten installieren
|
||||
### 3. Abhängigkeiten installieren
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
### Schritt 6 — Umgebungsvariablen konfigurieren
|
||||
### 4. Umgebungsvariablen konfigurieren
|
||||
|
||||
```bash
|
||||
# Windows (PowerShell)
|
||||
copy .env.example .env
|
||||
|
||||
# macOS (Terminal)
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
### Schritt 7 — App starten
|
||||
### 5. App starten
|
||||
|
||||
```bash
|
||||
# Windows & macOS
|
||||
streamlit run frontend/app.py
|
||||
```
|
||||
|
||||
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)
|
||||
### 6. Tests ausführen
|
||||
|
||||
```bash
|
||||
pytest tests/ -v
|
||||
```
|
||||
|
||||
**Optionale Abhängigkeit:** Die LaTeX-Unterstützung (`.tex`-Ausführung) erfordert `pdflatex`
|
||||
im System-`PATH`. Ohne `pdflatex` funktioniert der Editor weiterhin; beim Ausführen einer
|
||||
`.tex`-Datei erscheint ein `FileNotFoundError` im stderr-Panel.
|
||||
|
||||
---
|
||||
|
||||
## Frontend
|
||||
@ -252,12 +171,10 @@ vorausgefüllten Debug-Nachricht.
|
||||
Zwei sich gegenseitig ausschliessende Ansichten, umgeschaltet via `st.toggle("Agent Mode")`:
|
||||
|
||||
**Normaler Chat** (`render_normal_chat()`):
|
||||
- Kompakte 4-spaltige Toolbar direkt über dem Chat-Input:
|
||||
`[● Agent Mode]` `[🔍 Search]` `[🗑️ Clear]` `[⚙️ Settings]`
|
||||
— Web-Suche und Einstellungen jeweils als `st.popover`, Clear öffnet einen Bestätigungs-Dialog.
|
||||
- Websuch-Panel oben — öffnet sich automatisch, wenn Suchergebnisse aktiv sind.
|
||||
- System-Prompt wird vor jeder ausgehenden Nachricht neu generiert (`_set_system_prompt()`).
|
||||
- Unterstützt Slash-Befehle `/search <Abfrage>` und `/search clear`.
|
||||
- Settings-Popover: Dateikontext-Toggle, Modell-Auswahl, Max-Token-Slider,
|
||||
- Einstellungs-Expander: Dateikontext-Toggle, Modell-Auswahl, Max-Token-Slider,
|
||||
benutzerdefinierter System-Prompt.
|
||||
- «Mit KI debuggen»-Nachrichten vom Editor werden über `pending_debug_message` im
|
||||
Session-State weitergeleitet.
|
||||
@ -311,9 +228,7 @@ Generiert kontextbewusste System-Prompts. Signatur:
|
||||
SystemPrompter.generate_prompt(
|
||||
user_message="",
|
||||
file_context=None, # {"name": str, "content": str}
|
||||
search_context=None, # list[{"title", "url", "snippet"}] — in system_prompter.py implementiert,
|
||||
# aber in chat.py nicht verwendet: dort wird Search-Kontext direkt
|
||||
# als <search_context>-Block vor die Nachricht eingefügt
|
||||
search_context=None, # reserviert, noch nicht verdrahtet
|
||||
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`
|
||||
Führt Dateien in einem Subprocess mit `capture_output=True`, `text=True` und einem
|
||||
`RUN_TIMEOUT` von 30 Sekunden aus. Aktuell unterstützt:
|
||||
- `.py` — via `sys.executable` (plattformübergreifend; zeigt auf den aktuell aktiven Python-Interpreter)
|
||||
- `.py` — via `py`-Launcher (Windows) / `sys.executable`
|
||||
- `.tex` — via `pdflatex -interaction=nonstopmode` (erfordert pdflatex im PATH)
|
||||
|
||||
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 |
|
||||
| `propose_next_action()` | Ruft das LLM auf, parst JSON, speichert als `pending_action` |
|
||||
| `approve()` | Führt das ausstehende Tool via `dispatch_tool()` aus, loggt Ergebnis |
|
||||
| `reject(feedback)` | Injiziert Feedback + Replan-Tag in die History; `propose_next_action()` wird danach separat aufgerufen |
|
||||
| `reject(feedback)` | Injiziert Feedback + Replan-Tag; schlägt neue Aktion vor |
|
||||
| `follow_up(question)` | Fügt nach «done» eine Folgefrage ein, setzt Schleife fort |
|
||||
|
||||
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 |
|
||||
| `_strip_code_fences(text)` | Entfernt ` ```json `- / ` ``` `-Wrapper aus LLM-Antworten |
|
||||
| `dispatch_tool(name, arguments)` | Leitet weiter an `MCPToolAdapter.call_tool()` |
|
||||
| `build_all_tool_description()` | Erstellt eine menschenlesbare Tool-Liste für den System-Prompt |
|
||||
| `get_tool_descriptions()` | Erstellt eine menschenlesbare Tool-Liste für den System-Prompt |
|
||||
|
||||
### `mcp_server_adapter.py`
|
||||
Liest `mcp_server_config.json`, startet jeden Server als stdio-Subprocess (immer mit
|
||||
@ -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`)
|
||||
|
||||
**`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
|
||||
- `lint_code(code)` — pyflakes-Analyse
|
||||
- `python_code_validation(code)` — Sicherheits- + Syntaxprüfung ohne Ausführung
|
||||
- `run_python_sandboxed(code)` — Ausführung in einem Subprocess mit `PYTHONIOENCODING=utf-8`,
|
||||
15 s Timeout, Output begrenzt auf `MAX_OUTPUT_LENGTH`
|
||||
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
|
||||
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.
|
||||
- If an error occurs, analyse it and try to fix it (up to 3 retries).
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@ -6,7 +6,6 @@ processes from blocking the UI indefinitely.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from backend.managers.debug_logger import get_logger
|
||||
@ -41,7 +40,15 @@ class ExecutionEngine:
|
||||
|
||||
# Build the shell command depending on file type
|
||||
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:
|
||||
return {"stdout": "", "stderr": f"Unsupported file type: {suffix}", "rc": 1}
|
||||
|
||||
|
||||
@ -9,38 +9,26 @@ logger = get_logger(__name__)
|
||||
MAX_FILE_CHARS = 4000
|
||||
|
||||
# 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] = {
|
||||
"debug": (
|
||||
"You are a debugging expert integrated into a lightweight code editor. "
|
||||
"Focus on identifying and fixing errors. "
|
||||
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
|
||||
+ _CODE_SAFETY_NOTE
|
||||
),
|
||||
"explain": (
|
||||
"You are a code explainer integrated into a lightweight code editor. "
|
||||
"Use simple language and examples. "
|
||||
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
|
||||
+ _CODE_SAFETY_NOTE
|
||||
),
|
||||
"optimize": (
|
||||
"You are a code optimization expert integrated into a lightweight code editor. "
|
||||
"Focus on performance and readability. "
|
||||
"Be concise and precise. Use markdown and fenced code blocks where appropriate."
|
||||
+ _CODE_SAFETY_NOTE
|
||||
),
|
||||
"default": (
|
||||
"You are an expert code assistant integrated into a lightweight code editor. "
|
||||
"Help the user with code suggestions, debugging, explanations, and improvements. "
|
||||
"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(
|
||||
"""
|
||||
<style>
|
||||
.block-container { padding-top: 4rem; }
|
||||
[data-testid="stSidebarContent"] { padding-top: 0rem; }
|
||||
.block-container { padding-top: 1rem; }
|
||||
[data-testid="stSidebarContent"] { padding-top: 1rem; }
|
||||
</style>
|
||||
""",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
st.title("Lightweight code editor")
|
||||
|
||||
render_sidebar()
|
||||
|
||||
# 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_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):
|
||||
"""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']}")
|
||||
if step.get("arguments"):
|
||||
_render_arguments(step["arguments"])
|
||||
with st.expander("➡️ Result", expanded=False):
|
||||
result_text = step.get("result", "")
|
||||
# Colour the result based on whether the tool succeeded or failed.
|
||||
if result_text.startswith("ERROR") or result_text.startswith("SYNTAX ERROR"):
|
||||
st.error(result_text)
|
||||
elif result_text.startswith("OK") or result_text.startswith("DONE"):
|
||||
st.success(result_text)
|
||||
else:
|
||||
st.code(result_text, language=None)
|
||||
result_text = step.get("result", "")
|
||||
# Colour the result based on whether the tool succeeded or failed.
|
||||
if result_text.startswith("ERROR") or result_text.startswith("SYNTAX ERROR"):
|
||||
st.error(result_text)
|
||||
elif result_text.startswith("OK") or result_text.startswith("DONE"):
|
||||
st.success(result_text)
|
||||
else:
|
||||
st.code(result_text, language=None)
|
||||
|
||||
# ── Idle: task input ──────────────────────────────────────────────────────
|
||||
if agent_status == "idle":
|
||||
@ -232,10 +225,7 @@ def render_agent_mode():
|
||||
if args:
|
||||
_render_arguments(args)
|
||||
|
||||
if "agent_reject_feedback" not in st.session_state:
|
||||
st.session_state.agent_reject_feedback = ""
|
||||
|
||||
st.text_input(
|
||||
feedback = st.text_input(
|
||||
"Rejection feedback (optional):",
|
||||
key="agent_reject_feedback",
|
||||
placeholder="e.g. Use a different approach...",
|
||||
@ -248,11 +238,10 @@ def render_agent_mode():
|
||||
_approve_action()
|
||||
st.rerun()
|
||||
with col2:
|
||||
st.button(
|
||||
"Reject",
|
||||
use_container_width=True,
|
||||
on_click=_handle_reject,
|
||||
)
|
||||
if st.button("Reject", use_container_width=True):
|
||||
with st.spinner("Agent is replanning..."):
|
||||
_reject_action(feedback)
|
||||
st.rerun()
|
||||
with col3:
|
||||
if st.button("Abort Task", use_container_width=True):
|
||||
_reset_agent()
|
||||
@ -379,7 +368,7 @@ def _clear_chat_dialog():
|
||||
# ── Normal Chat ───────────────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
injected as context into the next message the user sends.
|
||||
@ -435,6 +424,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
|
||||
@ -464,58 +456,7 @@ def render_normal_chat():
|
||||
with st.chat_message(message["role"]):
|
||||
st.markdown(message["content"])
|
||||
|
||||
# ── 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.
|
||||
# Chat input — Enter to send, no extra button needed.
|
||||
# Supports /search <query> and /search clear as special commands.
|
||||
user_input = st.chat_input("Type a message or /search <query>...")
|
||||
if user_input:
|
||||
@ -594,6 +535,42 @@ def render_normal_chat():
|
||||
st.session_state.chat_history.append({"role": "assistant", "content": ai_response})
|
||||
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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
@ -13,12 +13,13 @@ logger = get_logger(__name__)
|
||||
|
||||
# Maps file extensions to Ace editor language modes for syntax highlighting.
|
||||
LANG_MAP = {
|
||||
".py": "python", ".js": "javascript",
|
||||
".py": "python", ".tex": "latex", ".js": "javascript",
|
||||
".html": "html", ".css": "css", ".sh": "bash",
|
||||
".json": "json", ".yaml": "yaml", ".yml": "yaml"
|
||||
}
|
||||
|
||||
|
||||
|
||||
# ── Modals ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@st.dialog("Rename File")
|
||||
@ -208,7 +209,7 @@ class FileViewer:
|
||||
|
||||
with cols[1]:
|
||||
st.download_button(
|
||||
label="Download",
|
||||
label="⬇ Download",
|
||||
data=st.session_state.files_content.get(file_path, ""),
|
||||
file_name=Path(file_path).name,
|
||||
mime="text/plain",
|
||||
|
||||
@ -110,6 +110,106 @@ def _add_folder_dialog(parent_path: str = ""):
|
||||
if cancel:
|
||||
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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def build_arborist_tree(tree, parent_path=Path()):
|
||||
@ -174,10 +274,10 @@ def render_filetree_arborist(tree):
|
||||
selected = tree_view(
|
||||
data=data,
|
||||
icons={"open": "📂", "closed": "📁"},
|
||||
height=350,
|
||||
height=400,
|
||||
selection=active_selection,
|
||||
select_internal_nodes=True, # allow clicking folder names, not just files
|
||||
open_by_default=True,
|
||||
open_by_default=True
|
||||
)
|
||||
|
||||
return selected
|
||||
@ -269,6 +369,17 @@ def render_sidebar():
|
||||
_add_folder_dialog(folder_rel)
|
||||
if st.button("Delete Folder", key="btn_delete_folder", use_container_width=True):
|
||||
_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:
|
||||
# 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.")
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
render_sidebar()
|
||||
|
||||
BIN
project_exercise.pdf
Normal file
BIN
project_exercise.pdf
Normal file
Binary file not shown.
@ -1,5 +1,5 @@
|
||||
# Core Framework
|
||||
streamlit==1.57.0
|
||||
streamlit>=1.28.0
|
||||
streamlit_arborist>=0.1.0
|
||||
|
||||
# AI/LLM Integration
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import sys
|
||||
import pytest
|
||||
import subprocess
|
||||
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"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 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
|
||||
@ -308,13 +327,34 @@ def test_run_unicode_filename(mock_run, engine, tmp_path):
|
||||
assert result["rc"] == 0
|
||||
|
||||
|
||||
|
||||
# ---------------------------------------------------------
|
||||
# 17. .py nutzt sys.executable als Interpreter
|
||||
# 16. .tex nutzt pdflatex
|
||||
# ---------------------------------------------------------
|
||||
|
||||
@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.write_text("print(1)")
|
||||
|
||||
@ -328,7 +368,7 @@ def test_python_uses_sys_executable(mock_run, engine, tmp_path):
|
||||
|
||||
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