second commit
This commit is contained in:
+247
@@ -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)
|
||||
Reference in New Issue
Block a user