vfven commited on
Commit
fb5356b
Β·
verified Β·
1 Parent(s): 4a0dd63

Upload 5 files

Browse files
Files changed (3) hide show
  1. Dockerfile +6 -0
  2. README.md +16 -0
  3. main.py +227 -77
Dockerfile CHANGED
@@ -1,9 +1,15 @@
1
  FROM python:3.11-slim
 
2
  WORKDIR /app
 
3
  COPY requirements.txt .
4
  RUN pip install --no-cache-dir -r requirements.txt
 
5
  RUN mkdir -p templates
 
6
  COPY templates/ templates/
7
  COPY main.py .
 
8
  EXPOSE 7860
 
9
  CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
 
1
  FROM python:3.11-slim
2
+
3
  WORKDIR /app
4
+
5
  COPY requirements.txt .
6
  RUN pip install --no-cache-dir -r requirements.txt
7
+
8
  RUN mkdir -p templates
9
+
10
  COPY templates/ templates/
11
  COPY main.py .
12
+
13
  EXPOSE 7860
14
+
15
  CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -6,3 +6,19 @@ colorTo: gray
6
  sdk: docker
7
  pinned: false
8
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  sdk: docker
7
  pinned: false
8
  ---
9
+
10
+ # Mission Control AI
11
+
12
+ Full-stack agent orchestration β€” FastAPI backend with direct LLM calls + pixel office frontend.
13
+
14
+ ## Setup
15
+
16
+ Add these secrets in your Space settings (Settings β†’ Variables and secrets):
17
+
18
+ - `GOOGLE_API_KEY` β€” Google AI Studio key
19
+ - `OPENROUTER_API_KEY` β€” OpenRouter key
20
+ - `GROQ_API_KEY` β€” Groq key (substitute/fallback)
21
+
22
+ ## Health check
23
+
24
+ Visit `/api/health` to verify all providers are configured.
main.py CHANGED
@@ -1,39 +1,208 @@
 
 
 
1
  from fastapi import FastAPI, Request
2
  from fastapi.responses import HTMLResponse, JSONResponse
3
  from fastapi.middleware.cors import CORSMiddleware
4
- import httpx
5
  from datetime import datetime
6
  from pathlib import Path
7
 
8
  app = FastAPI()
 
9
 
10
- app.add_middleware(
11
- CORSMiddleware,
12
- allow_origins=["*"],
13
- allow_methods=["*"],
14
- allow_headers=["*"],
15
- )
16
 
17
- N8N_WEBHOOK = "https://n8n-mission-control.fly.dev/webhook/mission-control"
18
-
19
- mission_history = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
 
 
21
  AGENTS = [
22
- {"key": "manager", "name": "Manager", "role": "Project Coordinator"},
23
- {"key": "developer", "name": "Developer", "role": "Senior Engineer"},
24
- {"key": "analyst", "name": "Analyst", "role": "Business Analyst"},
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  ]
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  @app.get("/", response_class=HTMLResponse)
29
  async def root():
30
- html = Path("templates/index.html").read_text()
31
- return HTMLResponse(content=html)
32
 
33
 
34
  @app.get("/api/agents")
35
  async def get_agents():
36
- return {"agents": AGENTS}
37
 
38
 
39
  @app.get("/api/history")
@@ -45,73 +214,54 @@ async def get_history():
45
  async def run_mission(request: Request):
46
  body = await request.json()
47
  task = body.get("task", "").strip()
48
-
49
  if not task:
50
  return JSONResponse({"error": "No task provided"}, status_code=400)
51
 
52
  started_at = datetime.now().isoformat()
53
 
54
- try:
55
- async with httpx.AsyncClient(timeout=300) as client:
56
- resp = await client.post(
57
- N8N_WEBHOOK,
58
- json={"task": task},
59
- headers={"Content-Type": "application/json"},
60
- )
61
-
62
- if resp.status_code != 200:
63
- return JSONResponse({"error": f"n8n returned {resp.status_code}"}, status_code=502)
64
-
65
- data = resp.json()
66
- if isinstance(data, list):
67
- data = data[0]
68
-
69
- results = {}
70
- for agent in AGENTS:
71
- k = agent["key"]
72
- raw = data.get(k, {})
73
-
74
- if isinstance(raw, dict):
75
- status = raw.get("status", "active")
76
- message = raw.get("message", "")
77
- model = raw.get("model", "") or raw.get("provider", "")
78
- else:
79
- status = "active"
80
- message = str(raw)
81
- model = ""
82
-
83
- if status in ("resting", "rate_limit"):
84
- status = "resting"
85
 
86
- results[k] = {
87
- "status": status,
88
- "message": message,
89
- "model": model,
 
90
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
91
 
92
- final_raw = data.get("final", {})
93
- final_msg = final_raw.get("message", "") if isinstance(final_raw, dict) else str(final_raw)
94
-
95
- entry = {
96
- "id": len(mission_history) + 1,
97
- "task": task,
98
- "started_at": started_at,
99
- "ended_at": datetime.now().isoformat(),
100
- "status": "completed",
101
- "results": results,
102
- "final": final_msg,
103
  }
104
- mission_history.append(entry)
105
-
106
- return JSONResponse({
107
- "success": True,
108
- "task": task,
109
- "results": results,
110
- "final": final_msg,
111
- "mission_id": entry["id"],
112
- })
113
-
114
- except httpx.TimeoutException:
115
- return JSONResponse({"error": "Request timed out"}, status_code=504)
116
- except Exception as e:
117
- return JSONResponse({"error": str(e)}, status_code=500)
 
1
+ import os
2
+ import asyncio
3
+ import httpx
4
  from fastapi import FastAPI, Request
5
  from fastapi.responses import HTMLResponse, JSONResponse
6
  from fastapi.middleware.cors import CORSMiddleware
 
7
  from datetime import datetime
8
  from pathlib import Path
9
 
10
  app = FastAPI()
11
+ app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
12
 
13
+ # ── ENV KEYS (set in HuggingFace Space Secrets) ──────────────────────────
14
+ GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")
15
+ OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "")
16
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
 
 
17
 
18
+ # ── PROVIDERS ─────────────────────────────────────────────────────────────
19
+ PROVIDERS = {
20
+ "gemini": {
21
+ "name": "Google Gemini",
22
+ "type": "gemini",
23
+ "key": GOOGLE_API_KEY,
24
+ },
25
+ "openrouter": {
26
+ "name": "OpenRouter",
27
+ "type": "openai_compat",
28
+ "key": OPENROUTER_API_KEY,
29
+ "base_url": "https://openrouter.ai/api/v1/chat/completions",
30
+ "headers": {
31
+ "HTTP-Referer": "https://huggingface.co/spaces/vfven/mission-control-ui",
32
+ "X-Title": "Mission Control AI",
33
+ },
34
+ },
35
+ "groq": {
36
+ "name": "Groq",
37
+ "type": "openai_compat",
38
+ "key": GROQ_API_KEY,
39
+ "base_url": "https://api.groq.com/openai/v1/chat/completions",
40
+ "headers": {},
41
+ },
42
+ }
43
 
44
+ # ── AGENTS ────────────────────────────────────────────────────────────────
45
  AGENTS = [
46
+ {
47
+ "key": "manager",
48
+ "name": "Manager",
49
+ "role": "Gerente de proyecto experto en coordinar equipos y planificar estrategias.",
50
+ "provider": "gemini",
51
+ "models": [
52
+ "gemini-2.5-flash-preview-04-17",
53
+ "gemini-2.0-flash",
54
+ "gemini-1.5-flash",
55
+ ],
56
+ },
57
+ {
58
+ "key": "developer",
59
+ "name": "Developer",
60
+ "role": "Programador senior especialista en crear aplicaciones y soluciones tΓ©cnicas.",
61
+ "provider": "openrouter",
62
+ "models": [
63
+ "qwen/qwen3-4b:free",
64
+ "meta-llama/llama-3.3-70b-instruct:free",
65
+ "mistralai/mistral-small-3.1-24b-instruct:free",
66
+ "google/gemma-3-12b-it:free",
67
+ "qwen/qwen-2.5-72b-instruct:free",
68
+ ],
69
+ },
70
+ {
71
+ "key": "analyst",
72
+ "name": "Analyst",
73
+ "role": "Analista de negocios experto en evaluar viabilidad, riesgos y oportunidades.",
74
+ "provider": "openrouter",
75
+ "models": [
76
+ "meta-llama/llama-3.3-70b-instruct:free",
77
+ "mistralai/mistral-small-3.1-24b-instruct:free",
78
+ "google/gemma-3-27b-it:free",
79
+ "qwen/qwen3-4b:free",
80
+ "google/gemma-3-12b-it:free",
81
+ ],
82
+ },
83
  ]
84
 
85
+ # Groq substitute β€” used when primary provider fails
86
+ SUBSTITUTE = {
87
+ "provider": "groq",
88
+ "models": [
89
+ "llama-3.3-70b-versatile",
90
+ "llama3-70b-8192",
91
+ "gemma2-9b-it",
92
+ "mixtral-8x7b-32768",
93
+ ],
94
+ }
95
+
96
+ mission_history = []
97
+
98
 
99
+ # ── CALL GEMINI ────────────────────────────────────────────────────────────
100
+ async def call_gemini(model: str, system: str, user: str, key: str) -> str:
101
+ url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={key}"
102
+ payload = {
103
+ "contents": [{"role": "user", "parts": [{"text": f"{system}\n\n{user}"}]}],
104
+ "generationConfig": {"maxOutputTokens": 1024, "temperature": 0.7},
105
+ }
106
+ async with httpx.AsyncClient(timeout=60) as client:
107
+ r = await client.post(url, json=payload)
108
+ r.raise_for_status()
109
+ data = r.json()
110
+ return data["candidates"][0]["content"]["parts"][0]["text"]
111
+
112
+
113
+ # ── CALL OPENAI-COMPAT (OpenRouter / Groq) ────────────────────────────────
114
+ async def call_openai_compat(base_url: str, model: str, system: str, user: str,
115
+ key: str, extra_headers: dict) -> str:
116
+ headers = {
117
+ "Authorization": f"Bearer {key}",
118
+ "Content-Type": "application/json",
119
+ **extra_headers,
120
+ }
121
+ payload = {
122
+ "model": model,
123
+ "messages": [
124
+ {"role": "system", "content": system},
125
+ {"role": "user", "content": user},
126
+ ],
127
+ "max_tokens": 1024,
128
+ "temperature": 0.7,
129
+ }
130
+ async with httpx.AsyncClient(timeout=60) as client:
131
+ r = await client.post(base_url, json=payload, headers=headers)
132
+ r.raise_for_status()
133
+ data = r.json()
134
+ return data["choices"][0]["message"]["content"]
135
+
136
+
137
+ # ── RUN ONE AGENT (with model fallback + groq substitute) ─────────────────
138
+ async def run_agent(agent: dict, task: str) -> dict:
139
+ prov_key = agent["provider"]
140
+ provider = PROVIDERS[prov_key]
141
+ models = agent["models"]
142
+ system = (
143
+ f"Eres {agent['name']}. {agent['role']} "
144
+ "Responde de forma concisa y profesional en espaΓ±ol. "
145
+ "MΓ‘ximo 3 pΓ‘rrafos."
146
+ )
147
+
148
+ # Try each model of primary provider
149
+ last_err = None
150
+ for model in models:
151
+ try:
152
+ if provider["type"] == "gemini":
153
+ text = await call_gemini(model, system, task, provider["key"])
154
+ else:
155
+ text = await call_openai_compat(
156
+ provider["base_url"], model, system, task,
157
+ provider["key"], provider.get("headers", {})
158
+ )
159
+ return {
160
+ "status": "active",
161
+ "message": text.strip(),
162
+ "model": model,
163
+ "provider": provider["name"],
164
+ }
165
+ except Exception as e:
166
+ last_err = str(e)
167
+ continue
168
+
169
+ # Primary failed β€” try Groq substitute
170
+ groq_prov = PROVIDERS["groq"]
171
+ if groq_prov["key"]:
172
+ for model in SUBSTITUTE["models"]:
173
+ try:
174
+ text = await call_openai_compat(
175
+ groq_prov["base_url"], model, system, task,
176
+ groq_prov["key"], {}
177
+ )
178
+ return {
179
+ "status": "active",
180
+ "message": text.strip(),
181
+ "model": f"{model} (via Groq substitute)",
182
+ "provider": "Groq",
183
+ }
184
+ except Exception as e:
185
+ last_err = str(e)
186
+ continue
187
+
188
+ # All failed
189
+ return {
190
+ "status": "resting",
191
+ "message": f"All providers unavailable. Last error: {last_err}",
192
+ "model": "",
193
+ "reason": "all_providers_failed",
194
+ }
195
+
196
+
197
+ # ── ROUTES ─────────────────────────────────────────────────────────────────
198
  @app.get("/", response_class=HTMLResponse)
199
  async def root():
200
+ return HTMLResponse(Path("templates/index.html").read_text())
 
201
 
202
 
203
  @app.get("/api/agents")
204
  async def get_agents():
205
+ return {"agents": [{"key": a["key"], "name": a["name"], "role": a["role"]} for a in AGENTS]}
206
 
207
 
208
  @app.get("/api/history")
 
214
  async def run_mission(request: Request):
215
  body = await request.json()
216
  task = body.get("task", "").strip()
 
217
  if not task:
218
  return JSONResponse({"error": "No task provided"}, status_code=400)
219
 
220
  started_at = datetime.now().isoformat()
221
 
222
+ # Run all agents in parallel
223
+ tasks = [run_agent(agent, task) for agent in AGENTS]
224
+ results_list = await asyncio.gather(*tasks, return_exceptions=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
+ results = {}
227
+ for agent, result in zip(AGENTS, results_list):
228
+ if isinstance(result, Exception):
229
+ results[agent["key"]] = {
230
+ "status": "resting", "message": str(result), "model": ""
231
  }
232
+ else:
233
+ results[agent["key"]] = result
234
+
235
+ # Simple final summary from manager response
236
+ manager_msg = results.get("manager", {}).get("message", "")
237
+ final = manager_msg[:200] + "..." if len(manager_msg) > 200 else manager_msg
238
+
239
+ entry = {
240
+ "id": len(mission_history) + 1,
241
+ "task": task,
242
+ "started_at": started_at,
243
+ "ended_at": datetime.now().isoformat(),
244
+ "results": results,
245
+ "final": final,
246
+ }
247
+ mission_history.append(entry)
248
+
249
+ return JSONResponse({
250
+ "success": True,
251
+ "task": task,
252
+ "results": results,
253
+ "final": final,
254
+ "mission_id": entry["id"],
255
+ })
256
+
257
 
258
+ @app.get("/api/health")
259
+ async def health():
260
+ return {
261
+ "status": "ok",
262
+ "providers": {
263
+ "gemini": "configured" if GOOGLE_API_KEY else "missing key",
264
+ "openrouter": "configured" if OPENROUTER_API_KEY else "missing key",
265
+ "groq": "configured" if GROQ_API_KEY else "missing key",
 
 
 
266
  }
267
+ }