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)