Ai Implementation
This commit is contained in:
parent
26b97a1cb4
commit
4760dbb3a3
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
@ -1,5 +1,6 @@
|
|||||||
from .file_manager import FileManager
|
from .file_manager import FileManager
|
||||||
from .chat_manager import ChatManager
|
from .chat_manager import ChatManager
|
||||||
|
from .tools import TOOL_REGISTRY, build_tool_description
|
||||||
from .system_prompter import SystemPrompter
|
from .system_prompter import SystemPrompter
|
||||||
from .search_manager import SearchManager
|
from .search_manager import SearchManager
|
||||||
from .execution_engine import ExecutionEngine
|
from .execution_engine import ExecutionEngine
|
||||||
|
|||||||
@ -1,15 +1,170 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
from .llm_client import get_client, chat, chat_json, MODEL
|
||||||
|
from .system_prompter import SystemPrompter
|
||||||
|
from .file_manager import FileManager
|
||||||
|
from .tools import TOOL_REGISTRY, build_tool_description
|
||||||
|
from .execution_engine import ExecutionEngine
|
||||||
|
import backend.tools as tools_module
|
||||||
|
|
||||||
|
|
||||||
class ChatManager:
|
class ChatManager:
|
||||||
"""
|
def __init__(self, file_manager=None, execution_engine=None):
|
||||||
Platzhalter für die Chat-Kommunikation [1]
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
self.conversation_history = []
|
self.conversation_history = []
|
||||||
|
self.system_prompter = SystemPrompter()
|
||||||
|
self.file_manager = file_manager if file_manager else FileManager()
|
||||||
|
self.execution_engine = execution_engine if execution_engine else ExecutionEngine()
|
||||||
|
self.client = get_client()
|
||||||
|
self.active_file_path = None
|
||||||
|
self.active_file_content = None
|
||||||
|
self.on_file_written = None
|
||||||
|
self.on_tool_used = None
|
||||||
|
tools_module.on_tool_used = self._tool_used_callback
|
||||||
|
|
||||||
def generate_response(self, prompt):
|
def _tool_used_callback(self, tool_name: str, summary: str):
|
||||||
"""
|
if self.on_tool_used:
|
||||||
Gibt eine Dummy-Antwort zurück [1]
|
self.on_tool_used(tool_name, summary)
|
||||||
"""
|
|
||||||
return "Antwort (Backend-Logik noch nicht implementiert)"
|
|
||||||
|
|
||||||
def get_history(self):
|
def set_active_file(self, file_path: str, content: str = None):
|
||||||
return []
|
self.active_file_path = file_path
|
||||||
|
self.active_file_content = content if content else None
|
||||||
|
|
||||||
|
def _build_context(self) -> str:
|
||||||
|
if self.active_file_content and self.active_file_path:
|
||||||
|
return self.system_prompter.add_context(Path(self.active_file_path), self.active_file_content)
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def _build_system_prompt(self) -> str:
|
||||||
|
context = self._build_context()
|
||||||
|
active_path = str(self.active_file_path) if self.active_file_path else None
|
||||||
|
return f"""You are a code assistant. You help users understand, modify, run, and research code.
|
||||||
|
|
||||||
|
### Current Active File:
|
||||||
|
{context if context else "No file currently active."}
|
||||||
|
|
||||||
|
### Active File Path:
|
||||||
|
{active_path if active_path else "No file active."}
|
||||||
|
|
||||||
|
### RESPONSE FORMAT — always reply with exactly one JSON object:
|
||||||
|
|
||||||
|
To use a tool:
|
||||||
|
{{
|
||||||
|
"thought": "why I need this tool",
|
||||||
|
"tool": "tool_name",
|
||||||
|
"arguments": {{}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
To give a final answer (NO tool needed):
|
||||||
|
{{
|
||||||
|
"thought": "my reasoning",
|
||||||
|
"answer": "my response to the user"
|
||||||
|
}}
|
||||||
|
|
||||||
|
### WHEN TO USE TOOLS vs ANSWER DIRECTLY:
|
||||||
|
- Questions about what code does, explanations, analysis → ANSWER DIRECTLY, no tool needed
|
||||||
|
- "run the file", "execute", "test it" → use execute_file
|
||||||
|
- "add a comment", "fix this", "change X" → use read_file then edit_file
|
||||||
|
- "search for X", "look up X", "what is X" (external info) → use search
|
||||||
|
- "run this snippet" → use execute_code
|
||||||
|
|
||||||
|
### Available Tools:
|
||||||
|
{build_tool_description()}
|
||||||
|
|
||||||
|
### Tool usage notes:
|
||||||
|
- edit_file REPLACES the whole file — always read_file first
|
||||||
|
- execute_file path: "{active_path}"
|
||||||
|
- After edit_file succeeds, return an "answer" confirming what you did
|
||||||
|
- Never use execute_code just to print an explanation — answer directly instead
|
||||||
|
"""
|
||||||
|
|
||||||
|
def generate_response(self, prompt: str, execution_results: dict = None) -> str:
|
||||||
|
system_prompt = self._build_system_prompt()
|
||||||
|
user_message = f"User request: {prompt}"
|
||||||
|
|
||||||
|
if self.active_file_path and self.active_file_content:
|
||||||
|
user_message += f"\n--- ACTIVE FILE ---\nPath: {self.active_file_path}"
|
||||||
|
elif self.active_file_path:
|
||||||
|
user_message += f"\n[File loaded: {self.active_file_path} — but content unavailable]"
|
||||||
|
|
||||||
|
if execution_results and execution_results.get("has_errors"):
|
||||||
|
user_message += "\n[Execution Feedback — errors found]:\n"
|
||||||
|
for log in execution_results.get("logs", []):
|
||||||
|
user_message += f" - [{log['level']}] {log['message']}\n"
|
||||||
|
|
||||||
|
messages = [
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": user_message},
|
||||||
|
]
|
||||||
|
|
||||||
|
max_iterations = 8
|
||||||
|
for iteration in range(max_iterations):
|
||||||
|
try:
|
||||||
|
raw_response = chat_json(self.client, messages, model=MODEL, temperature=0.2)
|
||||||
|
response_data = json.loads(raw_response)
|
||||||
|
|
||||||
|
# ---- Final answer (no tool) ----
|
||||||
|
tool_name = response_data.get("tool") or response_data.get("command")
|
||||||
|
if not tool_name or tool_name in ("thought", "finish", "answer", "none", ""):
|
||||||
|
return (
|
||||||
|
response_data.get("answer")
|
||||||
|
or response_data.get("thought")
|
||||||
|
or raw_response
|
||||||
|
)
|
||||||
|
|
||||||
|
arguments = response_data.get("arguments", {})
|
||||||
|
|
||||||
|
# Resolve path placeholders
|
||||||
|
if "path" in arguments and arguments["path"] in ("current_file", "current file", ""):
|
||||||
|
arguments["path"] = str(self.active_file_path) if self.active_file_path else ""
|
||||||
|
|
||||||
|
# Unknown tool
|
||||||
|
if tool_name not in TOOL_REGISTRY:
|
||||||
|
messages.append({"role": "assistant", "content": raw_response})
|
||||||
|
messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": (
|
||||||
|
f"ERROR: '{tool_name}' is not a valid tool. "
|
||||||
|
f"Available tools: {list(TOOL_REGISTRY.keys())}. "
|
||||||
|
"Use one of those, or respond with an \"answer\" if no tool is needed."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# ---- Execute tool ----
|
||||||
|
result = TOOL_REGISTRY[tool_name](**arguments)
|
||||||
|
|
||||||
|
# Notify UI on successful file write
|
||||||
|
if tool_name == "edit_file" and not str(result).startswith("ERROR"):
|
||||||
|
written_path = arguments.get("path", "")
|
||||||
|
new_content = arguments.get("content", "")
|
||||||
|
if self.on_file_written and written_path and new_content:
|
||||||
|
self.on_file_written(written_path, new_content)
|
||||||
|
|
||||||
|
messages.append({"role": "assistant", "content": raw_response})
|
||||||
|
messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": (
|
||||||
|
f"Tool '{tool_name}' result:\n{result}\n\n"
|
||||||
|
"Now give your final answer to the user using the "
|
||||||
|
"{\"answer\": \"...\"} format."
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if str(result).startswith("ERROR"):
|
||||||
|
messages[-1]["content"] += "\nThe tool returned an error — revise and try again, or explain the issue."
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
messages.append({"role": "assistant", "content": raw_response})
|
||||||
|
messages.append({
|
||||||
|
"role": "user",
|
||||||
|
"content": "Your response was not valid JSON. Reply with a single JSON object only."
|
||||||
|
})
|
||||||
|
|
||||||
|
# Hard fallback — plain chat without JSON constraint
|
||||||
|
return chat(self.client, messages, model=MODEL, temperature=0.7)
|
||||||
|
|
||||||
|
def get_history(self) -> list:
|
||||||
|
return self.conversation_history
|
||||||
|
|
||||||
|
def clear_history(self):
|
||||||
|
self.conversation_history.clear()
|
||||||
|
|||||||
@ -1,14 +1,15 @@
|
|||||||
import ast
|
import ast
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import tempfile
|
||||||
|
|
||||||
from backend.debug_logger import DebugLogger
|
from backend.debug_logger import DebugLogger
|
||||||
|
|
||||||
# ---------------- BLOCKED PATTERNS ----------------
|
# ---------------- BLOCKED PATTERNS ----------------
|
||||||
# These AST node patterns are refused before execution
|
|
||||||
BLOCKED_IMPORTS = {
|
BLOCKED_IMPORTS = {
|
||||||
"winreg", "ctypes", "msvcrt", # Windows internals
|
"winreg", "ctypes", "msvcrt",
|
||||||
}
|
}
|
||||||
|
|
||||||
BLOCKED_CALLS = {
|
BLOCKED_CALLS = {
|
||||||
@ -22,18 +23,11 @@ BLOCKED_CALLS = {
|
|||||||
|
|
||||||
BLOCKED_BUILTINS = {"__import__", "eval", "exec", "compile"}
|
BLOCKED_BUILTINS = {"__import__", "eval", "exec", "compile"}
|
||||||
|
|
||||||
# Paths that are never allowed as cwd or in file arguments
|
|
||||||
BLOCKED_PATH_PREFIXES = [
|
BLOCKED_PATH_PREFIXES = [
|
||||||
"C:\\Windows",
|
"C:\\Windows", "C:\\System32",
|
||||||
"C:\\System32",
|
"/etc", "/bin", "/sbin",
|
||||||
"/etc",
|
"/usr/bin", "/usr/sbin",
|
||||||
"/bin",
|
"/boot", "/sys", "/proc",
|
||||||
"/sbin",
|
|
||||||
"/usr/bin",
|
|
||||||
"/usr/sbin",
|
|
||||||
"/boot",
|
|
||||||
"/sys",
|
|
||||||
"/proc",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -47,20 +41,14 @@ class ExecutionEngine:
|
|||||||
|
|
||||||
# ---------------- SAFETY CHECK ----------------
|
# ---------------- SAFETY CHECK ----------------
|
||||||
def _check_safe(self, filepath: str) -> None:
|
def _check_safe(self, filepath: str) -> None:
|
||||||
"""
|
|
||||||
Raises SafetyError if the file contains dangerous patterns.
|
|
||||||
Only runs on .py files (other types are blocked outright).
|
|
||||||
"""
|
|
||||||
path = Path(filepath)
|
path = Path(filepath)
|
||||||
source = path.read_text(encoding="utf-8")
|
source = path.read_text(encoding="utf-8")
|
||||||
try:
|
try:
|
||||||
tree = ast.parse(source, filename=str(path))
|
tree = ast.parse(source, filename=str(path))
|
||||||
except SyntaxError:
|
except SyntaxError:
|
||||||
return # syntax errors handled separately
|
return
|
||||||
|
|
||||||
for node in ast.walk(tree):
|
for node in ast.walk(tree):
|
||||||
|
|
||||||
# Block dangerous imports
|
|
||||||
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
if isinstance(node, (ast.Import, ast.ImportFrom)):
|
||||||
names = (
|
names = (
|
||||||
[a.name for a in node.names]
|
[a.name for a in node.names]
|
||||||
@ -72,29 +60,21 @@ class ExecutionEngine:
|
|||||||
if root in BLOCKED_IMPORTS:
|
if root in BLOCKED_IMPORTS:
|
||||||
raise SafetyError(f"Blocked import: '{name}'")
|
raise SafetyError(f"Blocked import: '{name}'")
|
||||||
|
|
||||||
# Block dangerous attribute calls like os.remove(...)
|
|
||||||
if isinstance(node, ast.Call):
|
if isinstance(node, ast.Call):
|
||||||
func = node.func
|
func = node.func
|
||||||
if isinstance(func, ast.Attribute):
|
if isinstance(func, ast.Attribute):
|
||||||
if isinstance(func.value, ast.Name):
|
if isinstance(func.value, ast.Name):
|
||||||
pair = (func.value.id, func.attr)
|
pair = (func.value.id, func.attr)
|
||||||
if pair in BLOCKED_CALLS:
|
if pair in BLOCKED_CALLS:
|
||||||
raise SafetyError(
|
raise SafetyError(f"Blocked call: '{func.value.id}.{func.attr}()'")
|
||||||
f"Blocked call: '{func.value.id}.{func.attr}()'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Block dangerous builtins: eval(), exec(), __import__()
|
|
||||||
if isinstance(func, ast.Name) and func.id in BLOCKED_BUILTINS:
|
if isinstance(func, ast.Name) and func.id in BLOCKED_BUILTINS:
|
||||||
raise SafetyError(f"Blocked builtin: '{func.id}()'")
|
raise SafetyError(f"Blocked builtin: '{func.id}()'")
|
||||||
|
|
||||||
# Block absolute paths pointing at system directories in string literals
|
|
||||||
for node in ast.walk(tree):
|
for node in ast.walk(tree):
|
||||||
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
if isinstance(node, ast.Constant) and isinstance(node.value, str):
|
||||||
for prefix in BLOCKED_PATH_PREFIXES:
|
for prefix in BLOCKED_PATH_PREFIXES:
|
||||||
if node.value.lower().startswith(prefix.lower()):
|
if node.value.lower().startswith(prefix.lower()):
|
||||||
raise SafetyError(
|
raise SafetyError(f"Blocked system path in code: '{node.value}'")
|
||||||
f"Blocked system path in code: '{node.value}'"
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------------- SYNTAX CHECK ----------------
|
# ---------------- SYNTAX CHECK ----------------
|
||||||
def check_syntax(self, filepath: str) -> bool:
|
def check_syntax(self, filepath: str) -> bool:
|
||||||
@ -113,31 +93,27 @@ class ExecutionEngine:
|
|||||||
self.logger.log_exception(e)
|
self.logger.log_exception(e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ---------------- RUN ----------------
|
# ---------------- RUN FILE ----------------
|
||||||
def run_file(self, filepath: str) -> dict:
|
def run_file(self, filepath: str) -> dict:
|
||||||
self.logger.clear()
|
self.logger.clear()
|
||||||
path = Path(filepath)
|
path = Path(filepath)
|
||||||
|
|
||||||
# --- file exists ---
|
|
||||||
if not path.exists():
|
if not path.exists():
|
||||||
self.logger.log_stderr(f"File not found: {filepath}")
|
self.logger.log_stderr(f"File not found: {filepath}")
|
||||||
return self._result("", f"File not found: {filepath}", -1)
|
return self._result("", f"File not found: {filepath}", -1)
|
||||||
|
|
||||||
ext = path.suffix.lower()
|
ext = path.suffix.lower()
|
||||||
|
|
||||||
# --- only allow known safe types ---
|
|
||||||
if ext not in (".py", ".js", ".sh"):
|
if ext not in (".py", ".js", ".sh"):
|
||||||
msg = f"Unsupported file type: {ext}"
|
msg = f"Unsupported file type: {ext}"
|
||||||
self.logger.log_stderr(msg)
|
self.logger.log_stderr(msg)
|
||||||
return self._result("", msg, -1)
|
return self._result("", msg, -1)
|
||||||
|
|
||||||
# --- block shell scripts on Windows (too risky) ---
|
|
||||||
if ext == ".sh" and sys.platform == "win32":
|
if ext == ".sh" and sys.platform == "win32":
|
||||||
msg = "Shell scripts are not supported on Windows."
|
msg = "Shell scripts are not supported on Windows."
|
||||||
self.logger.log_stderr(msg)
|
self.logger.log_stderr(msg)
|
||||||
return self._result("", msg, -1)
|
return self._result("", msg, -1)
|
||||||
|
|
||||||
# --- Python: syntax + safety check ---
|
|
||||||
if ext == ".py":
|
if ext == ".py":
|
||||||
if not self.check_syntax(filepath):
|
if not self.check_syntax(filepath):
|
||||||
return self._result("", self.logger.format(), -1)
|
return self._result("", self.logger.format(), -1)
|
||||||
@ -148,14 +124,11 @@ class ExecutionEngine:
|
|||||||
self.logger.log_stderr(msg)
|
self.logger.log_stderr(msg)
|
||||||
return self._result("", msg, -1)
|
return self._result("", msg, -1)
|
||||||
cmd = [sys.executable, str(path)]
|
cmd = [sys.executable, str(path)]
|
||||||
|
|
||||||
elif ext == ".js":
|
elif ext == ".js":
|
||||||
cmd = ["node", str(path)]
|
cmd = ["node", str(path)]
|
||||||
|
|
||||||
elif ext == ".sh":
|
elif ext == ".sh":
|
||||||
cmd = ["bash", str(path)]
|
cmd = ["bash", str(path)]
|
||||||
|
|
||||||
# --- execute ---
|
|
||||||
self.logger.log(f"Running: {path.name}")
|
self.logger.log(f"Running: {path.name}")
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
@ -182,6 +155,39 @@ class ExecutionEngine:
|
|||||||
self.logger.log_exception(e)
|
self.logger.log_exception(e)
|
||||||
return self._result("", str(e), -1)
|
return self._result("", str(e), -1)
|
||||||
|
|
||||||
|
# ---------------- RUN CODE STRING ----------------
|
||||||
|
def run_code(self, code: str) -> str:
|
||||||
|
"""Execute a Python code string and return a human-readable result string."""
|
||||||
|
tmp_path = None
|
||||||
|
try:
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False, encoding='utf-8') as f:
|
||||||
|
f.write(code)
|
||||||
|
tmp_path = f.name
|
||||||
|
|
||||||
|
result = subprocess.run(
|
||||||
|
[sys.executable, tmp_path],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
|
||||||
|
parts = []
|
||||||
|
if result.stdout.strip():
|
||||||
|
parts.append(f"Output:\n{result.stdout.strip()}")
|
||||||
|
if result.stderr.strip():
|
||||||
|
parts.append(f"Errors:\n{result.stderr.strip()}")
|
||||||
|
if result.returncode != 0:
|
||||||
|
parts.append(f"Exit code: {result.returncode}")
|
||||||
|
return "\n".join(parts) if parts else "Code executed successfully with no output."
|
||||||
|
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return "ERROR: Execution timed out after 30 seconds."
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: {str(e)}"
|
||||||
|
finally:
|
||||||
|
if tmp_path and os.path.exists(tmp_path):
|
||||||
|
os.unlink(tmp_path)
|
||||||
|
|
||||||
# ---------------- HELPERS ----------------
|
# ---------------- HELPERS ----------------
|
||||||
def _result(self, stdout: str, stderr: str, returncode: int) -> dict:
|
def _result(self, stdout: str, stderr: str, returncode: int) -> dict:
|
||||||
return {
|
return {
|
||||||
|
|||||||
37
src/backend/llm_client.py
Normal file
37
src/backend/llm_client.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
# backend/llm_client.py
|
||||||
|
from openai import OpenAI
|
||||||
|
|
||||||
|
HOST = "silicon.fhgr.ch"
|
||||||
|
PORT = 7080
|
||||||
|
API_KEY = "EMPTY"
|
||||||
|
MODEL = "qwen3.5-35b-a3b"
|
||||||
|
|
||||||
|
def get_client() -> OpenAI:
|
||||||
|
"""Gibt einen OpenAI-kompatiblen Client zurück, der auf den vLLM-Server zeigt."""
|
||||||
|
base_url = f"http://{HOST}:{PORT}/v1"
|
||||||
|
return OpenAI(base_url=base_url, api_key=API_KEY)
|
||||||
|
|
||||||
|
def chat(client: OpenAI, messages: list[dict], model: str = MODEL,
|
||||||
|
temperature: float = 0.2, max_tokens: int = 2048) -> str:
|
||||||
|
"""Sendet Chat-Nachrichten an das LLM und gibt die Antworttext zurück."""
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
temperature=temperature,
|
||||||
|
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
|
||||||
|
)
|
||||||
|
return response.choices[0].message.content
|
||||||
|
|
||||||
|
def chat_json(client: OpenAI, messages: list[dict], model: str = MODEL,
|
||||||
|
temperature: float = 0.2, max_tokens: int = 2048) -> str:
|
||||||
|
"""Wie chat(), aber erzwingt syntaktisch gültiges JSON für Tool-Calls."""
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model=model,
|
||||||
|
messages=messages,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
temperature=temperature,
|
||||||
|
response_format={"type": "json_object"},
|
||||||
|
extra_body={"chat_template_kwargs": {"enable_thinking": False}},
|
||||||
|
)
|
||||||
|
return response.choices[0].message.content
|
||||||
@ -1,30 +1,172 @@
|
|||||||
|
import urllib.parse
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
HAS_REQUESTS = True
|
||||||
|
except ImportError:
|
||||||
|
import urllib.request
|
||||||
|
HAS_REQUESTS = False
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# SearchManager
|
||||||
|
# Uses two backends in order:
|
||||||
|
# 1. DuckDuckGo HTML (scrape) — broad web results, no API key needed
|
||||||
|
# 2. Wikipedia API — reliable fallback, great for tech topics
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_DDG_URL = "https://html.duckduckgo.com/html/"
|
||||||
|
_WIKI_URL = "https://en.wikipedia.org/w/api.php"
|
||||||
|
|
||||||
|
_HEADERS = {
|
||||||
|
"User-Agent": (
|
||||||
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||||
|
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||||
|
"Chrome/124.0.0.0 Safari/537.36"
|
||||||
|
),
|
||||||
|
"Accept-Language": "en-US,en;q=0.9",
|
||||||
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get(url, *, params=None, data=None, timeout=10) -> str:
|
||||||
|
"""Single HTTP helper that works with or without the `requests` library."""
|
||||||
|
if HAS_REQUESTS:
|
||||||
|
if data:
|
||||||
|
r = requests.post(url, headers=_HEADERS, data=data, timeout=timeout)
|
||||||
|
else:
|
||||||
|
r = requests.get(url, headers=_HEADERS, params=params, timeout=timeout)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.text
|
||||||
|
else:
|
||||||
|
if params:
|
||||||
|
url = url + "?" + urllib.parse.urlencode(params)
|
||||||
|
if data:
|
||||||
|
encoded = urllib.parse.urlencode(data).encode()
|
||||||
|
req = urllib.request.Request(url, data=encoded, headers=_HEADERS, method="POST")
|
||||||
|
else:
|
||||||
|
req = urllib.request.Request(url, headers=_HEADERS)
|
||||||
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||||
|
return resp.read().decode("utf-8", errors="replace")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Backend 1 — DuckDuckGo HTML scrape
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _ddg_search(query: str, max_results: int = 5) -> list[dict]:
|
||||||
|
"""POST to DuckDuckGo HTML endpoint and parse result anchors."""
|
||||||
|
try:
|
||||||
|
html = _get(_DDG_URL, data={"q": query, "b": "", "kl": "en-us"})
|
||||||
|
except Exception as e:
|
||||||
|
return [{"title": "DuckDuckGo unavailable", "url": "", "snippet": str(e)}]
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Each result block looks like:
|
||||||
|
# <a class="result__a" href="...">Title</a>
|
||||||
|
# <a class="result__snippet">Snippet</a>
|
||||||
|
title_pattern = re.compile(r'class="result__a"[^>]*href="([^"]+)"[^>]*>(.*?)</a>', re.S)
|
||||||
|
snippet_pattern = re.compile(r'class="result__snippet"[^>]*>(.*?)</a>', re.S)
|
||||||
|
|
||||||
|
titles = title_pattern.findall(html)
|
||||||
|
snippets = snippet_pattern.findall(html)
|
||||||
|
|
||||||
|
def clean(text: str) -> str:
|
||||||
|
text = re.sub(r'<[^>]+>', '', text) # strip tags
|
||||||
|
text = re.sub(r'&', '&', text)
|
||||||
|
text = re.sub(r'<', '<', text)
|
||||||
|
text = re.sub(r'>', '>', text)
|
||||||
|
text = re.sub(r'"', '"', text)
|
||||||
|
text = re.sub(r''', "'", text)
|
||||||
|
text = re.sub(r' ', ' ', text)
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
for i, (url, title) in enumerate(titles[:max_results]):
|
||||||
|
# DDG sometimes wraps URLs in redirects like //duckduckgo.com/l/?uddg=...
|
||||||
|
if url.startswith("//duckduckgo.com/l/?") or url.startswith("/l/?"):
|
||||||
|
m = re.search(r'uddg=([^&]+)', url)
|
||||||
|
if m:
|
||||||
|
url = urllib.parse.unquote(m.group(1))
|
||||||
|
snippet = clean(snippets[i]) if i < len(snippets) else ""
|
||||||
|
results.append({"title": clean(title), "url": url, "snippet": snippet})
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Backend 2 — Wikipedia API (reliable, no key, great for tech/science)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _wiki_search(query: str, max_results: int = 3) -> list[dict]:
|
||||||
|
try:
|
||||||
|
data = _get(_WIKI_URL, params={
|
||||||
|
"action": "query",
|
||||||
|
"list": "search",
|
||||||
|
"srsearch": query,
|
||||||
|
"format": "json",
|
||||||
|
"srlimit": max_results,
|
||||||
|
"srprop": "snippet|titlesnippet",
|
||||||
|
})
|
||||||
|
obj = json.loads(data)
|
||||||
|
results = []
|
||||||
|
for r in obj.get("query", {}).get("search", []):
|
||||||
|
title = r.get("title", "")
|
||||||
|
snippet = re.sub(r'<[^>]+>', '', r.get("snippet", ""))
|
||||||
|
url = f"https://en.wikipedia.org/wiki/{urllib.parse.quote(title.replace(' ', '_'))}"
|
||||||
|
results.append({"title": title, "url": url, "snippet": snippet})
|
||||||
|
return results
|
||||||
|
except Exception as e:
|
||||||
|
return [{"title": "Wikipedia unavailable", "url": "", "snippet": str(e)}]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Public class
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class SearchManager:
|
class SearchManager:
|
||||||
"""
|
"""
|
||||||
Verwaltet Suchanfragen und integriert externe APIs [1]
|
Web search using DuckDuckGo (primary) + Wikipedia (fallback/supplement).
|
||||||
|
No API key required.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, api_key=None):
|
def __init__(self, api_key=None):
|
||||||
self.api_key = api_key
|
self.api_key = api_key # kept for interface compatibility
|
||||||
self.search_history = []
|
self.search_history: list[str] = []
|
||||||
|
|
||||||
def perform_search(self, query):
|
def perform_search(self, query: str, max_results: int = 5) -> list[dict]:
|
||||||
"""
|
"""
|
||||||
Führt eine Internetsuche aus [1]
|
Return a list of dicts with keys: title, url, snippet.
|
||||||
|
Tries DuckDuckGo first; if it yields nothing, falls back to Wikipedia.
|
||||||
"""
|
"""
|
||||||
# Platzhalter-Implementierung für Startphase
|
self.search_history.append(query)
|
||||||
return []
|
|
||||||
|
|
||||||
def parse_results(self, raw_results):
|
results = _ddg_search(query, max_results)
|
||||||
"""
|
|
||||||
Extrahiert und formatiert relevante Ergebnisse [1]
|
# Filter out error-only results and supplement with Wikipedia if thin
|
||||||
"""
|
real = [r for r in results if r["url"]]
|
||||||
# Rückgabe einer formatierten Liste als Platzhalter
|
if len(real) < 2:
|
||||||
|
real = _wiki_search(query, max_results)
|
||||||
|
|
||||||
|
return real if real else [{"title": "No results", "url": "", "snippet": f"No results found for: {query}"}]
|
||||||
|
|
||||||
|
def parse_results(self, raw_results: list) -> list:
|
||||||
if isinstance(raw_results, list):
|
if isinstance(raw_results, list):
|
||||||
return raw_results
|
return raw_results
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def search_context(self, query, context_type="web"):
|
def search_context(self, query: str, context_type: str = "web") -> str:
|
||||||
"""
|
"""Formatted string ready to inject into a prompt."""
|
||||||
Fügt Suchergebnisse dem Kontext hinzu
|
|
||||||
"""
|
|
||||||
results = self.perform_search(query)
|
results = self.perform_search(query)
|
||||||
return f"Search results for '{query}': {results}"
|
if not results:
|
||||||
|
return f"No results found for '{query}'."
|
||||||
|
lines = [f"Search results for '{query}':"]
|
||||||
|
for i, r in enumerate(results, 1):
|
||||||
|
lines.append(f"\n{i}. {r['title']}")
|
||||||
|
if r["url"]:
|
||||||
|
lines.append(f" URL: {r['url']}")
|
||||||
|
if r["snippet"]:
|
||||||
|
lines.append(f" {r['snippet']}")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|||||||
@ -2,16 +2,14 @@ class SystemPrompter:
|
|||||||
"""
|
"""
|
||||||
Generiert System-Prompts mit Dateikontext für die KI [1]
|
Generiert System-Prompts mit Dateikontext für die KI [1]
|
||||||
"""
|
"""
|
||||||
def __init__(self):
|
def init(self):
|
||||||
self.default_prompt = "You are a code assistant helping to debug Python code."
|
self.default_prompt = "You are a code assistant helping to debug Python code."
|
||||||
|
|
||||||
def generate_prompt(self, user_message, file_context):
|
def generate_prompt(self, user_message, file_context):
|
||||||
"""
|
"""
|
||||||
Baut ein vollständiges Prompt mit Dateiinhalten oder Kontextinformationen auf [1]
|
Baut ein vollständiges Prompt mit Dateiinhalten oder Kontextinformationen auf [1]
|
||||||
"""
|
"""
|
||||||
# Platzhalter-Implementierung für Startphase
|
# Platzhalter-Implementierung für Startphase
|
||||||
return f"User: {user_message}\nContext: {file_context}"
|
return f"User: {user_message}\nContext: {file_context}"
|
||||||
|
|
||||||
def add_context(self, file_name, code_snippet=None):
|
def add_context(self, file_name, code_snippet=None):
|
||||||
"""
|
"""
|
||||||
Fügt Dateikontext hinzu (Dateiname, Änderungen oder markierte Code-Abschnitte) [1]
|
Fügt Dateikontext hinzu (Dateiname, Änderungen oder markierte Code-Abschnitte) [1]
|
||||||
@ -20,7 +18,6 @@ class SystemPrompter:
|
|||||||
if code_snippet:
|
if code_snippet:
|
||||||
context += f"\nCode:\n{code_snippet}"
|
context += f"\nCode:\n{code_snippet}"
|
||||||
return context
|
return context
|
||||||
|
|
||||||
def summarize_context(self, context):
|
def summarize_context(self, context):
|
||||||
"""
|
"""
|
||||||
Fasst Kontext zusammen, um den Prompt nicht zu überladen [1]
|
Fasst Kontext zusammen, um den Prompt nicht zu überladen [1]
|
||||||
|
|||||||
105
src/backend/tools.py
Normal file
105
src/backend/tools.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import json
|
||||||
|
import inspect
|
||||||
|
from pathlib import Path
|
||||||
|
from backend.file_manager import FileManager
|
||||||
|
from backend.search_manager import SearchManager
|
||||||
|
from backend.execution_engine import ExecutionEngine
|
||||||
|
|
||||||
|
TOOL_REGISTRY = {}
|
||||||
|
|
||||||
|
# Optional callback set by ChatManager so the UI can react to tool usage
|
||||||
|
# Signature: on_tool_used(tool_name: str, summary: str)
|
||||||
|
on_tool_used = None
|
||||||
|
|
||||||
|
def tool(func):
|
||||||
|
"""Decorator to register a function as an available tool"""
|
||||||
|
TOOL_REGISTRY[func.__name__] = func
|
||||||
|
return func
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def read_file(path: str) -> str:
|
||||||
|
"""Read the content of a file. Use this to examine code you need to modify."""
|
||||||
|
try:
|
||||||
|
fm = FileManager()
|
||||||
|
content = fm.read_file(path)
|
||||||
|
return f"Content of {path}:\n{content}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: Could not read {path}: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def search(query: str) -> str:
|
||||||
|
"""Search the internet for documentation, answers, or examples. Use whenever the user asks something that needs current or external information."""
|
||||||
|
try:
|
||||||
|
sm = SearchManager()
|
||||||
|
result = sm.search_context(query)
|
||||||
|
# Notify UI that a search was performed
|
||||||
|
if on_tool_used:
|
||||||
|
on_tool_used("search", f"🔍 Searched: \"{query}\"")
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: Search failed: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def execute_file(path: str) -> str:
|
||||||
|
"""Execute a Python (.py), JavaScript (.js), or shell (.sh) file and return its output, errors, and exit code."""
|
||||||
|
try:
|
||||||
|
ee = ExecutionEngine()
|
||||||
|
result = ee.run_file(path)
|
||||||
|
parts = []
|
||||||
|
if result.get("stdout", "").strip():
|
||||||
|
parts.append(f"Output:\n{result['stdout'].strip()}")
|
||||||
|
if result.get("stderr", "").strip():
|
||||||
|
parts.append(f"Errors:\n{result['stderr'].strip()}")
|
||||||
|
status = "✓ Success" if result.get("success") else f"✗ Failed (exit code {result.get('returncode', -1)})"
|
||||||
|
parts.append(f"Status: {status}")
|
||||||
|
output = "\n".join(parts) if parts else "Executed successfully with no output."
|
||||||
|
if on_tool_used:
|
||||||
|
on_tool_used("execute_file", f"▶ Executed: {path}")
|
||||||
|
return output
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: Could not execute {path}: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def execute_code(code: str) -> str:
|
||||||
|
"""Execute a Python code snippet and return its output or errors. Use to test logic or verify suggestions."""
|
||||||
|
try:
|
||||||
|
ee = ExecutionEngine()
|
||||||
|
result = ee.run_code(code)
|
||||||
|
if on_tool_used:
|
||||||
|
on_tool_used("execute_code", "▶ Executed code snippet")
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def edit_file(path: str, content: str) -> str:
|
||||||
|
"""Save content to a file, replacing its entire contents. Always read_file first so you preserve existing code."""
|
||||||
|
try:
|
||||||
|
fm = FileManager()
|
||||||
|
success = fm.save_file(path, content)
|
||||||
|
if success:
|
||||||
|
return f"Successfully saved to {path}"
|
||||||
|
return f"ERROR: Could not save to {path}"
|
||||||
|
except Exception as e:
|
||||||
|
return f"ERROR: {str(e)}"
|
||||||
|
|
||||||
|
|
||||||
|
def build_tool_description() -> str:
|
||||||
|
"""Build a description of all available tools from their signatures."""
|
||||||
|
lines = []
|
||||||
|
for name, func in TOOL_REGISTRY.items():
|
||||||
|
sig = inspect.signature(func)
|
||||||
|
params = []
|
||||||
|
for pname, param in sig.parameters.items():
|
||||||
|
if param.default is inspect.Parameter.empty:
|
||||||
|
params.append(f"{pname}")
|
||||||
|
else:
|
||||||
|
params.append(f"{pname}={param.default}")
|
||||||
|
doc = func.__doc__ or "No description"
|
||||||
|
lines.append(f"{name}: {doc}. Arguments: {params}")
|
||||||
|
return "\n".join(lines)
|
||||||
184
src/main.py
184
src/main.py
@ -2,9 +2,9 @@ import sys
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import streamlit as st
|
import streamlit as st
|
||||||
# ---------------- PATH FIX ----------------
|
|
||||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||||
# ---------------- BACKEND ----------------
|
|
||||||
from backend.file_manager import FileManager
|
from backend.file_manager import FileManager
|
||||||
from ui.navigation import FileNavigation
|
from ui.navigation import FileNavigation
|
||||||
from ui.chat import ChatInterface
|
from ui.chat import ChatInterface
|
||||||
@ -13,13 +13,7 @@ from ui.output import OutputDisplay
|
|||||||
from backend.chat_manager import ChatManager
|
from backend.chat_manager import ChatManager
|
||||||
from backend.execution_engine import ExecutionEngine
|
from backend.execution_engine import ExecutionEngine
|
||||||
|
|
||||||
# ---------------- PAGE CONFIG ----------------
|
st.set_page_config(page_title="AI Code Editor", page_icon="💻", layout="wide")
|
||||||
st.set_page_config(
|
|
||||||
page_title="AI Code Editor",
|
|
||||||
page_icon="💻",
|
|
||||||
layout="wide"
|
|
||||||
)
|
|
||||||
|
|
||||||
st.markdown("""
|
st.markdown("""
|
||||||
<style>
|
<style>
|
||||||
.stMainBlockContainer { padding-top: 2rem !important; }
|
.stMainBlockContainer { padding-top: 2rem !important; }
|
||||||
@ -28,41 +22,84 @@ st.markdown("""
|
|||||||
body.chat-open .main .block-container { margin-right: 400px; }
|
body.chat-open .main .block-container { margin-right: 400px; }
|
||||||
iframe[height="1"] { display: none !important; }
|
iframe[height="1"] { display: none !important; }
|
||||||
[data-testid="stSidebar"] { z-index: 99 !important; }
|
[data-testid="stSidebar"] { z-index: 99 !important; }
|
||||||
|
/* Tool-use badge styling */
|
||||||
|
.tool-badge {
|
||||||
|
display: inline-block;
|
||||||
|
background: #1e3a5f;
|
||||||
|
color: #7eb8f7;
|
||||||
|
border: 1px solid #2d5a9e;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
font-size: 0.82em;
|
||||||
|
margin: 2px 0 6px 0;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
/* Pin chat input to bottom of its column */
|
||||||
|
[data-testid="stChatInput"] {
|
||||||
|
position: sticky !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
z-index: 10 !important;
|
||||||
|
padding-top: 0.5rem !important;
|
||||||
|
background: var(--background-color, white) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
""", unsafe_allow_html=True)
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
# ---------------- MAIN ----------------
|
|
||||||
def main():
|
def main():
|
||||||
|
# --- Initialize Backend Components ---
|
||||||
file_manager = FileManager()
|
file_manager = FileManager()
|
||||||
navigation = FileNavigation()
|
navigation = FileNavigation()
|
||||||
chat = ChatInterface()
|
chat_manager = ChatManager(file_manager=file_manager)
|
||||||
chat_manager = ChatManager()
|
|
||||||
execution_engine = ExecutionEngine()
|
execution_engine = ExecutionEngine()
|
||||||
output_display = OutputDisplay()
|
output_display = OutputDisplay()
|
||||||
|
|
||||||
|
# --- UI Components ---
|
||||||
|
chat = ChatInterface(chat_manager=chat_manager)
|
||||||
|
editor = CodeEditor()
|
||||||
|
|
||||||
# ---------------- STATE ----------------
|
# ---------------- STATE ----------------
|
||||||
if "root_folder" not in st.session_state:
|
if "root_folder" not in st.session_state: st.session_state.root_folder = None
|
||||||
st.session_state.root_folder = None
|
if "selected_file" not in st.session_state: st.session_state.selected_file = None
|
||||||
if "selected_file" not in st.session_state:
|
if "active_file" not in st.session_state: st.session_state.active_file = None
|
||||||
st.session_state.selected_file = None
|
if "editor_content" not in st.session_state: st.session_state.editor_content = ""
|
||||||
if "active_file" not in st.session_state:
|
if "last_applied_content" not in st.session_state: st.session_state.last_applied_content = ""
|
||||||
st.session_state.active_file = None
|
if "run_result" not in st.session_state: st.session_state.run_result = None
|
||||||
if "editor_content" not in st.session_state:
|
if "chat_open" not in st.session_state: st.session_state.chat_open = False
|
||||||
st.session_state.editor_content = ""
|
if "chat_messages" not in st.session_state: st.session_state.chat_messages = []
|
||||||
if "last_applied_content" not in st.session_state:
|
if "ai_wrote_file" not in st.session_state: st.session_state.ai_wrote_file = False
|
||||||
st.session_state.last_applied_content = ""
|
if "tool_events" not in st.session_state: st.session_state.tool_events = []
|
||||||
if "run_result" not in st.session_state:
|
|
||||||
st.session_state.run_result = None
|
# --- Restore active file context on every rerender ---
|
||||||
if "chat_open" not in st.session_state:
|
if st.session_state.get("active_file") and st.session_state.get("editor_content"):
|
||||||
st.session_state.chat_open = False
|
chat_manager.set_active_file(
|
||||||
|
Path(st.session_state.active_file),
|
||||||
|
st.session_state.editor_content
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
chat_manager.set_active_file(None, "")
|
||||||
|
|
||||||
|
# --- Wire up the AI file-write callback ---
|
||||||
|
def on_file_written(path: str, new_content: str):
|
||||||
|
if str(path) == str(st.session_state.get("active_file")):
|
||||||
|
st.session_state.editor_content = new_content
|
||||||
|
st.session_state.last_applied_content = new_content
|
||||||
|
st.session_state.ai_wrote_file = True
|
||||||
|
|
||||||
|
chat_manager.on_file_written = on_file_written
|
||||||
|
|
||||||
|
# --- Wire up tool-use notification ---
|
||||||
|
def on_tool_used(tool_name: str, summary: str):
|
||||||
|
st.session_state.tool_events.append(summary)
|
||||||
|
|
||||||
|
chat_manager.on_tool_used = on_tool_used
|
||||||
|
|
||||||
# ---------------- SIDEBAR ----------------
|
# ---------------- SIDEBAR ----------------
|
||||||
|
st.sidebar.markdown("---")
|
||||||
|
|
||||||
selected_file = navigation.render_sidebar()
|
selected_file = navigation.render_sidebar()
|
||||||
if selected_file:
|
if selected_file:
|
||||||
st.session_state.selected_file = selected_file
|
st.session_state.selected_file = selected_file
|
||||||
|
|
||||||
st.sidebar.markdown("---")
|
st.sidebar.markdown("---")
|
||||||
|
|
||||||
current_file = st.session_state.selected_file
|
current_file = st.session_state.selected_file
|
||||||
|
|
||||||
# ---------------- FILE SWITCH ----------------
|
# ---------------- FILE SWITCH ----------------
|
||||||
@ -73,43 +110,104 @@ def main():
|
|||||||
content = file_manager.read_file(current_file)
|
content = file_manager.read_file(current_file)
|
||||||
st.session_state.editor_content = content
|
st.session_state.editor_content = content
|
||||||
st.session_state.last_applied_content = content
|
st.session_state.last_applied_content = content
|
||||||
|
chat_manager.set_active_file(Path(st.session_state.active_file), content)
|
||||||
|
st.success(f"File loaded: {Path(current_file)}")
|
||||||
else:
|
else:
|
||||||
st.session_state.editor_content = ""
|
st.session_state.editor_content = ""
|
||||||
|
chat_manager.set_active_file(None, "")
|
||||||
|
|
||||||
chat.inject_panel(st.session_state.chat_open)
|
if st.session_state.chat_open:
|
||||||
|
chat.inject_panel(True)
|
||||||
|
|
||||||
|
# ---------------- MAIN UI (split: editor left, chat right) ----------------
|
||||||
|
editor_col, chat_col = st.columns(2, gap="medium")
|
||||||
|
|
||||||
|
with editor_col:
|
||||||
|
st.subheader(
|
||||||
|
f"✏️ {Path(st.session_state.active_file).name}"
|
||||||
|
if st.session_state.active_file else "📂 Select a file from the explorer"
|
||||||
|
)
|
||||||
|
|
||||||
# ---------------- MAIN UI ----------------
|
|
||||||
if st.session_state.active_file:
|
if st.session_state.active_file:
|
||||||
st.subheader(f"✏️ {Path(st.session_state.active_file).name}")
|
|
||||||
|
|
||||||
editor = CodeEditor()
|
|
||||||
new_value, run_clicked = editor.display_code(
|
new_value, run_clicked = editor.display_code(
|
||||||
st.session_state.editor_content,
|
st.session_state.editor_content,
|
||||||
filename=st.session_state.active_file
|
filename=st.session_state.active_file
|
||||||
)
|
)
|
||||||
|
if new_value is not None and new_value != st.session_state.last_applied_content:
|
||||||
# ---------------- APPLY DETECTION ----------------
|
|
||||||
if new_value is not None:
|
|
||||||
if new_value != st.session_state.last_applied_content:
|
|
||||||
st.session_state.editor_content = new_value
|
st.session_state.editor_content = new_value
|
||||||
st.session_state.last_applied_content = new_value
|
st.session_state.last_applied_content = new_value
|
||||||
file_manager.save_file(st.session_state.active_file, new_value)
|
file_manager.save_file(st.session_state.active_file, new_value)
|
||||||
|
chat_manager.set_active_file(Path(st.session_state.active_file), new_value)
|
||||||
st.success("Saved ✔")
|
st.success("Saved ✔")
|
||||||
|
|
||||||
# ---------------- RUN ----------------
|
|
||||||
if run_clicked:
|
if run_clicked:
|
||||||
with st.spinner("Running…"):
|
with st.spinner("Running…"):
|
||||||
st.session_state.run_result = execution_engine.run_file(
|
st.session_state.run_result = execution_engine.run_file(st.session_state.active_file)
|
||||||
st.session_state.active_file
|
|
||||||
)
|
|
||||||
|
|
||||||
if st.session_state.run_result is not None:
|
if st.session_state.run_result is not None:
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
st.caption("Output")
|
st.caption("Output")
|
||||||
output_display.render_output(st.session_state.run_result)
|
output_display.render_output(st.session_state.run_result)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
st.info("Select a file from the explorer")
|
st.info("Select a file from the explorer")
|
||||||
|
|
||||||
|
# ---------------- CHAT COLUMN (right) ----------------
|
||||||
|
with chat_col:
|
||||||
|
if not st.session_state.chat_open:
|
||||||
|
st.subheader("💬 AI Assistant")
|
||||||
|
|
||||||
|
# Scrollable messages area — Streamlit's container(height=) gives it
|
||||||
|
# a fixed viewport that scrolls, so the input below is always visible.
|
||||||
|
messages_container = st.container(height=600, border=False)
|
||||||
|
with messages_container:
|
||||||
|
messages = st.session_state.chat_messages
|
||||||
|
if not messages:
|
||||||
|
st.caption("Ask anything about your code.")
|
||||||
|
for i, message in enumerate(messages):
|
||||||
|
with st.chat_message(message["role"]):
|
||||||
|
if (
|
||||||
|
message["role"] == "assistant"
|
||||||
|
and i == len(messages) - 1
|
||||||
|
and message.get("tool_events")
|
||||||
|
):
|
||||||
|
for event in message["tool_events"]:
|
||||||
|
st.markdown(f'<div class="tool-badge">{event}</div>', unsafe_allow_html=True)
|
||||||
|
st.markdown(message["content"])
|
||||||
|
|
||||||
|
# Input lives OUTSIDE the scrollable container — always at the bottom
|
||||||
|
if prompt := st.chat_input("Ask about your code..."):
|
||||||
|
st.session_state.tool_events = []
|
||||||
|
st.session_state.chat_messages.append({"role": "user", "content": prompt})
|
||||||
|
|
||||||
|
with messages_container:
|
||||||
|
with st.chat_message("user"):
|
||||||
|
st.markdown(prompt)
|
||||||
|
|
||||||
|
with st.chat_message("assistant"):
|
||||||
|
with st.spinner("AI is thinking..."):
|
||||||
|
response = chat_manager.generate_response(prompt, st.session_state.run_result)
|
||||||
|
|
||||||
|
tool_events = list(st.session_state.tool_events)
|
||||||
|
for event in tool_events:
|
||||||
|
st.markdown(f'<div class="tool-badge">{event}</div>', unsafe_allow_html=True)
|
||||||
|
|
||||||
|
st.markdown(response)
|
||||||
|
|
||||||
|
st.session_state.chat_messages.append({
|
||||||
|
"role": "assistant",
|
||||||
|
"content": response,
|
||||||
|
"tool_events": tool_events,
|
||||||
|
})
|
||||||
|
|
||||||
|
if st.session_state.ai_wrote_file:
|
||||||
|
st.session_state.ai_wrote_file = False
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
if st.sidebar.button("🗑️ Clear Chat"):
|
||||||
|
st.session_state.chat_messages.clear()
|
||||||
|
st.session_state.tool_events = []
|
||||||
|
chat_manager.clear_history()
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
# End of main.py
|
||||||
@ -6,13 +6,15 @@ class ChatInterface:
|
|||||||
Chat Interface for AI interaction, including the right-side slide-out panel.
|
Chat Interface for AI interaction, including the right-side slide-out panel.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, chat_manager=None):
|
||||||
|
# chat_manager accepted for compatibility with main.py
|
||||||
|
self.chat_manager = chat_manager
|
||||||
self.chat_messages = []
|
self.chat_messages = []
|
||||||
|
|
||||||
# ---------------- PANEL INJECTION ----------------
|
# ---------------- PANEL INJECTION ----------------
|
||||||
def inject_panel(self, is_open: bool):
|
def inject_panel(self, is_open: bool):
|
||||||
"""Injects or removes the fixed right-side AI chat panel via st.iframe."""
|
"""Injects or removes the fixed right-side AI chat panel via st.iframe."""
|
||||||
st.iframe(f"""
|
st.components.v1.html(f"""
|
||||||
<script>
|
<script>
|
||||||
const win = window.parent;
|
const win = window.parent;
|
||||||
const doc = win.document;
|
const doc = win.document;
|
||||||
@ -90,8 +92,9 @@ class ChatInterface:
|
|||||||
""", height=1)
|
""", height=1)
|
||||||
|
|
||||||
# ---------------- CHAT DISPLAY ----------------
|
# ---------------- CHAT DISPLAY ----------------
|
||||||
def display_chat(self, chat_manager):
|
def display_chat(self, chat_manager=None):
|
||||||
"""Renders the Streamlit chat interface (used when not in panel mode)."""
|
"""Renders the Streamlit chat interface (used when not in panel mode)."""
|
||||||
|
manager = chat_manager or self.chat_manager
|
||||||
st.title("AI Assistant")
|
st.title("AI Assistant")
|
||||||
|
|
||||||
for message in self.chat_messages:
|
for message in self.chat_messages:
|
||||||
@ -104,7 +107,7 @@ class ChatInterface:
|
|||||||
st.markdown(prompt)
|
st.markdown(prompt)
|
||||||
|
|
||||||
with st.chat_message("assistant"):
|
with st.chat_message("assistant"):
|
||||||
response = chat_manager.generate_response(prompt)
|
response = manager.generate_response(prompt) if manager else "No chat manager connected."
|
||||||
st.markdown(response)
|
st.markdown(response)
|
||||||
self.chat_messages.append({"role": "assistant", "content": response})
|
self.chat_messages.append({"role": "assistant", "content": response})
|
||||||
|
|
||||||
|
|||||||
@ -140,10 +140,6 @@ class FileNavigation:
|
|||||||
st.session_state.root_folder = picked
|
st.session_state.root_folder = picked
|
||||||
st.session_state._folder_snapshot = None
|
st.session_state._folder_snapshot = None
|
||||||
st.rerun()
|
st.rerun()
|
||||||
with btn_right:
|
|
||||||
if st.button("💬 Ask AI", key="toggle_chat", use_container_width=True):
|
|
||||||
st.session_state.chat_open = not st.session_state.get("chat_open", False)
|
|
||||||
st.rerun()
|
|
||||||
|
|
||||||
st.markdown("---")
|
st.markdown("---")
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user