diff --git a/agent.py b/agent.py new file mode 100644 index 0000000..6beb3fa --- /dev/null +++ b/agent.py @@ -0,0 +1,316 @@ +""" +Code Agent - Handles file operations, code execution, and testing +""" + +import os +import json +import requests +import subprocess +import webbrowser +import time +from pathlib import Path +from datetime import datetime +from typing import Dict, Any +from dotenv import load_dotenv + +load_dotenv() + +# ============================================================================ +# CONFIGURATION +# ============================================================================ + +LLM_SERVER = os.getenv("LLM_SERVER_URL", "http://localhost:8000") +OLLAMA_URL = os.getenv("OLLAMA_URL", "http://localhost:11434") +LLM_FAST = os.getenv("LLM_FAST_MODEL", "phi") +LLM_POWER = os.getenv("LLM_POWER_MODEL", "deepseek-coder:33b") + +# ============================================================================ +# CODE AGENT CLASS +# ============================================================================ + +class CodeAgent: + """Agent that modifies files, executes code, and tests in browser""" + + def __init__(self, work_dir: str = "./workspace"): + self.work_dir = Path(work_dir) + self.work_dir.mkdir(exist_ok=True) + print(f"📁 Workspace: {self.work_dir.absolute()}") + + # ======================================================================== + # FILE OPERATIONS + # ======================================================================== + + def read_file(self, file_path: str) -> str: + """Read file from workspace""" + file = self.work_dir / file_path + + if not file.exists(): + return f"❌ File not found: {file_path}" + + try: + with open(file, 'r', encoding='utf-8') as f: + content = f.read() + print(f"✅ Read: {file_path} ({len(content)} bytes)") + return content + except Exception as e: + return f"❌ Error reading {file_path}: {e}" + + def write_file(self, file_path: str, content: str) -> str: + """Write/modify file in workspace""" + file = self.work_dir / file_path + file.parent.mkdir(parents=True, exist_ok=True) + + try: + with open(file, 'w', encoding='utf-8') as f: + f.write(content) + print(f"✅ Wrote: {file_path}") + return f"File saved: {file_path}" + except Exception as e: + return f"❌ Error writing {file_path}: {e}" + + def delete_file(self, file_path: str) -> str: + """Delete file from workspace""" + file = self.work_dir / file_path + + if not file.exists(): + return f"❌ File not found: {file_path}" + + try: + file.unlink() + return f"✅ Deleted: {file_path}" + except Exception as e: + return f"❌ Error deleting {file_path}: {e}" + + def list_files(self) -> str: + """List all files in workspace""" + files = list(self.work_dir.rglob("*")) + file_list = "\n".join([ + f" {f.relative_to(self.work_dir)}" + for f in files if f.is_file() + ]) + return f"📁 Files in workspace:\n{file_list}" if file_list else "📁 No files" + + # ======================================================================== + # CODE EXECUTION + # ======================================================================== + + def execute_python(self, code: str, file_name: str = "exec.py") -> str: + """Execute Python code""" + script_path = self.work_dir / file_name + + try: + # Save script + with open(script_path, 'w', encoding='utf-8') as f: + f.write(code) + + # Execute + result = subprocess.run( + ["python3", str(script_path)], + capture_output=True, + text=True, + timeout=30, + cwd=self.work_dir + ) + + output = result.stdout + result.stderr + print(f"🐍 Python executed: {file_name}") + return output[:1000] # Limit output + + except subprocess.TimeoutExpired: + return "❌ Timeout: Script took more than 30 seconds" + except Exception as e: + return f"❌ Error: {e}" + + def execute_node(self, code: str, file_name: str = "exec.js") -> str: + """Execute Node.js code""" + script_path = self.work_dir / file_name + + try: + with open(script_path, 'w', encoding='utf-8') as f: + f.write(code) + + result = subprocess.run( + ["node", str(script_path)], + capture_output=True, + text=True, + timeout=30, + cwd=self.work_dir + ) + + output = result.stdout + result.stderr + print(f"📟 Node.js executed: {file_name}") + return output[:1000] + + except FileNotFoundError: + return "❌ Node.js not installed" + except Exception as e: + return f"❌ Error: {e}" + + def execute_shell(self, command: str) -> str: + """Execute shell command (whitelist safe commands only)""" + allowed_commands = [ + 'ls', 'pwd', 'mkdir', 'rm', 'cp', 'mv', + 'cat', 'grep', 'find', 'du', 'df', + 'git', 'npm', 'pip', 'python' + ] + + cmd_name = command.split()[0] if command else "" + if cmd_name not in allowed_commands: + return f"❌ Command not allowed: {cmd_name}" + + try: + result = subprocess.run( + command.split(), + capture_output=True, + text=True, + timeout=30, + cwd=self.work_dir + ) + output = result.stdout + result.stderr + return output[:500] + except Exception as e: + return f"❌ Error: {e}" + + # ======================================================================== + # TESTING & BROWSER + # ======================================================================== + + def test_html(self, html_file: str) -> str: + """Open HTML file in browser for testing""" + file_path = self.work_dir / html_file + + if not file_path.exists(): + return f"❌ File not found: {html_file}" + + try: + url = f"file:///{file_path.absolute()}".replace("\\", "/") + webbrowser.open(url) + print(f"🌐 Opened in browser: {html_file}") + return f"✅ HTML opened: {html_file}" + except Exception as e: + return f"❌ Error: {e}" + + def open_browser(self, url: str) -> str: + """Open URL in browser""" + try: + webbrowser.open(url) + return f"✅ Browser opened: {url}" + except Exception as e: + return f"❌ Error: {e}" + + # ======================================================================== + # GIT OPERATIONS + # ======================================================================== + + def git_commit(self, message: str = None) -> str: + """Auto-commit changes""" + try: + if not message: + message = f"Auto-commit from agent - {datetime.now().isoformat()}" + + # Git add + result = subprocess.run( + ['git', 'add', '.'], + cwd=self.work_dir, + capture_output=True, + text=True + ) + + if result.returncode != 0: + return f"❌ Git add failed: {result.stderr}" + + # Git commit + result = subprocess.run( + ['git', 'commit', '-m', message], + cwd=self.work_dir, + capture_output=True, + text=True + ) + + if result.returncode != 0: + return f"ℹ️ Nothing to commit" + + return f"✅ Committed: {message}" + except Exception as e: + return f"❌ Error: {e}" + + def git_push(self) -> str: + """Push to remote repository""" + try: + result = subprocess.run( + ['git', 'push'], + cwd=self.work_dir, + capture_output=True, + text=True, + timeout=30 + ) + + if result.returncode == 0: + return "✅ Pushed to remote" + else: + return f"❌ Push failed: {result.stderr}" + except Exception as e: + return f"❌ Error: {e}" + + # ======================================================================== + # LLM INTERACTION + # ======================================================================== + + def call_llm(self, prompt: str, model: str = "phi") -> str: + """Call LLM server""" + try: + response = requests.post( + f"{OLLAMA_URL}/api/generate", + json={ + "model": model, + "prompt": prompt, + "stream": False + }, + timeout=300 + ) + + if response.status_code == 200: + return response.json()["response"] + else: + return f"❌ LLM Error: {response.status_code}" + + except requests.exceptions.ConnectionError: + return f"❌ Cannot connect to {OLLAMA_URL}" + except Exception as e: + return f"❌ Error: {e}" + +# ============================================================================ +# GET AGENT TOOLS +# ============================================================================ + +def get_agent_tools(agent: CodeAgent) -> Dict[str, Any]: + """Return dictionary of available tools""" + return { + "read_file": agent.read_file, + "write_file": agent.write_file, + "delete_file": agent.delete_file, + "execute_python": agent.execute_python, + "execute_node": agent.execute_node, + "execute_shell": agent.execute_shell, + "test_html": agent.test_html, + "open_browser": agent.open_browser, + "git_commit": agent.git_commit, + "git_push": agent.git_push, + "list_files": agent.list_files, + "call_llm": agent.call_llm, + } + +# ============================================================================ +# MAIN (for testing) +# ============================================================================ + +if __name__ == "__main__": + agent = CodeAgent() + + # Test + print("\n🧪 Testing Agent...") + print(agent.list_files()) + + # Create test file + agent.write_file("test.txt", "Hello from agent!") + print(agent.read_file("test.txt")) \ No newline at end of file diff --git a/backend.py b/backend.py new file mode 100644 index 0000000..ddb2132 --- /dev/null +++ b/backend.py @@ -0,0 +1,247 @@ +from fastapi import FastAPI, File, UploadFile, Form, HTTPException +from fastapi.responses import StreamingResponse, JSONResponse +from fastapi.staticfiles import StaticFiles +from fastapi.cors import CORSMiddleware +import aiofiles +import base64 +import requests +import json +import os +from pathlib import Path +from dotenv import load_dotenv +import asyncio +import io +from PIL import Image +import uuid + +load_dotenv() + +app = FastAPI(title="Code Agent Chat") + +# CORS para que el frontend acceda +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Config +LLM_SERVER = os.getenv("LLM_SERVER_URL", "http://localhost:8000") +UPLOAD_DIR = Path("./uploads") +WORKSPACE_DIR = Path("./workspace") +UPLOAD_DIR.mkdir(exist_ok=True) +WORKSPACE_DIR.mkdir(exist_ok=True) + +# Importar agent +from agent import CodeAgent, get_agent_tools + +agent = CodeAgent(work_dir=str(WORKSPACE_DIR)) + +# ============================================================================ +# ENDPOINTS +# ============================================================================ + +@app.get("/api/health") +async def health(): + """Verificar que el servidor está vivo""" + return {"status": "ok", "workspace": str(WORKSPACE_DIR.absolute())} + +@app.get("/api/files") +async def list_files(): + """Listar archivos en workspace""" + files = [] + for f in WORKSPACE_DIR.rglob("*"): + if f.is_file(): + files.append({ + "name": str(f.relative_to(WORKSPACE_DIR)), + "size": f.stat().st_size, + "type": f.suffix + }) + return {"files": files} + +@app.get("/api/file/{file_path:path}") +async def read_file(file_path: str): + """Leer contenido de archivo""" + file = WORKSPACE_DIR / file_path + + if not file.exists(): + raise HTTPException(status_code=404, detail="Archivo no encontrado") + + try: + with open(file, 'r') as f: + content = f.read() + return {"content": content, "name": file_path} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/api/chat") +async def chat( + message: str = Form(...), + image: UploadFile = File(None), + model: str = Form("phi") +): + """ + Endpoint principal de chat + - message: tu prompt + - image: imagen adjunta (opcional) + - model: "phi" (rápido) o "deepseek-coder:33b" (potente) + """ + + # Procesar imagen si hay + image_data = None + image_base64 = None + + if image: + try: + contents = await image.read() + image_base64 = base64.b64encode(contents).decode() + + # Guardar imagen + image_path = UPLOAD_DIR / f"{uuid.uuid4()}.png" + with open(image_path, 'wb') as f: + f.write(contents) + + image_data = f"[Imagen adjunta: {image.filename}]" + print(f"📸 Imagen procesada: {image.filename}") + + except Exception as e: + raise HTTPException(status_code=400, detail=f"Error procesando imagen: {e}") + + # Construir prompt para el LLM + full_prompt = f""" +{message} + +{f"Contexto de imagen: {image_data}" if image_data else ""} + +Tienes acceso a estas herramientas: +- read_file(file_path): Leer archivo +- write_file(file_path, content): Escribir archivo +- execute_python(code): Ejecutar Python +- execute_node(code): Ejecutar Node.js +- test_html(file_name): Servir y testear HTML +- open_browser(url): Abrir navegador +- list_files(): Listar archivos + +Responde en JSON con tu pensamiento y la acción a ejecutar. +Ejemplo: +{{"thought": "Voy a leer el archivo", "action": "read_file", "args": {{"file_path": "app.js"}}}} +""" + + # Llamar LLM + try: + response = requests.post( + f"{LLM_SERVER}/code", + json={ + "message": full_prompt, + "model": model + }, + timeout=300, + stream=False + ) + + if response.status_code != 200: + raise HTTPException( + status_code=response.status_code, + detail=response.text + ) + + llm_response = response.json()["response"] + + # Parsear JSON y ejecutar acción + result = process_agent_response(llm_response) + + return { + "message": message, + "image": image.filename if image else None, + "model": model, + "llm_response": llm_response, + "action_result": result, + "image_base64": image_base64 + } + + except requests.exceptions.ConnectionError: + raise HTTPException( + status_code=503, + detail=f"No se puede conectar a LLM Server: {LLM_SERVER}" + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@app.post("/api/chat-stream") +async def chat_stream( + message: str = Form(...), + image: UploadFile = File(None), + model: str = Form("phi") +): + """Versión streaming (respuestas en tiempo real)""" + + image_data = None + if image: + contents = await image.read() + image_path = UPLOAD_DIR / f"{uuid.uuid4()}.png" + with open(image_path, 'wb') as f: + f.write(contents) + image_data = f"[Imagen: {image.filename}]" + + full_prompt = f"{message}\n{image_data if image_data else ''}" + + async def generate(): + try: + # Streaming desde LLM + response = requests.post( + f"{LLM_SERVER}/code", + json={ + "message": full_prompt, + "model": model + }, + timeout=300, + stream=True + ) + + for line in response.iter_lines(): + if line: + yield line + b'\n' + + except Exception as e: + yield json.dumps({"error": str(e)}).encode() + b'\n' + + return StreamingResponse(generate(), media_type="text/event-stream") + +def process_agent_response(response: str) -> dict: + """Parsear respuesta del LLM y ejecutar acción""" + tools = get_agent_tools(agent) + + try: + # Extraer JSON + json_start = response.find('{') + json_end = response.rfind('}') + 1 + json_str = response[json_start:json_end] + action = json.loads(json_str) + except: + return {"error": "No se pudo parsear la respuesta", "raw": response[:200]} + + action_name = action.get("action") or action.get("tool") + args = action.get("args", {}) + + if action_name not in tools: + return {"error": f"Acción desconocida: {action_name}"} + + try: + result = tools[action_name](**args) + return {"success": True, "action": action_name, "result": result} + except Exception as e: + return {"error": f"Error ejecutando {action_name}: {str(e)}"} + +# ============================================================================ +# SERVIR STATIC FILES (UI Frontend) +# ============================================================================ + +app.mount("/", StaticFiles(directory="static", html=True), name="static") + +if __name__ == "__main__": + import uvicorn + print(f"🚀 Backend en http://localhost:5000") + print(f"📁 Workspace: {WORKSPACE_DIR.absolute()}") + uvicorn.run(app, host="0.0.0.0", port=5000) \ No newline at end of file diff --git a/config/server.json b/config/server.json new file mode 100644 index 0000000..6c42982 --- /dev/null +++ b/config/server.json @@ -0,0 +1,74 @@ +{ + "server": { + "host": "0.0.0.0", + "api_port": 8000, + "web_port": 5000, + "workers": 4, + "debug": false, + "environment": "production" + }, + "models": { + "fast": { + "name": "phi", + "description": "Phi 2.7B - Rápido", + "vram_required_gb": 2, + "tokens_per_second": 50 + }, + "power": { + "name": "deepseek-coder:33b", + "description": "DeepSeek 33B - Potente", + "vram_required_gb": 18, + "tokens_per_second": 6 + } + }, + "gpu": { + "memory_fraction": 0.9, + "max_batch_size": 8, + "enable_cuda": true, + "device_id": 0 + }, + "logging": { + "level": "INFO", + "format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + "log_dir": "./logs", + "max_file_size_mb": 10, + "backup_count": 10 + }, + "database": { + "type": "sqlite", + "path": "./data/llm_history.db", + "enable_history": true + }, + "security": { + "enable_rate_limit": true, + "rate_limit_requests": 100, + "rate_limit_period_seconds": 60, + "require_api_key": false, + "allowed_origins": ["*"] + }, + "workspace": { + "path": "./workspace", + "max_file_size_mb": 100, + "allowed_extensions": [".py", ".js", ".html", ".css", ".json", ".txt", ".md"] + }, + "api_endpoints": { + "health": "/api/health", + "models": "/api/models", + "chat": "/api/chat", + "files": "/api/files", + "metrics": "/api/metrics" + }, + "ollama": { + "host": "localhost", + "port": 11434, + "timeout_seconds": 300, + "auto_download_models": true + }, + "features": { + "enable_git_commit": true, + "enable_code_execution": true, + "enable_file_operations": true, + "enable_browser_testing": true, + "enable_metrics": true + } +} \ No newline at end of file diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..6776450 --- /dev/null +++ b/static/index.html @@ -0,0 +1,549 @@ + + +
+ + +