diff --git a/README.md b/README.md index d284cc0..322b09b 100644 --- a/README.md +++ b/README.md @@ -171,10 +171,12 @@ vorausgefüllten Debug-Nachricht. Zwei sich gegenseitig ausschliessende Ansichten, umgeschaltet via `st.toggle("Agent Mode")`: **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()`). - Unterstützt Slash-Befehle `/search ` und `/search clear`. -- Einstellungs-Expander: Dateikontext-Toggle, Modell-Auswahl, Max-Token-Slider, +- Settings-Popover: 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. @@ -228,7 +230,9 @@ Generiert kontextbewusste System-Prompts. Signatur: SystemPrompter.generate_prompt( user_message="", 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 -Block vor die Nachricht eingefügt task_type="default", # "debug" | "explain" | "optimize" | "default" ) ``` @@ -241,7 +245,7 @@ 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 `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}`. @@ -297,7 +301,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; 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 | Hilfsfunktionen (Modul-Ebene): @@ -308,7 +312,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()` | -| `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` Liest `mcp_server_config.json`, startet jeden Server als stdio-Subprocess (immer mit @@ -335,12 +339,11 @@ 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`, - 10 s Timeout, Output begrenzt auf `MAX_OUTPUT_LENGTH` + 15 s Timeout, Output begrenzt auf `MAX_OUTPUT_LENGTH` --- diff --git a/backend/agent/coding_agent.py b/backend/agent/coding_agent.py index 4d7bd31..55873ea 100644 --- a/backend/agent/coding_agent.py +++ b/backend/agent/coding_agent.py @@ -167,6 +167,7 @@ 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 , acknowledge it and adjust your plan. - If you receive a tag, revise your plan before choosing the next tool. diff --git a/backend/managers/execution_engine.py b/backend/managers/execution_engine.py index 547d4bc..6ec0f03 100644 --- a/backend/managers/execution_engine.py +++ b/backend/managers/execution_engine.py @@ -6,6 +6,7 @@ processes from blocking the UI indefinitely. """ import subprocess +import sys from pathlib import Path from backend.managers.debug_logger import get_logger @@ -40,7 +41,7 @@ class ExecutionEngine: # Build the shell command depending on file type 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 = [ diff --git a/backend/managers/system_prompter.py b/backend/managers/system_prompter.py index e2f89e2..c6c8ea3 100644 --- a/backend/managers/system_prompter.py +++ b/backend/managers/system_prompter.py @@ -9,26 +9,38 @@ 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 ), } diff --git a/frontend/chat.py b/frontend/chat.py index decf29c..6e8fc34 100644 --- a/frontend/chat.py +++ b/frontend/chat.py @@ -378,7 +378,7 @@ def _clear_chat_dialog(): # ── Normal Chat ─────────────────────────────────────────────────────────────── 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 injected as context into the next message the user sends. @@ -434,9 +434,6 @@ 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 @@ -466,7 +463,66 @@ def render_normal_chat(): with st.chat_message(message["role"]): 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 [] + 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.", + ) + + # Chat input — sticky footer, always at the very bottom of the viewport. # Supports /search and /search clear as special commands. user_input = st.chat_input("Type a message or /search ...") if user_input: @@ -545,43 +601,6 @@ 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): - 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) - - - 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.", - ) # ── Entry point ─────────────────────────────────────────────────────────────── diff --git a/requirements.txt b/requirements.txt index 6ac5081..b66eb7d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ # Core Framework -streamlit>=1.28.0 +streamlit==1.57.0 streamlit_arborist>=0.1.0 # AI/LLM Integration