Spaces:
Paused
Paused
Update main.py
Browse files
main.py
CHANGED
|
@@ -61,8 +61,8 @@ DEFAULT_AGENTS = [
|
|
| 61 |
"5. Si hay frontend_dev en el equipo, TÚ haces servidor/backend, él hace HTML.\n"
|
| 62 |
"6. Si la tarea no requiere backend → responde: {\"skip\":\"no backend needed\"}"
|
| 63 |
),
|
| 64 |
-
"models":["
|
| 65 |
-
"
|
| 66 |
{"key":"frontend_dev","name":"Frontend","provider":"openrouter",
|
| 67 |
"role":(
|
| 68 |
"Eres desarrollador frontend senior. REGLAS ABSOLUTAS:\n"
|
|
@@ -71,8 +71,8 @@ DEFAULT_AGENTS = [
|
|
| 71 |
"3. Si la tarea NO requiere frontend → responde: {\"skip\":\"no frontend needed\"}\n"
|
| 72 |
"4. Entrega siempre HTML completo y funcional con los estilos incluidos."
|
| 73 |
),
|
| 74 |
-
"models":["
|
| 75 |
-
"
|
| 76 |
{"key":"analyst","name":"Analyst","provider":"openrouter",
|
| 77 |
"role":(
|
| 78 |
"Eres analista de negocios. REGLAS:\n"
|
|
@@ -80,8 +80,8 @@ DEFAULT_AGENTS = [
|
|
| 80 |
"2. NUNCA describas imágenes ni hagas trabajo de otros agentes.\n"
|
| 81 |
"3. Si la tarea no requiere análisis → responde: {\"skip\":\"no analysis needed\"}"
|
| 82 |
),
|
| 83 |
-
"models":["
|
| 84 |
-
"
|
| 85 |
{"key":"writer","name":"Writer","provider":"openrouter",
|
| 86 |
"role":(
|
| 87 |
"Eres redactor experto. Escribe SOLO contenido real y extenso (500+ palabras). "
|
|
@@ -89,8 +89,8 @@ DEFAULT_AGENTS = [
|
|
| 89 |
"Secciones: ## Resumen Ejecutivo, ### Introducción, ### Desarrollo, "
|
| 90 |
"### Hallazgos, ### Conclusiones, ### Recomendaciones"
|
| 91 |
),
|
| 92 |
-
"models":["
|
| 93 |
-
"
|
| 94 |
{"key":"image_agent","name":"ImageAgent","provider":"gemini",
|
| 95 |
"role":(
|
| 96 |
"Cuando se te pida imágenes, responde SOLO con: "
|
|
@@ -117,6 +117,87 @@ async def call_compat(base_url,model,system,user,key,headers):
|
|
| 117 |
r.raise_for_status()
|
| 118 |
return r.json()["choices"][0]["message"]["content"]
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
def is_rate_limit(err: str) -> bool:
|
| 121 |
e = err.lower()
|
| 122 |
return any(x in e for x in ["429","rate limit","quota","resource exhausted","too many requests","ratelimit"])
|
|
@@ -144,12 +225,12 @@ async def call_llm(agent, task):
|
|
| 144 |
if OPENROUTER_API_KEY and agent["provider"] != "openrouter":
|
| 145 |
or_prov = PROVIDERS["openrouter"]
|
| 146 |
for m in [
|
|
|
|
|
|
|
| 147 |
"meta-llama/llama-3.3-70b-instruct:free",
|
| 148 |
"mistralai/mistral-small-3.1-24b-instruct:free",
|
| 149 |
"qwen/qwen3-4b:free",
|
| 150 |
-
"google/gemma-3-12b-it:free",
|
| 151 |
"qwen/qwen-2.5-72b-instruct:free",
|
| 152 |
-
"microsoft/phi-4-reasoning-plus:free",
|
| 153 |
"deepseek/deepseek-r1-distill-llama-70b:free",
|
| 154 |
]:
|
| 155 |
try:
|
|
@@ -518,25 +599,66 @@ async def run_mission(request:Request):
|
|
| 518 |
|
| 519 |
|
| 520 |
|
|
|
|
|
|
|
|
|
|
| 521 |
@app.post("/api/chat")
|
| 522 |
async def chat_with_agent(request: Request):
|
| 523 |
-
body
|
| 524 |
-
agent_key
|
| 525 |
-
message
|
|
|
|
|
|
|
|
|
|
| 526 |
if not agent_key or not message:
|
| 527 |
return JSONResponse({"error": "agent and message required"}, status_code=400)
|
| 528 |
if agent_key not in agent_registry:
|
| 529 |
return JSONResponse({"error": f"Agent '{agent_key}' not found"}, status_code=404)
|
| 530 |
-
|
| 531 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
agent = dict(agent_registry[agent_key])
|
| 533 |
-
agent["role"] = f"HOY ES: {_today}. " + agent["role"]
|
|
|
|
| 534 |
try:
|
| 535 |
-
response = await
|
| 536 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
except Exception as e:
|
| 538 |
return JSONResponse({"error": str(e)}, status_code=500)
|
| 539 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
@app.get("/api/archive")
|
| 541 |
async def list_archive():
|
| 542 |
files = []
|
|
|
|
| 61 |
"5. Si hay frontend_dev en el equipo, TÚ haces servidor/backend, él hace HTML.\n"
|
| 62 |
"6. Si la tarea no requiere backend → responde: {\"skip\":\"no backend needed\"}"
|
| 63 |
),
|
| 64 |
+
"models":["google/gemma-3-27b-it:free","google/gemma-3-12b-it:free",
|
| 65 |
+
"meta-llama/llama-3.3-70b-instruct:free","mistralai/mistral-small-3.1-24b-instruct:free"]},
|
| 66 |
{"key":"frontend_dev","name":"Frontend","provider":"openrouter",
|
| 67 |
"role":(
|
| 68 |
"Eres desarrollador frontend senior. REGLAS ABSOLUTAS:\n"
|
|
|
|
| 71 |
"3. Si la tarea NO requiere frontend → responde: {\"skip\":\"no frontend needed\"}\n"
|
| 72 |
"4. Entrega siempre HTML completo y funcional con los estilos incluidos."
|
| 73 |
),
|
| 74 |
+
"models":["google/gemma-3-12b-it:free","google/gemma-3-27b-it:free",
|
| 75 |
+
"meta-llama/llama-3.3-70b-instruct:free","mistralai/mistral-small-3.1-24b-instruct:free"]},
|
| 76 |
{"key":"analyst","name":"Analyst","provider":"openrouter",
|
| 77 |
"role":(
|
| 78 |
"Eres analista de negocios. REGLAS:\n"
|
|
|
|
| 80 |
"2. NUNCA describas imágenes ni hagas trabajo de otros agentes.\n"
|
| 81 |
"3. Si la tarea no requiere análisis → responde: {\"skip\":\"no analysis needed\"}"
|
| 82 |
),
|
| 83 |
+
"models":["google/gemma-3-27b-it:free","google/gemma-3-12b-it:free",
|
| 84 |
+
"meta-llama/llama-3.3-70b-instruct:free","mistralai/mistral-small-3.1-24b-instruct:free"]},
|
| 85 |
{"key":"writer","name":"Writer","provider":"openrouter",
|
| 86 |
"role":(
|
| 87 |
"Eres redactor experto. Escribe SOLO contenido real y extenso (500+ palabras). "
|
|
|
|
| 89 |
"Secciones: ## Resumen Ejecutivo, ### Introducción, ### Desarrollo, "
|
| 90 |
"### Hallazgos, ### Conclusiones, ### Recomendaciones"
|
| 91 |
),
|
| 92 |
+
"models":["google/gemma-3-12b-it:free","google/gemma-3-27b-it:free",
|
| 93 |
+
"meta-llama/llama-3.3-70b-instruct:free","mistralai/mistral-small-3.1-24b-instruct:free"]},
|
| 94 |
{"key":"image_agent","name":"ImageAgent","provider":"gemini",
|
| 95 |
"role":(
|
| 96 |
"Cuando se te pida imágenes, responde SOLO con: "
|
|
|
|
| 117 |
r.raise_for_status()
|
| 118 |
return r.json()["choices"][0]["message"]["content"]
|
| 119 |
|
| 120 |
+
|
| 121 |
+
async def call_compat_multiturn(base_url, model, system, messages, key, extra_headers):
|
| 122 |
+
"""OpenAI-compatible chat with full message history for multi-turn conversations."""
|
| 123 |
+
h = {"Authorization": f"Bearer {key}", "Content-Type": "application/json", **extra_headers}
|
| 124 |
+
payload = {
|
| 125 |
+
"model": model,
|
| 126 |
+
"messages": [{"role": "system", "content": system}] + messages,
|
| 127 |
+
"max_tokens": 2048,
|
| 128 |
+
"temperature": 0.6,
|
| 129 |
+
}
|
| 130 |
+
async with httpx.AsyncClient(timeout=90) as c:
|
| 131 |
+
r = await c.post(base_url, json=payload, headers=h)
|
| 132 |
+
r.raise_for_status()
|
| 133 |
+
return r.json()["choices"][0]["message"]["content"]
|
| 134 |
+
|
| 135 |
+
async def call_gemini_multiturn(model, system, messages, key):
|
| 136 |
+
"""Gemini multi-turn conversation."""
|
| 137 |
+
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}"
|
| 138 |
+
# Convert messages to Gemini format
|
| 139 |
+
contents = []
|
| 140 |
+
for m in messages:
|
| 141 |
+
role = "user" if m["role"] == "user" else "model"
|
| 142 |
+
contents.append({"role": role, "parts": [{"text": m["content"]}]})
|
| 143 |
+
# Prepend system as first user message if contents start with model
|
| 144 |
+
full_system = system + "\n\n" + (contents[0]["parts"][0]["text"] if contents and contents[0]["role"] == "user" else "")
|
| 145 |
+
if contents and contents[0]["role"] == "user":
|
| 146 |
+
contents[0]["parts"][0]["text"] = full_system
|
| 147 |
+
payload = {
|
| 148 |
+
"contents": contents,
|
| 149 |
+
"generationConfig": {"maxOutputTokens": 2048, "temperature": 0.6},
|
| 150 |
+
}
|
| 151 |
+
async with httpx.AsyncClient(timeout=90) as c:
|
| 152 |
+
r = await c.post(url, json=payload)
|
| 153 |
+
r.raise_for_status()
|
| 154 |
+
return r.json()["candidates"][0]["content"]["parts"][0]["text"]
|
| 155 |
+
|
| 156 |
+
async def call_llm_multiturn(agent, messages):
|
| 157 |
+
"""Multi-turn LLM call with full conversation history. Cascades through providers."""
|
| 158 |
+
system = agent["role"]
|
| 159 |
+
last_err = None
|
| 160 |
+
|
| 161 |
+
# 1. Primary provider
|
| 162 |
+
p = PROVIDERS[agent["provider"]]
|
| 163 |
+
for m in agent["models"]:
|
| 164 |
+
try:
|
| 165 |
+
if p["type"] == "gemini":
|
| 166 |
+
return await call_gemini_multiturn(m, system, messages, p["key"])
|
| 167 |
+
else:
|
| 168 |
+
return await call_compat_multiturn(p["base_url"], m, system, messages,
|
| 169 |
+
p["key"], p.get("headers", {}))
|
| 170 |
+
except Exception as e:
|
| 171 |
+
last_err = str(e)
|
| 172 |
+
if is_rate_limit(last_err):
|
| 173 |
+
break
|
| 174 |
+
|
| 175 |
+
# 2. OpenRouter fallback (Gemma 3 first)
|
| 176 |
+
if OPENROUTER_API_KEY and agent["provider"] != "openrouter":
|
| 177 |
+
or_prov = PROVIDERS["openrouter"]
|
| 178 |
+
for m in ["google/gemma-3-27b-it:free", "google/gemma-3-12b-it:free",
|
| 179 |
+
"meta-llama/llama-3.3-70b-instruct:free",
|
| 180 |
+
"mistralai/mistral-small-3.1-24b-instruct:free"]:
|
| 181 |
+
try:
|
| 182 |
+
return await call_compat_multiturn(or_prov["base_url"], m, system, messages,
|
| 183 |
+
or_prov["key"], or_prov.get("headers", {}))
|
| 184 |
+
except Exception as e:
|
| 185 |
+
last_err = str(e)
|
| 186 |
+
if is_rate_limit(last_err):
|
| 187 |
+
break
|
| 188 |
+
|
| 189 |
+
# 3. Groq fallback
|
| 190 |
+
if GROQ_API_KEY and agent["provider"] != "groq":
|
| 191 |
+
groq = PROVIDERS["groq"]
|
| 192 |
+
for m in ["llama-3.1-8b-instant", "gemma2-9b-it"]:
|
| 193 |
+
try:
|
| 194 |
+
return await call_compat_multiturn(groq["base_url"], m, system, messages,
|
| 195 |
+
GROQ_API_KEY, {})
|
| 196 |
+
except Exception as e:
|
| 197 |
+
last_err = str(e)
|
| 198 |
+
|
| 199 |
+
raise Exception(f"All providers exhausted. Last: {last_err}")
|
| 200 |
+
|
| 201 |
def is_rate_limit(err: str) -> bool:
|
| 202 |
e = err.lower()
|
| 203 |
return any(x in e for x in ["429","rate limit","quota","resource exhausted","too many requests","ratelimit"])
|
|
|
|
| 225 |
if OPENROUTER_API_KEY and agent["provider"] != "openrouter":
|
| 226 |
or_prov = PROVIDERS["openrouter"]
|
| 227 |
for m in [
|
| 228 |
+
"google/gemma-3-27b-it:free",
|
| 229 |
+
"google/gemma-3-12b-it:free",
|
| 230 |
"meta-llama/llama-3.3-70b-instruct:free",
|
| 231 |
"mistralai/mistral-small-3.1-24b-instruct:free",
|
| 232 |
"qwen/qwen3-4b:free",
|
|
|
|
| 233 |
"qwen/qwen-2.5-72b-instruct:free",
|
|
|
|
| 234 |
"deepseek/deepseek-r1-distill-llama-70b:free",
|
| 235 |
]:
|
| 236 |
try:
|
|
|
|
| 599 |
|
| 600 |
|
| 601 |
|
| 602 |
+
# In-memory chat sessions: {session_id: [{role, content}]}
|
| 603 |
+
chat_sessions: dict = {}
|
| 604 |
+
|
| 605 |
@app.post("/api/chat")
|
| 606 |
async def chat_with_agent(request: Request):
|
| 607 |
+
body = await request.json()
|
| 608 |
+
agent_key = body.get("agent", "").strip()
|
| 609 |
+
message = body.get("message", "").strip()
|
| 610 |
+
session_id = body.get("session_id", agent_key) # default: one session per agent
|
| 611 |
+
clear = body.get("clear", False)
|
| 612 |
+
|
| 613 |
if not agent_key or not message:
|
| 614 |
return JSONResponse({"error": "agent and message required"}, status_code=400)
|
| 615 |
if agent_key not in agent_registry:
|
| 616 |
return JSONResponse({"error": f"Agent '{agent_key}' not found"}, status_code=404)
|
| 617 |
+
|
| 618 |
+
# Reset history if requested
|
| 619 |
+
if clear:
|
| 620 |
+
chat_sessions[session_id] = []
|
| 621 |
+
|
| 622 |
+
# Init session
|
| 623 |
+
if session_id not in chat_sessions:
|
| 624 |
+
chat_sessions[session_id] = []
|
| 625 |
+
|
| 626 |
+
# Build messages list (keep last 20 turns = 40 messages to stay within context)
|
| 627 |
+
history = chat_sessions[session_id][-40:]
|
| 628 |
+
history.append({"role": "user", "content": message})
|
| 629 |
+
|
| 630 |
+
# Inject today's date into agent role
|
| 631 |
+
_today = datetime.now().strftime("%A %d de %B de %Y, %H:%M")
|
| 632 |
agent = dict(agent_registry[agent_key])
|
| 633 |
+
agent["role"] = f"HOY ES: {_today}. Eres {agent['name']}. " + agent["role"]
|
| 634 |
+
|
| 635 |
try:
|
| 636 |
+
response = await call_llm_multiturn(agent, history)
|
| 637 |
+
|
| 638 |
+
# Save turn to session
|
| 639 |
+
chat_sessions[session_id].append({"role": "user", "content": message})
|
| 640 |
+
chat_sessions[session_id].append({"role": "assistant", "content": response})
|
| 641 |
+
|
| 642 |
+
# Keep sessions from growing too large (max 100 messages)
|
| 643 |
+
if len(chat_sessions[session_id]) > 100:
|
| 644 |
+
chat_sessions[session_id] = chat_sessions[session_id][-80:]
|
| 645 |
+
|
| 646 |
+
used_model = agent["models"][0] if agent.get("models") else ""
|
| 647 |
+
return JSONResponse({
|
| 648 |
+
"success": True,
|
| 649 |
+
"agent": agent_key,
|
| 650 |
+
"response": response,
|
| 651 |
+
"model": used_model,
|
| 652 |
+
"turn": len(chat_sessions[session_id]) // 2,
|
| 653 |
+
})
|
| 654 |
except Exception as e:
|
| 655 |
return JSONResponse({"error": str(e)}, status_code=500)
|
| 656 |
|
| 657 |
+
@app.delete("/api/chat/{session_id}")
|
| 658 |
+
async def clear_chat_session(session_id: str):
|
| 659 |
+
chat_sessions.pop(session_id, None)
|
| 660 |
+
return {"success": True}
|
| 661 |
+
|
| 662 |
@app.get("/api/archive")
|
| 663 |
async def list_archive():
|
| 664 |
files = []
|