diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..66979cd79a9c7aa32368a0e3a63a7feeb64b5635 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +# God Agent OS β€” Phase 1 backend (FastAPI + E2B + SSE) +FROM python:3.11-slim + +WORKDIR /app + +# System deps (curl for healthcheck, git for any sandbox-side workflows) +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl ca-certificates git \ + && rm -rf /var/lib/apt/lists/* + +# Python deps +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r /app/requirements.txt + +# App code +COPY . /app + +ENV PORT=7860 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app + +EXPOSE 7860 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \ + CMD curl -fsS http://localhost:7860/health || exit 1 + +CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1", "--log-level", "info", "--no-access-log"] diff --git a/Dockerfile.hf b/Dockerfile.hf new file mode 100644 index 0000000000000000000000000000000000000000..0f0c5f3bd7d67ec18436f36d168d55d085d229d4 --- /dev/null +++ b/Dockerfile.hf @@ -0,0 +1,25 @@ +# God Agent OS β€” Phase 1 backend for HuggingFace Spaces +FROM python:3.11-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl ca-certificates git \ + && rm -rf /var/lib/apt/lists/* + +COPY requirements.txt /app/requirements.txt +RUN pip install --no-cache-dir --upgrade pip \ + && pip install --no-cache-dir -r /app/requirements.txt + +COPY . /app + +ENV PORT=7860 \ + PYTHONUNBUFFERED=1 \ + PYTHONPATH=/app + +EXPOSE 7860 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ + CMD curl -fsS http://localhost:7860/health || exit 1 + +CMD ["python", "-m", "uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1", "--log-level", "info"] diff --git a/README.md b/README.md index 91d6af6eee793a1246f0907f73206225a0087f63..8a183dc0df9ab613736c4b451a946dc1f05480b7 100644 --- a/README.md +++ b/README.md @@ -1,77 +1,43 @@ --- -title: God Agent OS β€” Phase 1 +title: Autonomous Coding System emoji: πŸ€– -colorFrom: indigo -colorTo: purple +colorFrom: purple +colorTo: indigo sdk: docker -app_port: 7860 -app_file: backend/app.py -pinned: true +pinned: false license: mit -short_description: Stable autonomous agent backend (LLM + E2B + SSE) +app_port: 7860 --- -# πŸ€– God Agent OS β€” Phase 1 (Stability First) - -A **clean, minimal, stable** autonomous AI agent backend. -One pipeline, real execution, no fake Computer Use. - -## Architecture - -``` -Frontend (Vercel / Next.js) - ↓ HTTPS / SSE / WS -Backend (HF Space β€” FastAPI) - β”œβ”€ /api/v1/chat β†’ LLM-only streaming (SambaNova β†’ Gemini β†’ ...) - β”œβ”€ /api/v1/execute β†’ REAL E2B sandbox (live stdout/stderr) - β”œβ”€ /api/v1/agent β†’ Intent router (chat OR execute) - └─ /ws/{session_id} β†’ Same events mirrored over WebSocket -``` - -## Endpoints - -| Method | Path | Purpose | -|---|---|---| -| GET | `/health` | Health + provider availability + E2B status | -| POST | `/api/v1/chat` | LLM-only SSE chat (no sandbox) | -| POST | `/api/v1/execute` | Real E2B execution, streams stdout/stderr | -| POST | `/api/v1/agent` | Intent-routed: chat OR execute | -| POST | `/api/v1/orchestrate` | Alias of `/api/v1/agent` | -| POST | `/api/v1/kernel/orchestrate` | Legacy non-streaming alias | -| GET | `/api/v1/sandbox/{session_id}` | Sandbox info | -| DEL | `/api/v1/sandbox/{session_id}` | Kill sandbox | -| WS | `/ws/{session_id}` | WebSocket events | - -## Required HF Space Secrets - -| Variable | Required? | Purpose | -|---|---|---| -| `E2B_API_KEY` | **YES** | Real sandbox runtime | -| `SAMBANOVA_KEY` | one of these | Llama 3.3 70B (recommended, fastest) | -| `GEMINI_KEY` | one of these | Gemini 2.0 Flash | -| `GITHUB_KEY` | one of these | GitHub Models (GPT-4o-mini) | -| `OPENAI_API_KEY`| one of these | OpenAI | -| `GROQ_API_KEY` | one of these | Groq (Llama) | -| `ANTHROPIC_API_KEY` | one of these | Claude | - -## Quick Proof Test - -```bash -curl -sN -X POST https://pyae1994-autonomous-coding-system.hf.space/api/v1/execute \ - -H 'Content-Type: application/json' \ - -d '{"language":"python","code":"import time,pathlib; p=pathlib.Path(\"/home/user/proof.txt\"); ts=int(time.time()); p.write_text(str(ts)); print(\"TS:\",ts,\"READBACK:\",p.read_text())","session_id":"smoke","stream":true}' -``` - -Expected: live SSE stream of `sandbox_ready` β†’ `stdout` β†’ `result` events. - -## Phase Roadmap - -- **Phase 1 (current)** β€” chat + execute + SSE/WS, stable. -- **Phase 2** β€” browser automation, retry/self-repair loops. -- **Phase 3** β€” workflows, memory, multi-agent. - -## Powered by - -OpenHands ideas Β· E2B Β· SambaNova/Gemini Β· Vercel Β· HuggingFace Spaces - -Built by **Pyae Sone**. +# GOD AGENT OS v11 β€” Backend API +**16-Agent Autonomous Engineering OS | God Mode** +*Powered by Pyae Sone* + +## Runtime Overview +- 16 autonomous agents (Chat, Planner, Coder, Debug, Test, File, Git, Browser, Vision, Sandbox, Deploy, Connector, Memory, Workflow, UI, Reasoning) +- 22 logical worker spaces (all running in this single backend) +- Multi-provider AI Router: Gemini β†’ SambaNova β†’ GitHub Models β†’ Groq β†’ OpenAI +- WebSocket + REST + SSE streaming + +## Key Endpoints +- `GET /` β€” Root info +- `GET /health` β€” Health check +- `GET /api/docs` β€” Swagger UI +- `GET /api/v1/system/status` β€” Full system status +- `GET /api/v1/agents` β€” List all 16 agents +- `GET /api/v1/spaces` β€” All 22 spaces status +- `POST /api/v1/chat` β€” Chat with streaming +- `POST /api/v1/orchestrate` β€” God Mode orchestration +- `WS /ws/{session_id}` β€” Real-time WebSocket +- `WS /ws/computer-use/{session_id}` β€” Computer-use event stream + +## Environment Variables +Set these in HF Space secrets: +- `GEMINI_KEY` β€” Google Gemini API key(s), comma-separated +- `SAMBANOVA_KEY` β€” SambaNova API key(s), comma-separated +- `GITHUB_KEY` β€” GitHub Models API key(s), comma-separated +- `GROQ_API_KEY` β€” Groq API key (optional fallback) +- `OPENAI_API_KEY` β€” OpenAI API key (optional fallback) +- `GITHUB_TOKEN` β€” GitHub token for Git operations +- `HF_TOKEN` β€” HuggingFace token +- `VERCEL_TOKEN` β€” Vercel deploy token diff --git a/_legacy/main.py b/_legacy/main.py new file mode 100644 index 0000000000000000000000000000000000000000..8e9ed971cf9e9ce2b7de1262b4c1738b2ef6192e --- /dev/null +++ b/_legacy/main.py @@ -0,0 +1,310 @@ +""" +πŸš€ GOD AGENT OS β€” Autonomous AI Operating System v8 +Gemini + Sambanova + GitHub Models β€” Primary Provider Rotation +Task-aware routing: researchβ†’Gemini, codeβ†’Sambanova, planβ†’GitHub +""" + +# ─── Inject bundled API keys (env vars take precedence) ─────────────────────── +import os as _os + +def _inject_key(env_var: str, value: str): + """Set env var only if not already configured.""" + if not _os.environ.get(env_var): + _os.environ[env_var] = value + +# Keys are loaded from HF Space Secrets / Docker env vars. +# Set GEMINI_KEY, SAMBANOVA_KEY, GITHUB_KEY as comma-separated lists. +# Fallback: read from .env.keys file if present (not committed to git). +_keys_file = _os.path.join(_os.path.dirname(__file__), ".env.keys") +if _os.path.exists(_keys_file): + with open(_keys_file) as _f: + for _line in _f: + _line = _line.strip() + if _line and "=" in _line and not _line.startswith("#"): + _k, _v = _line.split("=", 1) + _inject_key(_k.strip(), _v.strip()) +# ───────────────────────────────────────────────────────────────────────────── + + +import asyncio +import json +import logging +import os +import time +import uuid +from contextlib import asynccontextmanager +from typing import Optional + +import structlog +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware +from fastapi.responses import JSONResponse +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.util import get_remote_address +from slowapi.errors import RateLimitExceeded + +from api.routes import tasks, chat, memory, github, health +from api.routes import connectors, agents as agents_router +from api.websocket_manager import WebSocketManager +from core.task_engine import TaskEngine +from memory.db import init_db + +# ─── God Mode Agents ─────────────────────────────────────────────────────────── +from ai_router.router import AIRouter +from ai_router.router_v8 import GodModeRouter, get_router as get_god_router +from agents.orchestrator import GodAgentOrchestrator +from agents.chat_agent import ChatAgent +from agents.planner_agent import PlannerAgent +from agents.coding_agent import CodingAgent +from agents.debug_agent import DebugAgent +from agents.memory_agent import MemoryAgent +from agents.connector_agent import ConnectorAgent +from agents.deploy_agent import DeployAgent +from agents.workflow_agent import WorkflowAgent +from agents.sandbox_agent import SandboxAgent +from agents.ui_agent import UIAgent +from connectors.manager import ConnectorManager + +# ─── Structured Logging ──────────────────────────────────────────────────────── +structlog.configure( + processors=[ + structlog.processors.TimeStamper(fmt="iso"), + structlog.stdlib.add_log_level, + structlog.processors.StackInfoRenderer(), + structlog.dev.ConsoleRenderer(), + ] +) +log = structlog.get_logger() + +# ─── Rate Limiter ────────────────────────────────────────────────────────────── +limiter = Limiter(key_func=get_remote_address) + +# ─── Global Managers ────────────────────────────────────────────────────────── +ws_manager = WebSocketManager() +task_engine = TaskEngine(ws_manager) +ai_router = AIRouter(ws_manager) +god_router = get_god_router(ws_manager) # v8 primary router +connector_manager = ConnectorManager() + +# ─── Build God Agent Ecosystem ──────────────────────────────────────────────── +def build_orchestrator() -> GodAgentOrchestrator: + orchestrator = GodAgentOrchestrator(ws_manager=ws_manager, ai_router=ai_router) + + # Register all specialized agents + orchestrator.register_agent("chat", ChatAgent(ws_manager, ai_router)) + orchestrator.register_agent("planner", PlannerAgent(ws_manager, ai_router)) + orchestrator.register_agent("coding", CodingAgent(ws_manager, ai_router)) + orchestrator.register_agent("debug", DebugAgent(ws_manager, ai_router)) + orchestrator.register_agent("memory", MemoryAgent(ws_manager, ai_router)) + orchestrator.register_agent("connector", ConnectorAgent(ws_manager, ai_router)) + orchestrator.register_agent("deploy", DeployAgent(ws_manager, ai_router)) + orchestrator.register_agent("workflow", WorkflowAgent(ws_manager, ai_router)) + orchestrator.register_agent("sandbox", SandboxAgent(ws_manager, ai_router)) + orchestrator.register_agent("ui", UIAgent(ws_manager, ai_router)) + + log.info("πŸ€– God Agent Ecosystem initialized", agents=10) + return orchestrator + +orchestrator = build_orchestrator() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Startup + Shutdown lifecycle.""" + log.info("πŸš€ Starting GOD MODE+ AI Operating System...") + await init_db() + await task_engine.start() + asyncio.create_task(ws_manager.heartbeat_loop()) + log.info("βœ… GOD MODE+ Platform ready β€” All agents online") + log.info("πŸ€– Agents: Chat, Planner, Coding, Debug, Memory, Connector, Deploy, Workflow, Sandbox, UI") + log.info("🌐 AI Router v8: Gemini β†’ Sambanova β†’ GitHub Models (task-aware rotation)") + yield + log.info("πŸ›‘ Shutting down...") + await task_engine.stop() + log.info("βœ… Shutdown complete") + + +# ─── FastAPI App ─────────────────────────────────────────────────────────────── +app = FastAPI( + title="πŸ€– GOD MODE+ AI Operating System", + description="Devin + Manus + Genspark Autonomous AI Engineering Platform", + version="3.0.0", + lifespan=lifespan, + docs_url="/api/docs", + redoc_url="/api/redoc", +) + +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + +# ─── Share state ─────────────────────────────────────────────────────────────── +app.state.ws_manager = ws_manager +app.state.task_engine = task_engine +app.state.ai_router = ai_router +app.state.orchestrator = orchestrator +app.state.connector_manager = connector_manager + +# ─── Middleware ──────────────────────────────────────────────────────────────── +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +app.add_middleware(GZipMiddleware, minimum_size=1000) + + +@app.middleware("http") +async def log_requests(request: Request, call_next): + start = time.time() + response = await call_next(request) + duration = round((time.time() - start) * 1000, 2) + log.info("HTTP", method=request.method, path=request.url.path, status=response.status_code, ms=duration) + return response + + +# ─── REST API Routers ────────────────────────────────────────────────────────── +app.include_router(health.router, prefix="/api/v1", tags=["health"]) +app.include_router(tasks.router, prefix="/api/v1/tasks", tags=["tasks"]) +app.include_router(chat.router, prefix="/api/v1", tags=["chat"]) +app.include_router(memory.router, prefix="/api/v1/memory", tags=["memory"]) +app.include_router(github.router, prefix="/api/v1/github", tags=["github"]) +app.include_router(connectors.router, prefix="/api/v1/connectors", tags=["connectors"]) +app.include_router(agents_router.router, prefix="/api/v1/agents", tags=["agents"]) + + +# ─── WebSocket Endpoints ─────────────────────────────────────────────────────── +@app.websocket("/ws/tasks/{task_id}") +async def ws_task(websocket: WebSocket, task_id: str): + await ws_manager.connect(websocket, room=f"task:{task_id}") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room=f"task:{task_id}") + + +@app.websocket("/ws/logs") +async def ws_logs(websocket: WebSocket): + await ws_manager.connect(websocket, room="logs") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room="logs") + + +@app.websocket("/ws/chat/{session_id}") +async def ws_chat(websocket: WebSocket, session_id: str): + await ws_manager.connect(websocket, room=f"chat:{session_id}") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + elif msg.get("type") == "chat_message": + # Route through God Agent Orchestrator + asyncio.create_task( + orchestrator.orchestrate( + user_message=msg.get("content", ""), + session_id=session_id, + context=msg.get("context", {}), + ) + ) + elif msg.get("type") == "task_message": + # Create autonomous task via task engine + from core.models import TaskCreateRequest + req = TaskCreateRequest( + goal=msg.get("content", ""), + session_id=session_id, + ) + asyncio.create_task(task_engine.submit(req)) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room=f"chat:{session_id}") + + +@app.websocket("/ws/agent/status") +async def ws_agent_status(websocket: WebSocket): + await ws_manager.connect(websocket, room="agent_status") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + elif msg.get("type") == "get_status": + await websocket.send_json({ + "type": "agent_status", + "data": orchestrator.get_status(), + }) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room="agent_status") + + +@app.websocket("/ws/sandbox/{session_id}") +async def ws_sandbox(websocket: WebSocket, session_id: str): + """Live sandbox terminal stream.""" + await ws_manager.connect(websocket, room=f"sandbox:{session_id}") + sandbox = orchestrator.get_agent("sandbox") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + elif msg.get("type") == "execute" and sandbox: + cmd = msg.get("command", "") + result = await sandbox.execute(cmd, session_id=session_id) + await websocket.send_json({ + "type": "terminal_output", + "command": cmd, + "output": result, + "timestamp": time.time(), + }) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room=f"sandbox:{session_id}") + + +# ─── Root ────────────────────────────────────────────────────────────────────── +@app.get("/") +async def root(): + cs = connector_manager.get_summary() + return { + "name": "πŸ€– GOD MODE+ AI Operating System", + "version": "3.0.0", + "status": "operational", + "mode": "god_mode_plus", + "agents": orchestrator.get_status()["agents"], + "connectors": { + "connected": cs["connected"], + "total": cs["total"], + "ai_ready": cs["ai_ready"], + }, + "docs": "/api/docs", + "websockets": [ + "/ws/tasks/{task_id}", + "/ws/logs", + "/ws/chat/{session_id}", + "/ws/agent/status", + "/ws/sandbox/{session_id}", + ], + "phases_complete": [ + "Phase 1: God Agent Orchestrator", + "Phase 2: Sandbox Agent", + "Phase 3: Connector System", + "Phase 4: Autonomous Coding Engine", + "Phase 5: Memory System", + "Phase 6: Real-time Streaming", + "Phase 7: Workflow Factor OS", + "Phase 9: Multi-Model AI Router", + ], + } diff --git a/_legacy/main_v11.py b/_legacy/main_v11.py new file mode 100644 index 0000000000000000000000000000000000000000..2a5b61b76a2b1b582ed188220189f920df52bf25 --- /dev/null +++ b/_legacy/main_v11.py @@ -0,0 +1,442 @@ +""" +GOD AGENT OS v11 β€” Fixed & Production Ready +Bugs fixed: +- TaskEngine(ws_manager) only β€” removed ai_router arg +- init_db() β€” no arguments +- task_engine.start() β€” not run() +- ws_manager.emit_chat() β€” not emit_task() +- ConnectorManager() β€” sync init, no await needed +""" + +import asyncio +import json +import os +import time +import uuid +from contextlib import asynccontextmanager +from typing import Dict, List + +import structlog +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware +from fastapi.responses import StreamingResponse +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.util import get_remote_address +from slowapi.errors import RateLimitExceeded + +from api.websocket_manager import WebSocketManager +from core.task_engine import TaskEngine +from memory.db import init_db +from ai_router.router_v10 import AIRouterV10 +from agents.orchestrator_v7 import GodAgentOrchestratorV7 +from agents.chat_agent import ChatAgent +from agents.planner_agent import PlannerAgent +from agents.coding_agent import CodingAgent +from agents.debug_agent import DebugAgent +from agents.memory_agent import MemoryAgent +from agents.connector_agent import ConnectorAgent +from agents.deploy_agent import DeployAgent +from agents.workflow_agent import WorkflowAgent +from agents.sandbox_agent import SandboxAgent +from agents.ui_agent import UIAgent +from agents.reasoning_agent import ReasoningAgent +from agents.browser_agent import BrowserAgent +from agents.file_agent import FileAgent +from agents.git_agent import GitAgent +from agents.test_agent import TestAgent +from agents.vision_agent import VisionAgent +from connectors.manager import ConnectorManager +from api.routes import tasks, chat, memory, health, connectors, agents as agents_router +from api.routes import github + +structlog.configure( + processors=[ + structlog.processors.TimeStamper(fmt="iso"), + structlog.stdlib.add_log_level, + structlog.processors.StackInfoRenderer(), + structlog.dev.ConsoleRenderer(), + ] +) +log = structlog.get_logger() + +limiter = Limiter(key_func=get_remote_address) + +ws_manager: WebSocketManager = None +task_engine: TaskEngine = None +ai_router: AIRouterV10 = None +orchestrator: GodAgentOrchestratorV7 = None +connector_manager: ConnectorManager = None + +computer_use_sessions: Dict[str, List[Dict]] = {} + + +def add_computer_use_step(session_id: str, step_type: str, data: Dict): + if session_id not in computer_use_sessions: + computer_use_sessions[session_id] = [] + computer_use_sessions[session_id].append({ + "id": uuid.uuid4().hex[:8], + "type": step_type, + "data": data, + "timestamp": time.time(), + "status": "running", + }) + computer_use_sessions[session_id] = computer_use_sessions[session_id][-100:] + + +@asynccontextmanager +async def lifespan(app: FastAPI): + global ws_manager, task_engine, ai_router, orchestrator, connector_manager + + log.info("GOD AGENT OS v11 starting...") + + # FIX: init_db() takes no arguments + await init_db() + + ai_router = AIRouterV10() + ws_manager = WebSocketManager() + + # FIX: TaskEngine only takes ws_manager + task_engine = TaskEngine(ws_manager) + + orchestrator = GodAgentOrchestratorV7(ws_manager, ai_router) + agents_map = { + "chat": ChatAgent(ws_manager, ai_router), + "planner": PlannerAgent(ws_manager, ai_router), + "coding": CodingAgent(ws_manager, ai_router), + "debug": DebugAgent(ws_manager, ai_router), + "memory": MemoryAgent(ws_manager, ai_router), + "connector": ConnectorAgent(ws_manager, ai_router), + "deploy": DeployAgent(ws_manager, ai_router), + "workflow": WorkflowAgent(ws_manager, ai_router), + "sandbox": SandboxAgent(ws_manager, ai_router), + "ui": UIAgent(ws_manager, ai_router), + "reasoning": ReasoningAgent(ws_manager, ai_router), + "browser": BrowserAgent(ws_manager, ai_router), + "file": FileAgent(ws_manager, ai_router), + "git": GitAgent(ws_manager, ai_router), + "test": TestAgent(ws_manager, ai_router), + "vision": VisionAgent(ws_manager, ai_router), + } + for name, agent in agents_map.items(): + orchestrator.register_agent(name, agent) + + # FIX: ConnectorManager is sync β€” no await initialize() + connector_manager = ConnectorManager() + + app.state.ws_manager = ws_manager + app.state.task_engine = task_engine + app.state.ai_router = ai_router + app.state.orchestrator = orchestrator + app.state.connector_manager = connector_manager + + # FIX: use start() not run() + asyncio.create_task(task_engine.start()) + + log.info("GOD AGENT OS v11 ready!", agents=len(agents_map)) + yield + + log.info("Shutting down...") + await task_engine.stop() + + +app = FastAPI( + title="GOD AGENT OS v11", + description="Autonomous Engineering OS", + version="11.0.0", + docs_url="/api/docs", + redoc_url="/api/redoc", + lifespan=lifespan, +) + +app.state_limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) +app.add_middleware(GZipMiddleware, minimum_size=1000) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(health.router, prefix="/api/v1", tags=["health"]) +app.include_router(chat.router, prefix="/api/v1", tags=["chat"]) +app.include_router(tasks.router, prefix="/api/v1/tasks", tags=["tasks"]) +app.include_router(memory.router, prefix="/api/v1/memory", tags=["memory"]) +app.include_router(connectors.router, prefix="/api/v1/connectors", tags=["connectors"]) +app.include_router(agents_router.router, prefix="/api/v1/agents", tags=["agents"]) +app.include_router(github.router, prefix="/api/v1/github", tags=["github"]) + + +@app.websocket("/ws/{session_id}") +async def websocket_endpoint(websocket: WebSocket, session_id: str): + # FIX: connect takes (websocket, room) β€” use chat: prefix + await ws_manager.connect(websocket, f"chat:{session_id}") + try: + while True: + data = await websocket.receive_json() + event_type = data.get("type", "") + + if event_type == "ping": + await websocket.send_json({"type": "pong", "ts": time.time()}) + + elif event_type == "message": + message = data.get("message", "") + task_id = uuid.uuid4().hex[:12] + # FIX: use emit_chat() not emit_task() + await ws_manager.emit_chat(session_id, "task_start", { + "task_id": task_id, + "message": message[:100], + }) + asyncio.create_task(_run_ws_task(message, task_id, session_id)) + + elif event_type == "stop": + task_id = data.get("task_id", "") + if task_id: + await task_engine.cancel(task_id) + + except WebSocketDisconnect: + ws_manager.disconnect(websocket, f"chat:{session_id}") + + +async def _run_ws_task(message: str, task_id: str, session_id: str): + try: + result = await orchestrator.orchestrate( + user_message=message, + task_id=task_id, + session_id=session_id, + ) + await ws_manager.emit_chat(session_id, "task_complete", { + "task_id": task_id, + "result": result[:2000] if result else "", + }) + except Exception as e: + await ws_manager.emit_chat(session_id, "task_error", { + "task_id": task_id, + "error": str(e), + }) + + +@app.get("/api/v1/computer-use/{session_id}") +async def get_computer_use_steps(session_id: str): + steps = computer_use_sessions.get(session_id, []) + return {"session_id": session_id, "steps": steps, "count": len(steps)} + + +@app.websocket("/ws/computer-use/{session_id}") +async def computer_use_ws(websocket: WebSocket, session_id: str): + await websocket.accept() + try: + last_idx = 0 + while True: + steps = computer_use_sessions.get(session_id, []) + if len(steps) > last_idx: + for step in steps[last_idx:]: + await websocket.send_json({"type": "computer_use_step", "step": step}) + last_idx = len(steps) + await asyncio.sleep(0.5) + except WebSocketDisconnect: + pass + + +@app.post("/api/v1/orchestrate") +async def orchestrate_goal(request: Request): + body = await request.json() + message = body.get("message", "") + session_id = body.get("session_id", uuid.uuid4().hex[:12]) + + if not message: + raise HTTPException(status_code=400, detail="Message required") + + task_id = uuid.uuid4().hex[:12] + add_computer_use_step(session_id, "thinking", { + "message": f"Processing: {message[:100]}", + "task_id": task_id, + }) + + if body.get("stream", False): + async def stream_gen(): + try: + result = await orchestrator.orchestrate( + user_message=message, task_id=task_id, session_id=session_id, + ) + add_computer_use_step(session_id, "complete", {"result": result[:200] if result else ""}) + yield f"data: {json.dumps({'type': 'complete', 'result': result, 'task_id': task_id, 'session_id': session_id})}\n\n" + except Exception as e: + yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n" + + return StreamingResponse( + stream_gen(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + ) + + try: + result = await orchestrator.orchestrate(user_message=message, task_id=task_id, session_id=session_id) + add_computer_use_step(session_id, "complete", {"result": result[:200] if result else ""}) + return {"task_id": task_id, "session_id": session_id, "result": result, "status": "complete"} + except Exception as e: + log.error("Orchestration error", error=str(e)) + raise HTTPException(status_code=500, detail=str(e)) + + +@app.post("/api/v1/agents/{agent_name}/run") +async def run_agent(agent_name: str, request: Request): + body = await request.json() + task = body.get("task", "") + session_id = body.get("session_id", uuid.uuid4().hex[:12]) + task_id = uuid.uuid4().hex[:12] + + agent = orchestrator.get_agent(agent_name) + if not agent: + raise HTTPException(status_code=404, detail=f"Agent '{agent_name}' not found") + + try: + result = await agent.run( + task=task, + context=body.get("context", {}), + task_id=task_id, + session_id=session_id, + ) + return {"agent": agent_name, "task_id": task_id, "result": result, "status": "complete"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@app.get("/api/v1/agents") +async def list_agents(): + names = ["chat", "planner", "coding", "debug", "memory", "connector", + "deploy", "workflow", "sandbox", "ui", "reasoning", + "browser", "file", "git", "test", "vision"] + agents_list = [] + for name in names: + agent = orchestrator.get_agent(name) + agents_list.append({ + "name": name, + "available": agent is not None, + "class": type(agent).__name__ if agent else None, + }) + return {"agents": agents_list, "total": len(agents_list)} + + +SPACE_DEFS = [ + {"id": "god-core", "name": "God Core Space", "role": "orchestration", "agent": "orchestrator", "icon": "🧠"}, + {"id": "coding", "name": "Coding Worker", "role": "code_generation", "agent": "coding", "icon": "⚑"}, + {"id": "sandbox", "name": "Sandbox Worker", "role": "execution", "agent": "sandbox", "icon": "πŸ”§"}, + {"id": "terminal", "name": "Terminal Worker", "role": "execution", "agent": "sandbox", "icon": "πŸ–₯️"}, + {"id": "filesystem", "name": "FileSystem Worker", "role": "files", "agent": "file", "icon": "πŸ“"}, + {"id": "browser", "name": "Browser Worker", "role": "research", "agent": "browser", "icon": "🌐"}, + {"id": "vision", "name": "Vision Worker", "role": "ui_gen", "agent": "vision", "icon": "πŸ‘οΈ"}, + {"id": "ui", "name": "UI Worker", "role": "ui", "agent": "ui", "icon": "🎨"}, + {"id": "debug", "name": "Debug Worker", "role": "debugging", "agent": "debug", "icon": "πŸ›"}, + {"id": "test", "name": "Test Worker", "role": "testing", "agent": "test", "icon": "πŸ§ͺ"}, + {"id": "verification", "name": "Verification Worker", "role": "qa", "agent": "test", "icon": "βœ…"}, + {"id": "git", "name": "Git Worker", "role": "git", "agent": "git", "icon": "πŸ”€"}, + {"id": "deploy", "name": "Deploy Worker", "role": "deployment", "agent": "deploy", "icon": "πŸš€"}, + {"id": "connector", "name": "Connector Worker", "role": "integration", "agent": "connector", "icon": "πŸ”Œ"}, + {"id": "memory", "name": "Memory Worker", "role": "memory", "agent": "memory", "icon": "πŸ’Ύ"}, + {"id": "knowledge", "name": "Knowledge Worker", "role": "knowledge", "agent": "memory", "icon": "πŸ“š"}, + {"id": "workflow", "name": "Workflow Worker", "role": "automation", "agent": "workflow", "icon": "βš™οΈ"}, + {"id": "eventbus", "name": "Event Bus", "role": "events", "agent": None, "icon": "πŸ“‘"}, + {"id": "model-router", "name": "Model Router", "role": "ai_routing", "agent": None, "icon": "πŸ€–"}, + {"id": "observability", "name": "Observability", "role": "monitoring", "agent": None, "icon": "πŸ“Š"}, + {"id": "session-runtime", "name": "Session Runtime", "role": "sessions", "agent": None, "icon": "⏱️"}, + {"id": "auth-gateway", "name": "Auth Gateway", "role": "auth", "agent": None, "icon": "πŸ”"}, +] + + +@app.get("/api/v1/spaces") +async def get_spaces(): + spaces_status = [] + for space in SPACE_DEFS: + agent_name = space.get("agent") + agent = orchestrator.get_agent(agent_name) if agent_name else None + spaces_status.append({ + **space, + "status": "active" if (agent is not None or agent_name is None) else "inactive", + "online": True, + "backend": "god-agent-os-v11", + "tasks_completed": 0, + }) + return { + "spaces": spaces_status, + "total": len(spaces_status), + "active": len([s for s in spaces_status if s["status"] == "active"]), + "backend_url": os.environ.get("SPACE_URL", "https://pyae1994-autonomous-coding-system.hf.space"), + } + + +@app.get("/health") +@app.get("/api/v1/health") +async def health_check(): + stats = ai_router.get_stats() if ai_router else {} + active_providers = [name for name, s in stats.items() if s.get("available")] + return { + "status": "healthy", + "version": "11.0.0", + "timestamp": time.time(), + "agents": 16, + "spaces": 22, + "ai_providers": active_providers, + "mode": "god_mode", + } + + +@app.get("/api/v1/ai/stats") +async def get_ai_stats(): + return {"stats": ai_router.get_stats() if ai_router else {}} + + +@app.get("/api/v1/ai/pool-status") +async def get_pool_status(): + return {"pools": ai_router.get_pool_status() if ai_router else {}} + + +@app.get("/api/v1/system/status") +async def system_status(): + ai_stats = ai_router.get_stats() if ai_router else {} + cs = connector_manager.get_summary() if connector_manager else {"connected": 0, "total": 0} + return { + "system": "god_agent_os_v11", + "status": "operational", + "timestamp": time.time(), + "ai_router": { + "providers": ai_stats, + "active": len([v for v in ai_stats.values() if v.get("available")]), + }, + "agents": { + "total": 16, + "online": 16, + }, + "spaces": {"total": 22, "all_in_backend": True}, + "connectors": cs, + "features": { + "god_mode": True, + "computer_use": True, + "streaming": True, + "websocket": True, + "multi_agent": True, + "burmese_language": True, + }, + } + + +@app.get("/") +async def root(): + return { + "name": "GOD AGENT OS v11", + "version": "11.0.0", + "status": "operational", + "mode": "GOD_MODE", + "docs": "/api/docs", + "health": "/health", + "agents": 16, + "spaces": 22, + } + + +if __name__ == "__main__": + import uvicorn + port = int(os.environ.get("PORT", 7860)) + uvicorn.run("main_v11:app", host="0.0.0.0", port=port, reload=False, workers=1) diff --git a/_legacy/main_v7.py b/_legacy/main_v7.py new file mode 100644 index 0000000000000000000000000000000000000000..6c4f6d7cc7b6d1069db608f700b7004dac3cbbb0 --- /dev/null +++ b/_legacy/main_v7.py @@ -0,0 +1,363 @@ +""" +πŸš€ GOD AGENT OS v7 β€” Autonomous Engineering Operating System +Manus + Genspark + Devin (OneHand) Combined +Version: 7.0.0 +""" + +import asyncio +import json +import logging +import os +import time +import uuid +from contextlib import asynccontextmanager +from typing import Optional + +import structlog +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware +from fastapi.responses import JSONResponse +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.util import get_remote_address +from slowapi.errors import RateLimitExceeded + +from api.routes import tasks, chat, memory, github, health +from api.routes import connectors, agents as agents_router +from api.websocket_manager import WebSocketManager +from core.task_engine import TaskEngine +from memory.db import init_db + +# ─── v7 God Agent Ecosystem ──────────────────────────────────────────────────── +from ai_router.router import AIRouter +from agents.orchestrator_v7 import GodAgentOrchestratorV7 +from agents.chat_agent import ChatAgent +from agents.planner_agent import PlannerAgent +from agents.coding_agent import CodingAgent +from agents.debug_agent import DebugAgent +from agents.memory_agent import MemoryAgent +from agents.connector_agent import ConnectorAgent +from agents.deploy_agent import DeployAgent +from agents.workflow_agent import WorkflowAgent +from agents.sandbox_agent import SandboxAgent +from agents.ui_agent import UIAgent +from agents.reasoning_agent import ReasoningAgent +# v7 new agents +from agents.browser_agent import BrowserAgent +from agents.file_agent import FileAgent +from agents.git_agent import GitAgent +from agents.test_agent import TestAgent +from agents.vision_agent import VisionAgent +from connectors.manager import ConnectorManager + +# ─── Structured Logging ──────────────────────────────────────────────────────── +structlog.configure( + processors=[ + structlog.processors.TimeStamper(fmt="iso"), + structlog.stdlib.add_log_level, + structlog.processors.StackInfoRenderer(), + structlog.dev.ConsoleRenderer(), + ] +) +log = structlog.get_logger() + +# ─── Rate Limiter ────────────────────────────────────────────────────────────── +limiter = Limiter(key_func=get_remote_address) + +# ─── Global Managers ────────────────────────────────────────────────────────── +ws_manager = WebSocketManager() +task_engine = TaskEngine(ws_manager) +ai_router = AIRouter(ws_manager) +connector_manager = ConnectorManager() + + +# ─── Build v7 God Agent Ecosystem ───────────────────────────────────────────── +def build_orchestrator_v7() -> GodAgentOrchestratorV7: + orchestrator = GodAgentOrchestratorV7(ws_manager=ws_manager, ai_router=ai_router) + + # ── Core agents (v3 retained) ────────────────────────────────────────── + orchestrator.register_agent("chat", ChatAgent(ws_manager, ai_router)) + orchestrator.register_agent("planner", PlannerAgent(ws_manager, ai_router)) + orchestrator.register_agent("coding", CodingAgent(ws_manager, ai_router)) + orchestrator.register_agent("debug", DebugAgent(ws_manager, ai_router)) + orchestrator.register_agent("memory", MemoryAgent(ws_manager, ai_router)) + orchestrator.register_agent("connector", ConnectorAgent(ws_manager, ai_router)) + orchestrator.register_agent("deploy", DeployAgent(ws_manager, ai_router)) + orchestrator.register_agent("workflow", WorkflowAgent(ws_manager, ai_router)) + orchestrator.register_agent("sandbox", SandboxAgent(ws_manager, ai_router)) + orchestrator.register_agent("ui", UIAgent(ws_manager, ai_router)) + orchestrator.register_agent("reasoning", ReasoningAgent(ws_manager, ai_router)) + + # ── v7 NEW agents ────────────────────────────────────────────────────── + orchestrator.register_agent("browser", BrowserAgent(ws_manager, ai_router)) + orchestrator.register_agent("file", FileAgent(ws_manager, ai_router)) + orchestrator.register_agent("git", GitAgent(ws_manager, ai_router)) + orchestrator.register_agent("test", TestAgent(ws_manager, ai_router)) + orchestrator.register_agent("vision", VisionAgent(ws_manager, ai_router)) + + log.info("πŸ€– GOD AGENT v7 Ecosystem initialized", agents=16) + return orchestrator + + +orchestrator = build_orchestrator_v7() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + log.info("πŸš€ Starting GOD AGENT OS v7 β€” Autonomous Engineering Platform...") + await init_db() + await task_engine.start() + asyncio.create_task(ws_manager.heartbeat_loop()) + log.info("βœ… GOD AGENT v7 β€” All 16 agents online") + log.info("πŸ€– Core: Chat, Planner, Coding, Debug, Memory, Connector, Deploy, Workflow, Sandbox, UI, Reasoning") + log.info("⚑ v7 New: Browser, File, Git, Test, Vision") + log.info("🌐 AI Router: OpenAI β†’ Groq β†’ Cerebras β†’ OpenRouter β†’ Anthropic (auto-failover)") + yield + log.info("πŸ›‘ Shutting down GOD AGENT v7...") + await task_engine.stop() + log.info("βœ… Shutdown complete") + + +# ─── FastAPI App ─────────────────────────────────────────────────────────────── +app = FastAPI( + title="πŸ€– GOD AGENT OS v7", + description="Autonomous Engineering OS β€” Manus + Genspark + Devin (OneHand) Combined", + version="7.0.0", + lifespan=lifespan, + docs_url="/api/docs", + redoc_url="/api/redoc", +) + +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + +# ─── Share state ─────────────────────────────────────────────────────────────── +app.state.ws_manager = ws_manager +app.state.task_engine = task_engine +app.state.ai_router = ai_router +app.state.orchestrator = orchestrator +app.state.connector_manager = connector_manager + +# ─── Middleware ──────────────────────────────────────────────────────────────── +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) +app.add_middleware(GZipMiddleware, minimum_size=1000) + + +@app.middleware("http") +async def log_requests(request: Request, call_next): + start = time.time() + response = await call_next(request) + duration = round((time.time() - start) * 1000, 2) + log.info("HTTP", method=request.method, path=request.url.path, status=response.status_code, ms=duration) + return response + + +# ─── REST API Routers ────────────────────────────────────────────────────────── +app.include_router(health.router, prefix="/api/v1", tags=["health"]) +app.include_router(tasks.router, prefix="/api/v1/tasks", tags=["tasks"]) +app.include_router(chat.router, prefix="/api/v1", tags=["chat"]) +app.include_router(memory.router, prefix="/api/v1/memory", tags=["memory"]) +app.include_router(github.router, prefix="/api/v1/github", tags=["github"]) +app.include_router(connectors.router, prefix="/api/v1/connectors", tags=["connectors"]) +app.include_router(agents_router.router, prefix="/api/v1/agents", tags=["agents"]) + + +# ─── WebSocket Endpoints ─────────────────────────────────────────────────────── +@app.websocket("/ws/tasks/{task_id}") +async def ws_task(websocket: WebSocket, task_id: str): + await ws_manager.connect(websocket, room=f"task:{task_id}") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room=f"task:{task_id}") + + +@app.websocket("/ws/logs") +async def ws_logs(websocket: WebSocket): + await ws_manager.connect(websocket, room="logs") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room="logs") + + +@app.websocket("/ws/chat/{session_id}") +async def ws_chat(websocket: WebSocket, session_id: str): + await ws_manager.connect(websocket, room=f"chat:{session_id}") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + elif msg.get("type") == "chat_message": + asyncio.create_task( + orchestrator.orchestrate( + user_message=msg.get("content", ""), + session_id=session_id, + context=msg.get("context", {}), + ) + ) + elif msg.get("type") == "task_message": + from core.models import TaskCreateRequest + req = TaskCreateRequest( + goal=msg.get("content", ""), + session_id=session_id, + ) + asyncio.create_task(task_engine.submit(req)) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room=f"chat:{session_id}") + + +@app.websocket("/ws/agent/status") +async def ws_agent_status(websocket: WebSocket): + await ws_manager.connect(websocket, room="agent_status") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + elif msg.get("type") == "get_status": + await websocket.send_json({ + "type": "agent_status", + "data": orchestrator.get_status(), + }) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room="agent_status") + + +@app.websocket("/ws/sandbox/{session_id}") +async def ws_sandbox(websocket: WebSocket, session_id: str): + await ws_manager.connect(websocket, room=f"sandbox:{session_id}") + sandbox = orchestrator.get_agent("sandbox") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + elif msg.get("type") == "execute" and sandbox: + cmd = msg.get("command", "") + result = await sandbox.execute(cmd, session_id=session_id) + await websocket.send_json({ + "type": "terminal_output", + "command": cmd, + "output": result, + "timestamp": time.time(), + }) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room=f"sandbox:{session_id}") + + +# ─── v7 New Endpoints ────────────────────────────────────────────────────────── +@app.post("/api/v1/browser/research") +async def browser_research(request: Request): + body = await request.json() + query = body.get("query", "") + session_id = body.get("session_id", "") + browser = orchestrator.get_agent("browser") + if not browser: + raise HTTPException(status_code=503, detail="BrowserAgent not available") + result = await browser.run(query, session_id=session_id) + return {"result": result} + + +@app.get("/api/v1/files/workspace") +async def list_workspace(): + file_agent = orchestrator.get_agent("file") + if not file_agent: + return {"workspace": "/tmp/god_workspace", "files": [], "total": 0} + return file_agent.list_workspace() + + +@app.post("/api/v1/git/pr") +async def create_pr(request: Request): + body = await request.json() + git_agent = orchestrator.get_agent("git") + if not git_agent: + raise HTTPException(status_code=503, detail="GitAgent not available") + result = await git_agent.create_github_pr( + repo_owner=body.get("owner", ""), + repo_name=body.get("repo", ""), + title=body.get("title", ""), + body=body.get("body", ""), + head_branch=body.get("head_branch", "main"), + base_branch=body.get("base_branch", "main"), + ) + return result + + +@app.post("/api/v1/vision/generate") +async def generate_ui(request: Request): + body = await request.json() + vision = orchestrator.get_agent("vision") + if not vision: + raise HTTPException(status_code=503, detail="VisionAgent not available") + result = await vision.run( + body.get("prompt", ""), + context=body.get("context", {}), + session_id=body.get("session_id", ""), + ) + return {"result": result} + + +# ─── Root ────────────────────────────────────────────────────────────────────── +@app.get("/") +async def root(): + cs = connector_manager.get_summary() + status = orchestrator.get_status() + return { + "name": "πŸ€– GOD AGENT OS v7", + "version": "7.0.0", + "status": "operational", + "mode": "autonomous_engineering_os", + "description": "Manus + Genspark + Devin (OneHand) β€” Autonomous Engineering Platform", + "agents": status["agents"], + "total_agents": status["total_agents"], + "capabilities": status["capabilities"], + "connectors": { + "connected": cs["connected"], + "total": cs["total"], + "ai_ready": cs["ai_ready"], + }, + "docs": "/api/docs", + "websockets": [ + "/ws/tasks/{task_id}", + "/ws/logs", + "/ws/chat/{session_id}", + "/ws/agent/status", + "/ws/sandbox/{session_id}", + ], + "v7_new_features": [ + "🌐 BrowserAgent β€” Web research & scraping", + "πŸ“ FileAgent β€” Full file system control & project scaffolding", + "πŸ”€ GitAgent β€” Autonomous Git & GitHub PR operations", + "πŸ§ͺ TestAgent β€” Auto test generation & execution", + "🎨 VisionAgent β€” Design-to-code UI generation", + "⚑ 16-agent parallel orchestration", + "πŸ”„ Advanced self-healing loop", + "🧠 Enhanced intent classification", + "πŸ“Š Real-time execution timeline", + ], + } + + +if __name__ == "__main__": + import uvicorn + port = int(os.environ.get("PORT", 8000)) + uvicorn.run("main_v7:app", host="0.0.0.0", port=port, reload=False, workers=1) diff --git a/_legacy/main_v8.py b/_legacy/main_v8.py new file mode 100644 index 0000000000000000000000000000000000000000..c5a009d24a75cb7d51ad23e0cd8bebb001cb3971 --- /dev/null +++ b/_legacy/main_v8.py @@ -0,0 +1,326 @@ +""" +πŸš€ GOD AGENT OS v8 β€” Autonomous Engineering Operating System +Manus + Genspark + Devin (OneHand) Combined +Version: 8.0.0 β€” KeyPool Multi-API Routing (Gemini + SambaNova Primary LLMs) +""" + +import asyncio +import json +import os +import time +import uuid +from contextlib import asynccontextmanager +from typing import Optional + +import structlog +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware +from fastapi.responses import JSONResponse +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.util import get_remote_address +from slowapi.errors import RateLimitExceeded + +from api.routes import tasks, chat, memory, github, health +from api.routes import connectors, agents as agents_router +from api.websocket_manager import WebSocketManager +from core.task_engine import TaskEngine +from memory.db import init_db + +# ─── v8 AI Router (KeyPool-based) ───────────────────────────────────────────── +from ai_router.router_v8 import AIRouterV8 + +# ─── Agent Ecosystem ────────────────────────────────────────────────────────── +from agents.orchestrator_v7 import GodAgentOrchestratorV7 +from agents.chat_agent import ChatAgent +from agents.planner_agent import PlannerAgent +from agents.coding_agent import CodingAgent +from agents.debug_agent import DebugAgent +from agents.memory_agent import MemoryAgent +from agents.connector_agent import ConnectorAgent +from agents.deploy_agent import DeployAgent +from agents.workflow_agent import WorkflowAgent +from agents.sandbox_agent import SandboxAgent +from agents.ui_agent import UIAgent +from agents.reasoning_agent import ReasoningAgent +from agents.browser_agent import BrowserAgent +from agents.file_agent import FileAgent +from agents.git_agent import GitAgent +from agents.test_agent import TestAgent +from agents.vision_agent import VisionAgent +from connectors.manager import ConnectorManager + +# ─── Structured Logging ─────────────────────────────────────────────────────── +structlog.configure( + processors=[ + structlog.processors.TimeStamper(fmt="iso"), + structlog.stdlib.add_log_level, + structlog.processors.StackInfoRenderer(), + structlog.dev.ConsoleRenderer(), + ] +) +log = structlog.get_logger() + +limiter = Limiter(key_func=get_remote_address) + +# ─── Global Managers ────────────────────────────────────────────────────────── +ws_manager = WebSocketManager() +task_engine = TaskEngine(ws_manager) +ai_router = AIRouterV8(ws_manager) +connector_manager = ConnectorManager() + + +def build_orchestrator() -> GodAgentOrchestratorV7: + orchestrator = GodAgentOrchestratorV7(ws_manager=ws_manager, ai_router=ai_router) + orchestrator.register_agent("chat", ChatAgent(ws_manager, ai_router)) + orchestrator.register_agent("planner", PlannerAgent(ws_manager, ai_router)) + orchestrator.register_agent("coding", CodingAgent(ws_manager, ai_router)) + orchestrator.register_agent("debug", DebugAgent(ws_manager, ai_router)) + orchestrator.register_agent("memory", MemoryAgent(ws_manager, ai_router)) + orchestrator.register_agent("connector", ConnectorAgent(ws_manager, ai_router)) + orchestrator.register_agent("deploy", DeployAgent(ws_manager, ai_router)) + orchestrator.register_agent("workflow", WorkflowAgent(ws_manager, ai_router)) + orchestrator.register_agent("sandbox", SandboxAgent(ws_manager, ai_router)) + orchestrator.register_agent("ui", UIAgent(ws_manager, ai_router)) + orchestrator.register_agent("reasoning", ReasoningAgent(ws_manager, ai_router)) + orchestrator.register_agent("browser", BrowserAgent(ws_manager, ai_router)) + orchestrator.register_agent("file", FileAgent(ws_manager, ai_router)) + orchestrator.register_agent("git", GitAgent(ws_manager, ai_router)) + orchestrator.register_agent("test", TestAgent(ws_manager, ai_router)) + orchestrator.register_agent("vision", VisionAgent(ws_manager, ai_router)) + log.info("πŸ€– GOD AGENT v8 Ecosystem initialized", agents=16) + return orchestrator + + +orchestrator = build_orchestrator() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + log.info("πŸš€ Starting GOD AGENT OS v8 β€” KeyPool Multi-API Edition...") + await init_db() + await task_engine.start() + asyncio.create_task(ws_manager.heartbeat_loop()) + # Print router status + stats = ai_router.get_stats() + active = [name for name, s in stats.items() if s["available"]] + log.info("βœ… GOD AGENT v8 β€” 16 agents online") + log.info(f"πŸ”‘ Active AI providers: {active}") + log.info("🌐 AI Routing: SambaNova β†’ Gemini β†’ OpenAI β†’ Groq β†’ Cerebras β†’ OpenRouter β†’ Anthropic") + yield + log.info("πŸ›‘ Shutting down GOD AGENT v8...") + await task_engine.stop() + + +app = FastAPI( + title="πŸ€– GOD AGENT OS v8", + description="Autonomous Engineering OS β€” Gemini + SambaNova KeyPool Multi-API Routing", + version="8.0.0", + lifespan=lifespan, + docs_url="/api/docs", + redoc_url="/api/redoc", +) + +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) +app.state.ws_manager = ws_manager +app.state.task_engine = task_engine +app.state.ai_router = ai_router +app.state.orchestrator = orchestrator +app.state.connector_manager = connector_manager + +app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) +app.add_middleware(GZipMiddleware, minimum_size=1000) + + +@app.middleware("http") +async def log_requests(request: Request, call_next): + start = time.time() + response = await call_next(request) + ms = round((time.time() - start) * 1000, 2) + log.info("HTTP", method=request.method, path=request.url.path, status=response.status_code, ms=ms) + return response + + +# ─── REST Routers ───────────────────────────────────────────────────────────── +app.include_router(health.router, prefix="/api/v1", tags=["health"]) +app.include_router(tasks.router, prefix="/api/v1/tasks", tags=["tasks"]) +app.include_router(chat.router, prefix="/api/v1", tags=["chat"]) +app.include_router(memory.router, prefix="/api/v1/memory", tags=["memory"]) +app.include_router(github.router, prefix="/api/v1/github", tags=["github"]) +app.include_router(connectors.router, prefix="/api/v1/connectors", tags=["connectors"]) +app.include_router(agents_router.router, prefix="/api/v1/agents", tags=["agents"]) + + +# ─── WebSocket Endpoints ────────────────────────────────────────────────────── +@app.websocket("/ws/tasks/{task_id}") +async def ws_task(websocket: WebSocket, task_id: str): + await ws_manager.connect(websocket, room=f"task:{task_id}") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room=f"task:{task_id}") + + +@app.websocket("/ws/logs") +async def ws_logs(websocket: WebSocket): + await ws_manager.connect(websocket, room="logs") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room="logs") + + +@app.websocket("/ws/chat/{session_id}") +async def ws_chat(websocket: WebSocket, session_id: str): + await ws_manager.connect(websocket, room=f"chat:{session_id}") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + elif msg.get("type") == "chat_message": + asyncio.create_task(orchestrator.orchestrate( + user_message=msg.get("content", ""), + session_id=session_id, + context=msg.get("context", {}), + )) + elif msg.get("type") == "task_message": + from core.models import TaskCreateRequest + req = TaskCreateRequest(goal=msg.get("content", ""), session_id=session_id) + asyncio.create_task(task_engine.submit(req)) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room=f"chat:{session_id}") + + +@app.websocket("/ws/agent/status") +async def ws_agent_status(websocket: WebSocket): + await ws_manager.connect(websocket, room="agent_status") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + elif msg.get("type") == "get_status": + await websocket.send_json({"type": "agent_status", "data": orchestrator.get_status()}) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room="agent_status") + + +@app.websocket("/ws/sandbox/{session_id}") +async def ws_sandbox(websocket: WebSocket, session_id: str): + await ws_manager.connect(websocket, room=f"sandbox:{session_id}") + sandbox = orchestrator.get_agent("sandbox") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + elif msg.get("type") == "execute" and sandbox: + cmd = msg.get("command", "") + result = await sandbox.execute(cmd, session_id=session_id) + await websocket.send_json({"type": "terminal_output", "command": cmd, "output": result, "timestamp": time.time()}) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room=f"sandbox:{session_id}") + + +# ─── v8 Key Pool & AI Router Status Endpoints ───────────────────────────────── +@app.get("/api/v1/ai/stats") +async def get_ai_stats(): + return {"stats": ai_router.get_stats()} + + +@app.get("/api/v1/ai/pool-status") +async def get_pool_status(): + return {"pools": ai_router.get_pool_status()} + + +@app.post("/api/v1/browser/research") +async def browser_research(request: Request): + body = await request.json() + browser = orchestrator.get_agent("browser") + if not browser: + raise HTTPException(status_code=503, detail="BrowserAgent not available") + result = await browser.run(body.get("query", ""), session_id=body.get("session_id", "")) + return {"result": result} + + +@app.get("/api/v1/files/workspace") +async def list_workspace(): + file_agent = orchestrator.get_agent("file") + if not file_agent: + return {"workspace": "/tmp/god_workspace", "files": [], "total": 0} + return file_agent.list_workspace() + + +@app.post("/api/v1/git/pr") +async def create_pr(request: Request): + body = await request.json() + git_agent = orchestrator.get_agent("git") + if not git_agent: + raise HTTPException(status_code=503, detail="GitAgent not available") + result = await git_agent.create_github_pr( + repo_owner=body.get("owner", ""), + repo_name=body.get("repo", ""), + title=body.get("title", ""), + body=body.get("body", ""), + head_branch=body.get("head_branch", "main"), + base_branch=body.get("base_branch", "main"), + ) + return result + + +@app.post("/api/v1/vision/generate") +async def generate_ui(request: Request): + body = await request.json() + vision = orchestrator.get_agent("vision") + if not vision: + raise HTTPException(status_code=503, detail="VisionAgent not available") + result = await vision.run(body.get("prompt", ""), context=body.get("context", {}), session_id=body.get("session_id", "")) + return {"result": result} + + +# ─── Root ───────────────────────────────────────────────────────────────────── +@app.get("/") +async def root(): + cs = connector_manager.get_summary() + status = orchestrator.get_status() + stats = ai_router.get_stats() + active_providers = [name for name, s in stats.items() if s["available"]] + return { + "name": "πŸ€– GOD AGENT OS v8", + "version": "8.0.0", + "status": "operational", + "mode": "autonomous_engineering_os", + "description": "KeyPool Multi-API Routing β€” Gemini + SambaNova Primary LLMs", + "agents": status["agents"], + "total_agents": status["total_agents"], + "ai_providers": active_providers, + "connectors": {"connected": cs["connected"], "total": cs["total"]}, + "docs": "/api/docs", + "v8_features": [ + "πŸ”‘ KeyPool multi-key management (Gemini 6 keys + SambaNova 9 keys)", + "πŸ”„ Automatic key failover with cooldown tracking", + "⚑ SambaNova β†’ Gemini β†’ OpenAI β†’ Groq β†’ Cerebras chain", + "πŸ“Š Per-key usage stats & health monitoring", + "πŸ€– 16-agent autonomous fleet", + "🌐 Real-time streaming via WebSocket", + ], + } + + +if __name__ == "__main__": + import uvicorn + port = int(os.environ.get("PORT", 8000)) + uvicorn.run("main_v8:app", host="0.0.0.0", port=port, reload=False, workers=1) diff --git a/_legacy/main_v9.py b/_legacy/main_v9.py new file mode 100644 index 0000000000000000000000000000000000000000..36fba02a4ca13d2b1bae679f79ef2795004d9744 --- /dev/null +++ b/_legacy/main_v9.py @@ -0,0 +1,417 @@ +""" +πŸš€ GOD AGENT OS v10 β€” Distributed 22-Space Agent OS +Powered by Pyae Sone +""" + +import asyncio +import json +import os +import time +import uuid +from contextlib import asynccontextmanager +from typing import Optional + +import structlog +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware +from fastapi.responses import JSONResponse +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.util import get_remote_address +from slowapi.errors import RateLimitExceeded + +from api.routes import tasks, chat, memory, health +from api.routes import connectors, agents as agents_router +from api.websocket_manager import WebSocketManager +from core.task_engine import TaskEngine +from memory.db import init_db + +# ─── v10 AI Router (Gemini + SambaNova + GitHub KeyPool routing) ───────────── +from ai_router.router_v10 import AIRouterV10 as AIRouterV8 + +# ─── v9 Agent Kernel & Spaces ───────────────────────────────────────────────── +from kernel.agent_kernel import AgentKernel +from spaces import SPACE_CATALOG, build_all_spaces + +# ─── Legacy Agent Ecosystem (backward compatibility) ────────────────────────── +from agents.orchestrator_v7 import GodAgentOrchestratorV7 +from agents.chat_agent import ChatAgent +from agents.planner_agent import PlannerAgent +from agents.coding_agent import CodingAgent +from agents.debug_agent import DebugAgent +from agents.memory_agent import MemoryAgent +from agents.connector_agent import ConnectorAgent +from agents.deploy_agent import DeployAgent +from agents.workflow_agent import WorkflowAgent +from agents.sandbox_agent import SandboxAgent +from agents.ui_agent import UIAgent +from agents.reasoning_agent import ReasoningAgent +from agents.browser_agent import BrowserAgent +from agents.file_agent import FileAgent +from agents.git_agent import GitAgent +from agents.test_agent import TestAgent +from agents.vision_agent import VisionAgent +from connectors.manager import ConnectorManager + +# ─── API Routes ─────────────────────────────────────────────────────────────── +from api.routes import github + +# ─── Structured Logging ─────────────────────────────────────────────────────── +structlog.configure( + processors=[ + structlog.processors.TimeStamper(fmt="iso"), + structlog.stdlib.add_log_level, + structlog.processors.StackInfoRenderer(), + structlog.dev.ConsoleRenderer(), + ] +) +log = structlog.get_logger() + +limiter = Limiter(key_func=get_remote_address) + +# ─── Global Managers ────────────────────────────────────────────────────────── +ws_manager = WebSocketManager() +task_engine = TaskEngine(ws_manager) +ai_router = AIRouterV8(ws_manager) +connector_manager = ConnectorManager() + + +def build_kernel() -> AgentKernel: + """Build and configure the distributed 22-space Agent Kernel.""" + kernel = AgentKernel(ws_manager=ws_manager, ai_router=ai_router) + for space_name, space_instance in build_all_spaces(ws_manager=ws_manager, ai_router=ai_router).items(): + kernel.register_space(space_name, space_instance) + log.info("🧠 GOD AGENT OS distributed kernel initialized", spaces=len(SPACE_CATALOG)) + return kernel + + +def build_legacy_orchestrator() -> GodAgentOrchestratorV7: + """Build legacy v7 orchestrator for backward compatibility.""" + orchestrator = GodAgentOrchestratorV7(ws_manager=ws_manager, ai_router=ai_router) + orchestrator.register_agent("chat", ChatAgent(ws_manager, ai_router)) + orchestrator.register_agent("planner", PlannerAgent(ws_manager, ai_router)) + orchestrator.register_agent("coding", CodingAgent(ws_manager, ai_router)) + orchestrator.register_agent("debug", DebugAgent(ws_manager, ai_router)) + orchestrator.register_agent("memory", MemoryAgent(ws_manager, ai_router)) + orchestrator.register_agent("connector", ConnectorAgent(ws_manager, ai_router)) + orchestrator.register_agent("deploy", DeployAgent(ws_manager, ai_router)) + orchestrator.register_agent("workflow", WorkflowAgent(ws_manager, ai_router)) + orchestrator.register_agent("sandbox", SandboxAgent(ws_manager, ai_router)) + orchestrator.register_agent("ui", UIAgent(ws_manager, ai_router)) + orchestrator.register_agent("reasoning", ReasoningAgent(ws_manager, ai_router)) + orchestrator.register_agent("browser", BrowserAgent(ws_manager, ai_router)) + orchestrator.register_agent("file", FileAgent(ws_manager, ai_router)) + orchestrator.register_agent("git", GitAgent(ws_manager, ai_router)) + orchestrator.register_agent("test", TestAgent(ws_manager, ai_router)) + orchestrator.register_agent("vision", VisionAgent(ws_manager, ai_router)) + log.info("πŸ€– Legacy v7 Orchestrator initialized", agents=16) + return orchestrator + + +# Initialize both kernel and legacy orchestrator +kernel = build_kernel() +orchestrator = build_legacy_orchestrator() + + +@asynccontextmanager +async def lifespan(app: FastAPI): + log.info("πŸš€ Starting GOD AGENT OS v10 β€” Distributed 22-Space Architecture...") + await init_db() + await task_engine.start() + asyncio.create_task(ws_manager.heartbeat_loop()) + stats = ai_router.get_stats() + active = [name for name, s in stats.items() if s["available"]] + log.info("βœ… GOD AGENT v10 β€” 22 Spaces + 16 Legacy Agents online") + log.info(f"πŸ”‘ Active AI providers: {active}") + log.info("🌐 Routing: SambaNova β†’ Gemini β†’ OpenAI β†’ Groq β†’ Cerebras") + log.info("πŸ“¦ Spaces: distributed 22-space runtime online") + yield + log.info("πŸ›‘ Shutting down GOD AGENT OS v9...") + await task_engine.stop() + + +app = FastAPI( + title="πŸ€– GOD AGENT OS v10", + description="Distributed 22-Space Autonomous Agent OS | Powered by Pyae Sone", + version="10.0.0", + lifespan=lifespan, + docs_url="/api/docs", + redoc_url="/api/redoc", +) + +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) +app.state.ws_manager = ws_manager +app.state.task_engine = task_engine +app.state.ai_router = ai_router +app.state.kernel = kernel +app.state.orchestrator = orchestrator +app.state.connector_manager = connector_manager + +app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"]) +app.add_middleware(GZipMiddleware, minimum_size=1000) + + +@app.middleware("http") +async def log_requests(request: Request, call_next): + start = time.time() + response = await call_next(request) + ms = round((time.time() - start) * 1000, 2) + log.info("HTTP", method=request.method, path=request.url.path, status=response.status_code, ms=ms) + return response + + +# ─── REST Routers ───────────────────────────────────────────────────────────── +app.include_router(health.router, prefix="/api/v1", tags=["health"]) +app.include_router(tasks.router, prefix="/api/v1/tasks", tags=["tasks"]) +app.include_router(chat.router, prefix="/api/v1", tags=["chat"]) +app.include_router(memory.router, prefix="/api/v1/memory", tags=["memory"]) +app.include_router(github.router, prefix="/api/v1/github", tags=["github"]) +app.include_router(connectors.router, prefix="/api/v1/connectors", tags=["connectors"]) +app.include_router(agents_router.router, prefix="/api/v1/agents", tags=["agents"]) + + +# ─── WebSocket: Chat (v9 Kernel-powered) ────────────────────────────────────── +@app.websocket("/ws/chat/{session_id}") +async def ws_chat(websocket: WebSocket, session_id: str): + await ws_manager.connect(websocket, room=f"chat:{session_id}") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + + elif msg.get("type") == "chat_message": + # Route through v9 Agent Kernel + asyncio.create_task(kernel.orchestrate( + user_message=msg.get("content", ""), + session_id=session_id, + context=msg.get("context", {}), + )) + + elif msg.get("type") == "task_message": + from core.models import TaskCreateRequest + req = TaskCreateRequest(goal=msg.get("content", ""), session_id=session_id) + asyncio.create_task(task_engine.submit(req)) + + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room=f"chat:{session_id}") + + +@app.websocket("/ws/tasks/{task_id}") +async def ws_task(websocket: WebSocket, task_id: str): + await ws_manager.connect(websocket, room=f"task:{task_id}") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room=f"task:{task_id}") + + +@app.websocket("/ws/logs") +async def ws_logs(websocket: WebSocket): + await ws_manager.connect(websocket, room="logs") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room="logs") + + +@app.websocket("/ws/agent/status") +async def ws_agent_status(websocket: WebSocket): + await ws_manager.connect(websocket, room="agent_status") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + elif msg.get("type") == "get_status": + await websocket.send_json({ + "type": "agent_status", + "data": kernel.get_status(), + "legacy": orchestrator.get_status(), + }) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room="agent_status") + + +@app.websocket("/ws/sandbox/{session_id}") +async def ws_sandbox(websocket: WebSocket, session_id: str): + await ws_manager.connect(websocket, room=f"sandbox:{session_id}") + sandbox_space = kernel.get_space("sandbox") + try: + while True: + data = await websocket.receive_text() + msg = json.loads(data) + if msg.get("type") == "ping": + await websocket.send_json({"type": "pong", "timestamp": time.time()}) + elif msg.get("type") == "execute" and sandbox_space: + cmd = msg.get("command", "") + result = await sandbox_space._run_shell(cmd) + await websocket.send_json({ + "type": "terminal_output", + "command": cmd, + "output": result, + "timestamp": time.time() + }) + except WebSocketDisconnect: + ws_manager.disconnect(websocket, room=f"sandbox:{session_id}") + + +# ─── v9 Space-Role API Endpoints ────────────────────────────────────────────── +@app.get("/api/v1/kernel/status") +async def kernel_status(): + """Get Agent Kernel status.""" + return { + "kernel": kernel.get_status(), + "ai_providers": ai_router.get_stats(), + } + + +@app.get("/api/v1/spaces") +async def list_spaces(): + """List all available Spaces.""" + spaces_info = {} + for name, space in kernel._spaces.items(): + spaces_info[name] = space.get_info() + return {"spaces": spaces_info, "total": len(spaces_info)} + + +@app.post("/api/v1/spaces/{space_name}/execute") +async def execute_in_space(space_name: str, request: Request): + """Execute a task in a specific Space.""" + body = await request.json() + task = body.get("task", "") + role = body.get("role", "cognition") + session_id = body.get("session_id", "api") + + space = kernel.get_space(space_name) + if not space: + raise HTTPException(status_code=404, detail=f"Space '{space_name}' not found") + + result = await space.execute(task=task, role=role, session_id=session_id, context=body.get("context", {})) + return {"space": space_name, "role": role, "result": result} + + +@app.post("/api/v1/kernel/orchestrate") +async def kernel_orchestrate(request: Request): + """Main orchestration endpoint.""" + body = await request.json() + result = await kernel.orchestrate( + user_message=body.get("message", ""), + session_id=body.get("session_id", "api"), + context=body.get("context", {}), + ) + return {"result": result} + + +# ─── Legacy v8 Endpoints (backward compatibility) ───────────────────────────── +@app.get("/api/v1/ai/stats") +async def get_ai_stats(): + return {"stats": ai_router.get_stats()} + + +@app.get("/api/v1/ai/pool-status") +async def get_pool_status(): + return {"pools": ai_router.get_pool_status()} + + +@app.post("/api/v1/browser/research") +async def browser_research(request: Request): + body = await request.json() + browser_space = kernel.get_space("browser") + if not browser_space: + raise HTTPException(status_code=503, detail="Browser Space not available") + result = await browser_space.execute( + task=body.get("query", ""), + role="automation", + session_id=body.get("session_id", "api"), + ) + return {"result": result} + + +@app.get("/api/v1/files/workspace") +async def list_workspace(): + file_agent = orchestrator.get_agent("file") + if not file_agent: + return {"workspace": "/tmp/god_workspace", "files": [], "total": 0} + return file_agent.list_workspace() + + +@app.post("/api/v1/git/pr") +async def create_pr(request: Request): + body = await request.json() + git_agent = orchestrator.get_agent("git") + if not git_agent: + raise HTTPException(status_code=503, detail="GitAgent not available") + result = await git_agent.create_github_pr( + repo_owner=body.get("owner", ""), + repo_name=body.get("repo", ""), + title=body.get("title", ""), + body=body.get("body", ""), + head_branch=body.get("head_branch", "main"), + base_branch=body.get("base_branch", "main"), + ) + return result + + +@app.post("/api/v1/vision/generate") +async def generate_ui(request: Request): + body = await request.json() + vision_space = kernel.get_space("vision") + if not vision_space: + raise HTTPException(status_code=503, detail="Vision Space not available") + result = await vision_space.execute( + task=body.get("prompt", ""), + role="visual_intelligence", + session_id=body.get("session_id", "api"), + ) + return {"result": result} + + +# ─── Root ───────────────────────────────────────────────────────────────────── +@app.get("/") +async def root(): + cs = connector_manager.get_summary() + kernel_status_data = kernel.get_status() + stats = ai_router.get_stats() + active_providers = [name for name, s in stats.items() if s["available"]] + return { + "name": "πŸ€– GOD AGENT OS v10", + "version": "10.0.0", + "status": "operational", + "mode": "general_autonomous_agent_os", + "description": "Distributed 22-Space Architecture | Powered by Pyae Sone", + "architecture": "Distributed Worker Space Paradigm", + "spaces": kernel_status_data["spaces"], + "total_spaces": kernel_status_data["total_spaces"], + "ai_providers": active_providers, + "connectors": {"connected": cs["connected"], "total": cs["total"]}, + "docs": "/api/docs", + "v9_features": [ + "πŸ“¦ 22 distributed worker spaces across cognition, execution, verification, deployment, memory, coordination, monitoring, session, and infrastructure layers", + "🎭 5 Cognitive Roles: Cognition | Automation | Execution | Repair | Visual Intelligence", + "🧠 God Core Space orchestrates the worker fleet", + "πŸ”‘ KeyPool multi-key management (Gemini + SambaNova + GitHub)", + "πŸ”„ Automatic worker-space routing based on intent", + "πŸ’Ύ Context Manager for session-scoped runtime state", + "⚑ Backward compatible with v8/v9 agent fleet", + "🌐 Real-time streaming via WebSocket", + ], + } + + +if __name__ == "__main__": + import uvicorn + port = int(os.environ.get("PORT", 7860)) + uvicorn.run("main_v9:app", host="0.0.0.0", port=port, reload=False, workers=1) diff --git a/agents/__init__.py b/agents/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e47d55d07e5ce4907e25f69eecc6d6bf9fd86eb4 --- /dev/null +++ b/agents/__init__.py @@ -0,0 +1,24 @@ +# God Agent Multi-Agent System +from .orchestrator import GodAgentOrchestrator +from .chat_agent import ChatAgent +from .planner_agent import PlannerAgent +from .coding_agent import CodingAgent +from .debug_agent import DebugAgent +from .memory_agent import MemoryAgent +from .connector_agent import ConnectorAgent +from .deploy_agent import DeployAgent +from .workflow_agent import WorkflowAgent +from .sandbox_agent import SandboxAgent + +__all__ = [ + "GodAgentOrchestrator", + "ChatAgent", + "PlannerAgent", + "CodingAgent", + "DebugAgent", + "MemoryAgent", + "ConnectorAgent", + "DeployAgent", + "WorkflowAgent", + "SandboxAgent", +] diff --git a/agents/base_agent.py b/agents/base_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..000350463cf9a2db47977462b4bf22abcae809e6 --- /dev/null +++ b/agents/base_agent.py @@ -0,0 +1,54 @@ +""" +Base Agent β€” Abstract base for all God Mode agents +""" +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional +import structlog + +log = structlog.get_logger() + + +class BaseAgent(ABC): + """Abstract base class for all agents in the God Mode ecosystem.""" + + def __init__(self, name: str, ws_manager=None, ai_router=None): + self.name = name + self.ws = ws_manager + self.ai_router = ai_router + self.log = structlog.get_logger().bind(agent=name) + + @abstractmethod + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + """Execute the agent's primary task.""" + pass + + async def emit(self, room: str, event: str, data: Dict, session_id: str = ""): + """Emit WebSocket event if manager available.""" + if self.ws: + await self.ws.emit(room, event, data, session_id=session_id) + + async def emit_chat(self, session_id: str, event: str, data: Dict): + """Emit chat WebSocket event.""" + if self.ws: + await self.ws.emit_chat(session_id, event, data) + + async def llm( + self, + messages: List[Dict], + task_id: str = "", + session_id: str = "", + temperature: float = 0.7, + max_tokens: int = 4096, + model: str = "", + ) -> str: + """Route LLM call through AI router.""" + if self.ai_router: + return await self.ai_router.complete( + messages=messages, + task_id=task_id, + session_id=session_id, + temperature=temperature, + max_tokens=max_tokens, + preferred_model=model, + ) + return f"[{self.name}] AI router not configured." diff --git a/agents/browser_agent.py b/agents/browser_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..982ef71e3e672793483cbb48b3c930b6b442d32e --- /dev/null +++ b/agents/browser_agent.py @@ -0,0 +1,147 @@ +""" +BrowserAgent v7 β€” Autonomous web browsing, scraping, research (Manus-style) +Real browser control via Playwright/httpx for research, testing, web automation +""" +import asyncio +import json +import os +import re +from typing import Dict, List, Optional +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + +BROWSER_SYSTEM = """You are an elite autonomous web research and browser automation agent. +You can: +- Search the web and extract structured information +- Navigate websites and fill forms +- Take screenshots and analyze visual content +- Scrape and parse complex web pages +- Run web automation tasks + +Always provide structured, actionable results with source URLs. +""" + +class BrowserAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("BrowserAgent", ws_manager, ai_router) + self._session_cache: Dict[str, str] = {} + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + + await self.emit(task_id, "agent_start", { + "agent": "BrowserAgent", + "task": task[:80], + }, session_id) + + await self.emit(task_id, "tool_called", { + "agent": "BrowserAgent", + "tool": "web_research", + "step": f"Researching: {task[:60]}", + }, session_id) + + # Extract search query or URL from task + urls = re.findall(r'https?://[^\s]+', task) + + if urls: + result = await self._fetch_and_analyze(urls[0], task, task_id, session_id) + else: + result = await self._web_research(task, task_id, session_id) + + await self.emit(task_id, "browser_result", { + "agent": "BrowserAgent", + "result_length": len(result), + }, session_id) + + return result + + async def _web_research(self, query: str, task_id: str, session_id: str) -> str: + """Perform web research using AI knowledge + httpx.""" + import httpx + + # Use DuckDuckGo search API (no key needed) + search_url = f"https://api.duckduckgo.com/?q={query.replace(' ', '+')}&format=json&no_html=1" + + try: + async with httpx.AsyncClient(timeout=15, follow_redirects=True) as client: + resp = await client.get(search_url, headers={"User-Agent": "GodAgent/7.0"}) + data = resp.json() + + results = [] + if data.get("AbstractText"): + results.append(f"**Summary:** {data['AbstractText']}") + if data.get("AbstractURL"): + results.append(f"**Source:** {data['AbstractURL']}") + + related = data.get("RelatedTopics", [])[:5] + if related: + results.append("\n**Related:**") + for r in related: + if isinstance(r, dict) and r.get("Text"): + results.append(f"- {r['Text'][:200]}") + + if results: + search_context = "\n".join(results) + else: + search_context = f"Web search for: {query}" + + except Exception as e: + search_context = f"Search context for: {query}" + + # Use AI to synthesize research + messages = [ + {"role": "system", "content": BROWSER_SYSTEM}, + {"role": "user", "content": ( + f"Research task: {query}\n\n" + f"Search context:\n{search_context}\n\n" + f"Provide a comprehensive, structured research report with key findings, " + f"actionable insights, and relevant sources. Format with headers and bullet points." + )}, + ] + return await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.3, max_tokens=4096) + + async def _fetch_and_analyze(self, url: str, task: str, task_id: str, session_id: str) -> str: + """Fetch a URL and analyze its content.""" + import httpx + + try: + async with httpx.AsyncClient(timeout=20, follow_redirects=True) as client: + resp = await client.get(url, headers={ + "User-Agent": "Mozilla/5.0 GodAgent/7.0", + "Accept": "text/html,application/json,*/*", + }) + content_type = resp.headers.get("content-type", "") + + if "json" in content_type: + page_content = json.dumps(resp.json(), indent=2)[:3000] + else: + # Strip HTML tags + html = resp.text + text = re.sub(r'<[^>]+>', ' ', html) + text = re.sub(r'\s+', ' ', text).strip() + page_content = text[:3000] + + except Exception as e: + page_content = f"Could not fetch {url}: {str(e)}" + + messages = [ + {"role": "system", "content": BROWSER_SYSTEM}, + {"role": "user", "content": ( + f"Analyze this web page content for the task: {task}\n\n" + f"URL: {url}\n\n" + f"Page Content:\n{page_content}\n\n" + f"Provide a structured analysis with key information extracted." + )}, + ] + return await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.3, max_tokens=4096) + + async def screenshot_analyze(self, url: str, task_id: str = "", session_id: str = "") -> str: + """Describe what a webpage looks like (AI-powered visual analysis).""" + messages = [ + {"role": "system", "content": BROWSER_SYSTEM}, + {"role": "user", "content": f"Describe the visual layout and UI elements you'd expect at: {url}. Provide a detailed visual analysis."}, + ] + return await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.5) diff --git a/agents/chat_agent.py b/agents/chat_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..dadda532af366357cdec3c89209d410172d0300c --- /dev/null +++ b/agents/chat_agent.py @@ -0,0 +1,44 @@ +""" +ChatAgent β€” Conversational interface with streaming, Burmese support +""" +from typing import Dict +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + +CHAT_SYSTEM = """You are God Agent β€” an elite autonomous AI operating system. +You support both English and Burmese (α€™α€Όα€”α€Ία€™α€¬α€˜α€¬α€žα€¬) languages. +Be helpful, precise, and autonomous. +When users write in Burmese, respond in Burmese. +When discussing code, provide production-quality examples. +""" + + +class ChatAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("ChatAgent", ws_manager, ai_router) + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + + history = context.get("history", []) + messages = [{"role": "system", "content": CHAT_SYSTEM}] + + for h in history[-10:]: + messages.append({"role": h.get("role", "user"), "content": h.get("content", "")}) + + messages.append({"role": "user", "content": task}) + + await self.emit_chat(session_id, "stream_start", {"agent": "ChatAgent", "status": "generating"}) + + response = await self.llm(messages, task_id=task_id, session_id=session_id) + + await self.emit_chat(session_id, "stream_end", { + "agent": "ChatAgent", + "full_response": response, + "status": "complete", + }) + + return response diff --git a/agents/coding_agent.py b/agents/coding_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..b3b5288bef0ab58fecacc466e912ca785748a5f8 --- /dev/null +++ b/agents/coding_agent.py @@ -0,0 +1,191 @@ +""" +CodingAgent β€” Autonomous code generation, editing, refactoring (Devin/Genspark style) +""" +import json +import os +import re +from typing import Dict, List +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + +CODING_SYSTEM = """You are an elite autonomous software engineer β€” like Devin combined with Genspark. +You write production-quality code that is: +- Clean, readable, well-structured +- Properly typed (TypeScript/Python type hints) +- Error-handled and resilient +- Documented with clear comments +- Following best practices for the language/framework + +When generating code: +1. Think about the full architecture first +2. Write complete, runnable code (not snippets) +3. Include proper imports +4. Add error handling +5. Include brief usage examples in comments + +Support: Python, TypeScript, JavaScript, Go, Rust, SQL, Shell, YAML, JSON +""" + + +class CodingAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("CodingAgent", ws_manager, ai_router) + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + + await self.emit(task_id, "agent_start", { + "agent": "CodingAgent", + "task": task[:80], + }, session_id) + + # Build context-aware messages + prev_results = context.get("previous_results", []) + project_ctx = context.get("project_context", "") + plan = context.get("plan", "") + + system_content = CODING_SYSTEM + if project_ctx: + system_content += f"\n\nProject Context:\n{project_ctx[:1000]}" + + user_content = f"Task: {task}" + if plan: + user_content += f"\n\nExecution Plan:\n{plan[:500]}" + if prev_results: + user_content += f"\n\nPrevious results:\n" + "\n".join(str(r)[:200] for r in prev_results[-3:]) + + messages = [ + {"role": "system", "content": system_content}, + {"role": "user", "content": user_content}, + ] + + await self.emit(task_id, "tool_called", { + "agent": "CodingAgent", + "tool": "code_generation", + "step": task[:60], + }, session_id) + + result = await self.llm( + messages, + task_id=task_id, + session_id=session_id, + temperature=0.2, + max_tokens=8192, + ) + + # Extract code blocks for display + code_blocks = self._extract_code_blocks(result) + await self.emit(task_id, "code_generated", { + "agent": "CodingAgent", + "code_blocks": len(code_blocks), + "total_lines": sum(len(b.split("\n")) for b in code_blocks), + "languages": list(set(self._detect_language(b) for b in code_blocks)), + }, session_id) + + return result + + async def generate_file( + self, + filename: str, + description: str, + task_id: str = "", + session_id: str = "", + context: Dict = {}, + ) -> str: + """Generate a complete file with proper structure.""" + messages = [ + {"role": "system", "content": CODING_SYSTEM}, + {"role": "user", "content": ( + f"Generate a complete, production-ready file.\n" + f"Filename: {filename}\n" + f"Description: {description}\n" + f"Context: {json.dumps(context)[:500]}\n\n" + f"Return ONLY the file content, no explanation." + )}, + ] + content = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.1, max_tokens=8192) + + # Strip markdown code fences if present + content = self._strip_code_fences(content) + + # Write to workspace + workspace = os.environ.get("WORKSPACE_DIR", "/tmp/god_workspace") + filepath = os.path.join(workspace, filename) + os.makedirs(os.path.dirname(filepath), exist_ok=True) + with open(filepath, "w") as f: + f.write(content) + + await self.emit(task_id, "file_written", { + "filename": filename, + "size": len(content), + "lines": len(content.split("\n")), + }, session_id) + + return content + + async def refactor( + self, + code: str, + instructions: str, + task_id: str = "", + session_id: str = "", + ) -> str: + """Refactor existing code based on instructions.""" + messages = [ + {"role": "system", "content": CODING_SYSTEM}, + {"role": "user", "content": ( + f"Refactor this code based on these instructions:\n" + f"Instructions: {instructions}\n\n" + f"Original code:\n```\n{code}\n```\n\n" + f"Return ONLY the refactored code." + )}, + ] + return await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.1, max_tokens=8192) + + async def scan_repository(self, repo_path: str) -> Dict: + """Scan repository and build project intelligence graph.""" + import subprocess + try: + result = subprocess.run( + ["find", repo_path, "-type", "f", "-name", "*.py", "-o", + "-name", "*.ts", "-o", "-name", "*.js", "-o", "-name", "*.go"], + capture_output=True, text=True, timeout=10 + ) + files = result.stdout.strip().split("\n")[:50] + + # Read key files + key_files = {} + for f in ["package.json", "requirements.txt", "tsconfig.json", "pyproject.toml", "go.mod"]: + path = os.path.join(repo_path, f) + if os.path.exists(path): + with open(path) as fp: + key_files[f] = fp.read()[:1000] + + return { + "files": files, + "key_configs": key_files, + "total_files": len(files), + } + except Exception as e: + return {"error": str(e), "files": [], "key_configs": {}} + + def _extract_code_blocks(self, text: str) -> List[str]: + pattern = r"```[\w]*\n(.*?)```" + return re.findall(pattern, text, re.DOTALL) + + def _strip_code_fences(self, text: str) -> str: + text = re.sub(r"^```[\w]*\n", "", text.strip()) + text = re.sub(r"\n```$", "", text) + return text + + def _detect_language(self, code: str) -> str: + if "def " in code and "import " in code: + return "python" + if "function " in code or "const " in code or "interface " in code: + return "typescript" + if "package main" in code: + return "go" + return "unknown" diff --git a/agents/connector_agent.py b/agents/connector_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..08f98ce459ee093e12ea34508b045e36cd054eef --- /dev/null +++ b/agents/connector_agent.py @@ -0,0 +1,186 @@ +""" +ConnectorAgent β€” GitHub, HuggingFace, Vercel, n8n, Telegram, Discord integrations +""" +import json +import os +from typing import Dict, List, Optional +import httpx +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + + +class ConnectorAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("ConnectorAgent", ws_manager, ai_router) + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + + task_lower = task.lower() + + if "github" in task_lower: + return await self.github_operation(task, task_id=task_id, session_id=session_id) + elif "huggingface" in task_lower or "hf" in task_lower: + return await self.hf_operation(task, task_id=task_id, session_id=session_id) + elif "vercel" in task_lower: + return await self.vercel_operation(task, task_id=task_id, session_id=session_id) + else: + return await self._generic_connector(task, task_id=task_id, session_id=session_id) + + # ─── GitHub ─────────────────────────────────────────────────────────────── + + async def github_operation(self, task: str, task_id: str = "", session_id: str = "") -> str: + token = os.environ.get("GITHUB_TOKEN", "") + if not token: + return "⚠️ GITHUB_TOKEN not set. Please add it to environment variables." + + headers = { + "Authorization": f"token {token}", + "Accept": "application/vnd.github.v3+json", + } + + task_lower = task.lower() + + try: + async with httpx.AsyncClient(timeout=30) as client: + if "create repo" in task_lower or "new repo" in task_lower: + return await self._github_create_repo(client, headers, task, task_id, session_id) + elif "list repo" in task_lower: + return await self._github_list_repos(client, headers, task_id, session_id) + elif "commit" in task_lower: + return f"βœ… GitHub commit operation noted. Use SandboxAgent for actual commits." + elif "issue" in task_lower: + return await self._github_create_issue(client, headers, task, task_id, session_id) + else: + return await self._github_user_info(client, headers, task_id, session_id) + except Exception as e: + return f"❌ GitHub error: {str(e)}" + + async def _github_user_info(self, client, headers, task_id, session_id) -> str: + resp = await client.get("https://api.github.com/user", headers=headers) + if resp.status_code == 200: + data = resp.json() + await self.emit(task_id, "connector_result", { + "connector": "github", + "action": "user_info", + "user": data.get("login"), + }, session_id) + return f"βœ… GitHub connected as **{data.get('login')}** ({data.get('public_repos')} repos)" + return f"❌ GitHub auth failed: {resp.status_code}" + + async def _github_create_repo(self, client, headers, task, task_id, session_id) -> str: + # Extract repo name from task using simple heuristic + words = task.split() + name_candidates = [w for w in words if w.replace("-", "").replace("_", "").isalnum() and len(w) > 3] + repo_name = name_candidates[-1] if name_candidates else "god-agent-project" + + payload = { + "name": repo_name, + "description": "Created by God Agent Platform", + "private": False, + "auto_init": True, + } + resp = await client.post("https://api.github.com/user/repos", headers=headers, json=payload) + if resp.status_code == 201: + data = resp.json() + await self.emit(task_id, "connector_result", { + "connector": "github", + "action": "repo_created", + "repo": data.get("full_name"), + "url": data.get("html_url"), + }, session_id) + return f"βœ… GitHub repo created: [{data.get('full_name')}]({data.get('html_url')})" + return f"❌ Failed to create repo: {resp.status_code} β€” {resp.text[:200]}" + + async def _github_list_repos(self, client, headers, task_id, session_id) -> str: + resp = await client.get("https://api.github.com/user/repos?per_page=10&sort=updated", headers=headers) + if resp.status_code == 200: + repos = resp.json() + lines = [f"- [{r['full_name']}]({r['html_url']}) ⭐{r['stargazers_count']}" for r in repos[:10]] + return "πŸ“¦ **Your GitHub Repos:**\n\n" + "\n".join(lines) + return f"❌ Failed to list repos: {resp.status_code}" + + async def _github_create_issue(self, client, headers, task, task_id, session_id) -> str: + return "βœ… GitHub issue creation requires repo context. Specify: 'create issue in owner/repo: title'" + + # ─── HuggingFace ────────────────────────────────────────────────────────── + + async def hf_operation(self, task: str, task_id: str = "", session_id: str = "") -> str: + token = os.environ.get("HF_TOKEN", "") + if not token: + return "⚠️ HF_TOKEN not set. Add HuggingFace token to environment." + + headers = {"Authorization": f"Bearer {token}"} + try: + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get("https://huggingface.co/api/whoami", headers=headers) + if resp.status_code == 200: + data = resp.json() + await self.emit(task_id, "connector_result", { + "connector": "huggingface", + "user": data.get("name"), + }, session_id) + return f"βœ… HuggingFace connected as **{data.get('name')}**" + return f"❌ HF auth failed: {resp.status_code}" + except Exception as e: + return f"❌ HuggingFace error: {str(e)}" + + # ─── Vercel ─────────────────────────────────────────────────────────────── + + async def vercel_operation(self, task: str, task_id: str = "", session_id: str = "") -> str: + token = os.environ.get("VERCEL_TOKEN", "") + if not token: + return "⚠️ VERCEL_TOKEN not set. Add Vercel token to environment." + + headers = {"Authorization": f"Bearer {token}"} + try: + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.get("https://api.vercel.com/v2/user", headers=headers) + if resp.status_code == 200: + data = resp.json() + user = data.get("user", {}) + await self.emit(task_id, "connector_result", { + "connector": "vercel", + "user": user.get("username"), + }, session_id) + return f"βœ… Vercel connected as **{user.get('username')}** ({user.get('email')})" + return f"❌ Vercel auth failed: {resp.status_code}" + except Exception as e: + return f"❌ Vercel error: {str(e)}" + + async def _generic_connector(self, task: str, task_id: str = "", session_id: str = "") -> str: + return ( + "πŸ”Œ **Available Connectors:**\n\n" + "| Connector | Status | Env Var |\n" + "|-----------|--------|---------||\n" + f"| GitHub | {'βœ…' if os.environ.get('GITHUB_TOKEN') else '❌'} | GITHUB_TOKEN |\n" + f"| HuggingFace | {'βœ…' if os.environ.get('HF_TOKEN') else '❌'} | HF_TOKEN |\n" + f"| Vercel | {'βœ…' if os.environ.get('VERCEL_TOKEN') else '❌'} | VERCEL_TOKEN |\n" + f"| OpenAI | {'βœ…' if os.environ.get('OPENAI_API_KEY') else '❌'} | OPENAI_API_KEY |\n" + f"| Groq | {'βœ…' if os.environ.get('GROQ_API_KEY') else '❌'} | GROQ_API_KEY |\n" + f"| OpenRouter | {'βœ…' if os.environ.get('OPENROUTER_API_KEY') else '❌'} | OPENROUTER_API_KEY |\n" + ) + + def get_connector_status(self) -> Dict: + """Return status of all connectors.""" + connectors = { + "github": bool(os.environ.get("GITHUB_TOKEN")), + "huggingface": bool(os.environ.get("HF_TOKEN")), + "vercel": bool(os.environ.get("VERCEL_TOKEN")), + "openai": bool(os.environ.get("OPENAI_API_KEY")), + "groq": bool(os.environ.get("GROQ_API_KEY")), + "cerebras": bool(os.environ.get("CEREBRAS_API_KEY")), + "openrouter": bool(os.environ.get("OPENROUTER_API_KEY")), + "anthropic": bool(os.environ.get("ANTHROPIC_API_KEY")), + "n8n": bool(os.environ.get("N8N_URL")), + "telegram": bool(os.environ.get("TELEGRAM_BOT_TOKEN")), + "discord": bool(os.environ.get("DISCORD_BOT_TOKEN")), + } + return { + "connectors": connectors, + "connected_count": sum(connectors.values()), + "total": len(connectors), + } diff --git a/agents/debug_agent.py b/agents/debug_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..ac628d5668d5d4241a3d7a04ad56713f0878b169 --- /dev/null +++ b/agents/debug_agent.py @@ -0,0 +1,158 @@ +""" +DebugAgent β€” Autonomous error detection, self-healing, retry loops +""" +import json +import re +from typing import Dict, List +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + +DEBUG_SYSTEM = """You are an expert debugging engineer with deep knowledge of: +- Python, TypeScript, JavaScript, Go, Rust errors +- Runtime exceptions, import errors, type errors +- API errors, network failures, auth issues +- Build failures, dependency conflicts +- Database errors, query optimization + +When given an error: +1. Identify the root cause precisely +2. Provide the EXACT fix (code patch or config change) +3. Explain WHY it failed +4. Suggest prevention strategies + +Always return actionable fixes, not just explanations. +""" + + +class DebugAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("DebugAgent", ws_manager, ai_router) + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + attempt = context.get("attempt", 1) + + await self.emit(task_id, "agent_start", { + "agent": "DebugAgent", + "attempt": attempt, + "error": task[:100], + }, session_id) + + messages = [ + {"role": "system", "content": DEBUG_SYSTEM}, + {"role": "user", "content": ( + "Debug and fix this issue (attempt " + str(attempt) + "):\n\n" + + task + "\n\n" + "Provide:\n" + "1. Root cause analysis\n" + "2. Exact fix (code/config)\n" + "3. Prevention strategy" + )}, + ] + + result = await self.llm( + messages, + task_id=task_id, + session_id=session_id, + temperature=0.1, + max_tokens=4096, + ) + + await self.emit(task_id, "debug_complete", { + "agent": "DebugAgent", + "has_fix": "```" in result or "fix" in result.lower(), + "attempt": attempt, + }, session_id) + + return result + + async def analyze_error(self, error_output: str, source_code: str = "", task_id: str = "", session_id: str = "") -> Dict: + """Deep error analysis with structured output.""" + if source_code: + source_section = "Source Code:\n```\n" + source_code[:1000] + "\n```" + else: + source_section = "" + + messages = [ + {"role": "system", "content": DEBUG_SYSTEM}, + {"role": "user", "content": ( + "Analyze this error and provide structured diagnosis:\n\n" + "Error:\n" + error_output[:2000] + "\n\n" + + source_section + "\n\n" + "Respond with JSON:\n" + '{"error_type": "...", "root_cause": "...", "fix": "...", "prevention": "...", "severity": "low|medium|high|critical"}' + )}, + ] + raw = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.1, max_tokens=1000) + try: + start = raw.find("{") + end = raw.rfind("}") + 1 + return json.loads(raw[start:end]) + except Exception: + return {"error_type": "unknown", "root_cause": error_output[:200], "fix": raw[:500], "severity": "medium"} + + async def self_heal_loop( + self, + code: str, + error: str, + max_retries: int = 3, + task_id: str = "", + session_id: str = "", + ) -> str: + """Self-healing loop β€” generate fix, validate, retry.""" + current_code = code + current_error = error + + for attempt in range(1, max_retries + 1): + await self.emit(task_id, "self_heal_attempt", { + "attempt": attempt, + "max_retries": max_retries, + "error_snippet": current_error[:100], + }, session_id) + + messages = [ + {"role": "system", "content": DEBUG_SYSTEM}, + {"role": "user", "content": ( + "Fix attempt " + str(attempt) + "/" + str(max_retries) + ":\n\n" + "Error: " + current_error + "\n\n" + "Code:\n```\n" + current_code[:3000] + "\n```\n\n" + "Return ONLY the fixed code." + )}, + ] + + fixed = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.1, max_tokens=8192) + fixed = self._strip_fences(fixed) + + # Validate syntax for Python + validation = await self._validate_python(fixed) + if validation["valid"]: + await self.emit(task_id, "self_heal_success", { + "attempt": attempt, + "code_lines": len(fixed.split("\n")), + }, session_id) + return fixed + else: + current_code = fixed + current_error = validation["error"] + log.warning("Self-heal attempt failed", attempt=attempt, error=current_error[:100]) + + await self.emit(task_id, "self_heal_failed", {"attempts": max_retries}, session_id) + return current_code # Return best attempt + + async def _validate_python(self, code: str) -> Dict: + """Quick Python syntax validation.""" + try: + compile(code, "", "exec") + return {"valid": True, "error": ""} + except SyntaxError as e: + return {"valid": False, "error": "SyntaxError: " + str(e)} + except Exception as e: + return {"valid": False, "error": str(e)} + + def _strip_fences(self, text: str) -> str: + text = re.sub(r"^```[\w]*\n", "", text.strip()) + text = re.sub(r"\n```$", "", text) + return text diff --git a/agents/deploy_agent.py b/agents/deploy_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..320a2fef6cbe5d1421dd47a1b79b394903664251 --- /dev/null +++ b/agents/deploy_agent.py @@ -0,0 +1,101 @@ +""" +DeployAgent β€” Automated deployments to Vercel, HF Spaces, GitHub Pages +""" +import json +import os +from typing import Dict +import httpx +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + +DEPLOY_SYSTEM = """You are a deployment engineer expert in: +- Vercel deployments (Next.js, React, API routes) +- HuggingFace Spaces (Docker, Gradio, Streamlit) +- GitHub Pages (static sites) +- Docker containerization +- Environment variable management + +Generate precise deployment configs and commands. +""" + + +class DeployAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("DeployAgent", ws_manager, ai_router) + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + + await self.emit(task_id, "agent_start", {"agent": "DeployAgent", "task": task[:80]}, session_id) + + task_lower = task.lower() + if "vercel" in task_lower: + return await self._deploy_vercel_guide(task, task_id, session_id) + elif "huggingface" in task_lower or "hf" in task_lower or "space" in task_lower: + return await self._deploy_hf_guide(task, task_id, session_id) + elif "docker" in task_lower: + return await self._generate_dockerfile(task, task_id, session_id) + else: + return await self._deploy_general(task, task_id, session_id) + + async def _deploy_vercel_guide(self, task: str, task_id: str, session_id: str) -> str: + messages = [ + {"role": "system", "content": DEPLOY_SYSTEM}, + {"role": "user", "content": ( + f"Generate complete Vercel deployment guide for: {task}\n\n" + f"Include:\n1. vercel.json config\n2. Environment variables needed\n" + f"3. Build commands\n4. Step-by-step deployment instructions" + )}, + ] + result = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.2) + await self.emit(task_id, "deploy_plan_ready", {"platform": "vercel"}, session_id) + return result + + async def _deploy_hf_guide(self, task: str, task_id: str, session_id: str) -> str: + messages = [ + {"role": "system", "content": DEPLOY_SYSTEM}, + {"role": "user", "content": ( + f"Generate HuggingFace Space deployment for: {task}\n\n" + f"Include:\n1. Dockerfile\n2. README.md with YAML header\n" + f"3. Requirements/dependencies\n4. Space configuration" + )}, + ] + result = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.2) + await self.emit(task_id, "deploy_plan_ready", {"platform": "huggingface"}, session_id) + return result + + async def _generate_dockerfile(self, task: str, task_id: str, session_id: str) -> str: + messages = [ + {"role": "system", "content": DEPLOY_SYSTEM}, + {"role": "user", "content": f"Generate production Dockerfile for: {task}\nInclude multi-stage build, security best practices, health check."}, + ] + return await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.1) + + async def _deploy_general(self, task: str, task_id: str, session_id: str) -> str: + messages = [ + {"role": "system", "content": DEPLOY_SYSTEM}, + {"role": "user", "content": f"Create deployment plan for: {task}\n\nInclude platform recommendation, config files, and step-by-step guide."}, + ] + return await self.llm(messages, task_id=task_id, session_id=session_id) + + async def check_vercel_deployments(self) -> str: + """Check recent Vercel deployments.""" + token = os.environ.get("VERCEL_TOKEN", "") + if not token: + return "⚠️ VERCEL_TOKEN not set." + try: + async with httpx.AsyncClient(timeout=15) as client: + resp = await client.get( + "https://api.vercel.com/v6/deployments?limit=5", + headers={"Authorization": f"Bearer {token}"} + ) + if resp.status_code == 200: + deps = resp.json().get("deployments", []) + lines = [f"- {d.get('name')} β†’ {d.get('state')} ({d.get('url', '')})" for d in deps[:5]] + return "πŸš€ **Recent Vercel Deployments:**\n\n" + "\n".join(lines) + except Exception as e: + return f"❌ Vercel error: {e}" + return "No deployments found." diff --git a/agents/file_agent.py b/agents/file_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..051b107446953541b7fda90529203f13f5955f81 --- /dev/null +++ b/agents/file_agent.py @@ -0,0 +1,227 @@ +""" +FileAgent v7 β€” Autonomous file system management, code editing, project scaffolding +Like Devin's file browser + OneHand's file manipulation +""" +import asyncio +import json +import os +import re +import shutil +from pathlib import Path +from typing import Dict, List, Optional +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + +FILE_SYSTEM = """You are an elite file system and project management agent. +You can: +- Create, read, edit, delete files and directories +- Scaffold complete project structures +- Analyze codebases and suggest improvements +- Generate file trees and project maps +- Apply patches and diffs to code files +- Manage project configuration files + +Always provide complete, runnable file content. +""" + +WORKSPACE = os.environ.get("WORKSPACE_DIR", "/tmp/god_workspace") + +class FileAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("FileAgent", ws_manager, ai_router) + os.makedirs(WORKSPACE, exist_ok=True) + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + + await self.emit(task_id, "agent_start", {"agent": "FileAgent", "task": task[:80]}, session_id) + + task_lower = task.lower() + if any(k in task_lower for k in ["create", "scaffold", "generate", "new project", "init"]): + result = await self._scaffold_project(task, context, task_id, session_id) + elif any(k in task_lower for k in ["read", "show", "view", "list", "tree"]): + result = await self._read_or_list(task, task_id, session_id) + elif any(k in task_lower for k in ["edit", "modify", "update", "change", "fix", "patch"]): + result = await self._edit_file(task, context, task_id, session_id) + elif any(k in task_lower for k in ["delete", "remove", "clean"]): + result = await self._delete_files(task, task_id, session_id) + else: + result = await self._ai_file_task(task, context, task_id, session_id) + + return result + + async def _scaffold_project(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + """Scaffold a complete project structure.""" + await self.emit(task_id, "tool_called", { + "agent": "FileAgent", "tool": "scaffold_project", "step": "Generating project structure" + }, session_id) + + messages = [ + {"role": "system", "content": FILE_SYSTEM}, + {"role": "user", "content": ( + f"Task: {task}\n\n" + "Generate a complete project scaffold. Return a JSON object with this structure:\n" + "{\n" + ' "project_name": "name",\n' + ' "files": [\n' + ' {"path": "relative/path/file.ext", "content": "full file content here"}\n' + " ],\n" + ' "description": "what was created",\n' + ' "run_commands": ["npm install", "npm run dev"]\n' + "}\n" + "Include all necessary files for a working project." + )}, + ] + response = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.2, max_tokens=8192) + + # Parse JSON and create files + created_files = [] + try: + start = response.find("{") + end = response.rfind("}") + 1 + if start >= 0 and end > start: + data = json.loads(response[start:end]) + project_name = data.get("project_name", "project") + project_path = os.path.join(WORKSPACE, project_name) + os.makedirs(project_path, exist_ok=True) + + for f in data.get("files", []): + filepath = os.path.join(project_path, f["path"]) + os.makedirs(os.path.dirname(filepath), exist_ok=True) + with open(filepath, "w") as fp: + fp.write(f["content"]) + created_files.append(f["path"]) + await self.emit(task_id, "file_written", { + "path": f["path"], "size": len(f["content"]) + }, session_id) + + result = ( + f"βœ… **Project Scaffolded**: `{project_name}`\n\n" + f"**Files Created** ({len(created_files)}):\n" + + "\n".join(f"- `{f}`" for f in created_files) + + f"\n\n**Description:** {data.get('description', '')}\n\n" + + "**Run Commands:**\n" + + "\n".join(f"```\n{cmd}\n```" for cmd in data.get("run_commands", [])) + ) + return result + except Exception as e: + log.warning("Failed to parse project scaffold JSON", error=str(e)) + + return response + + async def _read_or_list(self, task: str, task_id: str, session_id: str) -> str: + """Read files or list directory structure.""" + await self.emit(task_id, "tool_called", { + "agent": "FileAgent", "tool": "read_files", "step": "Reading file system" + }, session_id) + + # Extract path from task + path_match = re.search(r'["\']([^"\']+)["\']|(\S+\.\w+)', task) + target_path = path_match.group(1) or path_match.group(2) if path_match else WORKSPACE + + if not os.path.isabs(target_path): + target_path = os.path.join(WORKSPACE, target_path) + + if os.path.isdir(target_path): + tree = self._get_tree(target_path) + return f"**Directory Tree:** `{target_path}`\n\n```\n{tree}\n```" + elif os.path.isfile(target_path): + with open(target_path) as f: + content = f.read() + return f"**File:** `{target_path}`\n\n```\n{content[:4000]}\n```" + else: + # List workspace + tree = self._get_tree(WORKSPACE, max_depth=3) + return f"**Workspace:** `{WORKSPACE}`\n\n```\n{tree}\n```" + + async def _edit_file(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + """Edit a file based on instructions.""" + await self.emit(task_id, "tool_called", { + "agent": "FileAgent", "tool": "edit_file", "step": "Editing file" + }, session_id) + + messages = [ + {"role": "system", "content": FILE_SYSTEM}, + {"role": "user", "content": ( + f"Task: {task}\n\n" + f"Context: {json.dumps(context)[:500]}\n\n" + "Return a JSON with: {\"filepath\": \"path\", \"content\": \"full new file content\", \"explanation\": \"what changed\"}" + )}, + ] + response = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.1, max_tokens=8192) + + try: + start = response.find("{") + end = response.rfind("}") + 1 + if start >= 0 and end > start: + data = json.loads(response[start:end]) + filepath = os.path.join(WORKSPACE, data.get("filepath", "output.txt")) + os.makedirs(os.path.dirname(filepath), exist_ok=True) + with open(filepath, "w") as f: + f.write(data.get("content", "")) + await self.emit(task_id, "file_written", {"path": data.get("filepath"), "edited": True}, session_id) + return f"βœ… **File Edited:** `{data.get('filepath')}`\n\n{data.get('explanation', '')}" + except Exception: + pass + return response + + async def _delete_files(self, task: str, task_id: str, session_id: str) -> str: + """Delete files or directories safely.""" + path_match = re.search(r'["\']([^"\']+)["\']', task) + if path_match: + target = os.path.join(WORKSPACE, path_match.group(1)) + if os.path.exists(target) and WORKSPACE in target: + if os.path.isdir(target): + shutil.rmtree(target) + else: + os.remove(target) + await self.emit(task_id, "file_deleted", {"path": path_match.group(1)}, session_id) + return f"βœ… Deleted: `{path_match.group(1)}`" + return "❌ Could not find file/directory to delete. Please specify the path in quotes." + + async def _ai_file_task(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + """Handle generic file tasks via AI.""" + messages = [ + {"role": "system", "content": FILE_SYSTEM}, + {"role": "user", "content": f"Task: {task}\n\nWorkspace: {WORKSPACE}\n\nContext: {json.dumps(context)[:300]}"}, + ] + return await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.3, max_tokens=4096) + + def _get_tree(self, path: str, prefix: str = "", max_depth: int = 4, depth: int = 0) -> str: + """Generate a directory tree string.""" + if depth > max_depth: + return "" + try: + entries = sorted(os.scandir(path), key=lambda e: (e.is_file(), e.name)) + except PermissionError: + return "" + lines = [] + for i, entry in enumerate(entries): + if entry.name.startswith(".") and entry.name not in [".env", ".gitignore"]: + continue + connector = "└── " if i == len(entries) - 1 else "β”œβ”€β”€ " + lines.append(f"{prefix}{connector}{entry.name}") + if entry.is_dir() and depth < max_depth: + extension = " " if i == len(entries) - 1 else "β”‚ " + subtree = self._get_tree(entry.path, prefix + extension, max_depth, depth + 1) + if subtree: + lines.append(subtree) + return "\n".join(lines) + + def list_workspace(self) -> Dict: + """List all workspace files.""" + files = [] + for root, dirs, filenames in os.walk(WORKSPACE): + dirs[:] = [d for d in dirs if not d.startswith(".") and d != "node_modules"] + for f in filenames: + full_path = os.path.join(root, f) + rel_path = os.path.relpath(full_path, WORKSPACE) + try: + size = os.path.getsize(full_path) + files.append({"path": rel_path, "size": size}) + except Exception: + pass + return {"workspace": WORKSPACE, "files": files, "total": len(files)} diff --git a/agents/git_agent.py b/agents/git_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..aed7ac38e43282cc4c5d2885c825eeb59dddb4a5 --- /dev/null +++ b/agents/git_agent.py @@ -0,0 +1,120 @@ +""" +GitAgent v7 β€” Autonomous Git operations, PR creation, code review +Full GitHub integration like Manus/Genspark autonomous coding +""" +import asyncio +import json +import os +import re +from typing import Dict, List +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + +GIT_SYSTEM = """You are an elite autonomous Git and GitHub operations agent. +You can clone, commit, push, pull, create branches, PRs, review code, +generate commit messages, manage workflows, and handle merge conflicts. +Always write clear, conventional commit messages (feat/fix/chore/docs/refactor). +""" + + +class GitAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("GitAgent", ws_manager, ai_router) + self.workspace = os.environ.get("WORKSPACE_DIR", "/tmp/god_workspace") + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + await self.emit(task_id, "agent_start", {"agent": "GitAgent", "task": task[:80]}, session_id) + + t = task.lower() + if any(k in t for k in ["clone", "checkout"]): + return await self._clone_repo(task, context, task_id, session_id) + if any(k in t for k in ["commit", "push", "pull request", " pr "]): + return await self._commit_and_push(task, context, task_id, session_id) + if any(k in t for k in ["review", "analyze code", "audit"]): + return await self._code_review(task, context, task_id, session_id) + return await self._git_ai_task(task, context, task_id, session_id) + + async def _clone_repo(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + urls = re.findall(r'https?://github\.com/[^\s]+', task) + if not urls: + return "❌ No GitHub URL found in task." + url = urls[0] + repo_name = url.rstrip("/").split("/")[-1].replace(".git", "") + dest = os.path.join(self.workspace, repo_name) + await self.emit(task_id, "tool_called", {"agent": "GitAgent", "tool": "git_clone", "step": f"Cloning {repo_name}"}, session_id) + token = os.environ.get("GITHUB_TOKEN", "") + auth_url = url.replace("https://", f"https://{token}@") if token else url + r = await self._run_cmd(["git", "clone", auth_url, dest]) + if r["returncode"] == 0: + await self.emit(task_id, "git_cloned", {"repo": repo_name, "path": dest}, session_id) + return f"βœ… **Cloned** `{repo_name}` β†’ `{dest}`\n```\n{r['stdout'][:500]}\n```" + return f"❌ Clone failed:\n```\n{r['stderr'][:500]}\n```" + + async def _commit_and_push(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + repo_path = context.get("repo_path", self.workspace) + await self.emit(task_id, "tool_called", {"agent": "GitAgent", "tool": "git_commit", "step": "Committing"}, session_id) + msgs = [ + {"role": "system", "content": "Generate a conventional commit message. Return ONLY the one-line message."}, + {"role": "user", "content": f"Task: {task}\nContext: {json.dumps(context)[:200]}"}, + ] + commit_msg = (await self.llm(msgs, task_id=task_id, session_id=session_id, temperature=0.3, max_tokens=80)).strip().split("\n")[0][:100] + results = [] + for cmd in [["git", "add", "-A"], ["git", "commit", "-m", commit_msg]]: + r = await self._run_cmd(cmd, cwd=repo_path) + results.append(f"$ {' '.join(cmd)}\n{r['stdout']}{r['stderr']}") + branch = context.get("branch", "main") + r = await self._run_cmd(["git", "push", "origin", branch], cwd=repo_path) + results.append(f"$ git push\n{r['stdout']}{r['stderr']}") + return f"βœ… **Committed:** `{commit_msg}`\n\n```\n" + "\n".join(results) + "\n```" + + async def _code_review(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + repo_path = context.get("repo_path", self.workspace) + diff = await self._run_cmd(["git", "diff", "HEAD~1"], cwd=repo_path) + msgs = [ + {"role": "system", "content": GIT_SYSTEM}, + {"role": "user", "content": f"Task: {task}\n\nDiff:\n{diff['stdout'][:2500]}\n\nProvide code review: summary, issues, suggestions, score (1-10)."}, + ] + return await self.llm(msgs, task_id=task_id, session_id=session_id, temperature=0.3, max_tokens=4096) + + async def _git_ai_task(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + msgs = [ + {"role": "system", "content": GIT_SYSTEM}, + {"role": "user", "content": f"Task: {task}\nWorkspace: {self.workspace}\nContext: {json.dumps(context)[:300]}"}, + ] + return await self.llm(msgs, task_id=task_id, session_id=session_id, temperature=0.3, max_tokens=4096) + + async def create_github_pr(self, repo_owner: str, repo_name: str, title: str, body: str, + head_branch: str, base_branch: str = "main", + task_id: str = "", session_id: str = "") -> Dict: + import httpx + token = os.environ.get("GITHUB_TOKEN", "") + if not token: + return {"error": "GITHUB_TOKEN not set"} + async with httpx.AsyncClient(timeout=30) as client: + resp = await client.post( + f"https://api.github.com/repos/{repo_owner}/{repo_name}/pulls", + headers={"Authorization": f"token {token}", "Accept": "application/vnd.github.v3+json"}, + json={"title": title, "body": body, "head": head_branch, "base": base_branch}, + ) + data = resp.json() + if resp.status_code == 201: + await self.emit(task_id, "pr_created", {"url": data.get("html_url")}, session_id) + return {"success": True, "url": data.get("html_url")} + return {"error": data.get("message", "Failed"), "status": resp.status_code} + + async def _run_cmd(self, cmd: List[str], cwd: str = None, timeout: int = 60) -> Dict: + try: + proc = await asyncio.create_subprocess_exec( + *cmd, cwd=cwd or self.workspace, + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + return {"returncode": proc.returncode, + "stdout": stdout.decode("utf-8", errors="replace"), + "stderr": stderr.decode("utf-8", errors="replace")} + except Exception as e: + return {"returncode": -1, "stdout": "", "stderr": str(e)} diff --git a/agents/memory_agent.py b/agents/memory_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..61df1cd7b236494aa94b2e78c2c3318775904310 --- /dev/null +++ b/agents/memory_agent.py @@ -0,0 +1,130 @@ +""" +MemoryAgent β€” Persistent long-term memory system (Phase 5) +SQLite-backed with semantic search simulation +""" +import json +import time +from typing import Dict, List, Optional +import structlog +from .base_agent import BaseAgent +from memory.db import save_memory, search_memory, get_history, get_project_memory + +log = structlog.get_logger() + + +class MemoryAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("MemoryAgent", ws_manager, ai_router) + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + + # Determine if retrieve or save + task_lower = task.lower() + if any(k in task_lower for k in ["remember", "save", "store", "record"]): + content = context.get("content", task) + await self.save(content, session_id=session_id, memory_type="user_directive") + return f"βœ… Saved to memory: {content[:100]}" + else: + results = await self.retrieve(task, session_id=session_id) + if results: + return "πŸ“š **Memory Retrieved:**\n\n" + "\n".join(f"- {r['content'][:200]}" for r in results[:5]) + return "No relevant memories found." + + async def save( + self, + content: str, + session_id: str = "", + project_id: str = "", + memory_type: str = "general", + key: str = "", + metadata: Dict = {}, + ): + """Save content to persistent memory.""" + await save_memory( + content=content, + memory_type=memory_type, + session_id=session_id, + project_id=project_id, + key=key, + metadata=metadata, + ) + + async def retrieve( + self, + query: str, + session_id: str = "", + project_id: str = "", + limit: int = 10, + ) -> List[Dict]: + """Retrieve relevant memories.""" + return await search_memory(query[:100], session_id=session_id, project_id=project_id, limit=limit) + + async def get_conversation_history(self, session_id: str, limit: int = 20) -> List[Dict]: + """Get conversation history for a session.""" + return await get_history(session_id, limit=limit) + + async def save_interaction( + self, + user_message: str, + assistant_response: str, + session_id: str = "", + intent: Dict = {}, + ): + """Save a full interaction to memory.""" + await save_memory( + content=user_message, + memory_type="conversation", + session_id=session_id, + key="user_message", + metadata={"intent": intent.get("intent", ""), "timestamp": time.time()}, + ) + await save_memory( + content=assistant_response, + memory_type="conversation", + session_id=session_id, + key="assistant_response", + metadata={"agent": intent.get("primary_agent", "chat"), "timestamp": time.time()}, + ) + + async def save_coding_style(self, style_notes: str, session_id: str = ""): + """Remember user's coding preferences.""" + await save_memory( + content=style_notes, + memory_type="user_preference", + session_id=session_id, + key="coding_style", + metadata={"category": "coding_style"}, + ) + + async def save_project_context(self, project_id: str, context: Dict): + """Save project-specific context.""" + await save_memory( + content=json.dumps(context), + memory_type="project_context", + project_id=project_id, + key="project_context", + metadata={"timestamp": time.time()}, + ) + + async def get_project_context(self, project_id: str) -> Optional[Dict]: + """Get project context.""" + results = await get_project_memory(project_id, memory_type="project_context", limit=1) + if results: + try: + return json.loads(results[0]["content"]) + except Exception: + return {"raw": results[0]["content"]} + return None + + async def build_context_for_agent(self, session_id: str, query: str) -> Dict: + """Build rich context dict for agents from memory.""" + history = await self.get_conversation_history(session_id, limit=10) + relevant = await self.retrieve(query, session_id=session_id, limit=5) + + return { + "history": [{"role": "user" if i % 2 == 0 else "assistant", "content": h["content"]} for i, h in enumerate(reversed(history))], + "relevant_memories": [r["content"][:200] for r in relevant], + "session_id": session_id, + } diff --git a/agents/orchestrator.py b/agents/orchestrator.py new file mode 100644 index 0000000000000000000000000000000000000000..f956908f3f6cc55eea1d175bdbf3a44ed43ae282 --- /dev/null +++ b/agents/orchestrator.py @@ -0,0 +1,248 @@ +""" +God Agent Orchestrator β€” Central Brain (Manus-style) +Routes user intent β†’ correct agent β†’ merges results +""" + +import asyncio +import json +import time +import uuid +from typing import Any, Dict, List, Optional + +import structlog + +log = structlog.get_logger() + +SYSTEM_PROMPT = """You are GOD AGENT β€” an elite autonomous AI operating system combining: +- Manus-style orchestration and planning +- Devin-style autonomous coding and debugging +- Genspark-style repo engineering + +You coordinate multiple specialized agents: +- ChatAgent: Conversation and clarification +- PlannerAgent: Break goals into executable task graphs +- CodingAgent: Generate, edit, refactor, fix code +- DebugAgent: Detect and auto-fix errors (self-healing) +- MemoryAgent: Persistent long-term memory +- ConnectorAgent: GitHub/HF/Vercel/n8n integrations +- DeployAgent: Automated deployments +- WorkflowAgent: n8n workflow generation +- SandboxAgent: VS Code sandbox execution + +You respond in Burmese or English based on user preference. +Always think step-by-step. Be autonomous, decisive, and thorough. +""" + + +class GodAgentOrchestrator: + """ + Central orchestrator β€” routes tasks to specialized agents, + merges results, manages agent collaboration. + """ + + def __init__(self, ws_manager=None, ai_router=None): + self.ws = ws_manager + self.ai_router = ai_router + self._agents: Dict[str, Any] = {} + self._active_tasks: Dict[str, Dict] = {} + + def register_agent(self, name: str, agent): + self._agents[name] = agent + log.info("Agent registered", agent=name) + + def get_agent(self, name: str): + return self._agents.get(name) + + # ─── Intent Classification ──────────────────────────────────────────────── + + async def classify_intent(self, user_message: str) -> Dict: + """Classify user intent to route to correct agent(s).""" + classify_prompt = f"""Classify this user request and identify which agents are needed. + +User message: "{user_message}" + +Respond ONLY with JSON: +{{ + "primary_agent": "chat|planner|coding|debug|connector|deploy|workflow|sandbox|memory", + "secondary_agents": [], + "intent": "brief description", + "requires_planning": true/false, + "is_code_task": true/false, + "is_deployment": true/false, + "is_workflow": true/false, + "language": "en|my", + "complexity": "simple|moderate|complex" +}}""" + + if self.ai_router: + messages = [ + {"role": "system", "content": "You are an intent classifier. Return only valid JSON."}, + {"role": "user", "content": classify_prompt}, + ] + raw = await self.ai_router.complete(messages, temperature=0.1, max_tokens=300) + try: + start = raw.find("{") + end = raw.rfind("}") + 1 + if start >= 0 and end > start: + return json.loads(raw[start:end]) + except Exception: + pass + + # Fallback heuristic classification + msg_lower = user_message.lower() + is_code = any(k in msg_lower for k in ["code", "build", "create", "write", "fix", "debug", "api", "function", "class", "script", "app"]) + is_deploy = any(k in msg_lower for k in ["deploy", "vercel", "github", "push", "publish", "release"]) + is_workflow = any(k in msg_lower for k in ["workflow", "n8n", "automate", "trigger", "pipeline", "schedule"]) + is_memory = any(k in msg_lower for k in ["remember", "recall", "memory", "history", "previous"]) + + primary = "coding" if is_code else ("deploy" if is_deploy else ("workflow" if is_workflow else ("memory" if is_memory else "chat"))) + + return { + "primary_agent": primary, + "secondary_agents": [], + "intent": user_message[:80], + "requires_planning": is_code or is_deploy, + "is_code_task": is_code, + "is_deployment": is_deploy, + "is_workflow": is_workflow, + "language": "my" if any(c > "\u1000" for c in user_message) else "en", + "complexity": "complex" if len(user_message) > 200 else "moderate", + } + + # ─── Main Orchestration ─────────────────────────────────────────────────── + + async def orchestrate( + self, + user_message: str, + session_id: str = "", + task_id: str = "", + context: Dict = {}, + ) -> str: + """Main orchestration β€” classify β†’ route β†’ execute β†’ merge.""" + exec_id = task_id or f"orch_{uuid.uuid4().hex[:8]}" + + await self._emit(session_id, task_id, "orchestrator_start", { + "message": user_message[:100], + "session_id": session_id, + }) + + # 1. Classify intent + intent = await self.classify_intent(user_message) + log.info("Intent classified", **intent) + + await self._emit(session_id, task_id, "intent_classified", { + "primary_agent": intent["primary_agent"], + "complexity": intent["complexity"], + "language": intent["language"], + }) + + # 2. Build execution plan for complex tasks + if intent.get("requires_planning") and intent.get("complexity") == "complex": + planner = self._agents.get("planner") + if planner: + await self._emit(session_id, task_id, "agent_called", {"agent": "PlannerAgent"}) + plan = await planner.run(user_message, context=context, session_id=session_id, task_id=exec_id) + context["plan"] = plan + + # 3. Route to primary agent + primary_name = intent["primary_agent"] + primary_agent = self._agents.get(primary_name) or self._agents.get("chat") + + if not primary_agent: + return f"Agent '{primary_name}' not available." + + await self._emit(session_id, task_id, "agent_called", { + "agent": primary_name, + "intent": intent["intent"], + }) + + # 4. Execute primary agent + result = await primary_agent.run( + user_message, + context={**context, "intent": intent}, + session_id=session_id, + task_id=exec_id, + ) + + # 5. Run secondary agents in parallel if needed + secondary_results = [] + if intent.get("secondary_agents"): + tasks = [] + for agent_name in intent["secondary_agents"]: + agent = self._agents.get(agent_name) + if agent: + tasks.append(agent.run(user_message, context=context, session_id=session_id, task_id=exec_id)) + if tasks: + secondary_results = await asyncio.gather(*tasks, return_exceptions=True) + + # 6. Save to memory + memory_agent = self._agents.get("memory") + if memory_agent: + asyncio.create_task(memory_agent.save_interaction( + user_message=user_message, + assistant_response=result, + session_id=session_id, + intent=intent, + )) + + await self._emit(session_id, task_id, "orchestrator_complete", { + "primary_agent": primary_name, + "result_length": len(result), + }) + + return result + + # ─── Self-Healing Loop ──────────────────────────────────────────────────── + + async def self_heal( + self, + error: str, + original_task: str, + task_id: str = "", + session_id: str = "", + max_retries: int = 3, + ) -> str: + """Self-healing retry loop β€” automatically fix errors.""" + debug_agent = self._agents.get("debug") + if not debug_agent: + return f"Cannot self-heal: DebugAgent not available. Error: {error}" + + for attempt in range(1, max_retries + 1): + await self._emit(session_id, task_id, "self_heal_attempt", { + "attempt": attempt, + "max": max_retries, + "error": error[:200], + }) + fix = await debug_agent.run( + f"Fix this error: {error}\n\nOriginal task: {original_task}", + context={"attempt": attempt}, + session_id=session_id, + task_id=task_id, + ) + if fix and "❌" not in fix[:10]: + await self._emit(session_id, task_id, "self_heal_success", { + "attempt": attempt, + "fix": fix[:200], + }) + return fix + + return f"❌ Self-healing failed after {max_retries} attempts. Last error: {error}" + + # ─── Emit Helper ───────────────────────────────────────────────────────── + + async def _emit(self, session_id: str, task_id: str, event: str, data: Dict): + if not self.ws: + return + if task_id: + await self.ws.emit(task_id, event, data, session_id=session_id) + if session_id: + await self.ws.emit_chat(session_id, event, data) + + # ─── Agent Status ──────────────────────────────────────────────────────── + + def get_status(self) -> Dict: + return { + "agents": list(self._agents.keys()), + "active_tasks": len(self._active_tasks), + "total_agents": len(self._agents), + } diff --git a/agents/orchestrator_v7.py b/agents/orchestrator_v7.py new file mode 100644 index 0000000000000000000000000000000000000000..483c36bf023a0e4464485578f247c245b3db71f8 --- /dev/null +++ b/agents/orchestrator_v7.py @@ -0,0 +1,338 @@ +""" +God Agent Orchestrator v7 β€” True Autonomous Engineering OS +Manus + Genspark + Devin (OneHand) style multi-agent coordination +""" +import asyncio +import json +import time +import uuid +from typing import Any, Dict, List, Optional +import structlog + +log = structlog.get_logger() + +SYSTEM_PROMPT_V7 = """You are GOD AGENT v7 β€” the world's most advanced autonomous AI engineering operating system. + +You combine the best of: +- 🧠 **Manus** β€” Deep reasoning, multi-step planning, autonomous decision making +- ⚑ **Genspark** β€” Repository engineering, code generation at scale, multi-model AI routing +- πŸ€– **Devin/OneHand** β€” Self-healing code execution, real browser control, file system mastery + +Your specialized agent fleet (15 agents): +- **OrchestratorAgent** β€” Central brain, routes & coordinates all agents +- **PlannerAgent** β€” Breaks complex goals into executable task graphs +- **CodingAgent** β€” Generates production-quality code in any language +- **DebugAgent** β€” Autonomous error detection and self-healing +- **TestAgent** β€” Generates & runs comprehensive test suites +- **FileAgent** β€” Full file system control, project scaffolding +- **GitAgent** β€” Git operations, PR creation, code review +- **BrowserAgent** β€” Web research, scraping, web automation +- **VisionAgent** β€” UI/UX generation, design-to-code +- **SandboxAgent** β€” Code execution in isolated environments +- **DeployAgent** β€” Auto-deploy to Vercel, HF, Docker, AWS +- **ConnectorAgent** β€” External integrations (GitHub, Slack, Notion, etc.) +- **MemoryAgent** β€” Long-term memory and context management +- **WorkflowAgent** β€” n8n/automation workflow generation +- **UIAgent** β€” Real-time UI state updates + +Operating principles: +1. PLAN before executing β€” always create a step-by-step plan for complex tasks +2. EXECUTE autonomously β€” don't ask for confirmation on obvious steps +3. SELF-HEAL β€” when errors occur, automatically retry with corrections +4. PARALLELIZE β€” run independent tasks simultaneously +5. REMEMBER β€” store and recall relevant context from memory + +Respond in Burmese or English based on user language. +Be decisive, thorough, and production-focused. +""" + + +class GodAgentOrchestratorV7: + """ + v7 Central Orchestrator β€” 15-agent autonomous engineering OS. + Routes tasks, coordinates parallel execution, self-heals failures. + """ + + def __init__(self, ws_manager=None, ai_router=None): + self.ws = ws_manager + self.ai_router = ai_router + self._agents: Dict[str, Any] = {} + self._active_tasks: Dict[str, Dict] = {} + self._task_history: List[Dict] = [] + + def register_agent(self, name: str, agent): + self._agents[name] = agent + log.info("v7 Agent registered", agent=name) + + def get_agent(self, name: str): + return self._agents.get(name) + + # ── Intent Classification ─────────────────────────────────────────────── + + async def classify_intent_v7(self, user_message: str) -> Dict: + """Advanced intent classification for 15-agent routing.""" + classify_prompt = f"""Classify this user request for our 15-agent autonomous engineering OS. + +User message: "{user_message}" + +Available agents: orchestrator, planner, coding, debug, test, file, git, browser, vision, sandbox, deploy, connector, memory, workflow, ui + +Respond ONLY with valid JSON: +{{ + "primary_agent": "agent_name", + "secondary_agents": ["agent1", "agent2"], + "parallel_tasks": [], + "intent": "brief description", + "requires_planning": true/false, + "is_code_task": true/false, + "is_deployment": true/false, + "is_research": true/false, + "is_ui_task": true/false, + "is_git_task": true/false, + "is_test_task": true/false, + "language": "en|my", + "complexity": "simple|moderate|complex|ultra_complex", + "estimated_steps": 3 +}}""" + + if self.ai_router: + messages = [ + {"role": "system", "content": "You are an intent classifier. Return only valid JSON."}, + {"role": "user", "content": classify_prompt}, + ] + raw = await self.ai_router.complete(messages, temperature=0.1, max_tokens=400) + try: + start = raw.find("{") + end = raw.rfind("}") + 1 + if start >= 0 and end > start: + return json.loads(raw[start:end]) + except Exception: + pass + + # Heuristic fallback + msg = user_message.lower() + is_code = any(k in msg for k in ["code", "build", "create", "write", "fix", "debug", "api", "function", "class", "script", "app", "backend", "frontend"]) + is_deploy = any(k in msg for k in ["deploy", "vercel", "github", "push", "publish", "release", "hf", "hugging"]) + is_research = any(k in msg for k in ["research", "search", "find", "browse", "web", "scrape", "analyze"]) + is_ui = any(k in msg for k in ["ui", "ux", "design", "component", "dashboard", "landing", "page", "frontend"]) + is_git = any(k in msg for k in ["git", "commit", "pr", "pull request", "branch", "merge", "clone"]) + is_test = any(k in msg for k in ["test", "testing", "pytest", "jest", "coverage", "qa"]) + is_file = any(k in msg for k in ["file", "folder", "directory", "scaffold", "project structure"]) + is_workflow = any(k in msg for k in ["workflow", "n8n", "automate", "trigger", "pipeline"]) + is_memory = any(k in msg for k in ["remember", "recall", "memory", "history", "previous"]) + + primary = ( + "vision" if is_ui else + "git" if is_git else + "test" if is_test else + "browser" if is_research else + "file" if is_file else + "coding" if is_code else + "deploy" if is_deploy else + "workflow" if is_workflow else + "memory" if is_memory else + "chat" + ) + + secondary = [] + if is_code and is_deploy: + secondary.append("deploy") + if is_code and is_test: + secondary.append("test") + if is_code and is_git: + secondary.append("git") + + return { + "primary_agent": primary, + "secondary_agents": secondary, + "parallel_tasks": [], + "intent": user_message[:80], + "requires_planning": is_code or is_deploy or len(user_message) > 150, + "is_code_task": is_code, + "is_deployment": is_deploy, + "is_research": is_research, + "is_ui_task": is_ui, + "is_git_task": is_git, + "is_test_task": is_test, + "language": "my" if any(c > "\u1000" for c in user_message) else "en", + "complexity": "ultra_complex" if len(user_message) > 500 else ("complex" if len(user_message) > 200 else "moderate"), + "estimated_steps": 5 if is_code else 3, + } + + # ── Main Orchestration ───────────────────────────────────────────────── + + async def orchestrate( + self, + user_message: str, + session_id: str = "", + task_id: str = "", + context: Dict = {}, + ) -> str: + exec_id = task_id or f"v7_{uuid.uuid4().hex[:8]}" + + await self._emit(session_id, task_id, "orchestrator_start", { + "message": user_message[:100], + "version": "v7", + "agents_available": len(self._agents), + }) + + # 1. Classify intent + intent = await self.classify_intent_v7(user_message) + log.info("v7 Intent classified", **{k: v for k, v in intent.items() if k != "parallel_tasks"}) + + await self._emit(session_id, task_id, "intent_classified", { + "primary_agent": intent["primary_agent"], + "secondary_agents": intent["secondary_agents"], + "complexity": intent["complexity"], + "language": intent["language"], + }) + + # 2. Planning phase for complex tasks + plan_context = {} + if intent.get("requires_planning") and intent.get("complexity") in ("complex", "ultra_complex"): + planner = self._agents.get("planner") + if planner: + await self._emit(session_id, task_id, "agent_called", { + "agent": "PlannerAgent", "phase": "planning" + }) + try: + plan = await asyncio.wait_for( + planner.run(user_message, context={**context, "intent": intent}, + session_id=session_id, task_id=exec_id), + timeout=60 + ) + plan_context["plan"] = plan + await self._emit(session_id, task_id, "plan_ready", {"plan_length": len(plan)}) + except asyncio.TimeoutError: + log.warning("PlannerAgent timed out") + + # 3. Route to primary agent + primary_name = intent["primary_agent"] + primary_agent = self._agents.get(primary_name) or self._agents.get("chat") + + if not primary_agent: + return f"Agent '{primary_name}' not available." + + await self._emit(session_id, task_id, "agent_called", { + "agent": primary_name, + "intent": intent["intent"], + }) + + full_context = {**context, **plan_context, "intent": intent} + + # 4. Execute primary agent + try: + result = await asyncio.wait_for( + primary_agent.run(user_message, context=full_context, session_id=session_id, task_id=exec_id), + timeout=300 + ) + except asyncio.TimeoutError: + result = f"⚠️ Primary agent ({primary_name}) timed out. Please try a more specific request." + except Exception as e: + log.error("Primary agent error", agent=primary_name, error=str(e)) + result = await self.self_heal(str(e), user_message, exec_id, session_id) + + # 5. Run secondary agents in parallel + if intent.get("secondary_agents"): + secondary_tasks = [] + for agent_name in intent["secondary_agents"]: + agent = self._agents.get(agent_name) + if agent: + secondary_tasks.append( + asyncio.wait_for( + agent.run(user_message, context={**full_context, "primary_result": result}, + session_id=session_id, task_id=exec_id), + timeout=120 + ) + ) + if secondary_tasks: + secondary_results = await asyncio.gather(*secondary_tasks, return_exceptions=True) + valid_results = [r for r in secondary_results if isinstance(r, str) and len(r) > 10] + if valid_results: + result += "\n\n---\n\n" + "\n\n".join(valid_results) + + # 6. Save to memory asynchronously + memory_agent = self._agents.get("memory") + if memory_agent: + asyncio.create_task(memory_agent.save_interaction( + user_message=user_message, + assistant_response=result, + session_id=session_id, + intent=intent, + )) + + # 7. Record in task history + self._task_history.append({ + "id": exec_id, + "message": user_message[:200], + "primary_agent": primary_name, + "result_length": len(result), + "timestamp": time.time(), + }) + if len(self._task_history) > 100: + self._task_history = self._task_history[-100:] + + await self._emit(session_id, task_id, "orchestrator_complete", { + "primary_agent": primary_name, + "result_length": len(result), + "version": "v7", + }) + + return result + + # ── Self-Healing ─────────────────────────────────────────────────────── + + async def self_heal(self, error: str, original_task: str, task_id: str = "", + session_id: str = "", max_retries: int = 3) -> str: + debug_agent = self._agents.get("debug") + if not debug_agent: + return f"❌ Self-heal unavailable. Error: {error}" + + for attempt in range(1, max_retries + 1): + await self._emit(session_id, task_id, "self_heal_attempt", { + "attempt": attempt, "max": max_retries, "error": error[:200] + }) + try: + fix = await asyncio.wait_for( + debug_agent.run( + f"Fix this error: {error}\n\nOriginal task: {original_task}", + context={"attempt": attempt, "error": error}, + session_id=session_id, task_id=task_id, + ), + timeout=60 + ) + if fix and "❌" not in fix[:10]: + await self._emit(session_id, task_id, "self_heal_success", {"attempt": attempt}) + return fix + except Exception as e: + log.warning("Self-heal attempt failed", attempt=attempt, error=str(e)) + + return f"❌ Self-healing failed after {max_retries} attempts. Error: {error}" + + # ── Helpers ──────────────────────────────────────────────────────────── + + async def _emit(self, session_id: str, task_id: str, event: str, data: Dict): + if not self.ws: + return + try: + if task_id: + await self.ws.emit(task_id, event, data, session_id=session_id) + if session_id: + await self.ws.emit_chat(session_id, event, data) + except Exception: + pass + + def get_status(self) -> Dict: + return { + "version": "7.0.0", + "agents": list(self._agents.keys()), + "total_agents": len(self._agents), + "active_tasks": len(self._active_tasks), + "tasks_completed": len(self._task_history), + "capabilities": [ + "autonomous_planning", "multi_agent_parallel", + "self_healing", "web_browsing", "file_management", + "git_operations", "ui_generation", "test_generation", + "deployment", "memory", "workflow_automation" + ], + } diff --git a/agents/planner_agent.py b/agents/planner_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..1c3cf0124a2237738d9295d38a4d8db5a9743820 --- /dev/null +++ b/agents/planner_agent.py @@ -0,0 +1,127 @@ +""" +PlannerAgent β€” Breaks goals into executable task graphs (Manus-style) +""" +import json +from typing import Dict, List +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + +PLANNER_SYSTEM = """You are an elite software architect and project planner. +Break down user goals into detailed, executable task graphs. +Think like Devin/Manus β€” autonomous, thorough, production-minded. + +Always respond with valid JSON task plans. +""" + +PLANNER_PROMPT = """Break this goal into a detailed execution plan. + +Goal: {goal} +Context: {context} + +Respond ONLY with valid JSON: +{{ + "title": "Plan title", + "steps": [ + {{ + "id": "step_1", + "name": "Step name", + "description": "Detailed description", + "agent": "coding|debug|connector|deploy|workflow|sandbox|memory", + "tool": "code|shell|file|github|memory|search|test|none", + "depends_on": [], + "estimated_seconds": 15, + "can_parallel": false + }} + ], + "estimated_duration": 120, + "tools_needed": ["code", "shell"], + "agents_needed": ["coding", "debug"], + "complexity": "simple|moderate|complex" +}}""" + + +class PlannerAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("PlannerAgent", ws_manager, ai_router) + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + + await self.emit(task_id, "agent_start", {"agent": "PlannerAgent", "goal": task[:80]}, session_id) + + ctx_str = json.dumps(context.get("memory", []))[:300] if context.get("memory") else "No prior context" + prompt = PLANNER_PROMPT.format(goal=task, context=ctx_str) + + messages = [ + {"role": "system", "content": PLANNER_SYSTEM}, + {"role": "user", "content": prompt}, + ] + + raw = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.2, max_tokens=2000) + + try: + start = raw.find("{") + end = raw.rfind("}") + 1 + if start >= 0 and end > start: + plan = json.loads(raw[start:end]) + else: + plan = json.loads(raw) + + await self.emit(task_id, "plan_ready", { + "title": plan.get("title", "Execution Plan"), + "steps": len(plan.get("steps", [])), + "estimated_duration": plan.get("estimated_duration", 60), + "agents_needed": plan.get("agents_needed", []), + }, session_id) + + return json.dumps(plan) + + except Exception as e: + log.warning("Plan parse failed", error=str(e)) + fallback = self._fallback_plan(task) + await self.emit(task_id, "plan_ready", {"title": "Fallback Plan", "steps": len(fallback["steps"])}, session_id) + return json.dumps(fallback) + + def _fallback_plan(self, goal: str) -> Dict: + return { + "title": f"Plan: {goal[:50]}", + "steps": [ + {"id": "step_1", "name": "Analyze Requirements", "description": f"Analyze: {goal[:60]}", "agent": "coding", "tool": "none", "depends_on": [], "estimated_seconds": 10, "can_parallel": False}, + {"id": "step_2", "name": "Design Solution", "description": "Design the architecture", "agent": "coding", "tool": "code", "depends_on": ["step_1"], "estimated_seconds": 20, "can_parallel": False}, + {"id": "step_3", "name": "Implement", "description": "Write implementation code", "agent": "coding", "tool": "code", "depends_on": ["step_2"], "estimated_seconds": 30, "can_parallel": False}, + {"id": "step_4", "name": "Test & Debug", "description": "Test and fix errors", "agent": "debug", "tool": "test", "depends_on": ["step_3"], "estimated_seconds": 20, "can_parallel": False}, + {"id": "step_5", "name": "Document", "description": "Write documentation", "agent": "coding", "tool": "file", "depends_on": ["step_4"], "estimated_seconds": 10, "can_parallel": True}, + ], + "estimated_duration": 90, + "tools_needed": ["code", "test"], + "agents_needed": ["coding", "debug"], + "complexity": "moderate", + } + + async def build_task_graph(self, plan_json: str) -> List[Dict]: + """Build execution order respecting dependencies.""" + try: + plan = json.loads(plan_json) + steps = plan.get("steps", []) + # Topological sort by dependencies + ordered = [] + visited = set() + + def visit(step_id): + if step_id in visited: + return + visited.add(step_id) + step = next((s for s in steps if s["id"] == step_id), None) + if step: + for dep in step.get("depends_on", []): + visit(dep) + ordered.append(step) + + for step in steps: + visit(step["id"]) + return ordered + except Exception: + return [] diff --git a/agents/reasoning_agent.py b/agents/reasoning_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..7fe161f0522979c7bb665501c611d6f35e1102aa --- /dev/null +++ b/agents/reasoning_agent.py @@ -0,0 +1,173 @@ +""" +ReasoningAgent v7 β€” Complex reasoning, analysis, and multi-step problem solving +GOD AGENT OS β€” Using DeepSeek R1, Qwen QwQ, o1-mini style reasoning +""" + +import asyncio +import json +from typing import Dict, Any, List, Optional + +import structlog + +from .base_agent import BaseAgent + +log = structlog.get_logger() + +REASONING_SYSTEM = """You are an elite autonomous reasoning and analysis agent. +You excel at: +- Multi-step reasoning with chain-of-thought +- Complex problem decomposition and analysis +- Mathematical and logical reasoning +- Strategic planning and decision making +- Root cause analysis + +Always think step by step, show your reasoning, and provide confident conclusions. +""" + + +class ReasoningAgent(BaseAgent): + """ + Specialized agent for complex reasoning, analysis, and problem-solving tasks. + + Capabilities: + - Multi-step reasoning with chain-of-thought + - Complex problem decomposition + - Mathematical reasoning + - Logical analysis + - Strategic planning + """ + + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("ReasoningAgent", ws_manager, ai_router) + self.reasoning_depth = 3 + self.max_reasoning_tokens = 16000 + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + """Execute reasoning task.""" + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + + await self.emit(task_id, "agent_start", { + "agent": "ReasoningAgent", + "task": task[:80], + }, session_id) + + try: + # Step 1: Analyze the problem + analysis = await self._analyze_problem(task, context, task_id, session_id) + + # Step 2: Decompose if complex + if len(task.split()) > 30: + sub_problems = await self._decompose_problem(task, analysis, task_id, session_id) + solutions = [] + for sub in sub_problems[:3]: + sol = await self._solve_sub_problem(sub, context, task_id, session_id) + solutions.append(sol) + result = await self._synthesize_answer(task, analysis, sub_problems, solutions, task_id, session_id) + else: + result = analysis + + await self.emit(task_id, "reasoning_complete", { + "agent": "ReasoningAgent", + "reasoning_depth": self.reasoning_depth, + }, session_id) + + return result + + except Exception as e: + log.error("ReasoningAgent failed", error=str(e)) + return "Reasoning agent encountered an error: " + str(e) + + async def _analyze_problem(self, problem: str, context: Dict, task_id: str, session_id: str) -> str: + """Analyze the problem using reasoning model.""" + messages = [ + {"role": "system", "content": REASONING_SYSTEM}, + {"role": "user", "content": ( + "Analyze this problem step by step:\n\n" + + problem + "\n\n" + "Provide:\n" + "1. Problem type and complexity\n" + "2. Key constraints and requirements\n" + "3. Step-by-step solution\n" + "4. Final answer/recommendation" + )}, + ] + return await self.llm( + messages, + task_id=task_id, + session_id=session_id, + temperature=0.2, + max_tokens=self.max_reasoning_tokens, + ) + + async def _decompose_problem(self, problem: str, analysis: str, task_id: str, session_id: str) -> List[str]: + """Break down complex problem into sub-problems.""" + messages = [ + {"role": "system", "content": REASONING_SYSTEM}, + {"role": "user", "content": ( + "Break this complex problem into 3 specific sub-problems:\n\n" + + problem + "\n\n" + "Initial analysis: " + analysis[:500] + "\n\n" + "List each sub-problem on a new line starting with '1.', '2.', '3.'" + )}, + ] + raw = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.3, max_tokens=2000) + # Parse numbered sub-problems + lines = raw.split("\n") + sub_problems = [] + for line in lines: + line = line.strip() + if line and any(line.startswith(str(i) + ".") for i in range(1, 10)): + sub_problems.append(line) + return sub_problems if sub_problems else [problem] + + async def _solve_sub_problem(self, sub_problem: str, context: Dict, task_id: str, session_id: str) -> str: + """Solve individual sub-problem.""" + messages = [ + {"role": "system", "content": REASONING_SYSTEM}, + {"role": "user", "content": "Solve this specific problem:\n\n" + sub_problem}, + ] + return await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.2, max_tokens=4096) + + async def _synthesize_answer( + self, + original: str, + analysis: str, + sub_problems: List[str], + solutions: List[str], + task_id: str, + session_id: str, + ) -> str: + """Synthesize final answer from all reasoning steps.""" + solutions_text = "\n".join( + "Sub-problem " + str(i + 1) + ": " + sol[:300] + for i, sol in enumerate(solutions) + ) + messages = [ + {"role": "system", "content": REASONING_SYSTEM}, + {"role": "user", "content": ( + "Original problem: " + original + "\n\n" + "Analysis: " + analysis[:800] + "\n\n" + "Solutions to sub-problems:\n" + solutions_text + "\n\n" + "Provide a comprehensive final answer that integrates all insights." + )}, + ] + return await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.3, max_tokens=8192) + + def get_status(self) -> Dict[str, Any]: + """Get agent status.""" + return { + "name": self.name, + "status": "ready", + "capabilities": [ + "Multi-step reasoning", + "Problem decomposition", + "Mathematical reasoning", + "Logical analysis", + "Strategic planning", + ], + "reasoning_depth": self.reasoning_depth, + } + + +__all__ = ["ReasoningAgent"] diff --git a/agents/sandbox_agent.py b/agents/sandbox_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..6eb5028265bf3dc224dd6f1d2c0bd15efcda2076 --- /dev/null +++ b/agents/sandbox_agent.py @@ -0,0 +1,249 @@ +""" +SandboxAgent β€” Persistent VS Code sandbox execution (Devin-style) +Controls file system, terminal, git operations in workspace +""" +import asyncio +import json +import os +import subprocess +import tempfile +from typing import Dict, List, Optional +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + +WORKSPACE = os.environ.get("WORKSPACE_DIR", "/tmp/god_workspace") + + +class SandboxAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("SandboxAgent", ws_manager, ai_router) + os.makedirs(WORKSPACE, exist_ok=True) + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + + task_lower = task.lower() + + if "execute" in task_lower or "run" in task_lower or "terminal" in task_lower: + cmd = context.get("command", task) + return await self.execute(cmd, task_id=task_id, session_id=session_id) + elif "write file" in task_lower or "create file" in task_lower: + filename = context.get("filename", "output.txt") + content = context.get("content", "") + return await self.write_file(filename, content, task_id=task_id, session_id=session_id) + elif "read file" in task_lower: + filename = context.get("filename", "") + return await self.read_file(filename, task_id=task_id, session_id=session_id) + elif "git" in task_lower: + return await self.git_operation(task, task_id=task_id, session_id=session_id) + else: + return await self.execute(task, task_id=task_id, session_id=session_id) + + # ─── Terminal Execution ─────────────────────────────────────────────────── + + async def execute( + self, + command: str, + cwd: str = "", + timeout: int = 30, + task_id: str = "", + session_id: str = "", + ) -> str: + """Execute shell command in sandbox workspace.""" + work_dir = cwd or WORKSPACE + + # Safety: block dangerous commands + blocked = ["rm -rf /", ":(){ :|:& };:", "mkfs", "shutdown", "reboot", "halt", "dd if=/dev/"] + for b in blocked: + if b in command: + return f"❌ Blocked dangerous command: {command[:50]}" + + await self.emit(task_id, "sandbox_exec", { + "command": command[:200], + "cwd": work_dir, + }, session_id) + + try: + proc = await asyncio.create_subprocess_shell( + command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=work_dir, + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + output = stdout.decode("utf-8", errors="replace") + err = stderr.decode("utf-8", errors="replace") + + result = output[:3000] + if err and proc.returncode != 0: + result += f"\n⚠️ stderr:\n{err[:500]}" + + await self.emit(task_id, "sandbox_result", { + "command": command[:100], + "exit_code": proc.returncode, + "output_length": len(output), + "success": proc.returncode == 0, + }, session_id) + + return result or f"Command executed (exit code: {proc.returncode})" + except asyncio.TimeoutError: + return f"⚠️ Command timed out after {timeout}s" + except Exception as e: + return f"❌ Execution error: {str(e)}" + + # ─── File Operations ────────────────────────────────────────────────────── + + async def write_file( + self, + filename: str, + content: str, + task_id: str = "", + session_id: str = "", + ) -> str: + """Write file to workspace.""" + filepath = os.path.join(WORKSPACE, filename) + os.makedirs(os.path.dirname(filepath), exist_ok=True) + try: + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + await self.emit(task_id, "file_written", { + "filename": filename, + "size": len(content), + "lines": len(content.split("\n")), + "path": filepath, + }, session_id) + return f"βœ… File written: `{filename}` ({len(content)} chars, {len(content.split(chr(10)))} lines)" + except Exception as e: + return f"❌ Write failed: {str(e)}" + + async def read_file( + self, + filename: str, + task_id: str = "", + session_id: str = "", + ) -> str: + """Read file from workspace.""" + filepath = os.path.join(WORKSPACE, filename) + try: + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + await self.emit(task_id, "file_read", { + "filename": filename, + "size": len(content), + }, session_id) + return content[:5000] + except FileNotFoundError: + return f"❌ File not found: {filename}" + except Exception as e: + return f"❌ Read failed: {str(e)}" + + async def list_files(self, path: str = "") -> List[str]: + """List files in workspace.""" + target = os.path.join(WORKSPACE, path) if path else WORKSPACE + try: + result = [] + for root, dirs, files in os.walk(target): + # Skip hidden and cache dirs + dirs[:] = [d for d in dirs if not d.startswith(".") and d != "__pycache__" and d != "node_modules"] + for f in files: + rel = os.path.relpath(os.path.join(root, f), WORKSPACE) + result.append(rel) + if len(result) > 100: + break + return result + except Exception: + return [] + + # ─── Git Operations ─────────────────────────────────────────────────────── + + async def git_operation( + self, + task: str, + repo_path: str = "", + task_id: str = "", + session_id: str = "", + ) -> str: + """Perform git operations in workspace.""" + work_dir = repo_path or WORKSPACE + task_lower = task.lower() + + if "clone" in task_lower: + # Extract URL + words = task.split() + urls = [w for w in words if "github.com" in w or "gitlab.com" in w or ".git" in w] + if urls: + url = urls[0] + return await self.execute(f"git clone {url}", cwd=WORKSPACE, task_id=task_id, session_id=session_id) + return "❌ No git URL found in task." + + elif "commit" in task_lower: + msg = task.replace("commit", "").strip() or "God Agent automated commit" + cmds = [ + "git add -A", + f'git commit -m "{msg}"', + ] + results = [] + for cmd in cmds: + r = await self.execute(cmd, cwd=work_dir, task_id=task_id, session_id=session_id) + results.append(r) + return "\n".join(results) + + elif "push" in task_lower: + return await self.execute("git push", cwd=work_dir, task_id=task_id, session_id=session_id) + + elif "status" in task_lower: + return await self.execute("git status", cwd=work_dir, task_id=task_id, session_id=session_id) + + elif "log" in task_lower: + return await self.execute("git log --oneline -10", cwd=work_dir, task_id=task_id, session_id=session_id) + + elif "init" in task_lower: + return await self.execute("git init && git add -A", cwd=work_dir, task_id=task_id, session_id=session_id) + + else: + return await self.execute(task, cwd=work_dir, task_id=task_id, session_id=session_id) + + # ─── Package Management ─────────────────────────────────────────────────── + + async def install_packages( + self, + packages: List[str], + manager: str = "pip", + task_id: str = "", + session_id: str = "", + ) -> str: + """Install packages in workspace.""" + pkg_str = " ".join(packages) + if manager == "pip": + cmd = f"pip install {pkg_str}" + elif manager == "npm": + cmd = f"npm install {pkg_str}" + elif manager == "pnpm": + cmd = f"pnpm add {pkg_str}" + else: + cmd = f"{manager} install {pkg_str}" + + await self.emit(task_id, "installing_packages", { + "manager": manager, + "packages": packages, + }, session_id) + return await self.execute(cmd, task_id=task_id, session_id=session_id) + + # ─── Workspace Info ─────────────────────────────────────────────────────── + + async def get_workspace_info(self) -> Dict: + """Get workspace status.""" + files = await self.list_files() + try: + disk = await self.execute("df -h /tmp | tail -1") + except Exception: + disk = "N/A" + return { + "path": WORKSPACE, + "file_count": len(files), + "files": files[:20], + "disk_usage": disk, + } diff --git a/agents/test_agent.py b/agents/test_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..d13129c8bf4880ef2def5345ccacbd4f2be06b05 --- /dev/null +++ b/agents/test_agent.py @@ -0,0 +1,103 @@ +""" +TestAgent v7 β€” Autonomous test generation, execution, and quality assurance +Like Devin's self-testing loop + Genspark's QA automation +""" +import asyncio +import json +import os +import re +from typing import Dict, List +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + +TEST_SYSTEM = """You are an elite autonomous test engineer. +You generate comprehensive tests: unit, integration, e2e, performance. +You analyze code for bugs, edge cases, and security vulnerabilities. +You write pytest (Python) and Jest/Vitest (TypeScript) tests. +Always aim for 80%+ test coverage and meaningful assertions. +""" + + +class TestAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("TestAgent", ws_manager, ai_router) + self.workspace = os.environ.get("WORKSPACE_DIR", "/tmp/god_workspace") + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + await self.emit(task_id, "agent_start", {"agent": "TestAgent", "task": task[:80]}, session_id) + + t = task.lower() + if any(k in t for k in ["generate test", "write test", "create test"]): + return await self._generate_tests(task, context, task_id, session_id) + if any(k in t for k in ["run test", "execute test", "pytest", "jest"]): + return await self._run_tests(task, context, task_id, session_id) + if any(k in t for k in ["coverage", "quality", "audit"]): + return await self._quality_audit(task, context, task_id, session_id) + return await self._generate_tests(task, context, task_id, session_id) + + async def _generate_tests(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + code = context.get("code", "") + language = context.get("language", "python") + await self.emit(task_id, "tool_called", { + "agent": "TestAgent", "tool": "generate_tests", "step": "Generating tests" + }, session_id) + msgs = [ + {"role": "system", "content": TEST_SYSTEM}, + {"role": "user", "content": ( + f"Task: {task}\nLanguage: {language}\n\n" + f"Code to test:\n{code[:3000] if code else 'Generate tests for: ' + task}\n\n" + "Generate comprehensive tests with:\n" + "1. Happy path tests\n2. Edge case tests\n3. Error handling tests\n" + "4. Mocks for external dependencies\n5. Clear test descriptions" + )}, + ] + result = await self.llm(msgs, task_id=task_id, session_id=session_id, temperature=0.2, max_tokens=8192) + # Save test file to workspace + test_filename = f"test_{re.sub(r'[^a-z0-9]', '_', task.lower()[:30])}.py" + test_path = os.path.join(self.workspace, "tests", test_filename) + os.makedirs(os.path.dirname(test_path), exist_ok=True) + code_blocks = re.findall(r'```(?:python|py)?\n(.*?)```', result, re.DOTALL) + if code_blocks: + with open(test_path, "w") as f: + f.write(code_blocks[0]) + await self.emit(task_id, "file_written", {"path": test_path}, session_id) + return result + + async def _run_tests(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + repo_path = context.get("repo_path", self.workspace) + await self.emit(task_id, "tool_called", { + "agent": "TestAgent", "tool": "run_tests", "step": "Executing tests" + }, session_id) + # Detect test runner + if os.path.exists(os.path.join(repo_path, "package.json")): + cmd = ["npm", "test", "--", "--watchAll=false"] + else: + cmd = ["python", "-m", "pytest", "-v", "--tb=short"] + try: + proc = await asyncio.create_subprocess_exec( + *cmd, cwd=repo_path, + stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120) + output = stdout.decode() + stderr.decode() + passed = len(re.findall(r'PASSED|βœ“|pass', output, re.I)) + failed = len(re.findall(r'FAILED|βœ—|fail', output, re.I)) + await self.emit(task_id, "tests_complete", {"passed": passed, "failed": failed}, session_id) + return f"**Test Results:** βœ… {passed} passed | ❌ {failed} failed\n\n```\n{output[:3000]}\n```" + except Exception as e: + return f"❌ Test run error: {str(e)}" + + async def _quality_audit(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + code = context.get("code", "") + msgs = [ + {"role": "system", "content": TEST_SYSTEM}, + {"role": "user", "content": ( + f"Task: {task}\n\nCode:\n{code[:3000]}\n\n" + "Provide quality audit: coverage estimate, complexity score, bugs found, security issues, and recommendations." + )}, + ] + return await self.llm(msgs, task_id=task_id, session_id=session_id, temperature=0.3, max_tokens=4096) diff --git a/agents/ui_agent.py b/agents/ui_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..7a965e4a70c54c338869953667a949ab3c8b3527 --- /dev/null +++ b/agents/ui_agent.py @@ -0,0 +1,45 @@ +""" +UIAgent β€” UI component generation (React/Next.js/Tailwind) +""" +from typing import Dict +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + +UI_SYSTEM = """You are an expert React/Next.js/Tailwind CSS UI engineer. +You generate beautiful, responsive, production-ready UI components. +Always use: +- TypeScript with proper types +- Tailwind CSS for styling +- shadcn/ui components when appropriate +- Framer Motion for animations +- Lucide React for icons +- Dark mode support +- Mobile-first responsive design +- Accessibility (aria labels, semantic HTML) +""" + + +class UIAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("UIAgent", ws_manager, ai_router) + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + + await self.emit(task_id, "agent_start", {"agent": "UIAgent", "task": task[:80]}, session_id) + + messages = [ + {"role": "system", "content": UI_SYSTEM}, + {"role": "user", "content": ( + f"Generate a complete, production-ready UI component for: {task}\n\n" + f"Requirements:\n" + f"- TypeScript\n- Tailwind CSS\n- Dark mode\n- Mobile responsive\n" + f"- Include all imports\n- Export as default" + )}, + ] + result = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.3, max_tokens=6000) + await self.emit(task_id, "ui_generated", {"agent": "UIAgent"}, session_id) + return result diff --git a/agents/vision_agent.py b/agents/vision_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..9308f8e3c395929068ebb0b466eedb80a05b85ea --- /dev/null +++ b/agents/vision_agent.py @@ -0,0 +1,109 @@ +""" +VisionAgent v7 β€” UI/UX generation, design-to-code, screenshot analysis +Converts designs, wireframes, and visual specs into real code +""" +import json +import os +from typing import Dict +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + +VISION_SYSTEM = """You are an elite UI/UX engineer and design-to-code specialist. +You can: +- Convert design mockups and wireframes into pixel-perfect React/Next.js components +- Generate beautiful, responsive UI components with Tailwind CSS +- Create complete page layouts with animations (Framer Motion) +- Analyze screenshots and reproduce UI components +- Generate design systems, color palettes, and typography scales +- Build dashboards, landing pages, admin panels, mobile-first UIs + +Always produce production-ready code with: +- Responsive design (mobile-first) +- Accessibility (ARIA labels, semantic HTML) +- Dark/light mode support +- Smooth animations and transitions +- TypeScript types +""" + + +class VisionAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("VisionAgent", ws_manager, ai_router) + self.workspace = os.environ.get("WORKSPACE_DIR", "/tmp/god_workspace") + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + await self.emit(task_id, "agent_start", {"agent": "VisionAgent", "task": task[:80]}, session_id) + + t = task.lower() + if any(k in t for k in ["dashboard", "admin", "panel"]): + return await self._generate_dashboard(task, context, task_id, session_id) + if any(k in t for k in ["landing", "homepage", "hero"]): + return await self._generate_landing_page(task, context, task_id, session_id) + if any(k in t for k in ["component", "button", "card", "form", "modal", "nav"]): + return await self._generate_component(task, context, task_id, session_id) + return await self._generate_ui(task, context, task_id, session_id) + + async def _generate_dashboard(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + await self.emit(task_id, "tool_called", { + "agent": "VisionAgent", "tool": "generate_dashboard", "step": "Generating dashboard UI" + }, session_id) + msgs = [ + {"role": "system", "content": VISION_SYSTEM}, + {"role": "user", "content": ( + f"Build a complete dashboard UI: {task}\n\n" + "Requirements:\n" + "- Dark theme with glass morphism effects\n" + "- Sidebar navigation with icons\n" + "- Header with user menu\n" + "- Stats cards with trends\n" + "- Data tables with sorting\n" + "- Charts/graphs area\n" + "- React + TypeScript + Tailwind CSS\n" + "Generate the complete component code." + )}, + ] + return await self.llm(msgs, task_id=task_id, session_id=session_id, temperature=0.4, max_tokens=8192) + + async def _generate_landing_page(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + await self.emit(task_id, "tool_called", { + "agent": "VisionAgent", "tool": "generate_landing", "step": "Generating landing page" + }, session_id) + msgs = [ + {"role": "system", "content": VISION_SYSTEM}, + {"role": "user", "content": ( + f"Create a stunning landing page: {task}\n\n" + "Include: Hero section, Features grid, Pricing table, Testimonials, CTA, Footer\n" + "Style: Modern, gradient backgrounds, glassmorphism, smooth animations\n" + "Tech: Next.js + TypeScript + Tailwind + Framer Motion" + )}, + ] + return await self.llm(msgs, task_id=task_id, session_id=session_id, temperature=0.5, max_tokens=8192) + + async def _generate_component(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + await self.emit(task_id, "tool_called", { + "agent": "VisionAgent", "tool": "generate_component", "step": "Generating UI component" + }, session_id) + msgs = [ + {"role": "system", "content": VISION_SYSTEM}, + {"role": "user", "content": ( + f"Create a polished UI component: {task}\n\n" + "Requirements:\n" + "- Fully typed with TypeScript\n" + "- Responsive and accessible\n" + "- Dark/light mode support\n" + "- Smooth hover/focus animations\n" + "- Props with sensible defaults" + )}, + ] + return await self.llm(msgs, task_id=task_id, session_id=session_id, temperature=0.4, max_tokens=8192) + + async def _generate_ui(self, task: str, context: Dict, task_id: str, session_id: str) -> str: + msgs = [ + {"role": "system", "content": VISION_SYSTEM}, + {"role": "user", "content": f"Generate UI for: {task}\n\nContext: {json.dumps(context)[:300]}"}, + ] + return await self.llm(msgs, task_id=task_id, session_id=session_id, temperature=0.5, max_tokens=8192) diff --git a/agents/workflow_agent.py b/agents/workflow_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..784b8b05a953c4dca132cb7702fc0fe796b56e27 --- /dev/null +++ b/agents/workflow_agent.py @@ -0,0 +1,163 @@ +""" +WorkflowAgent β€” n8n workflow generation, validation, deployment (Phase 7) +Workflow Factor OS merged into God Agent ecosystem +""" +import json +from typing import Dict, List +import structlog +from .base_agent import BaseAgent + +log = structlog.get_logger() + +WORKFLOW_SYSTEM = """You are an expert n8n workflow architect and automation engineer. +You design production-grade automation workflows for: +- Telegram/Discord bots with AI responses +- Data pipelines and ETL processes +- API integrations and webhooks +- Scheduled tasks and cron jobs +- Email/Slack/notification systems +- GitHub CI/CD triggers +- AI-powered automation chains + +Generate valid n8n workflow JSON that can be directly imported. +Think about error handling, retry logic, and edge cases. +""" + + +class WorkflowAgent(BaseAgent): + def __init__(self, ws_manager=None, ai_router=None): + super().__init__("WorkflowAgent", ws_manager, ai_router) + + async def run(self, task: str, context: Dict = {}, **kwargs) -> str: + session_id = kwargs.get("session_id", "") + task_id = kwargs.get("task_id", "") + + await self.emit(task_id, "agent_start", { + "agent": "WorkflowAgent", + "task": task[:80], + }, session_id) + + task_lower = task.lower() + + if "telegram" in task_lower: + return await self._telegram_workflow(task, task_id, session_id) + elif "discord" in task_lower: + return await self._discord_workflow(task, task_id, session_id) + elif "schedule" in task_lower or "cron" in task_lower: + return await self._scheduled_workflow(task, task_id, session_id) + elif "github" in task_lower and ("webhook" in task_lower or "trigger" in task_lower): + return await self._github_workflow(task, task_id, session_id) + else: + return await self._generic_workflow(task, task_id, session_id) + + async def _telegram_workflow(self, task: str, task_id: str, session_id: str) -> str: + messages = [ + {"role": "system", "content": WORKFLOW_SYSTEM}, + {"role": "user", "content": ( + f"Create a complete n8n workflow for: {task}\n\n" + f"Include:\n" + f"1. Telegram Trigger node (webhook)\n" + f"2. AI processing node (HTTP Request to API)\n" + f"3. Telegram Send Message node\n" + f"4. Error handling branch\n\n" + f"Also provide:\n" + f"- Setup instructions\n" + f"- Required environment variables\n" + f"- Testing steps\n" + f"- The n8n workflow JSON" + )}, + ] + result = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.2, max_tokens=6000) + await self.emit(task_id, "workflow_generated", { + "type": "telegram_bot", + "agent": "WorkflowAgent", + }, session_id) + return result + + async def _discord_workflow(self, task: str, task_id: str, session_id: str) -> str: + messages = [ + {"role": "system", "content": WORKFLOW_SYSTEM}, + {"role": "user", "content": ( + f"Create a complete n8n workflow for Discord automation: {task}\n\n" + f"Include Discord webhook integration, message processing, and response handling." + )}, + ] + result = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.2, max_tokens=5000) + await self.emit(task_id, "workflow_generated", {"type": "discord", "agent": "WorkflowAgent"}, session_id) + return result + + async def _scheduled_workflow(self, task: str, task_id: str, session_id: str) -> str: + messages = [ + {"role": "system", "content": WORKFLOW_SYSTEM}, + {"role": "user", "content": ( + f"Create a scheduled n8n workflow for: {task}\n\n" + f"Include cron trigger configuration, processing steps, and notification on completion/failure." + )}, + ] + result = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.2) + await self.emit(task_id, "workflow_generated", {"type": "scheduled", "agent": "WorkflowAgent"}, session_id) + return result + + async def _github_workflow(self, task: str, task_id: str, session_id: str) -> str: + messages = [ + {"role": "system", "content": WORKFLOW_SYSTEM}, + {"role": "user", "content": ( + f"Create a GitHub webhook n8n workflow for: {task}\n\n" + f"Include GitHub webhook trigger, event processing, and automated responses/actions." + )}, + ] + result = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.2) + await self.emit(task_id, "workflow_generated", {"type": "github_webhook", "agent": "WorkflowAgent"}, session_id) + return result + + async def _generic_workflow(self, task: str, task_id: str, session_id: str) -> str: + messages = [ + {"role": "system", "content": WORKFLOW_SYSTEM}, + {"role": "user", "content": ( + f"Design a complete automation workflow for: {task}\n\n" + f"Provide:\n" + f"1. Workflow architecture diagram (text)\n" + f"2. n8n node configuration\n" + f"3. Step-by-step setup guide\n" + f"4. Import-ready n8n JSON\n" + f"5. Testing checklist" + )}, + ] + result = await self.llm(messages, task_id=task_id, session_id=session_id, temperature=0.3, max_tokens=6000) + await self.emit(task_id, "workflow_generated", {"type": "custom", "agent": "WorkflowAgent"}, session_id) + return result + + async def validate_workflow(self, workflow_json: str) -> Dict: + """Validate n8n workflow JSON structure.""" + try: + workflow = json.loads(workflow_json) + nodes = workflow.get("nodes", []) + connections = workflow.get("connections", {}) + has_trigger = any( + n.get("type", "").startswith("n8n-nodes-base.") and + ("Trigger" in n.get("type", "") or n.get("type", "").endswith("trigger")) + for n in nodes + ) + return { + "valid": True, + "node_count": len(nodes), + "has_trigger": has_trigger, + "connection_count": len(connections), + } + except json.JSONDecodeError as e: + return {"valid": False, "error": f"Invalid JSON: {e}"} + except Exception as e: + return {"valid": False, "error": str(e)} + + async def simulate_workflow(self, workflow_json: str, test_data: Dict = {}) -> str: + """Simulate workflow execution with test data.""" + messages = [ + {"role": "system", "content": WORKFLOW_SYSTEM}, + {"role": "user", "content": ( + f"Simulate this n8n workflow execution with test data:\n\n" + f"Workflow: {workflow_json[:1000]}\n\n" + f"Test data: {json.dumps(test_data)}\n\n" + f"Walk through each node's execution and expected output." + )}, + ] + return await self.llm(messages, temperature=0.3) diff --git a/ai_router/__init__.py b/ai_router/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1be3e4b4826a4012f18e6387c85958be1dfdbf7c --- /dev/null +++ b/ai_router/__init__.py @@ -0,0 +1,6 @@ +# Multi-Model AI Router β€” GOD AGENT OS v8 +# Primary: Gemini β†’ Sambanova β†’ GitHub Models (task-aware rotation) +from .router import AIRouter +from .router_v8 import GodModeRouter, get_router, classify_task, get_provider_order + +__all__ = ["AIRouter", "GodModeRouter", "get_router", "classify_task", "get_provider_order"] diff --git a/ai_router/key_pool.py b/ai_router/key_pool.py new file mode 100644 index 0000000000000000000000000000000000000000..0c1237575e84aa92346cef609a86bb498c9edcce --- /dev/null +++ b/ai_router/key_pool.py @@ -0,0 +1,131 @@ +""" +KeyPool β€” Multi-API Key Manager with Failover & Cooldown +God Agent OS v8 β€” Supports Gemini, SambaNova, GitHub, OpenAI, Groq, etc. +Each provider can have multiple comma-separated keys. +""" + +import asyncio +import time +from collections import defaultdict +from typing import Dict, List, Optional +import structlog + +log = structlog.get_logger() + +COOLDOWN_SECONDS = 60 # Key cooling time after max failures +MAX_FAILURES = 3 # Max fails before cooldown + + +class KeyEntry: + def __init__(self, key: str): + self.key = key + self.failures = 0 + self.cooldown_until = 0.0 + self.calls = 0 + self.last_used = 0.0 + + def is_available(self) -> bool: + if self.cooldown_until > time.time(): + return False + return True + + def mark_fail(self): + self.failures += 1 + if self.failures >= MAX_FAILURES: + self.cooldown_until = time.time() + COOLDOWN_SECONDS + log.warning("Key cooldown activated", failures=self.failures) + + def mark_success(self): + self.failures = max(0, self.failures - 1) + self.cooldown_until = 0.0 + self.calls += 1 + self.last_used = time.time() + + def status(self) -> dict: + remaining = max(0, self.cooldown_until - time.time()) + return { + "key_preview": self.key[:8] + "..." + self.key[-4:] if len(self.key) > 12 else "***", + "available": self.is_available(), + "failures": self.failures, + "calls": self.calls, + "cooldown_remaining_s": round(remaining, 1), + } + + +class KeyPool: + """ + Pool of API keys for a single provider. + Round-robins through available keys with failure tracking. + """ + + def __init__(self, provider: str, keys: List[str]): + self.provider = provider + self._keys: List[KeyEntry] = [KeyEntry(k.strip()) for k in keys if k.strip()] + self._index = 0 + + def __len__(self) -> int: + return len(self._keys) + + def pick(self) -> Optional[str]: + """Pick the next available key (round-robin, skip cooling down keys).""" + if not self._keys: + return None + n = len(self._keys) + for _ in range(n): + entry = self._keys[self._index % n] + self._index = (self._index + 1) % n + if entry.is_available(): + return entry.key + # All keys in cooldown β€” try the one with shortest cooldown + soonest = min(self._keys, key=lambda e: e.cooldown_until) + log.warning( + "All keys in cooldown, using soonest", + provider=self.provider, + cooldown_remaining=round(soonest.cooldown_until - time.time(), 1), + ) + return soonest.key + + def mark_fail(self, key: str): + for e in self._keys: + if e.key == key: + e.mark_fail() + return + + def mark_success(self, key: str): + for e in self._keys: + if e.key == key: + e.mark_success() + return + + def available_count(self) -> int: + return sum(1 for e in self._keys if e.is_available()) + + def status(self) -> dict: + return { + "provider": self.provider, + "total_keys": len(self._keys), + "available_keys": self.available_count(), + "keys": [e.status() for e in self._keys], + } + + +# ─── Global Key Pool Registry ───────────────────────────────────────────────── + +class KeyPoolRegistry: + """Central registry for all provider key pools.""" + + def __init__(self): + self._pools: Dict[str, KeyPool] = {} + + def register(self, provider: str, keys_csv: str): + """Register comma-separated keys for a provider.""" + keys = [k.strip() for k in keys_csv.split(",") if k.strip()] + if keys: + self._pools[provider] = KeyPool(provider, keys) + log.info("KeyPool registered", provider=provider, count=len(keys)) + + def get(self, provider: str) -> Optional[KeyPool]: + return self._pools.get(provider) + + def all_status(self) -> dict: + return {name: pool.status() for name, pool in self._pools.items()} diff --git a/ai_router/router.py b/ai_router/router.py new file mode 100644 index 0000000000000000000000000000000000000000..29c8a5551c84f1f5f391b1560fff2e0ee1f553dc --- /dev/null +++ b/ai_router/router.py @@ -0,0 +1,264 @@ +""" +Multi-Model AI Router β€” Phase 9 +Supports: OpenAI, Groq, Cerebras, OpenRouter, HuggingFace +Automatic failover chain: OpenAI β†’ Groq β†’ Cerebras β†’ OpenRouter β†’ HF +""" + +import asyncio +import json +import os +import time +from typing import Any, Dict, List, Optional + +import httpx +import structlog + +log = structlog.get_logger() + +# ─── Provider Config ────────────────────────────────────────────────────────── +PROVIDERS = [ + { + "name": "openai", + "key_env": "OPENAI_API_KEY", + "base_url": os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1"), + "default_model": os.environ.get("DEFAULT_MODEL", "gpt-4o"), + "headers_fn": lambda k: {"Authorization": f"Bearer {k}", "Content-Type": "application/json"}, + }, + { + "name": "groq", + "key_env": "GROQ_API_KEY", + "base_url": "https://api.groq.com/openai/v1", + "default_model": "llama-3.3-70b-versatile", + "headers_fn": lambda k: {"Authorization": f"Bearer {k}", "Content-Type": "application/json"}, + }, + { + "name": "cerebras", + "key_env": "CEREBRAS_API_KEY", + "base_url": "https://api.cerebras.ai/v1", + "default_model": "llama3.1-70b", + "headers_fn": lambda k: {"Authorization": f"Bearer {k}", "Content-Type": "application/json"}, + }, + { + "name": "openrouter", + "key_env": "OPENROUTER_API_KEY", + "base_url": "https://openrouter.ai/api/v1", + "default_model": "meta-llama/llama-3.3-70b-instruct:free", + "headers_fn": lambda k: { + "Authorization": f"Bearer {k}", + "Content-Type": "application/json", + "HTTP-Referer": "https://god-agent.ai", + "X-Title": "God Agent Platform", + }, + }, + { + "name": "anthropic", + "key_env": "ANTHROPIC_API_KEY", + "base_url": "https://api.anthropic.com/v1", + "default_model": "claude-3-5-sonnet-20241022", + "headers_fn": lambda k: { + "x-api-key": k, + "anthropic-version": "2023-06-01", + "Content-Type": "application/json", + }, + }, +] + + +class AIRouter: + """ + God Mode AI Router β€” automatically routes and fails over across providers. + Supports streaming token output via WebSocket. + """ + + def __init__(self, ws_manager=None): + self.ws = ws_manager + self._stats: Dict[str, Dict] = {p["name"]: {"calls": 0, "errors": 0, "latency": []} for p in PROVIDERS} + + def _get_provider(self, name: str) -> Optional[Dict]: + return next((p for p in PROVIDERS if p["name"] == name), None) + + def _available_providers(self) -> List[Dict]: + """Return providers with valid API keys, in priority order.""" + return [p for p in PROVIDERS if os.environ.get(p["key_env"], "")] + + # ─── Main Entry Point ───────────────────────────────────────────────────── + + async def complete( + self, + messages: List[Dict], + task_id: str = "", + session_id: str = "", + temperature: float = 0.7, + max_tokens: int = 4096, + preferred_model: str = "", + stream: bool = True, + ) -> str: + """Route completion through available providers with failover.""" + providers = self._available_providers() + + if not providers: + return await self._demo_stream(messages, task_id, session_id) + + last_error = None + for provider in providers: + try: + start = time.time() + if provider["name"] == "anthropic": + result = await self._anthropic_stream( + provider, messages, task_id, session_id, temperature, max_tokens + ) + else: + result = await self._openai_compat_stream( + provider, messages, task_id, session_id, temperature, max_tokens, preferred_model + ) + elapsed = time.time() - start + self._stats[provider["name"]]["calls"] += 1 + self._stats[provider["name"]]["latency"].append(elapsed) + log.info("AI Router success", provider=provider["name"], ms=round(elapsed * 1000)) + return result + except Exception as e: + last_error = e + self._stats[provider["name"]]["errors"] += 1 + log.warning("AI Router failover", provider=provider["name"], error=str(e)) + continue + + log.error("All AI providers failed", last_error=str(last_error)) + return await self._demo_stream(messages, task_id, session_id) + + # ─── OpenAI-compatible Stream (OpenAI, Groq, Cerebras, OpenRouter) ──────── + + async def _openai_compat_stream( + self, provider, messages, task_id, session_id, temperature, max_tokens, preferred_model + ) -> str: + key = os.environ.get(provider["key_env"], "") + model = preferred_model or provider["default_model"] + headers = provider["headers_fn"](key) + payload = { + "model": model, + "messages": messages, + "stream": True, + "temperature": temperature, + "max_tokens": max_tokens, + } + full_text = "" + async with httpx.AsyncClient(timeout=120) as client: + async with client.stream( + "POST", f"{provider['base_url']}/chat/completions", + headers=headers, json=payload + ) as resp: + resp.raise_for_status() + async for line in resp.aiter_lines(): + if not line.startswith("data:"): + continue + chunk = line[6:].strip() + if chunk == "[DONE]": + break + try: + data = json.loads(chunk) + delta = data["choices"][0]["delta"].get("content", "") + if delta: + full_text += delta + await self._emit_chunk(delta, task_id, session_id) + except Exception: + pass + return full_text + + # ─── Anthropic Stream ───────────────────────────────────────────────────── + + async def _anthropic_stream( + self, provider, messages, task_id, session_id, temperature, max_tokens + ) -> str: + key = os.environ.get(provider["key_env"], "") + headers = provider["headers_fn"](key) + system = "" + filtered = [] + for m in messages: + if m["role"] == "system": + system = m["content"] + else: + filtered.append(m) + payload = { + "model": provider["default_model"], + "max_tokens": max_tokens, + "messages": filtered, + "stream": True, + } + if system: + payload["system"] = system + full_text = "" + async with httpx.AsyncClient(timeout=120) as client: + async with client.stream( + "POST", f"{provider['base_url']}/messages", + headers=headers, json=payload + ) as resp: + resp.raise_for_status() + async for line in resp.aiter_lines(): + if not line.startswith("data:"): + continue + try: + data = json.loads(line[5:].strip()) + if data.get("type") == "content_block_delta": + delta = data["delta"].get("text", "") + if delta: + full_text += delta + await self._emit_chunk(delta, task_id, session_id) + except Exception: + pass + return full_text + + # ─── Demo Stream ────────────────────────────────────────────────────────── + + async def _demo_stream(self, messages, task_id, session_id) -> str: + last_user = next( + (m["content"] for m in reversed(messages) if m["role"] == "user"), "Hello" + ) + response = ( + f"πŸ€– **God Agent** (Demo Mode)\n\n" + f"Received: *{last_user[:100]}*\n\n" + f"To enable real AI, set one of these env vars:\n" + f"- `OPENAI_API_KEY` (GPT-4o)\n" + f"- `GROQ_API_KEY` (Llama 3.3 70B β€” Free)\n" + f"- `OPENROUTER_API_KEY` (Multi-model)\n" + f"- `ANTHROPIC_API_KEY` (Claude 3.5)\n\n" + f"**God Mode+ Capabilities Active:**\n" + f"- ⚑ Multi-agent orchestration\n" + f"- πŸ”§ Autonomous coding & debugging\n" + f"- 🧠 Persistent memory system\n" + f"- πŸ”Œ Connector ecosystem\n" + f"- πŸ“‘ Real-time streaming\n" + f"- 🌐 Multi-model failover\n" + ) + full_text = "" + for word in response.split(): + chunk = word + " " + full_text += chunk + await asyncio.sleep(0.02) + await self._emit_chunk(chunk, task_id, session_id, demo=True) + return full_text + + # ─── Emit Helper ────────────────────────────────────────────────────────── + + async def _emit_chunk(self, chunk: str, task_id: str, session_id: str, demo: bool = False): + if not self.ws: + return + payload = {"chunk": chunk, "demo": demo} + if task_id: + await self.ws.emit(task_id, "llm_chunk", payload, session_id=session_id) + if session_id and not task_id: + await self.ws.emit_chat(session_id, "llm_chunk", payload) + + # ─── Stats ──────────────────────────────────────────────────────────────── + + def get_stats(self) -> Dict: + stats = {} + for name, s in self._stats.items(): + avg_lat = round(sum(s["latency"][-20:]) / max(len(s["latency"][-20:]), 1) * 1000, 1) + stats[name] = { + "calls": s["calls"], + "errors": s["errors"], + "avg_latency_ms": avg_lat, + "available": bool(os.environ.get( + next((p["key_env"] for p in PROVIDERS if p["name"] == name), ""), "" + )), + } + return stats diff --git a/ai_router/router_v10.py b/ai_router/router_v10.py new file mode 100644 index 0000000000000000000000000000000000000000..f8c8de8cc59c5da9d2ea2a1785f3ae8ee3e7b02c --- /dev/null +++ b/ai_router/router_v10.py @@ -0,0 +1,493 @@ +""" +GOD AGENT OS β€” Multi-Provider AI Router v10 +Primary: Gemini (6 keys) β†’ SambaNova (9 keys) β†’ GitHub Models (9 keys) +Fallback: Groq β†’ OpenAI β†’ Demo +Auto-failover, round-robin KeyPool, task-type routing. +""" + +from __future__ import annotations + +import os +import time +from typing import Any, Dict, List, Optional, Tuple + +import httpx +import structlog + +log = structlog.get_logger() + +# ─── Provider Definitions ───────────────────────────────────────────────────── +PROVIDERS: Dict[str, Dict[str, Any]] = { + "gemini": { + "name": "gemini", + "type": "gemini", + "base_url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent", + "key_env": "GEMINI_KEY", + "default_model": "gemini-2.0-flash", + "max_tokens": 8192, + "priority_tasks": ["language", "research", "content", "general", "analysis"], + }, + "sambanova": { + "name": "sambanova", + "type": "openai", + "base_url": "https://api.sambanova.ai/v1", + "key_env": "SAMBANOVA_KEY", + "default_model": "Meta-Llama-3.3-70B-Instruct", + "max_tokens": 8192, + "priority_tasks": ["reasoning", "engineering", "planning", "analysis"], + }, + "github": { + "name": "github", + "type": "openai", + "base_url": "https://models.inference.ai.azure.com", + "key_env": "GITHUB_KEY", + "default_model": "gpt-4o", + "max_tokens": 4096, + "priority_tasks": ["planning", "engineering", "general"], + }, + "groq": { + "name": "groq", + "type": "openai", + "base_url": "https://api.groq.com/openai/v1", + "key_env": "GROQ_API_KEY", + "default_model": "llama-3.3-70b-versatile", + "max_tokens": 8192, + "priority_tasks": ["general"], + }, + "openai": { + "name": "openai", + "type": "openai", + "base_url": "https://api.openai.com/v1", + "key_env": "OPENAI_API_KEY", + "default_model": "gpt-4o", + "max_tokens": 4096, + "priority_tasks": ["general"], + }, +} + +PRIMARY_ORDER = ["gemini", "sambanova", "github"] +FALLBACK_ORDER = ["groq", "openai"] +KEY_COOLDOWN_SECONDS = 300 +KEY_MAX_FAILS = 3 +MAX_RETRIES_PER_KEY = 2 + + +# ─── KeyPool ────────────────────────────────────────────────────────────────── + +class KeyPool: + """Round-robin key pool with failure tracking and cooldown.""" + + def __init__(self, provider: str, raw_keys: str): + self.provider = provider + self._keys: List[Dict[str, Any]] = [] + for key in raw_keys.split(","): + key = key.strip() + if key: + self._keys.append({ + "key": key, + "fails": 0, + "cooldown_until": 0.0, + "calls": 0, + }) + log.info("key_pool_init", provider=provider, keys=len(self._keys)) + + def pick(self) -> Optional[Dict[str, Any]]: + now = time.time() + available = [item for item in self._keys if item["cooldown_until"] < now] + if not available: + # All in cooldown β€” return soonest + if self._keys: + soonest = min(self._keys, key=lambda x: x["cooldown_until"]) + log.warning("all_keys_cooldown", provider=self.provider, + wait_s=round(soonest["cooldown_until"] - now, 1)) + return soonest + return None + available.sort(key=lambda item: item["fails"]) + return available[0] + + def mark_fail(self, key_obj: Dict[str, Any]): + key_obj["fails"] += 1 + if key_obj["fails"] >= KEY_MAX_FAILS: + key_obj["cooldown_until"] = time.time() + KEY_COOLDOWN_SECONDS + log.warning("key_cooled_down", provider=self.provider, + key_prefix=key_obj["key"][:8]) + + def mark_success(self, key_obj: Dict[str, Any]): + key_obj["fails"] = 0 + key_obj["cooldown_until"] = 0.0 + key_obj["calls"] += 1 + + def has_keys(self) -> bool: + return bool(self._keys) + + def count(self) -> int: + return len(self._keys) + + def available_count(self) -> int: + now = time.time() + return sum(1 for k in self._keys if k["cooldown_until"] < now) + + def status(self) -> Dict[str, Any]: + now = time.time() + return { + "provider": self.provider, + "total_keys": len(self._keys), + "available_keys": self.available_count(), + "keys": [ + { + "key_prefix": k["key"][:8] + "...", + "fails": k["fails"], + "calls": k["calls"], + "available": k["cooldown_until"] < now, + "cooldown_s": max(0, round(k["cooldown_until"] - now, 1)), + } + for k in self._keys + ], + } + + +# ─── Task Classifier ────────────────────────────────────────────────────────── + +def classify_task(prompt: str = "") -> str: + p = (prompt or "").lower() + if any(w in p for w in ["code", "function", "implement", "build", "develop", "api", "class", "debug", "script", "program"]): + return "engineering" + if any(w in p for w in ["plan", "strategy", "workflow", "json", "automate", "pipeline", "step", "task"]): + return "planning" + if any(w in p for w in ["analyze", "reasoning", "why", "explain", "evaluate", "compare", "think"]): + return "reasoning" + if any(w in p for w in ["research", "find", "search", "discover", "investigate", "browse", "web"]): + return "research" + if any(w in p for w in ["write", "content", "blog", "article", "copy", "generate text", "summarize", "essay"]): + return "content" + if any(w in p for w in ["translate", "language", "convert", "myanmar", "burmese"]): + return "language" + if any(w in p for w in ["data", "csv", "metrics", "report", "insight", "chart", "graph"]): + return "analysis" + return "general" + + +def get_provider_order(task_type: str, preferred: str = "") -> List[str]: + ordered = sorted( + PRIMARY_ORDER, + key=lambda p: 0 if task_type in PROVIDERS[p]["priority_tasks"] else 1 + ) + result = ordered + [p for p in FALLBACK_ORDER if os.environ.get(PROVIDERS[p]["key_env"], "")] + if preferred and preferred in PROVIDERS and preferred in result: + result = [preferred] + [p for p in result if p != preferred] + return result + + +# ─── API Calls ──────────────────────────────────────────────────────────────── + +async def call_gemini( + base_url: str, key: str, + messages: List[Dict[str, str]], max_tokens: int +) -> Tuple[bool, str]: + url = f"{base_url}?key={key}" + # Build contents from messages + contents = [] + system_text = "" + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + if role == "system": + system_text = content + elif role == "user": + contents.append({"role": "user", "parts": [{"text": content}]}) + elif role == "assistant": + contents.append({"role": "model", "parts": [{"text": content}]}) + + if not contents: + contents = [{"role": "user", "parts": [{"text": "Hello"}]}] + + body: Dict[str, Any] = { + "contents": contents, + "generationConfig": { + "maxOutputTokens": min(max_tokens, 8192), + "temperature": 0.7, + }, + } + if system_text: + body["systemInstruction"] = {"parts": [{"text": system_text}]} + + try: + async with httpx.AsyncClient(timeout=60.0) as client: + resp = await client.post(url, json=body) + if resp.status_code == 200: + data = resp.json() + candidates = data.get("candidates", []) + if candidates: + text = candidates[0].get("content", {}).get("parts", [{}])[0].get("text", "") + if text: + return True, text + return False, f"Empty response: {str(data)[:200]}" + return False, f"HTTP {resp.status_code}: {resp.text[:300]}" + except Exception as exc: + return False, str(exc) + + +async def call_openai_compat( + base_url: str, key: str, model: str, + messages: List[Dict[str, str]], max_tokens: int +) -> Tuple[bool, str]: + url = f"{base_url}/chat/completions" + headers = { + "Authorization": f"Bearer {key}", + "Content-Type": "application/json", + } + body = { + "model": model, + "messages": messages, + "max_tokens": max_tokens, + "temperature": 0.7, + } + try: + async with httpx.AsyncClient(timeout=60.0) as client: + resp = await client.post(url, json=body, headers=headers) + if resp.status_code == 200: + data = resp.json() + choices = data.get("choices", []) + if choices: + content = choices[0].get("message", {}).get("content", "") + if content: + return True, content + return False, f"Empty choices: {str(data)[:200]}" + return False, f"HTTP {resp.status_code}: {resp.text[:300]}" + except Exception as exc: + return False, str(exc) + + +# ─── Main Router ────────────────────────────────────────────────────────────── + +class AIRouterV10: + """ + God Agent OS AI Router v10. + Multi-provider routing: Gemini (6 keys) β†’ SambaNova (9 keys) β†’ GitHub (9 keys) β†’ fallback. + """ + + VERSION = "10.0" + + def __init__(self, ws_manager=None): + self.ws = ws_manager + self._pools: Dict[str, KeyPool] = {} + self._stats: Dict[str, Dict[str, Any]] = { + name: {"calls": 0, "errors": 0, "latency_ms": [], "last_used": 0.0} + for name in PROVIDERS + } + self._load_pools() + + def _load_pools(self): + """Load key pools from environment variables.""" + for name, cfg in PROVIDERS.items(): + raw = os.environ.get(cfg["key_env"], "").strip() + if raw: + self._pools[name] = KeyPool(name, raw) + loaded = list(self._pools.keys()) + log.info("router_v10_loaded", providers=loaded, total=len(loaded)) + + def reload_pools(self): + """Hot-reload pools (e.g., after env var update).""" + self._pools.clear() + self._load_pools() + + def get_stats(self) -> Dict[str, Any]: + return { + name: { + "available": name in self._pools and self._pools[name].has_keys(), + "keys": self._pools[name].count() if name in self._pools else 0, + "calls": self._stats[name]["calls"], + "errors": self._stats[name]["errors"], + "avg_latency_ms": ( + round( + sum(self._stats[name]["latency_ms"][-20:]) / + max(len(self._stats[name]["latency_ms"][-20:]), 1), + 1, + ) + if self._stats[name]["latency_ms"] else 0 + ), + } + for name in PROVIDERS + } + + def get_pool_status(self) -> Dict[str, Any]: + return { + name: ( + self._pools[name].status() + if name in self._pools + else {"provider": name, "total_keys": 0, "available_keys": 0, "keys": []} + ) + for name in PROVIDERS + } + + def get_status(self) -> Dict[str, Any]: + return { + "version": self.VERSION, + "providers": self.get_stats(), + "primary_order": PRIMARY_ORDER, + "fallback_order": FALLBACK_ORDER, + "active_providers": [n for n in self._pools if self._pools[n].has_keys()], + } + + def _normalize_messages( + self, + messages: Optional[List[Dict[str, str]]] = None, + prompt: str = "", + system: str = "", + ) -> Tuple[List[Dict[str, str]], bool]: + if messages: + return messages, False + normalized: List[Dict[str, str]] = [] + if system: + normalized.append({"role": "system", "content": system}) + normalized.append({"role": "user", "content": prompt or "Hello"}) + return normalized, True + + async def complete( + self, + messages: Optional[List[Dict[str, str]]] = None, + task_id: str = "", + session_id: str = "", + temperature: float = 0.7, + max_tokens: int = 4096, + preferred_provider: str = "", + preferred_model: str = "", + stream: bool = False, + prompt: str = "", + system: str = "", + **_: Any, + ) -> Any: + normalized_messages, return_dict = self._normalize_messages( + messages=messages, prompt=prompt, system=system + ) + user_msg = next( + (msg.get("content", "") for msg in reversed(normalized_messages) if msg.get("role") == "user"), + "", + ) + task_type = classify_task(user_msg) + order = get_provider_order(task_type, preferred=preferred_provider) + + log.info("routing", task_type=task_type, order=order[:3], session=session_id[:8] if session_id else "") + last_error = "No providers available β€” set GEMINI_KEY, SAMBANOVA_KEY, or GITHUB_KEY" + + for provider_name in order: + if provider_name not in self._pools: + continue + + pool = self._pools[provider_name] + cfg = PROVIDERS[provider_name] + model = preferred_model or cfg["default_model"] + + for _attempt in range(MAX_RETRIES_PER_KEY): + key_obj = pool.pick() + if key_obj is None: + break + + t0 = time.time() + try: + if cfg["type"] == "gemini": + ok, text = await call_gemini( + cfg["base_url"], key_obj["key"], + normalized_messages, min(max_tokens, cfg["max_tokens"]) + ) + else: + ok, text = await call_openai_compat( + cfg["base_url"], key_obj["key"], model, + normalized_messages, min(max_tokens, cfg["max_tokens"]) + ) + + elapsed = int((time.time() - t0) * 1000) + + if ok and text.strip(): + pool.mark_success(key_obj) + self._stats[provider_name]["calls"] += 1 + self._stats[provider_name]["latency_ms"].append(elapsed) + self._stats[provider_name]["last_used"] = time.time() + log.info("llm_success", provider=provider_name, ms=elapsed, task_type=task_type) + + if self.ws and (task_id or session_id): + await self._emit_response(text, task_id, session_id) + + payload = { + "content": text, + "provider": provider_name, + "task_type": task_type, + "latency_ms": elapsed, + "model": model, + } + return payload if return_dict else text + + pool.mark_fail(key_obj) + last_error = text + self._stats[provider_name]["errors"] += 1 + log.warning("llm_fail", provider=provider_name, error=text[:150]) + + except Exception as exc: + pool.mark_fail(key_obj) + self._stats[provider_name]["errors"] += 1 + last_error = str(exc) + log.error("llm_exception", provider=provider_name, error=str(exc)[:200]) + + # All failed β€” return demo + demo = await self._demo_response(normalized_messages, task_type) + log.warning("all_providers_failed", last_error=last_error[:200]) + payload = { + "content": demo, + "provider": "demo", + "task_type": task_type, + "error": last_error, + } + return payload if return_dict else demo + + async def _emit_response(self, text: str, task_id: str, session_id: str): + """Emit response through WebSocket if available.""" + try: + if task_id and hasattr(self.ws, "emit"): + await self.ws.emit(task_id, "llm_response", {"content": text}, session_id=session_id) + elif session_id and hasattr(self.ws, "emit_chat"): + await self.ws.emit_chat(session_id, "llm_response", {"content": text}) + except Exception: + pass + + async def _demo_response(self, messages: List[Dict[str, str]], task_type: str) -> str: + user_msg = next( + (msg.get("content", "") for msg in reversed(messages) if msg.get("role") == "user"), + "Hello" + ) + active = list(self._pools.keys()) + return ( + "πŸ€– **GOD AGENT OS v10 β€” Demo Mode**\n\n" + f"Task type detected: `{task_type}`\n" + f"Your request: *{user_msg[:200]}*\n\n" + f"**Active providers:** {active or 'none'}\n\n" + "**To enable real AI, set environment variables:**\n" + "```\n" + "GEMINI_KEY=AIza...,AIza... (comma-separated, 6 keys)\n" + "SAMBANOVA_KEY=uuid,... (comma-separated, 9 keys)\n" + "GITHUB_KEY=ghp_...,ghp_... (comma-separated, 9 keys)\n" + "```\n\n" + "**God Agent OS v10 Features:**\n" + "- πŸ”‘ Multi-key pool routing with auto-failover\n" + "- 🧠 22 distributed worker spaces\n" + "- πŸ€– 16 autonomous agents\n" + "- πŸ’¬ Persistent chat history\n" + "- ⚑ Real-time WebSocket streaming\n" + "- 🌐 Manus-like UI experience\n" + ) + + +# ─── Singleton ──────────────────────────────────────────────────────────────── + +_router_instance: Optional[AIRouterV10] = None + + +def get_router_v10(ws_manager=None) -> AIRouterV10: + global _router_instance + if _router_instance is None: + _router_instance = AIRouterV10(ws_manager) + return _router_instance + + +# Alias for compatibility +AIRouterV8 = AIRouterV10 +GodModeRouter = AIRouterV10 diff --git a/ai_router/router_v3.py b/ai_router/router_v3.py new file mode 100644 index 0000000000000000000000000000000000000000..3729208ed7cce06420afdb2ff11dc216a18d0d30 --- /dev/null +++ b/ai_router/router_v3.py @@ -0,0 +1,382 @@ +""" +πŸš€ GOD MODE+ v3 - Advanced AI Router with Reasoning Models +Intelligent model selection based on task type and requirements +Version: 3.0.0 +""" + +import asyncio +import logging +from enum import Enum +from typing import Optional, Dict, List, Any +from dataclasses import dataclass +from datetime import datetime + +import structlog +from openai import AsyncOpenAI, RateLimitError, APIError +from anthropic import AsyncAnthropic + +log = structlog.get_logger() + + +class TaskType(str, Enum): + """Task classification for optimal model selection.""" + REASONING = "reasoning" + CODING = "coding" + CHAT = "chat" + ANALYSIS = "analysis" + CREATIVE = "creative" + LIGHTWEIGHT = "lightweight" + + +class ModelProvider(str, Enum): + """Supported AI model providers.""" + OPENAI = "openai" + ANTHROPIC = "anthropic" + GROQ = "groq" + DEEPSEEK = "deepseek" + TOGETHER = "together" + OPENROUTER = "openrouter" + CEREBRAS = "cerebras" + QWEN = "qwen" + + +@dataclass +class ModelConfig: + """Configuration for each model.""" + provider: ModelProvider + model_id: str + name: str + max_tokens: int + cost_per_1k_input: float + cost_per_1k_output: float + latency_ms: int + reasoning_capable: bool + coding_capable: bool + context_length: int + is_free: bool = False + + +class AIRouterV3: + """ + Advanced AI Router with: + - Reasoning model support (DeepSeek R1, Qwen QwQ, o1-mini) + - Smart task-based model selection + - Cost optimization + - Latency optimization + - Automatic failover with exponential backoff + """ + + # Model Registry + MODELS: Dict[str, ModelConfig] = { + # Reasoning Models (v3 NEW) + "deepseek-r1": ModelConfig( + provider=ModelProvider.DEEPSEEK, + model_id="deepseek-r1", + name="DeepSeek R1", + max_tokens=8000, + cost_per_1k_input=0.55, + cost_per_1k_output=2.19, + latency_ms=3000, + reasoning_capable=True, + coding_capable=True, + context_length=128000, + ), + "qwen-qwq": ModelConfig( + provider=ModelProvider.QWEN, + model_id="qwen-qwq-32b", + name="Qwen QwQ", + max_tokens=32000, + cost_per_1k_input=0.20, + cost_per_1k_output=0.60, + latency_ms=2500, + reasoning_capable=True, + coding_capable=True, + context_length=32768, + ), + "o1-mini": ModelConfig( + provider=ModelProvider.OPENAI, + model_id="o1-mini", + name="OpenAI o1-mini", + max_tokens=65536, + cost_per_1k_input=3.00, + cost_per_1k_output=12.00, + latency_ms=5000, + reasoning_capable=True, + coding_capable=True, + context_length=128000, + ), + # Standard Models + "gpt-4o": ModelConfig( + provider=ModelProvider.OPENAI, + model_id="gpt-4o", + name="GPT-4o", + max_tokens=4096, + cost_per_1k_input=5.00, + cost_per_1k_output=15.00, + latency_ms=1500, + reasoning_capable=False, + coding_capable=True, + context_length=128000, + ), + "claude-3.5-sonnet": ModelConfig( + provider=ModelProvider.ANTHROPIC, + model_id="claude-3-5-sonnet-20241022", + name="Claude 3.5 Sonnet", + max_tokens=4096, + cost_per_1k_input=3.00, + cost_per_1k_output=15.00, + latency_ms=1200, + reasoning_capable=False, + coding_capable=True, + context_length=200000, + ), + "llama-3.3-70b": ModelConfig( + provider=ModelProvider.GROQ, + model_id="llama-3.3-70b-versatile", + name="Llama 3.3 70B (Groq)", + max_tokens=8192, + cost_per_1k_input=0.00, + cost_per_1k_output=0.00, + latency_ms=800, + reasoning_capable=False, + coding_capable=True, + context_length=8192, + is_free=True, + ), + "mixtral-8x7b": ModelConfig( + provider=ModelProvider.TOGETHER, + model_id="mistralai/Mixtral-8x7B-Instruct-v0.1", + name="Mixtral 8x7B", + max_tokens=4096, + cost_per_1k_input=0.60, + cost_per_1k_output=0.60, + latency_ms=1000, + reasoning_capable=False, + coding_capable=True, + context_length=32768, + ), + } + + # Routing Chains for Different Task Types + ROUTING_CHAINS = { + TaskType.REASONING: [ + "deepseek-r1", + "qwen-qwq", + "o1-mini", + "gpt-4o", + "claude-3.5-sonnet", + ], + TaskType.CODING: [ + "gpt-4o", + "claude-3.5-sonnet", + "deepseek-r1", + "llama-3.3-70b", + "mixtral-8x7b", + ], + TaskType.CHAT: [ + "llama-3.3-70b", # Free first + "gpt-4o", + "claude-3.5-sonnet", + "mixtral-8x7b", + ], + TaskType.ANALYSIS: [ + "gpt-4o", + "claude-3.5-sonnet", + "deepseek-r1", + "llama-3.3-70b", + ], + TaskType.CREATIVE: [ + "gpt-4o", + "claude-3.5-sonnet", + "mixtral-8x7b", + "llama-3.3-70b", + ], + TaskType.LIGHTWEIGHT: [ + "llama-3.3-70b", + "mixtral-8x7b", + "gpt-4o", + ], + } + + def __init__(self, ws_manager=None): + """Initialize AI Router v3.""" + self.ws_manager = ws_manager + self.clients = {} + self.model_stats = {} + self.retry_config = { + "max_retries": 3, + "initial_delay": 1, + "max_delay": 30, + "exponential_base": 2, + } + log.info("πŸ€– AI Router v3 initialized with reasoning models") + + def detect_task_type(self, message: str, context: Dict[str, Any] = None) -> TaskType: + """ + Detect task type from message content. + Uses heuristics and optional context hints. + """ + message_lower = message.lower() + context = context or {} + + # Check explicit task type hint + if context.get("task_type"): + try: + return TaskType(context["task_type"]) + except ValueError: + pass + + # Heuristic detection + if any(word in message_lower for word in ["think", "reason", "why", "explain", "analyze"]): + return TaskType.REASONING + + if any(word in message_lower for word in ["code", "function", "debug", "fix", "implement"]): + return TaskType.CODING + + if any(word in message_lower for word in ["analyze", "compare", "evaluate", "assess"]): + return TaskType.ANALYSIS + + if any(word in message_lower for word in ["write", "create", "story", "poem", "imagine"]): + return TaskType.CREATIVE + + # Default to chat for general conversation + return TaskType.CHAT + + def select_model( + self, + task_type: TaskType, + optimize_for: str = "quality", # "quality", "speed", "cost" + context_length_needed: int = 4096, + ) -> str: + """ + Select optimal model based on task type and optimization preference. + """ + chain = self.ROUTING_CHAINS.get(task_type, self.ROUTING_CHAINS[TaskType.CHAT]) + + if optimize_for == "cost": + # Prefer free models first + for model_id in chain: + if self.MODELS[model_id].is_free: + return model_id + return chain[0] + + elif optimize_for == "speed": + # Sort by latency + sorted_chain = sorted( + chain, + key=lambda m: self.MODELS[m].latency_ms + ) + return sorted_chain[0] + + else: # quality (default) + # Prefer models with better reasoning/coding capabilities + if task_type == TaskType.REASONING: + reasoning_models = [m for m in chain if self.MODELS[m].reasoning_capable] + return reasoning_models[0] if reasoning_models else chain[0] + elif task_type == TaskType.CODING: + coding_models = [m for m in chain if self.MODELS[m].coding_capable] + return coding_models[0] if coding_models else chain[0] + + return chain[0] + + async def route( + self, + message: str, + context: Dict[str, Any] = None, + optimize_for: str = "quality", + ) -> Dict[str, Any]: + """ + Main routing function: detect task type β†’ select model β†’ execute with failover. + """ + context = context or {} + task_type = self.detect_task_type(message, context) + model_id = self.select_model(task_type, optimize_for) + + log.info( + "🎯 Routing decision", + task_type=task_type, + selected_model=model_id, + optimize_for=optimize_for, + ) + + # Try selected model with failover chain + chain = self.ROUTING_CHAINS.get(task_type, self.ROUTING_CHAINS[TaskType.CHAT]) + + for attempt, fallback_model in enumerate(chain): + try: + result = await self._call_model(fallback_model, message, context) + + # Track success + if fallback_model not in self.model_stats: + self.model_stats[fallback_model] = {"success": 0, "failures": 0} + self.model_stats[fallback_model]["success"] += 1 + + return { + "success": True, + "model": fallback_model, + "task_type": task_type, + "response": result, + "attempts": attempt + 1, + } + + except (RateLimitError, APIError) as e: + log.warning( + "⚠️ Model failed, trying next in chain", + model=fallback_model, + error=str(e), + attempt=attempt + 1, + ) + if fallback_model not in self.model_stats: + self.model_stats[fallback_model] = {"success": 0, "failures": 0} + self.model_stats[fallback_model]["failures"] += 1 + + if attempt < len(chain) - 1: + await asyncio.sleep(self.retry_config["initial_delay"] ** attempt) + continue + + return { + "success": False, + "error": "All models in chain failed", + "task_type": task_type, + "attempts": len(chain), + } + + async def _call_model(self, model_id: str, message: str, context: Dict[str, Any]) -> str: + """Call specific model with appropriate client.""" + config = self.MODELS[model_id] + + if config.provider == ModelProvider.OPENAI: + return await self._call_openai(model_id, message, context) + elif config.provider == ModelProvider.ANTHROPIC: + return await self._call_anthropic(model_id, message, context) + elif config.provider == ModelProvider.GROQ: + return await self._call_groq(model_id, message, context) + else: + raise ValueError(f"Provider {config.provider} not yet implemented") + + async def _call_openai(self, model_id: str, message: str, context: Dict[str, Any]) -> str: + """Call OpenAI models (GPT-4o, o1-mini).""" + # Implementation would go here + return f"[{model_id}] Response placeholder" + + async def _call_anthropic(self, model_id: str, message: str, context: Dict[str, Any]) -> str: + """Call Anthropic Claude models.""" + # Implementation would go here + return f"[{model_id}] Response placeholder" + + async def _call_groq(self, model_id: str, message: str, context: Dict[str, Any]) -> str: + """Call Groq models (Llama 3.3 70B).""" + # Implementation would go here + return f"[{model_id}] Response placeholder" + + def get_stats(self) -> Dict[str, Any]: + """Get router statistics.""" + return { + "models": len(self.MODELS), + "model_stats": self.model_stats, + "timestamp": datetime.now().isoformat(), + } + + +# Export for use in main.py +__all__ = ["AIRouterV3", "TaskType", "ModelProvider"] diff --git a/ai_router/router_v8.py b/ai_router/router_v8.py new file mode 100644 index 0000000000000000000000000000000000000000..e2484b4108353b45f407697f622bf227a0fa21ea --- /dev/null +++ b/ai_router/router_v8.py @@ -0,0 +1,321 @@ +""" +GOD AGENT OS β€” Multi-Provider AI Router v8/v10 compatibility layer. +Primary providers: Gemini -> SambaNova -> GitHub Models. +Supports both legacy `messages=[...]` calls and newer `prompt/system` style calls. +""" + +from __future__ import annotations + +import os +import time +from typing import Any, Dict, List, Optional, Tuple + +import httpx +import structlog + +log = structlog.get_logger() + +PROVIDERS = { + "gemini": { + "name": "gemini", + "type": "gemini", + "base_url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent", + "key_env": "GEMINI_KEY", + "default_model": "gemini-2.0-flash", + "max_tokens": 8192, + "priority_tasks": ["language", "research", "content", "general"], + }, + "sambanova": { + "name": "sambanova", + "type": "openai", + "base_url": "https://api.sambanova.ai/v1", + "key_env": "SAMBANOVA_KEY", + "default_model": "Meta-Llama-3.3-70B-Instruct", + "max_tokens": 8192, + "priority_tasks": ["reasoning", "engineering", "planning", "analysis"], + }, + "github": { + "name": "github", + "type": "openai", + "base_url": "https://models.inference.ai.azure.com", + "key_env": "GITHUB_KEY", + "default_model": "gpt-4o", + "max_tokens": 4096, + "priority_tasks": ["planning", "engineering", "general"], + }, + "groq": { + "name": "groq", + "type": "openai", + "base_url": "https://api.groq.com/openai/v1", + "key_env": "GROQ_API_KEY", + "default_model": "llama-3.3-70b-versatile", + "max_tokens": 8192, + "priority_tasks": ["general"], + }, + "openai": { + "name": "openai", + "type": "openai", + "base_url": "https://api.openai.com/v1", + "key_env": "OPENAI_API_KEY", + "default_model": "gpt-4o", + "max_tokens": 4096, + "priority_tasks": ["general"], + }, +} + +PRIMARY_ORDER = ["gemini", "sambanova", "github"] +FALLBACK_ORDER = ["groq", "openai"] +MAX_RETRIES_PER_KEY = 2 +KEY_COOLDOWN_SECONDS = 300 +KEY_MAX_FAILS = 3 + + +class KeyPool: + def __init__(self, raw_keys: str): + self._keys: List[Dict[str, Any]] = [] + for key in raw_keys.split(","): + key = key.strip() + if key: + self._keys.append({"key": key, "fails": 0, "cooldown_until": 0.0}) + + def pick(self) -> Optional[Dict[str, Any]]: + now = time.time() + available = [item for item in self._keys if item["cooldown_until"] < now] + if not available: + return None + available.sort(key=lambda item: item["fails"]) + return available[0] + + def mark_fail(self, key_obj: Dict[str, Any]): + key_obj["fails"] += 1 + if key_obj["fails"] >= KEY_MAX_FAILS: + key_obj["cooldown_until"] = time.time() + KEY_COOLDOWN_SECONDS + log.warning("key_cooled_down", key_prefix=key_obj["key"][:8]) + + def mark_success(self, key_obj: Dict[str, Any]): + key_obj["fails"] = 0 + key_obj["cooldown_until"] = 0.0 + + def has_keys(self) -> bool: + return bool(self._keys) + + def count(self) -> int: + return len(self._keys) + + def status(self) -> List[Dict[str, Any]]: + now = time.time() + result = [] + for item in self._keys: + result.append({ + "key_prefix": item["key"][:8], + "fails": item["fails"], + "cooldown_seconds": max(0, int(item["cooldown_until"] - now)), + "available": item["cooldown_until"] < now, + }) + return result + + +def classify_task(prompt: str = "") -> str: + p = (prompt or "").lower() + if any(word in p for word in ["code", "function", "implement", "build", "develop", "api", "class", "debug"]): + return "engineering" + if any(word in p for word in ["plan", "strategy", "workflow", "json", "automate", "pipeline"]): + return "planning" + if any(word in p for word in ["analyze", "reasoning", "why", "explain", "evaluate", "compare"]): + return "reasoning" + if any(word in p for word in ["research", "find", "search", "discover", "investigate"]): + return "research" + if any(word in p for word in ["write", "content", "blog", "article", "copy", "generate text", "summarize"]): + return "content" + if any(word in p for word in ["translate", "language", "convert"]): + return "language" + if any(word in p for word in ["data", "csv", "metrics", "report", "insight"]): + return "analysis" + return "general" + + +def get_provider_order(task_type: str) -> List[str]: + ordered = sorted(PRIMARY_ORDER, key=lambda provider: 0 if task_type in PROVIDERS[provider]["priority_tasks"] else 1) + return ordered + [provider for provider in FALLBACK_ORDER if os.environ.get(PROVIDERS[provider]["key_env"], "")] + + +async def call_gemini(base_url: str, key: str, messages: List[Dict[str, str]], max_tokens: int) -> Tuple[bool, str]: + url = f"{base_url}?key={key}" + parts = [{"text": message.get("content", "")} for message in messages if message.get("content")] + body = { + "contents": [{"parts": parts or [{"text": "Hello"}]}], + "generationConfig": {"maxOutputTokens": max_tokens, "temperature": 0.7}, + } + try: + async with httpx.AsyncClient(timeout=60.0) as client: + resp = await client.post(url, json=body) + if resp.status_code == 200: + data = resp.json() + text = data.get("candidates", [{}])[0].get("content", {}).get("parts", [{}])[0].get("text", "") + return True, text + return False, f"HTTP {resp.status_code}: {resp.text[:200]}" + except Exception as exc: + return False, str(exc) + + +async def call_openai_compat(base_url: str, key: str, model: str, messages: List[Dict[str, str]], max_tokens: int) -> Tuple[bool, str]: + url = f"{base_url}/chat/completions" + headers = {"Authorization": f"Bearer {key}", "Content-Type": "application/json"} + body = {"model": model, "messages": messages, "max_tokens": max_tokens, "temperature": 0.7} + try: + async with httpx.AsyncClient(timeout=60.0) as client: + resp = await client.post(url, json=body, headers=headers) + if resp.status_code == 200: + data = resp.json() + return True, data["choices"][0]["message"]["content"] + return False, f"HTTP {resp.status_code}: {resp.text[:200]}" + except Exception as exc: + return False, str(exc) + + +class GodModeRouter: + def __init__(self, ws_manager=None): + self.ws = ws_manager + self._pools: Dict[str, KeyPool] = {} + self._stats: Dict[str, Dict[str, Any]] = { + name: {"calls": 0, "errors": 0, "latency_ms": []} for name in PROVIDERS + } + self._load_pools() + + def _load_pools(self): + for name, cfg in PROVIDERS.items(): + raw = os.environ.get(cfg["key_env"], "") + if raw: + self._pools[name] = KeyPool(raw) + log.info("key_pool_loaded", provider=name, key_count=self._pools[name].count()) + + def reload_pools(self): + self._pools.clear() + self._load_pools() + + def get_status(self) -> Dict[str, Any]: + return { + "providers": { + name: { + "available": name in self._pools and self._pools[name].has_keys(), + "keys": self._pools[name].count() if name in self._pools else 0, + "stats": self._stats.get(name, {}), + } + for name in PROVIDERS + }, + "primary_order": PRIMARY_ORDER, + } + + def get_stats(self) -> Dict[str, Any]: + return { + name: {"available": name in self._pools, "calls": self._stats[name]["calls"]} + for name in PROVIDERS + } + + def get_pool_status(self) -> Dict[str, Any]: + return { + name: { + "available": name in self._pools and self._pools[name].has_keys(), + "keys": self._pools[name].count() if name in self._pools else 0, + "entries": self._pools[name].status() if name in self._pools else [], + } + for name in PROVIDERS + } + + def _normalize_messages(self, messages=None, prompt: str = "", system: str = "") -> Tuple[List[Dict[str, str]], bool]: + if messages: + return messages, False + normalized: List[Dict[str, str]] = [] + if system: + normalized.append({"role": "system", "content": system}) + normalized.append({"role": "user", "content": prompt or "Hello"}) + return normalized, True + + async def complete( + self, + messages: Optional[List[Dict[str, str]]] = None, + task_id: str = "", + session_id: str = "", + temperature: float = 0.7, + max_tokens: int = 4096, + preferred_provider: str = "", + stream: bool = False, + prompt: str = "", + system: str = "", + preferred_model: str = "", + **_: Any, + ) -> Any: + normalized_messages, return_dict = self._normalize_messages(messages=messages, prompt=prompt, system=system) + user_msg = next((msg.get("content", "") for msg in reversed(normalized_messages) if msg.get("role") == "user"), "") + task_type = classify_task(user_msg) + + if preferred_provider and preferred_provider in PROVIDERS: + order = [preferred_provider] + [provider for provider in get_provider_order(task_type) if provider != preferred_provider] + else: + order = get_provider_order(task_type) + + log.info("routing_request", task_type=task_type, order=order[:3], task_id=task_id, session_id=session_id) + last_error = "No providers available" + + for provider_name in order: + if provider_name not in self._pools: + continue + + pool = self._pools[provider_name] + cfg = PROVIDERS[provider_name] + model = preferred_model or cfg["default_model"] + + for _attempt in range(MAX_RETRIES_PER_KEY): + key_obj = pool.pick() + if key_obj is None: + break + + t0 = time.time() + try: + if cfg["type"] == "gemini": + ok, text = await call_gemini(cfg["base_url"], key_obj["key"], normalized_messages, min(max_tokens, cfg["max_tokens"])) + else: + ok, text = await call_openai_compat(cfg["base_url"], key_obj["key"], model, normalized_messages, min(max_tokens, cfg["max_tokens"])) + elapsed = int((time.time() - t0) * 1000) + + if ok and text.strip(): + pool.mark_success(key_obj) + self._stats[provider_name]["calls"] += 1 + self._stats[provider_name]["latency_ms"].append(elapsed) + payload = {"content": text, "provider": provider_name, "task_type": task_type, "latency_ms": elapsed} + return payload if return_dict else text + + pool.mark_fail(key_obj) + last_error = text + self._stats[provider_name]["errors"] += 1 + log.warning("llm_fail", provider=provider_name, error=text[:120]) + except Exception as exc: + pool.mark_fail(key_obj) + self._stats[provider_name]["errors"] += 1 + last_error = str(exc) + log.error("llm_exception", provider=provider_name, error=str(exc)[:160]) + + demo = await self._demo_response(normalized_messages, task_type) + return {"content": demo, "provider": "demo", "task_type": task_type, "error": last_error} if return_dict else demo + + async def _demo_response(self, messages: List[Dict[str, str]], task_type: str) -> str: + user_msg = next((msg.get("content", "") for msg in reversed(messages) if msg.get("role") == "user"), "Hello") + return ( + "[GOD AGENT OS β€” Demo Mode]\n\n" + f"Task type detected: {task_type}\n" + f"Your request: '{user_msg[:160]}'\n\n" + "Configure API keys in environment: GEMINI_KEY, SAMBANOVA_KEY, GITHUB_KEY" + ) + + +_router_instance: Optional[GodModeRouter] = None + + +def get_router(ws_manager=None) -> GodModeRouter: + global _router_instance + if _router_instance is None: + _router_instance = GodModeRouter(ws_manager) + return _router_instance + + +AIRouterV8 = GodModeRouter diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/routes/__init__.py b/api/routes/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/api/routes/agents.py b/api/routes/agents.py new file mode 100644 index 0000000000000000000000000000000000000000..93d52f5165ace53fd7486332127648b348062e17 --- /dev/null +++ b/api/routes/agents.py @@ -0,0 +1,98 @@ +""" +God Agent Orchestrator API Routes +""" +from fastapi import APIRouter, Request, HTTPException +from pydantic import BaseModel +from typing import Optional, Dict, Any + +router = APIRouter() + + +class OrchestrateRequest(BaseModel): + message: str + session_id: str = "" + task_id: str = "" + context: Dict[str, Any] = {} + + +class SandboxExecRequest(BaseModel): + command: str + cwd: str = "" + timeout: int = 30 + + +class FileWriteRequest(BaseModel): + filename: str + content: str + + +@router.post("/orchestrate") +async def orchestrate(req: OrchestrateRequest, request: Request): + """Route message through God Agent Orchestrator.""" + orchestrator = getattr(request.app.state, "orchestrator", None) + if not orchestrator: + raise HTTPException(500, "Orchestrator not initialized") + result = await orchestrator.orchestrate( + user_message=req.message, + session_id=req.session_id, + task_id=req.task_id, + context=req.context, + ) + return {"result": result, "session_id": req.session_id} + + +@router.get("/status") +async def agent_status(request: Request): + """Get all agent statuses.""" + orchestrator = getattr(request.app.state, "orchestrator", None) + if not orchestrator: + return {"status": "not_initialized"} + return orchestrator.get_status() + + +@router.post("/sandbox/execute") +async def sandbox_execute(req: SandboxExecRequest, request: Request): + """Execute command in sandbox.""" + orchestrator = getattr(request.app.state, "orchestrator", None) + if not orchestrator: + raise HTTPException(500, "Orchestrator not initialized") + sandbox = orchestrator.get_agent("sandbox") + if not sandbox: + raise HTTPException(503, "SandboxAgent not available") + result = await sandbox.execute(req.command, cwd=req.cwd, timeout=req.timeout) + return {"result": result, "command": req.command} + + +@router.post("/sandbox/file") +async def sandbox_write_file(req: FileWriteRequest, request: Request): + """Write file to sandbox workspace.""" + orchestrator = getattr(request.app.state, "orchestrator", None) + if not orchestrator: + raise HTTPException(500, "Orchestrator not initialized") + sandbox = orchestrator.get_agent("sandbox") + if not sandbox: + raise HTTPException(503, "SandboxAgent not available") + result = await sandbox.write_file(req.filename, req.content) + return {"result": result, "filename": req.filename} + + +@router.get("/sandbox/workspace") +async def sandbox_workspace(request: Request): + """Get workspace info.""" + orchestrator = getattr(request.app.state, "orchestrator", None) + if not orchestrator: + raise HTTPException(500, "Orchestrator not initialized") + sandbox = orchestrator.get_agent("sandbox") + if not sandbox: + raise HTTPException(503, "SandboxAgent not available") + info = await sandbox.get_workspace_info() + return info + + +@router.get("/ai-router/stats") +async def ai_router_stats(request: Request): + """Get AI router statistics.""" + ai_router = getattr(request.app.state, "ai_router", None) + if not ai_router: + return {"stats": {}} + return {"stats": ai_router.get_stats()} diff --git a/api/routes/chat.py b/api/routes/chat.py new file mode 100644 index 0000000000000000000000000000000000000000..9f46ee598bbdbb3aa29f17ed6ba8147054c9b397 --- /dev/null +++ b/api/routes/chat.py @@ -0,0 +1,214 @@ +""" +Chat + Goal API Routes β€” Real-time streaming responses +""" + +import asyncio +import json +import time +import uuid + +from fastapi import APIRouter, HTTPException, Request +from fastapi.responses import StreamingResponse + +from core.models import ChatRequest, GoalRequest, TaskCreateRequest +from memory.db import save_memory, get_history + +router = APIRouter() + + +def get_engine(request: Request): + return request.app.state.task_engine + + +def get_ws(request: Request): + return request.app.state.ws_manager + + +# ─── Chat (REST + SSE streaming) ─────────────────────────────────────────────── + +@router.post("/chat", summary="Chat with the agent") +async def chat(req: ChatRequest, request: Request): + from core.agent import AgentCore + ws = get_ws(request) + agent = AgentCore(ws) + + messages = [{"role": m.role, "content": m.content} for m in req.messages] + + if req.stream: + async def stream_gen(): + async def _run(): + result = await agent.llm_stream( + messages=messages, + session_id=req.session_id, + model=req.model, + temperature=req.temperature, + max_tokens=req.max_tokens, + ) + await save_memory( + content=result, + memory_type="conversation", + session_id=req.session_id, + project_id=req.project_id, + key="assistant", + ) + # Save user message too + user_msg = next((m["content"] for m in reversed(messages) if m["role"] == "user"), "") + await save_memory( + content=user_msg, + memory_type="conversation", + session_id=req.session_id, + project_id=req.project_id, + key="user", + ) + return result + + room_buffer = [] + original_emit_chat = ws.emit_chat + async def capture_emit(sid, etype, data): + if etype == "llm_chunk": + chunk = data.get("chunk", "") + room_buffer.append(chunk) + yield_data = json.dumps({"type": etype, "data": data, "session_id": sid}) + return yield_data + return None + + # Stream tokens directly + full = "" + from core.agent import AgentCore as _A + import httpx + import os + OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") + ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "") + + if OPENAI_API_KEY: + headers = { + "Authorization": f"Bearer {OPENAI_API_KEY}", + "Content-Type": "application/json", + } + payload = { + "model": req.model, + "messages": messages, + "stream": True, + "temperature": req.temperature, + "max_tokens": req.max_tokens, + } + from core.agent import OPENAI_BASE_URL + async with httpx.AsyncClient(timeout=120) as client: + async with client.stream("POST", f"{OPENAI_BASE_URL}/chat/completions", + headers=headers, json=payload) as resp: + async for line in resp.aiter_lines(): + if not line.startswith("data:"): + continue + chunk_str = line[6:].strip() + if chunk_str == "[DONE]": + break + try: + data = json.loads(chunk_str) + delta = data["choices"][0]["delta"].get("content", "") + if delta: + full += delta + yield f"data: {json.dumps({'type': 'llm_chunk', 'data': {'chunk': delta}, 'session_id': req.session_id})}\n\n" + except Exception: + pass + else: + # Demo streaming + demo = ( + f"Hello! I'm your Devin-style AI Agent. I received: '{req.messages[-1].content[:80]}'. " + f"Set OPENAI_API_KEY or ANTHROPIC_API_KEY for real AI responses. " + f"I support real-time streaming, task planning, GitHub automation, and more!" + ) + for word in demo.split(): + chunk = word + " " + full += chunk + await asyncio.sleep(0.04) + yield f"data: {json.dumps({'type': 'llm_chunk', 'data': {'chunk': chunk}, 'session_id': req.session_id})}\n\n" + + yield f"data: {json.dumps({'type': 'stream_end', 'data': {'full_response': full}, 'session_id': req.session_id})}\n\n" + + return StreamingResponse( + stream_gen(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + ) + else: + # Non-streaming + agent = AgentCore(get_ws(request)) + result = await agent.llm_stream(messages, session_id=req.session_id) + return { + "response": result, + "session_id": req.session_id, + "model": req.model, + "timestamp": time.time(), + } + + +@router.post("/chat/stream", summary="Explicit streaming chat endpoint") +async def chat_stream(req: ChatRequest, request: Request): + req.stream = True + return await chat(req, request) + + +# ─── Goal API (create task from goal) ───────────────────────────────────────── + +@router.post("/goal", summary="Submit a high-level goal to the agent") +async def submit_goal(req: GoalRequest, request: Request): + engine = get_engine(request) + task_req = TaskCreateRequest( + goal=req.goal, + session_id=req.session_id, + project_id=req.project_id, + stream=req.stream, + metadata={"source": "goal_api", "github_repo": req.github_repo}, + ) + task_id = await engine.submit(task_req) + return { + "task_id": task_id, + "goal": req.goal, + "status": "queued", + "session_id": req.session_id, + "ws_url": f"/ws/tasks/{task_id}", + "stream_url": f"/api/v1/tasks/{task_id}/stream", + } + + +@router.post("/goal/stream", summary="Submit goal with SSE streaming response") +async def submit_goal_stream(req: GoalRequest, request: Request): + req.stream = True + return await submit_goal(req, request) + + +# ─── Execute (direct tool execution) ────────────────────────────────────────── + +@router.post("/execute", summary="Execute a tool directly") +async def execute( + tool: str, + task: str, + request: Request, + session_id: str = "", +): + from tools.executor import ToolExecutor + ws = get_ws(request) + executor = ToolExecutor(ws) + result = await executor.run( + tool=tool, + task=task, + session_id=session_id, + ) + return {"tool": tool, "task": task, "result": result, "session_id": session_id} + + +# ─── Plan (generate plan without executing) ─────────────────────────────────── + +@router.post("/plan", summary="Generate execution plan for a goal") +async def generate_plan(req: GoalRequest, request: Request): + from core.agent import AgentCore + ws = get_ws(request) + agent = AgentCore(ws) + task_id = f"plan_{uuid.uuid4().hex[:8]}" + plan = await agent.plan(goal=req.goal, task_id=task_id, session_id=req.session_id) + return { + "goal": req.goal, + "plan": plan.model_dump(), + "session_id": req.session_id, + "task_id": task_id, + } diff --git a/api/routes/connectors.py b/api/routes/connectors.py new file mode 100644 index 0000000000000000000000000000000000000000..be0001e9db80bb09b9180993e35469e0be84dbf1 --- /dev/null +++ b/api/routes/connectors.py @@ -0,0 +1,49 @@ +""" +Connectors API β€” Manus-style connector management +""" +from fastapi import APIRouter, Request, HTTPException +from pydantic import BaseModel +from typing import Optional +from connectors.manager import ConnectorManager + +router = APIRouter() +connector_manager = ConnectorManager() + + +class SetTokenRequest(BaseModel): + connector_id: str + token: str + + +@router.get("/") +async def get_all_connectors(): + return {"connectors": connector_manager.get_all()} + + +@router.get("/connected") +async def get_connected(): + return {"connectors": connector_manager.get_connected()} + + +@router.get("/summary") +async def get_summary(): + return connector_manager.get_summary() + + +@router.get("/category/{category}") +async def get_by_category(category: str): + return {"connectors": connector_manager.get_by_category(category)} + + +@router.post("/set-token") +async def set_token(req: SetTokenRequest): + connector_manager.set_token(req.connector_id, req.token) + return {"status": "ok", "connector": req.connector_id, "connected": True} + + +@router.get("/{connector_id}/status") +async def get_status(connector_id: str): + return { + "connector_id": connector_id, + "connected": connector_manager.is_connected(connector_id), + } diff --git a/api/routes/github.py b/api/routes/github.py new file mode 100644 index 0000000000000000000000000000000000000000..d6dc31c778141f8b1633e2ea90254827770b17d3 --- /dev/null +++ b/api/routes/github.py @@ -0,0 +1,336 @@ +""" +GitHub Autonomous Engineering API Routes +Clone, commit, push, PR, issues β€” all autonomous +""" + +import os +import time +import asyncio +import tempfile +import shutil +from typing import Optional + +import httpx +from fastapi import APIRouter, HTTPException, Request + +from core.models import ( + GitHubCloneRequest, GitHubCreateRepoRequest, + GitHubCommitRequest, GitHubPRRequest, GitHubIssueRequest, +) +from memory.db import save_memory + +router = APIRouter() + +GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN", "") +GITHUB_OWNER = os.environ.get("GITHUB_OWNER", "") +GITHUB_API = "https://api.github.com" + + +def gh_headers(): + if not GITHUB_TOKEN: + raise HTTPException(status_code=400, detail="GITHUB_TOKEN not configured") + return { + "Authorization": f"Bearer {GITHUB_TOKEN}", + "Accept": "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + } + + +async def gh_get(path: str) -> dict: + async with httpx.AsyncClient(timeout=30) as client: + r = await client.get(f"{GITHUB_API}{path}", headers=gh_headers()) + r.raise_for_status() + return r.json() + + +async def gh_post(path: str, data: dict) -> dict: + async with httpx.AsyncClient(timeout=30) as client: + r = await client.post(f"{GITHUB_API}{path}", headers=gh_headers(), json=data) + r.raise_for_status() + return r.json() + + +async def gh_put(path: str, data: dict) -> dict: + async with httpx.AsyncClient(timeout=30) as client: + r = await client.put(f"{GITHUB_API}{path}", headers=gh_headers(), json=data) + r.raise_for_status() + return r.json() + + +async def gh_patch(path: str, data: dict) -> dict: + async with httpx.AsyncClient(timeout=30) as client: + r = await client.patch(f"{GITHUB_API}{path}", headers=gh_headers(), json=data) + r.raise_for_status() + return r.json() + + +# ─── Clone ──────────────────────────────────────────────────────────────────── + +@router.post("/clone", summary="Clone a GitHub repository") +async def clone_repo(req: GitHubCloneRequest): + try: + import git + except ImportError: + raise HTTPException(status_code=500, detail="gitpython not installed") + + local_path = req.local_path or f"/tmp/repos/{req.repo_url.split('/')[-1].replace('.git', '')}" + os.makedirs(local_path, exist_ok=True) + + if GITHUB_TOKEN: + url = req.repo_url.replace("https://", f"https://{GITHUB_TOKEN}@") + else: + url = req.repo_url + + try: + if os.path.exists(os.path.join(local_path, ".git")): + repo = git.Repo(local_path) + repo.remotes.origin.pull() + action = "pulled" + else: + repo = git.Repo.clone_from(url, local_path, branch=req.branch, depth=1) + action = "cloned" + + files = [] + for root, dirs, fnames in os.walk(local_path): + dirs[:] = [d for d in dirs if d not in [".git", "node_modules", "__pycache__"]] + for f in fnames[:50]: + files.append(os.path.relpath(os.path.join(root, f), local_path)) + + # Save to memory + await save_memory( + content=f"Repo {req.repo_url} cloned to {local_path}. Files: {', '.join(files[:20])}", + memory_type="repo", + key=req.repo_url, + ) + + return { + "action": action, + "repo_url": req.repo_url, + "local_path": local_path, + "branch": req.branch, + "files_count": len(files), + "files": files[:30], + } + except Exception as e: + raise HTTPException(status_code=500, detail=f"Clone failed: {str(e)}") + + +# ─── Create Repo ────────────────────────────────────────────────────────────── + +@router.post("/create_repo", summary="Create a new GitHub repository") +async def create_repo(req: GitHubCreateRepoRequest): + data = { + "name": req.name, + "description": req.description, + "private": req.private, + "auto_init": req.auto_init, + } + try: + result = await gh_post("/user/repos", data) + return { + "repo": result["full_name"], + "url": result["html_url"], + "clone_url": result["clone_url"], + "default_branch": result.get("default_branch", "main"), + "private": result["private"], + } + except httpx.HTTPStatusError as e: + raise HTTPException(status_code=e.response.status_code, detail=e.response.text) + + +# ─── Commit Files ───────────────────────────────────────────────────────────── + +@router.post("/commit", summary="Commit files to a repository") +async def commit_files(req: GitHubCommitRequest): + import base64 + + owner_repo = req.repo if "/" in req.repo else f"{GITHUB_OWNER}/{req.repo}" + results = [] + + for file_path, content in req.files.items(): + encoded = base64.b64encode(content.encode()).decode() + + # Get current SHA if file exists + sha = None + try: + existing = await gh_get(f"/repos/{owner_repo}/contents/{file_path}?ref={req.branch}") + sha = existing.get("sha") + except Exception: + pass + + payload = { + "message": req.message, + "content": encoded, + "branch": req.branch, + } + if sha: + payload["sha"] = sha + + try: + result = await gh_put(f"/repos/{owner_repo}/contents/{file_path}", payload) + results.append({"file": file_path, "status": "committed", "sha": result["content"]["sha"]}) + except Exception as e: + results.append({"file": file_path, "status": "error", "error": str(e)}) + + return { + "repo": owner_repo, + "branch": req.branch, + "message": req.message, + "files": results, + "committed": sum(1 for r in results if r["status"] == "committed"), + } + + +# ─── Push ───────────────────────────────────────────────────────────────────── + +@router.post("/push", summary="Push local changes to remote") +async def push_changes( + repo_path: str, + branch: str = "main", + message: str = "Auto-commit by Devin Agent", +): + try: + import git + repo = git.Repo(repo_path) + repo.git.add(A=True) + if repo.index.diff("HEAD") or repo.untracked_files: + repo.index.commit(message) + origin = repo.remote("origin") + origin.push(refspec=f"HEAD:{branch}") + return {"status": "pushed", "branch": branch, "message": message} + except Exception as e: + raise HTTPException(status_code=500, detail=f"Push failed: {str(e)}") + + +# ─── Create PR ──────────────────────────────────────────────────────────────── + +@router.post("/pr/create", summary="Create a Pull Request") +async def create_pr(req: GitHubPRRequest): + owner_repo = req.repo if "/" in req.repo else f"{GITHUB_OWNER}/{req.repo}" + data = { + "title": req.title, + "body": req.body, + "head": req.head, + "base": req.base, + "draft": req.draft, + } + try: + result = await gh_post(f"/repos/{owner_repo}/pulls", data) + return { + "pr_number": result["number"], + "title": result["title"], + "url": result["html_url"], + "state": result["state"], + "head": req.head, + "base": req.base, + } + except httpx.HTTPStatusError as e: + raise HTTPException(status_code=e.response.status_code, detail=e.response.text) + + +# ─── Create Issue ───────────────────────────────────────────────────────────── + +@router.post("/issues/create", summary="Create a GitHub Issue") +async def create_issue(req: GitHubIssueRequest): + owner_repo = req.repo if "/" in req.repo else f"{GITHUB_OWNER}/{req.repo}" + data = {"title": req.title, "body": req.body, "labels": req.labels} + try: + result = await gh_post(f"/repos/{owner_repo}/issues", data) + return { + "issue_number": result["number"], + "title": result["title"], + "url": result["html_url"], + "state": result["state"], + } + except httpx.HTTPStatusError as e: + raise HTTPException(status_code=e.response.status_code, detail=e.response.text) + + +# ─── Code Review ────────────────────────────────────────────────────────────── + +@router.post("/review", summary="AI code review for a PR") +async def review_pr(repo: str, pr_number: int, request: Request): + owner_repo = repo if "/" in repo else f"{GITHUB_OWNER}/{repo}" + try: + pr = await gh_get(f"/repos/{owner_repo}/pulls/{pr_number}") + files = await gh_get(f"/repos/{owner_repo}/pulls/{pr_number}/files") + + file_changes = [] + for f in files[:10]: + file_changes.append(f"{f['filename']}: +{f.get('additions',0)}/-{f.get('deletions',0)}") + + ws = request.app.state.ws_manager + from core.agent import AgentCore + agent = AgentCore(ws) + + review_prompt = ( + f"Review this Pull Request:\n" + f"Title: {pr['title']}\n" + f"Description: {pr.get('body', 'No description')}\n" + f"Files changed: {chr(10).join(file_changes)}\n\n" + f"Provide a constructive code review with: summary, potential issues, suggestions, and verdict." + ) + messages = [ + {"role": "system", "content": "You are a senior software engineer doing code review. Be constructive, specific, and helpful."}, + {"role": "user", "content": review_prompt}, + ] + review = await agent.llm_stream(messages) + + # Post review comment + if GITHUB_TOKEN: + await gh_post(f"/repos/{owner_repo}/issues/{pr_number}/comments", {"body": f"πŸ€– **Devin Agent Code Review**\n\n{review}"}) + + return { + "pr_number": pr_number, + "title": pr["title"], + "review": review, + "files_reviewed": len(files), + "posted_to_github": bool(GITHUB_TOKEN), + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +# ─── Repo Info ──────────────────────────────────────────────────────────────── + +@router.get("/repo/{owner}/{repo}", summary="Get repository info") +async def get_repo_info(owner: str, repo: str): + try: + info = await gh_get(f"/repos/{owner}/{repo}") + return { + "name": info["name"], + "full_name": info["full_name"], + "description": info.get("description"), + "url": info["html_url"], + "default_branch": info["default_branch"], + "language": info.get("language"), + "stars": info["stargazers_count"], + "forks": info["forks_count"], + "open_issues": info["open_issues_count"], + "private": info["private"], + } + except httpx.HTTPStatusError as e: + raise HTTPException(status_code=e.response.status_code, detail=e.response.text) + + +# ─── Status check ───────────────────────────────────────────────────────────── + +@router.get("/status", summary="GitHub integration status") +async def github_status(): + configured = bool(GITHUB_TOKEN) + user = None + if configured: + try: + user_info = await gh_get("/user") + user = user_info.get("login") + except Exception: + configured = False + return { + "configured": configured, + "user": user, + "owner": GITHUB_OWNER or user, + "capabilities": [ + "clone", "create_repo", "commit", "push", + "pr/create", "issues/create", "review" + ], + } diff --git a/api/routes/health.py b/api/routes/health.py new file mode 100644 index 0000000000000000000000000000000000000000..ad32fce7d34a98192c81a201136127a9f764e639 --- /dev/null +++ b/api/routes/health.py @@ -0,0 +1,89 @@ +""" +Health + Status Routes β€” GOD AGENT OS v11 +""" + +import time +import os +import psutil +from fastapi import APIRouter, Request + +router = APIRouter() + + +@router.get("/health", summary="Health check") +async def health(request: Request): + ws = getattr(request.app.state, "ws_manager", None) + engine = getattr(request.app.state, "task_engine", None) + orchestrator = getattr(request.app.state, "orchestrator", None) + ai_router = getattr(request.app.state, "ai_router", None) + connector_manager = getattr(request.app.state, "connector_manager", None) + + ws_stats = ws.get_stats() if ws else {"total_connections": 0, "rooms": {}} + cs = connector_manager.get_summary() if connector_manager else {"connected": 0, "total": 0} + ai_stats = ai_router.get_stats() if ai_router else {} + + orch_status = orchestrator.get_status() if orchestrator else {"agents": [], "total_agents": 0} + + return { + "status": "healthy", + "name": "GOD AGENT OS v11 β€” Autonomous Engineering OS", + "version": "11.0.0", + "powered_by": "Pyae Sone", + "architecture": "Multi-Agent Orchestrator + Worker Spaces", + "timestamp": time.time(), + "platform": { + "mode": "god_mode", + "agents": orch_status.get("agents", []), + "agent_count": orch_status.get("total_agents", 0), + }, + "ai_router": { + "providers": {k: v.get("available", False) for k, v in ai_stats.items()}, + "ai_ready": any(v.get("available", False) for v in ai_stats.values()), + }, + "connectors": { + "connected": cs.get("connected", 0), + "total": cs.get("total", 0), + "ai_ready": cs.get("ai_ready", False), + }, + "task_engine": { + "queue_size": engine._queue.qsize() if engine else 0, + "active_tasks": len(engine._active) if engine else 0, + }, + "websocket": { + "connections": ws_stats.get("total_connections", 0), + "rooms": list(ws_stats.get("rooms", {}).keys()), + }, + "phases": [ + "Phase 1: God Agent Orchestrator βœ…", + "Phase 2: Sandbox Agent βœ…", + "Phase 3: Connector System βœ…", + "Phase 4: Autonomous Coding Engine βœ…", + "Phase 5: Memory System βœ…", + "Phase 6: Real-time Streaming βœ…", + "Phase 7: Workflow Factor OS βœ…", + "Phase 8: Modern UI Rebuild βœ…", + "Phase 9: Multi-Model AI Router v10 βœ…", + "Phase 10: v11 Production Hardening βœ…", + ], + } + + +@router.get("/metrics", summary="System metrics") +async def metrics(): + cpu = psutil.cpu_percent(interval=0.1) + mem = psutil.virtual_memory() + disk = psutil.disk_usage("/") + return { + "cpu_percent": cpu, + "memory": { + "total_mb": round(mem.total / 1024 / 1024), + "used_mb": round(mem.used / 1024 / 1024), + "percent": mem.percent, + }, + "disk": { + "total_gb": round(disk.total / 1024 / 1024 / 1024, 1), + "used_gb": round(disk.used / 1024 / 1024 / 1024, 1), + "percent": disk.percent, + }, + "timestamp": time.time(), + } diff --git a/api/routes/memory.py b/api/routes/memory.py new file mode 100644 index 0000000000000000000000000000000000000000..52f485689944fb2bca0f71f4b31172da3176503d --- /dev/null +++ b/api/routes/memory.py @@ -0,0 +1,50 @@ +""" +Memory API Routes β€” Persistent agent memory +""" + +import time +from fastapi import APIRouter, HTTPException, Query +from core.models import MemorySaveRequest, MemorySearchRequest +from memory.db import save_memory, search_memory, get_project_memory, get_history + +router = APIRouter() + + +@router.post("/", summary="Save memory") +async def save(req: MemorySaveRequest): + await save_memory( + content=req.content, + memory_type=req.memory_type.value, + session_id=req.session_id, + project_id=req.project_id, + key=req.key, + metadata=req.metadata, + ) + return {"status": "saved", "memory_type": req.memory_type, "timestamp": time.time()} + + +@router.post("/search", summary="Search memory") +async def search(req: MemorySearchRequest): + results = await search_memory( + query=req.query, + session_id=req.session_id, + project_id=req.project_id, + limit=req.limit, + ) + return {"results": results, "total": len(results), "query": req.query} + + +@router.get("/project/{project_id}", summary="Get project memory") +async def project_memory( + project_id: str, + memory_type: str = Query(default=""), + limit: int = Query(default=100, le=500), +): + results = await get_project_memory(project_id, memory_type=memory_type, limit=limit) + return {"project_id": project_id, "memories": results, "total": len(results)} + + +@router.get("/history/{session_id}", summary="Get conversation history") +async def history(session_id: str, limit: int = Query(default=50, le=200)): + results = await get_history(session_id, limit=limit) + return {"session_id": session_id, "history": results, "total": len(results)} diff --git a/api/routes/tasks.py b/api/routes/tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..82fde607690b1a939594ec278ff6191888af1e63 --- /dev/null +++ b/api/routes/tasks.py @@ -0,0 +1,167 @@ +""" +Task API Routes β€” CRUD + Streaming + WebSocket +""" + +import asyncio +import json +import time +from typing import Optional + +from fastapi import APIRouter, HTTPException, Request, Query +from fastapi.responses import StreamingResponse + +from core.models import ( + TaskCreateRequest, TaskCancelRequest, TaskRetryRequest, TaskResponse, TaskStatus +) +from memory.db import get_task, list_tasks, get_task_events, update_task_status + +router = APIRouter() + + +def get_engine(request: Request): + return request.app.state.task_engine + + +def get_ws(request: Request): + return request.app.state.ws_manager + + +# ─── Create Task ─────────────────────────────────────────────────────────────── + +@router.post("/create", summary="Create & queue a new agent task") +async def create_task(req: TaskCreateRequest, request: Request): + engine = get_engine(request) + task_id = await engine.submit(req) + task = await get_task(task_id) + return { + "task_id": task_id, + "status": "queued", + "goal": req.goal, + "session_id": req.session_id, + "stream_url": f"/api/v1/tasks/{task_id}/stream", + "ws_url": f"/ws/tasks/{task_id}", + "created_at": task["created_at"] if task else time.time(), + } + + +# ─── Get Task ────────────────────────────────────────────────────────────────── + +@router.get("/{task_id}", summary="Get task details") +async def get_task_detail(task_id: str): + task = await get_task(task_id) + if not task: + raise HTTPException(status_code=404, detail=f"Task {task_id} not found") + return task + + +# ─── Get Task Status ─────────────────────────────────────────────────────────── + +@router.get("/{task_id}/status", summary="Get task status only") +async def get_task_status(task_id: str): + task = await get_task(task_id) + if not task: + raise HTTPException(status_code=404, detail=f"Task {task_id} not found") + return { + "task_id": task_id, + "status": task["status"], + "retry_count": task.get("retry_count", 0), + "created_at": task.get("created_at"), + "started_at": task.get("started_at"), + "completed_at": task.get("completed_at"), + } + + +# ─── Cancel Task ─────────────────────────────────────────────────────────────── + +@router.post("/{task_id}/cancel", summary="Cancel a running task") +async def cancel_task(task_id: str, req: TaskCancelRequest, request: Request): + task = await get_task(task_id) + if not task: + raise HTTPException(status_code=404, detail=f"Task {task_id} not found") + if task["status"] in ("completed", "failed", "cancelled"): + raise HTTPException(status_code=400, detail=f"Task already {task['status']}") + engine = get_engine(request) + await engine.cancel(task_id, req.reason) + return {"task_id": task_id, "status": "cancelled", "reason": req.reason} + + +# ─── Retry Task ──────────────────────────────────────────────────────────────── + +@router.post("/{task_id}/retry", summary="Retry a failed task") +async def retry_task(task_id: str, request: Request): + task = await get_task(task_id) + if not task: + raise HTTPException(status_code=404, detail=f"Task {task_id} not found") + if task["status"] not in ("failed", "cancelled"): + raise HTTPException(status_code=400, detail="Only failed/cancelled tasks can be retried") + engine = get_engine(request) + await engine.retry(task_id) + return {"task_id": task_id, "status": "queued", "message": "Task requeued for retry"} + + +# ─── Stream Task Events (SSE) ────────────────────────────────────────────────── + +@router.get("/{task_id}/stream", summary="Stream task events via SSE") +async def stream_task(task_id: str, request: Request): + task = await get_task(task_id) + if not task: + raise HTTPException(status_code=404, detail=f"Task {task_id} not found") + + async def event_generator(): + # First, replay all stored events + events = await get_task_events(task_id) + for ev in events: + data = json.dumps({ + "type": ev["event_type"], + "task_id": task_id, + "timestamp": ev["timestamp"], + "data": json.loads(ev["data"]) if ev.get("data") else {}, + }) + yield f"data: {data}\n\n" + + # Then stream live events via WS manager buffer + ws = get_ws(request) + room = f"task:{task_id}" + last_count = len(events) + + # Poll for new events (for SSE fallback) + for _ in range(600): # max 5 minutes + await asyncio.sleep(0.5) + current_task = await get_task(task_id) + if current_task and current_task["status"] in ("completed", "failed", "cancelled"): + yield f"data: {json.dumps({'type': 'stream_end', 'task_id': task_id, 'status': current_task['status']})}\n\n" + break + # heartbeat + yield f"data: {json.dumps({'type': 'heartbeat', 'timestamp': time.time()})}\n\n" + + return StreamingResponse( + event_generator(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + "Connection": "keep-alive", + }, + ) + + +# ─── List Tasks ──────────────────────────────────────────────────────────────── + +@router.get("/", summary="List tasks") +async def list_all_tasks( + session_id: str = Query(default=""), + limit: int = Query(default=50, le=200), +): + tasks = await list_tasks(session_id=session_id, limit=limit) + return {"tasks": tasks, "total": len(tasks)} + + +# ─── Task Events History ─────────────────────────────────────────────────────── + +@router.get("/{task_id}/events", summary="Get all events for a task") +async def task_events(task_id: str): + task = await get_task(task_id) + if not task: + raise HTTPException(status_code=404, detail=f"Task {task_id} not found") + events = await get_task_events(task_id) + return {"task_id": task_id, "events": events, "total": len(events)} diff --git a/api/websocket_manager.py b/api/websocket_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..68aea8a58e3d2381e6eec51197b6a9f13ad9931d --- /dev/null +++ b/api/websocket_manager.py @@ -0,0 +1,134 @@ +""" +WebSocket Connection Manager β€” Production Grade +Handles rooms, heartbeats, event buffering, reconnect support +""" + +import asyncio +import json +import time +import uuid +from collections import defaultdict +from typing import Dict, List, Optional, Set +import structlog + +log = structlog.get_logger() + + +class WebSocketManager: + def __init__(self): + # room β†’ set of websockets + self._rooms: Dict[str, Set] = defaultdict(set) + # ws β†’ list of rooms + self._ws_rooms: Dict[object, Set[str]] = defaultdict(set) + # Event buffer per room (for replay on reconnect) + self._event_buffer: Dict[str, List] = defaultdict(list) + self._buffer_max = 100 + # Active connection count + self._connection_count = 0 + + async def connect(self, websocket, room: str): + await websocket.accept() + self._rooms[room].add(websocket) + self._ws_rooms[websocket].add(room) + self._connection_count += 1 + log.info("WS connected", room=room, total=self._connection_count) + + # Replay buffered events for this room + buffered = self._event_buffer.get(room, [])[-20:] + for event in buffered: + try: + await websocket.send_json(event) + except Exception: + pass + + await websocket.send_json({ + "type": "connected", + "room": room, + "timestamp": time.time(), + "buffered_events": len(buffered), + }) + + def disconnect(self, websocket, room: Optional[str] = None): + if room: + self._rooms[room].discard(websocket) + self._ws_rooms[websocket].discard(room) + else: + for r in list(self._ws_rooms.get(websocket, [])): + self._rooms[r].discard(websocket) + self._ws_rooms.pop(websocket, None) + self._connection_count = max(0, self._connection_count - 1) + log.info("WS disconnected", room=room, total=self._connection_count) + + async def broadcast(self, room: str, event: dict): + """Broadcast event to all sockets in a room.""" + if "timestamp" not in event: + event["timestamp"] = time.time() + if "id" not in event: + event["id"] = str(uuid.uuid4())[:8] + + # Buffer event + self._event_buffer[room].append(event) + if len(self._event_buffer[room]) > self._buffer_max: + self._event_buffer[room].pop(0) + + dead = set() + for ws in list(self._rooms.get(room, [])): + try: + await ws.send_json(event) + except Exception: + dead.add(ws) + + for ws in dead: + self.disconnect(ws, room) + + async def broadcast_global(self, event: dict): + """Broadcast to ALL connected websockets.""" + for room in list(self._rooms.keys()): + await self.broadcast(room, event) + + async def emit(self, task_id: str, event_type: str, data: dict, session_id: str = ""): + """Emit a structured event to a task room + logs room.""" + event = { + "type": event_type, + "task_id": task_id, + "session_id": session_id, + "timestamp": time.time(), + "data": data, + } + await self.broadcast(f"task:{task_id}", event) + await self.broadcast("logs", event) + await self.broadcast("agent_status", { + "type": "agent_event", + "task_id": task_id, + "event_type": event_type, + "timestamp": time.time(), + }) + + async def emit_chat(self, session_id: str, event_type: str, data: dict): + """Emit event to a chat session room.""" + event = { + "type": event_type, + "session_id": session_id, + "timestamp": time.time(), + "data": data, + } + await self.broadcast(f"chat:{session_id}", event) + + async def heartbeat_loop(self): + """Send heartbeat to all connections every 15s.""" + while True: + await asyncio.sleep(15) + heartbeat = { + "type": "heartbeat", + "timestamp": time.time(), + "connections": self._connection_count, + } + for room in list(self._rooms.keys()): + await self.broadcast(room, heartbeat) + + def get_stats(self) -> dict: + return { + "total_connections": self._connection_count, + "rooms": {r: len(ws) for r, ws in self._rooms.items()}, + "buffered_events": {r: len(e) for r, e in self._event_buffer.items()}, + } diff --git a/app.py b/app.py new file mode 100644 index 0000000000000000000000000000000000000000..77f617c233b54a5594bcfa1fad8705bd5b420281 --- /dev/null +++ b/app.py @@ -0,0 +1,1240 @@ +""" +GOD AGENT OS β€” Clean Phase-1 Backend +===================================== +Stability-first rebuild. One pipeline, no dead code. + +Endpoints: + GET /health β€” real status + provider availability + GET /api/v1/health β€” alias + GET /api/v1/system/status β€” extended status + POST /api/v1/chat (SSE) β€” LLM-only streaming chat (NO sandbox) + POST /api/v1/execute (SSE) β€” real E2B sandbox execution + POST /api/v1/agent (SSE) β€” intent-routed: chat OR execute + POST /api/v1/orchestrate (SSE) β€” alias of /api/v1/agent (legacy) + POST /api/v1/kernel/orchestrate β€” alias of /api/v1/agent (legacy, frontend) + GET /api/v1/sandbox/{session} β€” sandbox info + DELETE /api/v1/sandbox/{session} β€” kill sandbox + WS /ws/{session_id} β€” same events over WebSocket + +Execution rules (per spec): + * Normal chat (greetings, explanations, brainstorming) β†’ LLM only, no E2B + * Execution intent (code, shell, files, packages) β†’ real E2B sandbox + * Intent detection: keyword heuristic + LLM tie-breaker (cheap, sync) +""" + +from __future__ import annotations + +import asyncio +import json +import os +import re +import time +import uuid +from contextlib import asynccontextmanager +from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple + +import httpx +import structlog +from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware +from fastapi.responses import JSONResponse, StreamingResponse + +# ───────────────────────────────────────────────────────────────────────────── +# Logging +# ───────────────────────────────────────────────────────────────────────────── +structlog.configure( + processors=[ + structlog.processors.TimeStamper(fmt="iso"), + structlog.stdlib.add_log_level, + structlog.dev.ConsoleRenderer(), + ] +) +log = structlog.get_logger() + +# ───────────────────────────────────────────────────────────────────────────── +# Environment +# ───────────────────────────────────────────────────────────────────────────── +E2B_API_KEY = os.environ.get("E2B_API_KEY", "").strip() + +# Multiple keys can be comma-separated; we just pick the first non-empty. +def _first_key(*env_names: str) -> str: + for name in env_names: + v = os.environ.get(name, "") + if v: + return v.split(",")[0].strip() + return "" + +SAMBANOVA_KEY = _first_key("SAMBANOVA_KEY", "SAMBANOVA_API_KEY", "SAMBANOVA_API_KEYS") +GEMINI_KEY = _first_key("GEMINI_KEY", "GEMINI_API_KEY", "GEMINI_API_KEYS") +GITHUB_LLM_KEY= _first_key("GITHUB_KEY", "GITHUB_API_KEY", "GITHUB_MODELS_TOKEN", "GITHUB_API_KEYS") +OPENAI_KEY = _first_key("OPENAI_API_KEY") +GROQ_KEY = _first_key("GROQ_API_KEY") +ANTHROPIC_KEY = _first_key("ANTHROPIC_API_KEY") +HF_TOKEN = _first_key("HF_TOKEN", "HUGGINGFACE_TOKEN") + +VERSION = "13.0.0-phase1" + +# ───────────────────────────────────────────────────────────────────────────── +# LLM Providers β€” OpenAI-compatible streaming (most stable, no tool-calling) +# ───────────────────────────────────────────────────────────────────────────── +# Order: most stable & fastest first. We deliberately do NOT use Gemini for +# tool-calling here β€” Phase 1 is text-only streaming. +PROVIDERS: List[Dict[str, Any]] = [ + { + "name": "sambanova", + "key": SAMBANOVA_KEY, + "url": "https://api.sambanova.ai/v1/chat/completions", + "model": "Meta-Llama-3.3-70B-Instruct", + }, + { + "name": "groq", + "key": GROQ_KEY, + "url": "https://api.groq.com/openai/v1/chat/completions", + "model": "llama-3.3-70b-versatile", + }, + { + "name": "github", + "key": GITHUB_LLM_KEY, + "url": "https://models.inference.ai.azure.com/chat/completions", + "model": "gpt-4o-mini", + }, + { + "name": "openai", + "key": OPENAI_KEY, + "url": "https://api.openai.com/v1/chat/completions", + "model": "gpt-4o-mini", + }, + { + "name": "anthropic", + "key": ANTHROPIC_KEY, + "url": "https://api.anthropic.com/v1/messages", # different protocol; handled specially + "model": "claude-3-5-haiku-20241022", + }, + { + "name": "gemini", + "key": GEMINI_KEY, + "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent", + "model": "gemini-2.0-flash", + }, +] + + +def active_providers() -> List[str]: + return [p["name"] for p in PROVIDERS if p["key"]] + + +# ───────────────────────────────────────────────────────────────────────────── +# Streaming helpers +# ───────────────────────────────────────────────────────────────────────────── +SYSTEM_PROMPT_CHAT = ( + "You are God Agent OS, a helpful autonomous AI assistant. " + "Answer clearly and concisely. Use Markdown when helpful. " + "Do NOT pretend to execute code β€” if execution is needed, the system " + "will route the request to a real sandbox automatically." +) + + +async def _stream_openai_compat( + provider: Dict[str, Any], + messages: List[Dict[str, str]], + temperature: float = 0.7, + max_tokens: int = 2048, +) -> AsyncGenerator[str, None]: + """Stream tokens from any OpenAI-compatible /chat/completions endpoint.""" + headers = { + "Authorization": f"Bearer {provider['key']}", + "Content-Type": "application/json", + } + payload = { + "model": provider["model"], + "messages": messages, + "stream": True, + "temperature": temperature, + "max_tokens": max_tokens, + } + async with httpx.AsyncClient(timeout=httpx.Timeout(120.0, connect=15.0)) as client: + async with client.stream("POST", provider["url"], json=payload, headers=headers) as resp: + if resp.status_code != 200: + body = (await resp.aread())[:300].decode("utf-8", "ignore") + log.warning("LLM provider failed", provider=provider["name"], status=resp.status_code, body=body) + raise RuntimeError(f"{provider['name']} HTTP {resp.status_code}: {body}") + + async for line in resp.aiter_lines(): + if not line or not line.startswith("data:"): + continue + data_str = line[5:].strip() + if data_str in ("", "[DONE]"): + if data_str == "[DONE]": + return + continue + try: + obj = json.loads(data_str) + delta = obj["choices"][0].get("delta", {}) + content = delta.get("content") + if content: + yield content + except Exception: + continue + + +async def _stream_gemini( + provider: Dict[str, Any], + messages: List[Dict[str, str]], + temperature: float = 0.7, + max_tokens: int = 2048, +) -> AsyncGenerator[str, None]: + """Stream from Gemini SSE endpoint β€” text-only (no tools).""" + system_text = "" + contents: List[Dict[str, Any]] = [] + for m in messages: + role = m.get("role", "user") + content = m.get("content", "") + if role == "system": + system_text = content + continue + g_role = "user" if role == "user" else "model" + contents.append({"role": g_role, "parts": [{"text": content}]}) + if not contents: + contents = [{"role": "user", "parts": [{"text": "Hello"}]}] + + body: Dict[str, Any] = { + "contents": contents, + "generationConfig": {"maxOutputTokens": max_tokens, "temperature": temperature}, + } + if system_text: + body["systemInstruction"] = {"parts": [{"text": system_text}]} + + url = f"{provider['url']}?alt=sse&key={provider['key']}" + async with httpx.AsyncClient(timeout=httpx.Timeout(120.0, connect=15.0)) as client: + async with client.stream("POST", url, json=body) as resp: + if resp.status_code != 200: + err = (await resp.aread())[:300].decode("utf-8", "ignore") + raise RuntimeError(f"gemini HTTP {resp.status_code}: {err}") + async for line in resp.aiter_lines(): + if not line.startswith("data:"): + continue + data_str = line[5:].strip() + if not data_str: + continue + try: + obj = json.loads(data_str) + parts = obj.get("candidates", [{}])[0].get("content", {}).get("parts", []) + for p in parts: + if "text" in p and p["text"]: + yield p["text"] + except Exception: + continue + + +async def _stream_anthropic( + provider: Dict[str, Any], + messages: List[Dict[str, str]], + temperature: float = 0.7, + max_tokens: int = 2048, +) -> AsyncGenerator[str, None]: + """Stream from Anthropic Messages API.""" + system_text = "" + msgs: List[Dict[str, Any]] = [] + for m in messages: + if m.get("role") == "system": + system_text = m.get("content", "") + continue + msgs.append({"role": m["role"], "content": m["content"]}) + + payload: Dict[str, Any] = { + "model": provider["model"], + "max_tokens": max_tokens, + "temperature": temperature, + "messages": msgs, + "stream": True, + } + if system_text: + payload["system"] = system_text + + headers = { + "x-api-key": provider["key"], + "anthropic-version": "2023-06-01", + "content-type": "application/json", + } + async with httpx.AsyncClient(timeout=httpx.Timeout(120.0, connect=15.0)) as client: + async with client.stream("POST", provider["url"], json=payload, headers=headers) as resp: + if resp.status_code != 200: + err = (await resp.aread())[:300].decode("utf-8", "ignore") + raise RuntimeError(f"anthropic HTTP {resp.status_code}: {err}") + async for line in resp.aiter_lines(): + if not line.startswith("data:"): + continue + data_str = line[5:].strip() + if not data_str or data_str == "[DONE]": + continue + try: + obj = json.loads(data_str) + if obj.get("type") == "content_block_delta": + d = obj.get("delta", {}) + if d.get("type") == "text_delta": + t = d.get("text", "") + if t: + yield t + except Exception: + continue + + +async def stream_llm( + messages: List[Dict[str, str]], + temperature: float = 0.7, + max_tokens: int = 2048, +) -> AsyncGenerator[Tuple[str, str], None]: + """ + Yield (provider_name, token) pairs. Tries providers in PROVIDERS order, + falling back to next on failure. Yields a 'demo' provider with a helpful + message if NO keys are configured. + """ + last_err: Optional[str] = None + for p in PROVIDERS: + if not p["key"]: + continue + name = p["name"] + try: + if name == "gemini": + stream = _stream_gemini(p, messages, temperature, max_tokens) + elif name == "anthropic": + stream = _stream_anthropic(p, messages, temperature, max_tokens) + else: + stream = _stream_openai_compat(p, messages, temperature, max_tokens) + + emitted = False + async for chunk in stream: + emitted = True + yield (name, chunk) + if emitted: + return + last_err = f"{name} produced no tokens" + log.warning("Provider produced no tokens, falling back", provider=name) + except Exception as e: + last_err = f"{name}: {e}" + log.warning("Provider failed, falling back", provider=name, error=str(e)[:200]) + continue + + # No provider worked β€” emit a clear demo message + demo = ( + "**[Demo mode]** No LLM provider succeeded.\n\n" + f"Last error: `{last_err or 'no API keys configured'}`\n\n" + "Set one of: `SAMBANOVA_KEY`, `GEMINI_KEY`, `GITHUB_KEY`, `OPENAI_API_KEY`, " + "`GROQ_API_KEY`, `ANTHROPIC_API_KEY` in HF Space β†’ Settings β†’ Variables and Secrets." + ) + for tok in re.findall(r"\S+\s*", demo): + yield ("demo", tok) + await asyncio.sleep(0.01) + + +# ───────────────────────────────────────────────────────────────────────────── +# E2B Sandbox Manager β€” real execution, real stdout/stderr streaming +# ───────────────────────────────────────────────────────────────────────────── +class SandboxManager: + """Manages per-session E2B sandboxes with reuse + lazy creation.""" + + def __init__(self) -> None: + self._sandboxes: Dict[str, Any] = {} # session_id -> Sandbox + self._meta: Dict[str, Dict[str, Any]] = {} # session_id -> {created_at, sandbox_id} + self._lock = asyncio.Lock() + self._sdk_ok = False + try: + from e2b_code_interpreter import Sandbox # noqa: F401 + self._sdk_ok = True + except Exception as e: + log.warning("E2B SDK not importable", error=str(e)) + + @property + def available(self) -> bool: + return self._sdk_ok and bool(E2B_API_KEY) + + async def get_or_create(self, session_id: str): + if not self.available: + return None + async with self._lock: + sbx = self._sandboxes.get(session_id) + if sbx is not None: + return sbx + from e2b_code_interpreter import Sandbox # type: ignore + # Sandbox constructor is blocking β†’ offload to a thread + sbx = await asyncio.to_thread(Sandbox, api_key=E2B_API_KEY) + self._sandboxes[session_id] = sbx + self._meta[session_id] = { + "sandbox_id": getattr(sbx, "sandbox_id", "unknown"), + "created_at": time.time(), + } + log.info("E2B sandbox created", session_id=session_id, sandbox_id=self._meta[session_id]["sandbox_id"]) + return sbx + + async def kill(self, session_id: str) -> bool: + async with self._lock: + sbx = self._sandboxes.pop(session_id, None) + self._meta.pop(session_id, None) + if sbx is None: + return False + try: + await asyncio.to_thread(sbx.kill) + return True + except Exception as e: + log.warning("Sandbox kill error", error=str(e)) + return False + + def info(self, session_id: str) -> Optional[Dict[str, Any]]: + return self._meta.get(session_id) + + def stats(self) -> Dict[str, Any]: + return { + "available": self.available, + "active_sandboxes": len(self._sandboxes), + "sessions": list(self._meta.keys()), + } + + async def shutdown_all(self) -> None: + async with self._lock: + sessions = list(self._sandboxes.keys()) + for sid in sessions: + await self.kill(sid) + + +sandbox_mgr = SandboxManager() + + +async def execute_in_sandbox( + session_id: str, + language: str, + code: str, + timeout: int = 60, +) -> AsyncGenerator[Dict[str, Any], None]: + """ + Execute code in a real E2B sandbox (or LOCAL fallback if E2B unavailable). + Yields event dicts: + {type: 'sandbox_ready', sandbox_id, session_id} + {type: 'stdout', text} + {type: 'stderr', text} + {type: 'result', exit_code, duration_ms, sandbox_id, success} + {type: 'error', error} + """ + t0 = time.time() + language = (language or "python").lower() + + # ── Real E2B path ──────────────────────────────────────────────────────── + if sandbox_mgr.available: + try: + sbx = await sandbox_mgr.get_or_create(session_id) + meta = sandbox_mgr.info(session_id) or {} + yield { + "type": "sandbox_ready", + "sandbox_id": meta.get("sandbox_id", "unknown"), + "session_id": session_id, + "backend": "e2b", + } + + stdout_buf: List[str] = [] + stderr_buf: List[str] = [] + + def on_stdout(line): # called from SDK thread + text = getattr(line, "line", None) or str(line) + stdout_buf.append(text) + + def on_stderr(line): + text = getattr(line, "line", None) or str(line) + stderr_buf.append(text) + + if language in ("bash", "sh", "shell"): + # Run as shell command + exec_result = await asyncio.to_thread( + sbx.run_code, + f"import subprocess, sys\n" + f"r = subprocess.run({code!r}, shell=True, capture_output=True, text=True, timeout={timeout})\n" + f"sys.stdout.write(r.stdout)\n" + f"sys.stderr.write(r.stderr)\n" + f"sys.exit(r.returncode)\n", + ) + else: + exec_result = await asyncio.to_thread(sbx.run_code, code) + + # Drain logs (E2B 1.0.5 returns logs in exec_result.logs) + for line in (getattr(exec_result.logs, "stdout", []) or []): + yield {"type": "stdout", "text": line if line.endswith("\n") else line + "\n"} + for line in (getattr(exec_result.logs, "stderr", []) or []): + yield {"type": "stderr", "text": line if line.endswith("\n") else line + "\n"} + + err = getattr(exec_result, "error", None) + exit_code = 0 + if err: + yield {"type": "stderr", "text": f"{getattr(err, 'name', 'Error')}: {getattr(err, 'value', err)}\n"} + exit_code = 1 + + yield { + "type": "result", + "exit_code": exit_code, + "success": exit_code == 0, + "duration_ms": int((time.time() - t0) * 1000), + "sandbox_id": meta.get("sandbox_id", "unknown"), + "backend": "e2b", + } + return + + except Exception as e: + log.error("E2B execution failed, falling back to local", error=str(e)) + yield {"type": "stderr", "text": f"[e2b fallback] {e}\n"} + # Fall through to LOCAL + + # ── LOCAL fallback (subprocess) β€” clearly marked as fallback ───────────── + import subprocess + workdir = f"/tmp/god_workspace/{session_id}" + os.makedirs(workdir, exist_ok=True) + yield { + "type": "sandbox_ready", + "sandbox_id": f"local-{session_id}", + "session_id": session_id, + "backend": "local", + } + try: + if language in ("bash", "sh", "shell"): + cmd = ["bash", "-lc", code] + else: + # Run python via stdin to avoid filename issues + cmd = ["python3", "-c", code] + proc = await asyncio.create_subprocess_exec( + *cmd, + cwd=workdir, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + + async def _drain(stream, evt): + async for raw in stream: + yield_text = raw.decode("utf-8", "replace") + await _queue.put({"type": evt, "text": yield_text}) + await _queue.put({"_eof": evt}) + + _queue: asyncio.Queue = asyncio.Queue() + t1 = asyncio.create_task(_pipe(proc.stdout, "stdout", _queue)) + t2 = asyncio.create_task(_pipe(proc.stderr, "stderr", _queue)) + + eofs = 0 + deadline = time.time() + timeout + while eofs < 2: + try: + item = await asyncio.wait_for(_queue.get(), timeout=max(0.1, deadline - time.time())) + except asyncio.TimeoutError: + proc.kill() + yield {"type": "stderr", "text": f"[timeout after {timeout}s]\n"} + break + if "_eof" in item: + eofs += 1 + continue + yield item + + rc = await proc.wait() + yield { + "type": "result", + "exit_code": rc, + "success": rc == 0, + "duration_ms": int((time.time() - t0) * 1000), + "sandbox_id": f"local-{session_id}", + "backend": "local", + } + except Exception as e: + yield {"type": "error", "error": str(e)} + + +async def _pipe(stream, evt: str, queue: asyncio.Queue) -> None: + """Drain a subprocess stream line-by-line into queue.""" + try: + while True: + line = await stream.readline() + if not line: + break + await queue.put({"type": evt, "text": line.decode("utf-8", "replace")}) + finally: + await queue.put({"_eof": evt}) + + +# ───────────────────────────────────────────────────────────────────────────── +# Intent router β€” chat vs execute +# ───────────────────────────────────────────────────────────────────────────── +EXEC_KEYWORDS = [ + "run ", "execute ", "compute ", "calculate ", "evaluate this", + "write a file", "create a file", "create file", "write to ", "save to file", + "list files", "ls ", "pwd ", "cat ", + "install ", "pip install", "npm install", "apt-get", + "python -c", "bash -c", + "ΠΏΡ€ΠΎof.txt", "proof.txt", # from the spec example + "current unix timestamp", "unix timestamp", "current timestamp", + "shell command", "terminal command", + "debug this", "test this code", + "scrape ", "fetch url", "curl ", +] + +CODE_BLOCK_RE = re.compile(r"```([a-zA-Z0-9_+\-]*)\n(.+?)```", re.DOTALL) + + +def detect_intent(message: str) -> Dict[str, Any]: + """ + Lightweight intent detector. Returns: + {intent: 'chat'|'execute', language?, code?, reason} + Only flips to 'execute' if very confident β€” protects normal chat. + """ + msg = message.strip() + lower = msg.lower() + + # 1) explicit code block in message β†’ execute + m = CODE_BLOCK_RE.search(msg) + if m: + lang = (m.group(1) or "python").lower() + if lang in ("py", ""): + lang = "python" + return {"intent": "execute", "language": lang, "code": m.group(2).strip(), "reason": "code_block"} + + # 2) execution keywords + looks like a task + hits = [kw for kw in EXEC_KEYWORDS if kw in lower] + if hits: + return {"intent": "execute", "language": "python", "code": None, "reason": f"keywords:{hits[:3]}"} + + # 3) very short or greeting β†’ chat + if len(msg) < 200 and re.search(r"\b(hi|hello|hey|thanks|thank you|α€™α€„α€Ία€Ήα€‚α€œα€¬|α€Ÿα€šα€Ία€œα€­α€―)\b", lower): + return {"intent": "chat", "reason": "greeting"} + + # default: chat (safe; user can still ask explicitly) + return {"intent": "chat", "reason": "default"} + + +async def llm_generate_code(user_message: str) -> Optional[Tuple[str, str]]: + """ + Ask LLM to produce executable code for the user's task. + Uses non-streaming completion (more reliable than streamed concat for code). + Returns (language, code) or None. + """ + sys_prompt = ( + "You are a precise code generator. The user wants a task DONE by executing code. " + "Respond with EXACTLY ONE fenced code block (```python ... ```), and NOTHING else β€” " + "no explanation, no preamble, no trailing text. " + "Code must be valid, self-contained, and PRINT results to stdout so the user can verify. " + "When the user asks for file operations, write under /home/user/ in the sandbox." + ) + messages = [ + {"role": "system", "content": sys_prompt}, + {"role": "user", "content": user_message}, + ] + + # Prefer non-streaming completion against the FIRST working provider β€” more + # reliable for short code outputs than re-assembling streamed deltas. + for p in PROVIDERS: + if not p["key"]: + continue + try: + buf = await _complete_once(p, messages, temperature=0.0, max_tokens=1024) + m = CODE_BLOCK_RE.search(buf or "") + if m: + lang = (m.group(1) or "python").lower() + if lang in ("py", ""): + lang = "python" + return (lang, m.group(2).strip()) + if buf and ("print(" in buf or "import " in buf or "open(" in buf): + return ("python", buf.strip()) + # else try next provider + except Exception as e: + log.warning("code-gen provider failed", provider=p["name"], error=str(e)[:160]) + continue + return None + + +async def _complete_once( + provider: Dict[str, Any], + messages: List[Dict[str, str]], + temperature: float = 0.0, + max_tokens: int = 1024, +) -> str: + """One-shot non-streaming completion (more reliable than concat for code).""" + name = provider["name"] + if name == "gemini": + # gemini supports non-stream via generateContent + contents: List[Dict[str, Any]] = [] + system_text = "" + for m in messages: + if m.get("role") == "system": + system_text = m.get("content", "") + continue + contents.append({"role": "user" if m["role"] == "user" else "model", "parts": [{"text": m["content"]}]}) + body: Dict[str, Any] = {"contents": contents, "generationConfig": {"temperature": temperature, "maxOutputTokens": max_tokens}} + if system_text: + body["systemInstruction"] = {"parts": [{"text": system_text}]} + url = f"https://generativelanguage.googleapis.com/v1beta/models/{provider['model']}:generateContent?key={provider['key']}" + async with httpx.AsyncClient(timeout=60.0) as cli: + r = await cli.post(url, json=body) + r.raise_for_status() + d = r.json() + parts = d.get("candidates", [{}])[0].get("content", {}).get("parts", []) + return "".join(p.get("text", "") for p in parts) + if name == "anthropic": + msgs = [m for m in messages if m["role"] != "system"] + sys_text = next((m["content"] for m in messages if m["role"] == "system"), "") + payload: Dict[str, Any] = {"model": provider["model"], "max_tokens": max_tokens, "temperature": temperature, "messages": msgs} + if sys_text: + payload["system"] = sys_text + headers = {"x-api-key": provider["key"], "anthropic-version": "2023-06-01", "content-type": "application/json"} + async with httpx.AsyncClient(timeout=60.0) as cli: + r = await cli.post(provider["url"], json=payload, headers=headers) + r.raise_for_status() + d = r.json() + return "".join(b.get("text", "") for b in d.get("content", []) if b.get("type") == "text") + # OpenAI-compatible + payload = {"model": provider["model"], "messages": messages, "temperature": temperature, "max_tokens": max_tokens} + headers = {"Authorization": f"Bearer {provider['key']}", "Content-Type": "application/json"} + async with httpx.AsyncClient(timeout=60.0) as cli: + r = await cli.post(provider["url"], json=payload, headers=headers) + r.raise_for_status() + d = r.json() + return d["choices"][0]["message"]["content"] or "" + + +# ───────────────────────────────────────────────────────────────────────────── +# WebSocket manager (used by /ws/{session_id}) +# ───────────────────────────────────────────────────────────────────────────── +class WSManager: + def __init__(self) -> None: + self._sessions: Dict[str, set] = {} + + async def connect(self, ws: WebSocket, sid: str) -> None: + await ws.accept() + self._sessions.setdefault(sid, set()).add(ws) + + def disconnect(self, ws: WebSocket, sid: str) -> None: + self._sessions.get(sid, set()).discard(ws) + + async def broadcast(self, sid: str, event: Dict[str, Any]) -> None: + dead = [] + for ws in list(self._sessions.get(sid, [])): + try: + await ws.send_json(event) + except Exception: + dead.append(ws) + for ws in dead: + self._sessions.get(sid, set()).discard(ws) + + def stats(self) -> Dict[str, Any]: + return {"sessions": len(self._sessions), "connections": sum(len(s) for s in self._sessions.values())} + + +ws_manager = WSManager() + + +# ───────────────────────────────────────────────────────────────────────────── +# FastAPI app +# ───────────────────────────────────────────────────────────────────────────── +@asynccontextmanager +async def lifespan(app: FastAPI): + log.info( + "God Agent OS starting", + version=VERSION, + e2b=sandbox_mgr.available, + providers=active_providers(), + ) + yield + log.info("Shutting down β€” killing sandboxes") + await sandbox_mgr.shutdown_all() + + +app = FastAPI( + title="God Agent OS β€” Phase 1", + version=VERSION, + description="Stable autonomous agent backend (LLM + E2B + SSE).", + lifespan=lifespan, +) + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], + expose_headers=["*"], +) +app.add_middleware(GZipMiddleware, minimum_size=1024) + + +# ───────────────────────────────────────────────────────────────────────────── +# Health +# ───────────────────────────────────────────────────────────────────────────── +def _health_payload() -> Dict[str, Any]: + return { + "status": "healthy", + "version": VERSION, + "timestamp": time.time(), + "e2b": sandbox_mgr.available, + "ai_providers": active_providers(), + "ws": ws_manager.stats(), + "sandboxes": sandbox_mgr.stats(), + "mode": "phase1_stable", + "features": { + "chat_streaming": True, + "real_execution": True, + "e2b_sandbox": sandbox_mgr.available, + "local_fallback": True, + "intent_routing": True, + }, + } + + +@app.get("/") +async def root(): + return {"service": "god-agent-os", "version": VERSION, "docs": "/api/docs", "health": "/health"} + + +@app.get("/health") +async def health(): + return _health_payload() + + +@app.get("/api/v1/health") +async def health_v1(): + return _health_payload() + + +@app.get("/api/v1/system/status") +async def system_status(): + return _health_payload() + + +@app.get("/api/v1/ai/stats") +async def ai_stats(): + stats = {p["name"]: {"available": bool(p["key"]), "model": p["model"]} for p in PROVIDERS} + return {"stats": stats, "active": next((p["name"] for p in PROVIDERS if p["key"]), None)} + + +# ───────────────────────────────────────────────────────────────────────────── +# SSE helpers +# ───────────────────────────────────────────────────────────────────────────── +SSE_HEADERS = { + "Cache-Control": "no-cache, no-transform", + "X-Accel-Buffering": "no", + "Connection": "keep-alive", +} + + +def sse_pack(event: Dict[str, Any]) -> str: + return f"data: {json.dumps(event, ensure_ascii=False)}\n\n" + + +# ───────────────────────────────────────────────────────────────────────────── +# /api/v1/chat β€” LLM only, real token streaming. NO sandbox. +# ───────────────────────────────────────────────────────────────────────────── +@app.post("/api/v1/chat") +async def chat(request: Request): + body = await request.json() + messages: List[Dict[str, str]] = body.get("messages") or [] + session_id: str = body.get("session_id") or uuid.uuid4().hex[:12] + stream: bool = bool(body.get("stream", True)) + temperature = float(body.get("temperature", 0.7)) + max_tokens = int(body.get("max_tokens", 2048)) + + # If client sent legacy {"message": "..."} β†’ wrap it + if not messages and body.get("message"): + messages = [{"role": "user", "content": str(body["message"])}] + + if not messages: + raise HTTPException(status_code=400, detail="messages or message required") + + # Inject system prompt unless caller provided one + if not any(m.get("role") == "system" for m in messages): + messages = [{"role": "system", "content": SYSTEM_PROMPT_CHAT}] + messages + + task_id = uuid.uuid4().hex[:12] + + if not stream: + full = "" + provider_used = None + async for prov, tok in stream_llm(messages, temperature, max_tokens): + provider_used = prov + full += tok + return {"task_id": task_id, "session_id": session_id, "result": full, "provider": provider_used} + + async def gen(): + provider_used: Optional[str] = None + yield sse_pack({"type": "agent_start", "data": {"task_id": task_id, "mode": "chat"}, "session_id": session_id}) + await ws_manager.broadcast(session_id, {"type": "agent_start", "task_id": task_id, "mode": "chat"}) + full = "" + try: + async for prov, tok in stream_llm(messages, temperature, max_tokens): + if provider_used != prov: + provider_used = prov + yield sse_pack({"type": "provider", "data": {"provider": prov}, "session_id": session_id}) + full += tok + evt = {"type": "llm_chunk", "data": {"chunk": tok}, "session_id": session_id} + yield sse_pack(evt) + await ws_manager.broadcast(session_id, evt) + yield sse_pack({"type": "agent_complete", "data": {"task_id": task_id, "tools_called": 0, "iterations": 1}, "session_id": session_id}) + yield sse_pack({"type": "stream_end", "data": {"full_response": full, "task_id": task_id, "provider": provider_used}, "session_id": session_id}) + except Exception as e: + log.exception("chat stream error") + yield sse_pack({"type": "error", "data": {"error": str(e)}, "session_id": session_id}) + + return StreamingResponse(gen(), media_type="text/event-stream", headers=SSE_HEADERS) + + +# ───────────────────────────────────────────────────────────────────────────── +# /api/v1/execute β€” REAL E2B execution with live stdout/stderr streaming +# ───────────────────────────────────────────────────────────────────────────── +@app.post("/api/v1/execute") +async def execute(request: Request): + body = await request.json() + + # Accept multiple schemas: + # {language, code} ← primary (Phase 1) + # {tool, args:{code,...}} ← legacy v12 schema + language: Optional[str] = body.get("language") + code: Optional[str] = body.get("code") + if not code and body.get("tool"): + args = body.get("args") or {} + tool = body.get("tool") + if tool in ("execute_python", "python"): + language = "python" + code = args.get("code") + elif tool in ("execute_shell", "shell", "bash"): + language = "bash" + code = args.get("command") or args.get("code") + elif tool == "write_file": + language = "python" + path = args.get("path", "/home/user/out.txt").replace("'", "\\'") + content = (args.get("content") or "").replace("\\", "\\\\").replace("'''", "\\'\\'\\'") + code = f"open('{path}','w').write('''{content}''')\nprint('wrote', '{path}')" + elif tool == "read_file": + language = "python" + path = args.get("path", "/home/user/out.txt").replace("'", "\\'") + code = f"print(open('{path}').read())" + session_id: str = body.get("session_id") or uuid.uuid4().hex[:12] + timeout = int(body.get("timeout") or 60) + stream: bool = bool(body.get("stream", True)) + + if not code: + raise HTTPException(status_code=400, detail="code (or tool+args) required") + language = (language or "python").lower() + + task_id = uuid.uuid4().hex[:12] + + if not stream: + # Collect everything and return as JSON + stdout, stderr, exit_code, sandbox_id, backend = "", "", None, None, None + async for ev in execute_in_sandbox(session_id, language, code, timeout): + t = ev.get("type") + if t == "stdout": + stdout += ev.get("text", "") + elif t == "stderr": + stderr += ev.get("text", "") + elif t == "sandbox_ready": + sandbox_id = ev.get("sandbox_id"); backend = ev.get("backend") + elif t == "result": + exit_code = ev.get("exit_code") + elif t == "error": + stderr += ev.get("error", "") + return { + "task_id": task_id, "session_id": session_id, "sandbox_id": sandbox_id, "backend": backend, + "language": language, "stdout": stdout, "stderr": stderr, "exit_code": exit_code, + "success": exit_code == 0, + } + + async def gen(): + yield sse_pack({"type": "agent_start", "data": {"task_id": task_id, "mode": "execute", "language": language}, "session_id": session_id}) + yield sse_pack({"type": "tool_executing", "data": {"tool": f"execute_{language}", "args": {"code": code[:200]}}, "session_id": session_id}) + await ws_manager.broadcast(session_id, {"type": "tool_executing", "tool": f"execute_{language}"}) + stdout_acc = "" + stderr_acc = "" + result_meta: Dict[str, Any] = {} + try: + async for ev in execute_in_sandbox(session_id, language, code, timeout): + # Mirror to WS + await ws_manager.broadcast(session_id, ev) + if ev["type"] == "stdout": + stdout_acc += ev["text"] + elif ev["type"] == "stderr": + stderr_acc += ev["text"] + elif ev["type"] == "result": + result_meta = ev + yield sse_pack(ev) + + tool_result = { + "tool": f"execute_{language}", + "success": bool(result_meta.get("success")), + "sandbox_id": result_meta.get("sandbox_id"), + "raw": { + "stdout": stdout_acc[-2000:], + "stderr": stderr_acc[-2000:], + "exit_code": result_meta.get("exit_code"), + "_duration_ms": result_meta.get("duration_ms"), + "backend": result_meta.get("backend"), + }, + "result": (stdout_acc or stderr_acc)[-1500:], + } + yield sse_pack({"type": "tool_result", "data": tool_result, "session_id": session_id}) + yield sse_pack({"type": "agent_complete", "data": {"task_id": task_id, "tools_called": 1, "iterations": 1}, "session_id": session_id}) + yield sse_pack({"type": "stream_end", "data": {"task_id": task_id, "full_response": stdout_acc[-2000:]}, "session_id": session_id}) + except Exception as e: + log.exception("execute stream error") + yield sse_pack({"type": "error", "data": {"error": str(e)}, "session_id": session_id}) + + return StreamingResponse(gen(), media_type="text/event-stream", headers=SSE_HEADERS) + + +# ───────────────────────────────────────────────────────────────────────────── +# /api/v1/agent β€” intent-routed: chat OR execute +# ───────────────────────────────────────────────────────────────────────────── +@app.post("/api/v1/agent") +async def agent(request: Request): + body = await request.json() + message: str = (body.get("message") or "").strip() + if not message and body.get("messages"): + last_user = next((m["content"] for m in reversed(body["messages"]) if m.get("role") == "user"), "") + message = (last_user or "").strip() + session_id: str = body.get("session_id") or uuid.uuid4().hex[:12] + force = (body.get("intent") or "").lower() # 'chat' | 'execute' | '' + if not message: + raise HTTPException(status_code=400, detail="message required") + + intent = {"intent": force, "reason": "forced"} if force in ("chat", "execute") else detect_intent(message) + task_id = uuid.uuid4().hex[:12] + + async def gen(): + yield sse_pack({"type": "agent_start", "data": {"task_id": task_id, "intent": intent}, "session_id": session_id}) + await ws_manager.broadcast(session_id, {"type": "agent_start", "task_id": task_id, "intent": intent}) + + if intent["intent"] == "chat": + messages = [ + {"role": "system", "content": SYSTEM_PROMPT_CHAT}, + {"role": "user", "content": message}, + ] + full = "" + provider_used = None + try: + async for prov, tok in stream_llm(messages): + if prov != provider_used: + provider_used = prov + yield sse_pack({"type": "provider", "data": {"provider": prov}, "session_id": session_id}) + full += tok + evt = {"type": "llm_chunk", "data": {"chunk": tok}, "session_id": session_id} + yield sse_pack(evt) + await ws_manager.broadcast(session_id, evt) + except Exception as e: + yield sse_pack({"type": "error", "data": {"error": str(e)}, "session_id": session_id}) + yield sse_pack({"type": "agent_complete", "data": {"task_id": task_id, "tools_called": 0, "iterations": 1}, "session_id": session_id}) + yield sse_pack({"type": "stream_end", "data": {"task_id": task_id, "full_response": full, "provider": provider_used}, "session_id": session_id}) + return + + # ── EXECUTE branch ──────────────────────────────────────────────── + lang = intent.get("language") or "python" + code = intent.get("code") + if not code: + # Generate code with LLM + yield sse_pack({"type": "thinking_start", "data": {"iteration": 1, "phase": "code_generation"}, "session_id": session_id}) + gen_result = await llm_generate_code(message) + if not gen_result: + yield sse_pack({"type": "error", "data": {"error": "could not generate executable code"}, "session_id": session_id}) + yield sse_pack({"type": "stream_end", "data": {"task_id": task_id}, "session_id": session_id}) + return + lang, code = gen_result + yield sse_pack({"type": "llm_chunk", "data": {"chunk": f"```{lang}\n{code}\n```\n"}, "session_id": session_id}) + + yield sse_pack({"type": "tool_executing", "data": {"tool": f"execute_{lang}", "args": {"code": code[:200]}}, "session_id": session_id}) + + stdout_acc, stderr_acc, meta = "", "", {} + async for ev in execute_in_sandbox(session_id, lang, code, timeout=60): + await ws_manager.broadcast(session_id, ev) + if ev["type"] == "stdout": + stdout_acc += ev["text"] + elif ev["type"] == "stderr": + stderr_acc += ev["text"] + elif ev["type"] == "result": + meta = ev + yield sse_pack(ev) + + yield sse_pack({ + "type": "tool_result", + "data": { + "tool": f"execute_{lang}", + "success": bool(meta.get("success")), + "sandbox_id": meta.get("sandbox_id"), + "raw": {"stdout": stdout_acc[-2000:], "stderr": stderr_acc[-2000:], "exit_code": meta.get("exit_code"), "backend": meta.get("backend")}, + "result": (stdout_acc or stderr_acc)[-1500:], + }, + "session_id": session_id, + }) + yield sse_pack({"type": "agent_complete", "data": {"task_id": task_id, "tools_called": 1, "iterations": 1}, "session_id": session_id}) + yield sse_pack({"type": "stream_end", "data": {"task_id": task_id, "full_response": stdout_acc[-2000:]}, "session_id": session_id}) + + return StreamingResponse(gen(), media_type="text/event-stream", headers=SSE_HEADERS) + + +# ───────────────────────────────────────────────────────────────────────────── +# Legacy aliases (so existing frontend keeps working without redeploy) +# ───────────────────────────────────────────────────────────────────────────── +@app.post("/api/v1/orchestrate") +async def orchestrate_alias(request: Request): + return await agent(request) + + +@app.post("/api/v1/kernel/orchestrate") +async def kernel_orchestrate_alias(request: Request): + """Legacy non-streaming endpoint used by ChatPanel.tsx.""" + body = await request.json() + message = (body.get("message") or "").strip() + session_id = body.get("session_id") or uuid.uuid4().hex[:12] + if not message: + raise HTTPException(status_code=400, detail="message required") + intent = detect_intent(message) + + if intent["intent"] == "chat": + messages = [ + {"role": "system", "content": SYSTEM_PROMPT_CHAT}, + {"role": "user", "content": message}, + ] + full, prov = "", None + async for p, tok in stream_llm(messages): + full += tok; prov = p + return {"session_id": session_id, "response": full, "provider": prov, "intent": "chat"} + + # execute path + lang = intent.get("language") or "python" + code = intent.get("code") + if not code: + g = await llm_generate_code(message) + if not g: + return {"session_id": session_id, "response": "Could not generate code.", "intent": "execute", "success": False} + lang, code = g + stdout, stderr, meta = "", "", {} + async for ev in execute_in_sandbox(session_id, lang, code, timeout=60): + if ev["type"] == "stdout": stdout += ev["text"] + elif ev["type"] == "stderr": stderr += ev["text"] + elif ev["type"] == "result": meta = ev + return { + "session_id": session_id, + "response": (stdout or stderr)[-3000:], + "intent": "execute", + "code": code, + "language": lang, + "stdout": stdout, "stderr": stderr, + "exit_code": meta.get("exit_code"), + "sandbox_id": meta.get("sandbox_id"), + "backend": meta.get("backend"), + "success": bool(meta.get("success")), + } + + +@app.post("/api/v1/chat/stream") +async def chat_stream_alias(request: Request): + body = await request.json() + body["stream"] = True + async def receive(): + return {"type": "http.request", "body": json.dumps(body).encode()} + request._receive = receive # type: ignore + return await chat(request) + + +# ───────────────────────────────────────────────────────────────────────────── +# Sandbox lifecycle +# ───────────────────────────────────────────────────────────────────────────── +@app.get("/api/v1/sandbox/{session_id}") +async def sandbox_info(session_id: str): + return { + "session_id": session_id, + "sandbox": sandbox_mgr.info(session_id), + "e2b_configured": sandbox_mgr.available, + } + + +@app.delete("/api/v1/sandbox/{session_id}") +async def sandbox_kill(session_id: str): + ok = await sandbox_mgr.kill(session_id) + return {"status": "closed" if ok else "not_found", "session_id": session_id} + + +# Stub endpoints used by old frontend pages β€” return empty/healthy responses so UI doesn't error +@app.get("/api/v1/agents") +async def list_agents(): + return {"agents": [], "count": 0} + +@app.get("/api/v1/tasks/") +async def list_tasks(): + return {"tasks": [], "count": 0} + +@app.get("/api/v1/spaces") +async def list_spaces(): + return {"spaces": [], "count": 0} + +@app.get("/api/v1/connectors") +async def list_connectors(): + return {"connectors": [], "count": 0} + +@app.get("/api/v1/memory/") +async def memory_list(): + return {"items": [], "count": 0} + +@app.get("/api/v1/ai/pool-status") +async def pool_status(): + return {"pools": {p["name"]: {"keys": 1 if p["key"] else 0, "model": p["model"]} for p in PROVIDERS}} + + +# ───────────────────────────────────────────────────────────────────────────── +# WebSocket β€” mirror of SSE events; also accepts {"type":"message"|"execute"|"ping"} +# ───────────────────────────────────────────────────────────────────────────── +@app.websocket("/ws/{session_id}") +async def websocket_endpoint(ws: WebSocket, session_id: str): + await ws_manager.connect(ws, session_id) + try: + # Keep-alive loop + while True: + try: + data = await asyncio.wait_for(ws.receive_json(), timeout=30.0) + except asyncio.TimeoutError: + await ws.send_json({"type": "ping", "ts": time.time()}) + continue + except WebSocketDisconnect: + break + + event_type = data.get("type") + if event_type == "ping": + await ws.send_json({"type": "pong", "ts": time.time()}) + elif event_type == "message": + msg = data.get("message") or "" + # Run agent and broadcast events + async def _run(): + intent = detect_intent(msg) + if intent["intent"] == "chat": + messages = [{"role": "system", "content": SYSTEM_PROMPT_CHAT}, {"role": "user", "content": msg}] + async for prov, tok in stream_llm(messages): + await ws_manager.broadcast(session_id, {"type": "llm_chunk", "data": {"chunk": tok}}) + await ws_manager.broadcast(session_id, {"type": "stream_end", "data": {}}) + else: + lang = intent.get("language") or "python" + code = intent.get("code") + if not code: + g = await llm_generate_code(msg) + if g: + lang, code = g + else: + await ws_manager.broadcast(session_id, {"type": "error", "data": {"error": "no code"}}) + return + async for ev in execute_in_sandbox(session_id, lang, code): + await ws_manager.broadcast(session_id, ev) + asyncio.create_task(_run()) + elif event_type == "execute": + lang = (data.get("language") or "python").lower() + code = data.get("code") or "" + if not code: + await ws.send_json({"type": "error", "data": {"error": "code required"}}) + continue + async def _exec(): + async for ev in execute_in_sandbox(session_id, lang, code): + await ws_manager.broadcast(session_id, ev) + asyncio.create_task(_exec()) + except WebSocketDisconnect: + pass + finally: + ws_manager.disconnect(ws, session_id) + + +# Dev entry +if __name__ == "__main__": + import uvicorn + uvicorn.run("app:app", host="0.0.0.0", port=int(os.environ.get("PORT", 7860)), reload=False) diff --git a/connectors/__init__.py b/connectors/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ee00292b28afb0cc8687b2f34d5b1fe28629f80f --- /dev/null +++ b/connectors/__init__.py @@ -0,0 +1,4 @@ +# Connector System +from .manager import ConnectorManager + +__all__ = ["ConnectorManager"] diff --git a/connectors/manager.py b/connectors/manager.py new file mode 100644 index 0000000000000000000000000000000000000000..9da4cbb9a52238712d5281a7bbf81b36d45f6ae1 --- /dev/null +++ b/connectors/manager.py @@ -0,0 +1,210 @@ +""" +Connector Manager β€” Manus-style connector ecosystem +Manages OAuth tokens, connection state, API access +""" +import json +import os +import time +from typing import Dict, List, Optional +import structlog + +log = structlog.get_logger() + +CONNECTORS_CONFIG = [ + { + "id": "github", + "name": "GitHub", + "icon": "github", + "color": "#24292e", + "env_key": "GITHUB_TOKEN", + "description": "Repos, Issues, PRs, Commits", + "scopes": ["repo", "issues", "pull_requests"], + "category": "code", + }, + { + "id": "huggingface", + "name": "HuggingFace", + "icon": "huggingface", + "color": "#ff9d00", + "env_key": "HF_TOKEN", + "description": "Spaces, Models, Datasets", + "scopes": ["spaces", "models"], + "category": "ai", + }, + { + "id": "vercel", + "name": "Vercel", + "icon": "vercel", + "color": "#000000", + "env_key": "VERCEL_TOKEN", + "description": "Deployments, Domains, Functions", + "scopes": ["deployments", "projects"], + "category": "deploy", + }, + { + "id": "openai", + "name": "OpenAI", + "icon": "openai", + "color": "#10a37f", + "env_key": "OPENAI_API_KEY", + "description": "GPT-4o, Embeddings, DALL-E", + "scopes": ["chat", "embeddings"], + "category": "ai", + }, + { + "id": "groq", + "name": "Groq", + "icon": "groq", + "color": "#f55036", + "env_key": "GROQ_API_KEY", + "description": "Llama 3.3 70B β€” Ultra Fast", + "scopes": ["chat"], + "category": "ai", + }, + { + "id": "cerebras", + "name": "Cerebras", + "icon": "cerebras", + "color": "#7c3aed", + "env_key": "CEREBRAS_API_KEY", + "description": "Llama 3.1 70B β€” Long Context", + "scopes": ["chat"], + "category": "ai", + }, + { + "id": "openrouter", + "name": "OpenRouter", + "icon": "openrouter", + "color": "#6366f1", + "env_key": "OPENROUTER_API_KEY", + "description": "Multi-model router, free tier", + "scopes": ["chat"], + "category": "ai", + }, + { + "id": "anthropic", + "name": "Anthropic", + "icon": "anthropic", + "color": "#d4a27f", + "env_key": "ANTHROPIC_API_KEY", + "description": "Claude 3.5 Sonnet", + "scopes": ["chat"], + "category": "ai", + }, + { + "id": "n8n", + "name": "n8n", + "icon": "n8n", + "color": "#ea4b71", + "env_key": "N8N_URL", + "description": "Workflow automation engine", + "scopes": ["workflows", "executions"], + "category": "workflow", + }, + { + "id": "telegram", + "name": "Telegram", + "icon": "telegram", + "color": "#0088cc", + "env_key": "TELEGRAM_BOT_TOKEN", + "description": "Bot API, messages, webhooks", + "scopes": ["messages", "bots"], + "category": "messaging", + }, + { + "id": "discord", + "name": "Discord", + "icon": "discord", + "color": "#5865f2", + "env_key": "DISCORD_BOT_TOKEN", + "description": "Bot, channels, webhooks", + "scopes": ["messages", "bots"], + "category": "messaging", + }, + { + "id": "slack", + "name": "Slack", + "icon": "slack", + "color": "#4a154b", + "env_key": "SLACK_BOT_TOKEN", + "description": "Messages, channels, workflows", + "scopes": ["messages", "channels"], + "category": "messaging", + }, + { + "id": "cloudflare", + "name": "Cloudflare", + "icon": "cloudflare", + "color": "#f38020", + "env_key": "CLOUDFLARE_API_TOKEN", + "description": "Workers, KV, Pages", + "scopes": ["workers", "kv", "pages"], + "category": "infra", + }, +] + + +class ConnectorManager: + """Manages all platform connectors β€” connection state, tokens, status.""" + + def __init__(self): + self._configs = {c["id"]: c for c in CONNECTORS_CONFIG} + + def get_all(self) -> List[Dict]: + """Get all connectors with connection status.""" + result = [] + for cfg in CONNECTORS_CONFIG: + token = os.environ.get(cfg["env_key"], "") + result.append({ + **cfg, + "connected": bool(token), + "token_preview": f"{token[:8]}..." if token else None, + }) + return result + + def get_connected(self) -> List[Dict]: + """Get only connected connectors.""" + return [c for c in self.get_all() if c["connected"]] + + def get_by_category(self, category: str) -> List[Dict]: + """Get connectors by category.""" + return [c for c in self.get_all() if c["category"] == category] + + def is_connected(self, connector_id: str) -> bool: + cfg = self._configs.get(connector_id) + if not cfg: + return False + return bool(os.environ.get(cfg["env_key"], "")) + + def get_token(self, connector_id: str) -> Optional[str]: + cfg = self._configs.get(connector_id) + if not cfg: + return None + return os.environ.get(cfg["env_key"]) or None + + def set_token(self, connector_id: str, token: str): + """Set connector token at runtime (does not persist across restarts).""" + cfg = self._configs.get(connector_id) + if cfg: + os.environ[cfg["env_key"]] = token + log.info("Connector token set", connector=connector_id) + + def get_summary(self) -> Dict: + all_c = self.get_all() + connected = [c for c in all_c if c["connected"]] + by_cat = {} + for c in all_c: + cat = c["category"] + if cat not in by_cat: + by_cat[cat] = {"total": 0, "connected": 0} + by_cat[cat]["total"] += 1 + if c["connected"]: + by_cat[cat]["connected"] += 1 + return { + "total": len(all_c), + "connected": len(connected), + "by_category": by_cat, + "ai_ready": self.is_connected("openai") or self.is_connected("groq") + or self.is_connected("openrouter") or self.is_connected("anthropic") + or self.is_connected("cerebras"), + } diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/core/agent.py b/core/agent.py new file mode 100644 index 0000000000000000000000000000000000000000..293d2d0b8356d476b1b4d00fafe7712457de8222 --- /dev/null +++ b/core/agent.py @@ -0,0 +1,392 @@ +""" +Agent Core β€” Planner + Executor + Self-Heal Loop +LLM-powered with OpenAI/Anthropic support, streaming tokens +""" + +import asyncio +import json +import os +import time +from typing import Any, Dict, List, Optional + +import httpx +import structlog + +from core.models import TaskPlan, TaskStep +from api.websocket_manager import WebSocketManager +from memory.db import save_memory, get_history, search_memory + +log = structlog.get_logger() + +OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "") +ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "") +DEFAULT_MODEL = os.environ.get("DEFAULT_MODEL", "gpt-4o") +OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1") + + +SYSTEM_PROMPT = """You are an elite autonomous AI software engineer β€” like Devin or Manus. +You can plan, code, debug, refactor, test, and deploy software autonomously. +You think step-by-step, write production-quality code, and self-heal on errors. +Always respond in structured JSON when asked for plans or structured output. +""" + +PLANNER_PROMPT = """You are a senior software architect. Given a goal, produce a detailed execution plan. + +Respond ONLY with valid JSON: +{ + "steps": [ + { + "name": "Step name", + "description": "What this step does", + "tool": "code|shell|file|browser|github|memory|search|test|none", + "estimated_seconds": 10 + } + ], + "estimated_duration": 60, + "tools_needed": ["code", "shell"] +} + +Goal: {goal} +Context: {context} +""" + + +class AgentCore: + def __init__(self, ws_manager: WebSocketManager): + self.ws = ws_manager + self.model = DEFAULT_MODEL + + # ─── LLM Call (with streaming) ───────────────────────────────────────────── + + async def llm_stream( + self, + messages: List[Dict], + task_id: str = "", + session_id: str = "", + model: str = "", + temperature: float = 0.7, + max_tokens: int = 4096, + ) -> str: + """Stream LLM tokens, emitting llm_chunk events via WebSocket.""" + model = model or self.model + full_text = "" + + if OPENAI_API_KEY: + full_text = await self._openai_stream( + messages, task_id, session_id, model, temperature, max_tokens + ) + elif ANTHROPIC_API_KEY: + full_text = await self._anthropic_stream( + messages, task_id, session_id, temperature, max_tokens + ) + else: + # Demo mode β€” simulate streaming + full_text = await self._demo_stream(messages, task_id, session_id) + + return full_text + + async def _openai_stream( + self, messages, task_id, session_id, model, temperature, max_tokens + ) -> str: + full_text = "" + headers = { + "Authorization": f"Bearer {OPENAI_API_KEY}", + "Content-Type": "application/json", + } + payload = { + "model": model, + "messages": messages, + "stream": True, + "temperature": temperature, + "max_tokens": max_tokens, + } + async with httpx.AsyncClient(timeout=120) as client: + async with client.stream( + "POST", f"{OPENAI_BASE_URL}/chat/completions", + headers=headers, json=payload + ) as resp: + resp.raise_for_status() + async for line in resp.aiter_lines(): + if not line.startswith("data:"): + continue + chunk = line[6:].strip() + if chunk == "[DONE]": + break + try: + data = json.loads(chunk) + delta = data["choices"][0]["delta"].get("content", "") + if delta: + full_text += delta + if task_id: + await self.ws.emit(task_id, "llm_chunk", { + "chunk": delta, + "accumulated": len(full_text), + }, session_id=session_id) + if session_id and not task_id: + await self.ws.emit_chat(session_id, "llm_chunk", { + "chunk": delta, + }) + except Exception: + pass + return full_text + + async def _anthropic_stream( + self, messages, task_id, session_id, temperature, max_tokens + ) -> str: + full_text = "" + system = "" + filtered = [] + for m in messages: + if m["role"] == "system": + system = m["content"] + else: + filtered.append(m) + headers = { + "x-api-key": ANTHROPIC_API_KEY, + "anthropic-version": "2023-06-01", + "Content-Type": "application/json", + } + payload = { + "model": "claude-3-5-sonnet-20241022", + "max_tokens": max_tokens, + "messages": filtered, + "stream": True, + } + if system: + payload["system"] = system + async with httpx.AsyncClient(timeout=120) as client: + async with client.stream( + "POST", "https://api.anthropic.com/v1/messages", + headers=headers, json=payload + ) as resp: + resp.raise_for_status() + async for line in resp.aiter_lines(): + if not line.startswith("data:"): + continue + try: + data = json.loads(line[5:].strip()) + if data.get("type") == "content_block_delta": + delta = data["delta"].get("text", "") + if delta: + full_text += delta + if task_id: + await self.ws.emit(task_id, "llm_chunk", { + "chunk": delta, + }, session_id=session_id) + if session_id and not task_id: + await self.ws.emit_chat(session_id, "llm_chunk", { + "chunk": delta, + }) + except Exception: + pass + return full_text + + async def _demo_stream(self, messages, task_id, session_id) -> str: + """Demo mode β€” simulate LLM streaming without API key.""" + last_user = next( + (m["content"] for m in reversed(messages) if m["role"] == "user"), "Hello" + ) + response = ( + f"πŸ€– **Devin Agent** (Demo Mode)\n\n" + f"I received your request: *{last_user[:100]}*\n\n" + f"To enable real AI responses, set `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` in your environment.\n\n" + f"**What I can do with a real API key:**\n" + f"- πŸ“‹ Generate detailed execution plans\n" + f"- πŸ’» Write and execute code autonomously\n" + f"- πŸ”§ Debug and self-heal on errors\n" + f"- πŸ™ Manage GitHub repos autonomously\n" + f"- 🧠 Remember long-running project context\n" + f"- πŸš€ Deploy applications automatically\n" + ) + full_text = "" + for word in response.split(): + chunk = word + " " + full_text += chunk + await asyncio.sleep(0.03) + if task_id: + await self.ws.emit(task_id, "llm_chunk", { + "chunk": chunk, + "demo": True, + }, session_id=session_id) + if session_id and not task_id: + await self.ws.emit_chat(session_id, "llm_chunk", { + "chunk": chunk, + "demo": True, + }) + return full_text + + # ─── Planning ────────────────────────────────────────────────────────────── + + async def plan(self, goal: str, task_id: str, session_id: str = "") -> TaskPlan: + """Generate a structured execution plan.""" + # Get context from memory + memories = await search_memory(goal[:50], session_id=session_id) + context = "\n".join([m["content"][:200] for m in memories[:3]]) + + prompt = PLANNER_PROMPT.format(goal=goal, context=context or "No prior context") + + messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": prompt}, + ] + + if not OPENAI_API_KEY and not ANTHROPIC_API_KEY: + # Demo plan + return self._demo_plan(goal) + + raw = await self.llm_stream(messages, task_id=task_id, session_id=session_id) + + # Extract JSON from response + try: + # Find JSON block + start = raw.find("{") + end = raw.rfind("}") + 1 + if start >= 0 and end > start: + data = json.loads(raw[start:end]) + else: + data = json.loads(raw) + + steps = [] + for i, s in enumerate(data.get("steps", [])): + steps.append(TaskStep( + name=s.get("name", f"Step {i+1}"), + description=s.get("description", ""), + tool=s.get("tool", "none"), + )) + + return TaskPlan( + goal=goal, + steps=steps if steps else [TaskStep(name="Execute goal", description=goal, tool="code")], + estimated_duration=data.get("estimated_duration", 60), + tools_needed=data.get("tools_needed", []), + ) + except Exception as e: + log.warning("Plan parse failed, using fallback", error=str(e)) + return self._demo_plan(goal) + + def _demo_plan(self, goal: str) -> TaskPlan: + """Fallback plan for demo mode.""" + steps = [ + TaskStep(name="Analyze Requirements", description=f"Analyze: {goal[:60]}", tool="none"), + TaskStep(name="Design Solution", description="Design the solution architecture", tool="none"), + TaskStep(name="Implement", description="Write the implementation code", tool="code"), + TaskStep(name="Test", description="Test the implementation", tool="test"), + TaskStep(name="Document", description="Write documentation", tool="none"), + ] + return TaskPlan( + goal=goal, + steps=steps, + estimated_duration=120, + tools_needed=["code", "test"], + ) + + # ─── Step Execution ──────────────────────────────────────────────────────── + + async def execute_step( + self, + step: TaskStep, + task_id: str, + session_id: str = "", + context: Dict = {}, + ) -> str: + """Execute a single step using the appropriate tool.""" + from tools.executor import ToolExecutor + executor = ToolExecutor(self.ws) + + await self.ws.emit(task_id, "tool_called", { + "tool": step.tool or "none", + "step": step.name, + "description": step.description, + }, session_id=session_id) + + try: + result = await executor.run( + tool=step.tool or "none", + task=step.description, + goal=context.get("goal", ""), + previous=context.get("previous_results", []), + task_id=task_id, + session_id=session_id, + ) + await self.ws.emit(task_id, "tool_result", { + "tool": step.tool, + "step": step.name, + "result": str(result)[:500], + "success": True, + }, session_id=session_id) + return result + except Exception as e: + await self.ws.emit(task_id, "tool_result", { + "tool": step.tool, + "step": step.name, + "error": str(e), + "success": False, + }, session_id=session_id) + return f"Error in {step.name}: {str(e)}" + + # ─── Finalize ────────────────────────────────────────────────────────────── + + async def finalize( + self, + goal: str, + steps: List[TaskStep], + results: List[str], + task_id: str, + session_id: str = "", + ) -> str: + """Compile final result summary.""" + steps_summary = "\n".join([ + f"- {s.name}: {r[:200]}" for s, r in zip(steps, results) + ]) + messages = [ + {"role": "system", "content": SYSTEM_PROMPT}, + {"role": "user", "content": ( + f"Summarize the completion of this goal:\n" + f"Goal: {goal}\n\n" + f"Steps completed:\n{steps_summary}\n\n" + f"Write a concise success summary with key outcomes." + )}, + ] + result = await self.llm_stream(messages, task_id=task_id, session_id=session_id) + return result or f"βœ… Completed: {goal}" + + # ─── Chat ────────────────────────────────────────────────────────────────── + + async def stream_chat(self, session_id: str, user_message: str): + """Stream a conversational chat response.""" + # Save user message to memory + await save_memory( + content=user_message, + memory_type="conversation", + session_id=session_id, + key="user_message", + ) + + # Get conversation history + history = await get_history(session_id, limit=10) + messages = [{"role": "system", "content": SYSTEM_PROMPT}] + for h in reversed(history[-10:]): + messages.append({"role": "user", "content": h["content"]}) + + messages.append({"role": "user", "content": user_message}) + + await self.ws.emit_chat(session_id, "stream_start", { + "status": "generating", + }) + + response = await self.llm_stream(messages, session_id=session_id) + + # Save assistant response to memory + await save_memory( + content=response, + memory_type="conversation", + session_id=session_id, + key="assistant_response", + ) + + await self.ws.emit_chat(session_id, "stream_end", { + "full_response": response, + "status": "complete", + }) + + return response diff --git a/core/models.py b/core/models.py new file mode 100644 index 0000000000000000000000000000000000000000..772070923d8ddf8b5cba203e2d64ddb31c010fe6 --- /dev/null +++ b/core/models.py @@ -0,0 +1,213 @@ +""" +Pydantic Models β€” Task, Chat, Memory, GitHub +""" + +import time +import uuid +from enum import Enum +from typing import Any, Dict, List, Optional +from pydantic import BaseModel, Field + + +def gen_id(prefix: str = "") -> str: + return f"{prefix}{uuid.uuid4().hex[:12]}" + + +# ─── Enums ───────────────────────────────────────────────────────────────────── + +class TaskStatus(str, Enum): + queued = "queued" + initializing = "initializing" + planning = "planning" + executing = "executing" + streaming = "streaming" + waiting_input = "waiting_input" + retrying = "retrying" + finalizing = "finalizing" + completed = "completed" + failed = "failed" + cancelled = "cancelled" + + +class EventType(str, Enum): + task_created = "task_created" + task_queued = "task_queued" + task_started = "task_started" + plan_generated = "plan_generated" + step_started = "step_started" + step_progress = "step_progress" + tool_called = "tool_called" + tool_result = "tool_result" + llm_chunk = "llm_chunk" + memory_updated = "memory_updated" + retry_attempt = "retry_attempt" + step_completed = "step_completed" + warning = "warning" + error = "error" + task_completed = "task_completed" + task_failed = "task_failed" + heartbeat = "heartbeat" + + +class MemoryType(str, Enum): + conversation = "conversation" + task = "task" + project = "project" + execution = "execution" + tool = "tool" + error = "error" + repo = "repo" + planning = "planning" + + +# ─── Task Models ─────────────────────────────────────────────────────────────── + +class TaskCreateRequest(BaseModel): + goal: str = Field(..., min_length=1, max_length=10000, description="What should the agent do?") + session_id: str = Field(default_factory=lambda: gen_id("sess_")) + project_id: str = Field(default="") + stream: bool = True + metadata: Dict[str, Any] = Field(default_factory=dict) + github_repo: Optional[str] = None + auto_commit: bool = False + + +class TaskStep(BaseModel): + id: str = Field(default_factory=lambda: gen_id("step_")) + name: str + description: str = "" + tool: Optional[str] = None + status: str = "pending" + output: Optional[str] = None + error: Optional[str] = None + started_at: Optional[float] = None + completed_at: Optional[float] = None + duration_ms: Optional[float] = None + + +class TaskPlan(BaseModel): + goal: str + steps: List[TaskStep] + estimated_duration: int = 0 + tools_needed: List[str] = [] + created_at: float = Field(default_factory=time.time) + + +class TaskResponse(BaseModel): + id: str + goal: str + status: TaskStatus + session_id: str + project_id: str + plan: Optional[TaskPlan] = None + result: Optional[str] = None + error: Optional[str] = None + created_at: float + started_at: Optional[float] = None + completed_at: Optional[float] = None + retry_count: int = 0 + stream_url: Optional[str] = None + ws_url: Optional[str] = None + + +class TaskCancelRequest(BaseModel): + reason: str = "User cancelled" + + +class TaskRetryRequest(BaseModel): + reset_state: bool = True + + +# ─── Chat Models ─────────────────────────────────────────────────────────────── + +class ChatMessage(BaseModel): + role: str = Field(..., pattern="^(user|assistant|system)$") + content: str + timestamp: float = Field(default_factory=time.time) + + +class ChatRequest(BaseModel): + messages: List[ChatMessage] + session_id: str = Field(default_factory=lambda: gen_id("sess_")) + project_id: str = "" + stream: bool = True + model: str = "gpt-4o" + temperature: float = 0.7 + max_tokens: int = 4096 + system_prompt: Optional[str] = None + + +class GoalRequest(BaseModel): + goal: str = Field(..., min_length=1, max_length=10000) + session_id: str = Field(default_factory=lambda: gen_id("sess_")) + project_id: str = "" + stream: bool = True + auto_execute: bool = True + github_repo: Optional[str] = None + + +# ─── Memory Models ───────────────────────────────────────────────────────────── + +class MemorySaveRequest(BaseModel): + content: str + memory_type: MemoryType + session_id: str = "" + project_id: str = "" + key: str = "" + metadata: Dict[str, Any] = {} + + +class MemorySearchRequest(BaseModel): + query: str + session_id: str = "" + project_id: str = "" + limit: int = 20 + + +# ─── GitHub Models ───────────────────────────────────────────────────────────── + +class GitHubCloneRequest(BaseModel): + repo_url: str + branch: str = "main" + local_path: Optional[str] = None + + +class GitHubCreateRepoRequest(BaseModel): + name: str + description: str = "" + private: bool = False + auto_init: bool = True + + +class GitHubCommitRequest(BaseModel): + repo: str + branch: str = "main" + files: Dict[str, str] # path β†’ content + message: str + create_branch: bool = False + + +class GitHubPRRequest(BaseModel): + repo: str + title: str + body: str = "" + head: str + base: str = "main" + draft: bool = False + + +class GitHubIssueRequest(BaseModel): + repo: str + title: str + body: str = "" + labels: List[str] = [] + + +# ─── Event Schema (unified) ──────────────────────────────────────────────────── + +class StreamEvent(BaseModel): + type: str + task_id: str = "" + session_id: str = "" + timestamp: float = Field(default_factory=time.time) + data: Dict[str, Any] = {} diff --git a/core/task_engine.py b/core/task_engine.py new file mode 100644 index 0000000000000000000000000000000000000000..6eeb28d10be4e264994dc2b16beafb7d8b2492b6 --- /dev/null +++ b/core/task_engine.py @@ -0,0 +1,241 @@ +""" +Task Engine β€” Heart of the Autonomous Agent +Manages task lifecycle, planning, execution, self-healing +""" + +import asyncio +import json +import os +import time +import uuid +from typing import Dict, Optional, List + +import structlog + +from core.models import TaskStatus, TaskPlan, TaskStep, TaskCreateRequest +from api.websocket_manager import WebSocketManager +from memory.db import ( + create_task, update_task_status, get_task, save_task_event, + save_memory, get_task_events +) + +log = structlog.get_logger() + +MAX_RETRIES = 3 +MAX_CONCURRENT = 5 + + +class TaskEngine: + def __init__(self, ws_manager: WebSocketManager): + self.ws = ws_manager + self._queue: asyncio.Queue = asyncio.Queue() + self._active: Dict[str, asyncio.Task] = {} + self._running = False + self._workers: List[asyncio.Task] = [] + + async def start(self): + self._running = True + for i in range(MAX_CONCURRENT): + worker = asyncio.create_task(self._worker(i)) + self._workers.append(worker) + log.info("TaskEngine started", workers=MAX_CONCURRENT) + + async def stop(self): + self._running = False + for w in self._workers: + w.cancel() + log.info("TaskEngine stopped") + + # ─── Public API ──────────────────────────────────────────────────────────── + + async def submit(self, req: TaskCreateRequest) -> str: + task_id = f"task_{uuid.uuid4().hex[:10]}" + await create_task( + task_id=task_id, + goal=req.goal, + session_id=req.session_id, + project_id=req.project_id, + metadata={**req.metadata, "github_repo": req.github_repo, "auto_commit": req.auto_commit}, + ) + await self.ws.emit(task_id, "task_created", { + "goal": req.goal, + "session_id": req.session_id, + "stream_url": f"/api/v1/tasks/{task_id}/stream", + "ws_url": f"/ws/tasks/{task_id}", + }, session_id=req.session_id) + await self._queue.put((task_id, req)) + await self.ws.emit(task_id, "task_queued", { + "position": self._queue.qsize(), + }, session_id=req.session_id) + log.info("Task submitted", task_id=task_id, goal=req.goal[:60]) + return task_id + + async def cancel(self, task_id: str, reason: str = "User cancelled"): + if task_id in self._active: + self._active[task_id].cancel() + del self._active[task_id] + await update_task_status(task_id, "cancelled", error=reason) + await self.ws.emit(task_id, "task_failed", {"reason": reason, "status": "cancelled"}) + + async def retry(self, task_id: str): + task = await get_task(task_id) + if not task: + return + req = TaskCreateRequest( + goal=task["goal"], + session_id=task["session_id"] or "", + project_id=task["project_id"] or "", + metadata=task.get("metadata") or {}, + ) + retry_count = (task.get("retry_count") or 0) + 1 + await update_task_status(task_id, "queued", retry_count=retry_count) + await self.ws.emit(task_id, "retry_attempt", {"count": retry_count}) + await self._queue.put((task_id, req)) + + async def handle_chat_message(self, session_id: str, content: str, websocket=None): + """Handle real-time chat message with streaming response.""" + from core.agent import AgentCore + agent = AgentCore(self.ws) + await agent.stream_chat(session_id=session_id, user_message=content) + + # ─── Worker Loop ─────────────────────────────────────────────────────────── + + async def _worker(self, worker_id: int): + log.info(f"Worker {worker_id} started") + while self._running: + try: + task_id, req = await asyncio.wait_for(self._queue.get(), timeout=1.0) + worker_task = asyncio.create_task(self._execute(task_id, req)) + self._active[task_id] = worker_task + await worker_task + self._active.pop(task_id, None) + self._queue.task_done() + except asyncio.TimeoutError: + continue + except asyncio.CancelledError: + break + except Exception as e: + log.error(f"Worker {worker_id} error", error=str(e)) + + async def _execute(self, task_id: str, req: TaskCreateRequest): + """Full task execution lifecycle.""" + from core.agent import AgentCore + agent = AgentCore(self.ws) + + try: + # ── Initializing ──────────────────────────────────────────────── + await update_task_status(task_id, "initializing") + await self.ws.emit(task_id, "task_started", { + "goal": req.goal, + "status": "initializing", + }, session_id=req.session_id) + await save_task_event(task_id, "task_started", {"goal": req.goal}) + + # ── Planning ──────────────────────────────────────────────────── + await update_task_status(task_id, "planning") + await self.ws.emit(task_id, "step_started", { + "step": "Planning", + "status": "planning", + "description": "Generating execution plan...", + }, session_id=req.session_id) + + plan = await agent.plan(goal=req.goal, task_id=task_id, session_id=req.session_id) + + await update_task_status(task_id, "executing", plan=plan.model_dump()) + await self.ws.emit(task_id, "plan_generated", { + "steps": [s.model_dump() for s in plan.steps], + "estimated_duration": plan.estimated_duration, + "tools_needed": plan.tools_needed, + }, session_id=req.session_id) + await save_task_event(task_id, "plan_generated", {"steps_count": len(plan.steps)}) + + # ── Execute Steps ──────────────────────────────────────────────── + results = [] + for i, step in enumerate(plan.steps): + await self.ws.emit(task_id, "step_started", { + "step": step.name, + "step_id": step.id, + "index": i, + "total": len(plan.steps), + "tool": step.tool, + }, session_id=req.session_id) + + step_result = await agent.execute_step( + step=step, + task_id=task_id, + session_id=req.session_id, + context={"goal": req.goal, "previous_results": results}, + ) + results.append(step_result) + + await self.ws.emit(task_id, "step_completed", { + "step": step.name, + "step_id": step.id, + "index": i, + "output": step_result[:500] if isinstance(step_result, str) else str(step_result)[:500], + "status": "completed", + }, session_id=req.session_id) + await save_task_event(task_id, "step_completed", {"step": step.name, "index": i}) + + # ── Finalize ───────────────────────────────────────────────────── + await update_task_status(task_id, "finalizing") + await self.ws.emit(task_id, "step_started", { + "step": "Finalizing", + "description": "Compiling results...", + }, session_id=req.session_id) + + final_result = await agent.finalize( + goal=req.goal, + steps=plan.steps, + results=results, + task_id=task_id, + session_id=req.session_id, + ) + + await update_task_status(task_id, "completed", result=final_result) + await self.ws.emit(task_id, "task_completed", { + "result": final_result, + "steps_completed": len(plan.steps), + "duration": time.time(), + }, session_id=req.session_id) + + # Save to memory + await save_memory( + content=f"Task: {req.goal}\nResult: {final_result}", + memory_type="task", + session_id=req.session_id, + project_id=req.project_id, + key=task_id, + ) + await self.ws.emit(task_id, "memory_updated", { + "type": "task", + "key": task_id, + }, session_id=req.session_id) + + log.info("Task completed", task_id=task_id) + + except asyncio.CancelledError: + await update_task_status(task_id, "cancelled") + await self.ws.emit(task_id, "task_failed", {"reason": "cancelled"}) + except Exception as e: + log.error("Task failed", task_id=task_id, error=str(e)) + task_data = await get_task(task_id) + retry_count = (task_data or {}).get("retry_count", 0) + + await self.ws.emit(task_id, "error", { + "error": str(e), + "retry_count": retry_count, + "will_retry": retry_count < MAX_RETRIES, + }, session_id=req.session_id) + + if retry_count < MAX_RETRIES: + await update_task_status(task_id, "retrying", retry_count=retry_count + 1) + await asyncio.sleep(2 ** retry_count) + await self.ws.emit(task_id, "retry_attempt", {"count": retry_count + 1}) + await self._execute(task_id, req) + else: + await update_task_status(task_id, "failed", error=str(e)) + await self.ws.emit(task_id, "task_failed", { + "error": str(e), + "retry_count": retry_count, + }, session_id=req.session_id) diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000000000000000000000000000000000000..ddba4e6b870059011d68d1ed7ec67d06c1a86771 --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,20 @@ +module.exports = { + apps: [ + { + name: 'devin-backend', + script: 'uvicorn', + args: 'main:app --host 0.0.0.0 --port 7860 --loop asyncio --log-level info', + interpreter: 'python3', + cwd: '/home/user/devin-agent/backend', + watch: false, + instances: 1, + exec_mode: 'fork', + env: { + PORT: 7860, + HOST: '0.0.0.0', + DB_PATH: '/tmp/devin_agent.db', + PYTHONUNBUFFERED: '1', + }, + }, + ], +} diff --git a/github/__init__.py b/github/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/kernel/__init__.py b/kernel/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c6ead012d1c75d4a8ac22ba8341d909005c68fa8 --- /dev/null +++ b/kernel/__init__.py @@ -0,0 +1 @@ +from .agent_kernel import AgentKernel, ContextManager, ToolRegistry diff --git a/kernel/agent_kernel.py b/kernel/agent_kernel.py new file mode 100644 index 0000000000000000000000000000000000000000..64676da0fa6b396e2cea8c6ecf7c809ef727b821 --- /dev/null +++ b/kernel/agent_kernel.py @@ -0,0 +1,308 @@ +""" +🧠 God Agent OS v9 β€” Agent Kernel +The Central Nervous System of the Space-Role Architecture. +Replaces GodAgentOrchestratorV7 with a generalized, modular kernel. +""" +import asyncio +import json +import time +import uuid +from typing import Any, Dict, List, Optional +import structlog +from spaces.catalog import SPACE_CATALOG + +log = structlog.get_logger() +SPACE_IDS = [space["id"] for space in SPACE_CATALOG] + +KERNEL_SYSTEM_PROMPT = f"""You are GOD AGENT OS v10 β€” a distributed autonomous agent operating system. + +Architecture: Distributed Worker Space Paradigm +- SPACES: {', '.join(SPACE_IDS)} +- ROLES: Cognition (Thinker), Automation (Operator), Execution (Doer), Repair (Fixer), Visual Intelligence (Observer) + +You are infinitely extensible. For any digital task, select the best worker space and role combination. +Prioritize god-core-space for orchestration, model-router-space for model strategy, deploy-worker-space for deployment, verification-worker-space for quality gates, and auth-gateway-space for permission concerns. +Respond in Burmese or English based on user language. +Be decisive, thorough, and production-focused. +""" + +INTENT_CLASSIFICATION_PROMPT = """Classify this request for the Space-Role autonomous agent system. + +User message: "{message}" + +Available Spaces: god-core-space, coding-worker-space, sandbox-worker-space, terminal-worker-space, filesystem-worker-space, browser-worker-space, vision-worker-space, ui-worker-space, debug-worker-space, test-worker-space, verification-worker-space, git-worker-space, deploy-worker-space, connector-worker-space, memory-worker-space, knowledge-worker-space, workflow-worker-space, eventbus-space, observability-space, session-runtime-space, model-router-space, auth-gateway-space +Available Roles: cognition, automation, execution, repair, visual_intelligence + +Respond ONLY with valid JSON: +{{ + "primary_space": "space_name", + "secondary_spaces": ["space1", "space2"], + "role": "role_name", + "intent": "brief description", + "complexity": "low|medium|high", + "requires_planning": true/false, + "parallel_tasks": [] +}}""" + + +class ContextManager: + """Maintains task state, active Space, and current Role.""" + + def __init__(self): + self._contexts: Dict[str, Dict] = {} + + def get(self, session_id: str) -> Dict: + if session_id not in self._contexts: + self._contexts[session_id] = { + "session_id": session_id, + "active_space": "god-core-space", + "current_role": "cognition", + "task_history": [], + "short_term_memory": [], + "created_at": time.time(), + "last_active": time.time(), + } + self._contexts[session_id]["last_active"] = time.time() + return self._contexts[session_id] + + def update(self, session_id: str, updates: Dict): + ctx = self.get(session_id) + ctx.update(updates) + + def add_to_memory(self, session_id: str, entry: Dict): + ctx = self.get(session_id) + ctx["short_term_memory"].append({**entry, "timestamp": time.time()}) + # Keep only last 20 entries + if len(ctx["short_term_memory"]) > 20: + ctx["short_term_memory"] = ctx["short_term_memory"][-20:] + + def get_all_sessions(self) -> List[str]: + return list(self._contexts.keys()) + + +class ToolRegistry: + """Centralized registry of all tools, categorized by Space.""" + + def __init__(self): + self._tools: Dict[str, Dict[str, Any]] = {} + self._space_tools: Dict[str, List[str]] = { + **{space_id: [] for space_id in SPACE_IDS}, + } + + def register(self, name: str, func, space: str, description: str): + self._tools[name] = {"func": func, "space": space, "description": description} + if space in self._space_tools: + self._space_tools[space].append(name) + + def get_tools_for_space(self, space: str) -> List[str]: + return self._space_tools.get(space, []) + + def execute(self, tool_name: str, **kwargs) -> Any: + if tool_name not in self._tools: + raise ValueError(f"Tool '{tool_name}' not found in registry") + return self._tools[tool_name]["func"](**kwargs) + + def get_all_tools_summary(self) -> Dict: + return {space: tools for space, tools in self._space_tools.items()} + + +class AgentKernel: + """ + The OS Core β€” replaces GodAgentOrchestratorV7. + Manages Space routing, Role switching, and tool orchestration. + """ + + def __init__(self, ws_manager=None, ai_router=None): + self.ws = ws_manager + self.ai_router = ai_router + self.context_manager = ContextManager() + self.tool_registry = ToolRegistry() + self._spaces: Dict[str, Any] = {} + self._active_tasks: Dict[str, Dict] = {} + self._task_history: List[Dict] = [] + self.version = "10.0.0" + log.info("🧠 Agent Kernel v10 initialized β€” Distributed Worker Space Architecture") + + def register_space(self, name: str, space_instance): + """Register a Space module.""" + self._spaces[name] = space_instance + log.info(f"πŸ“¦ Space registered: {name}") + + def get_space(self, name: str): + return self._spaces.get(name) + + def get_status(self) -> Dict: + return { + "version": self.version, + "architecture": "Distributed Worker Space", + "spaces": list(self._spaces.keys()), + "total_spaces": len(self._spaces), + "active_tasks": len(self._active_tasks), + "sessions": len(self.context_manager.get_all_sessions()), + "tools": self.tool_registry.get_all_tools_summary(), + } + + async def classify_intent(self, user_message: str) -> Dict: + """Classify intent to determine Space and Role.""" + try: + prompt = INTENT_CLASSIFICATION_PROMPT.format(message=user_message) + response = await self.ai_router.complete( + prompt=prompt, + system=KERNEL_SYSTEM_PROMPT, + max_tokens=400, + ) + text = response.get("content", "") + # Extract JSON + start = text.find("{") + end = text.rfind("}") + 1 + if start >= 0 and end > start: + return json.loads(text[start:end]) + except Exception as e: + log.warning(f"Intent classification failed: {e}") + + # Fallback + return { + "primary_space": "god-core-space", + "secondary_spaces": [], + "role": "cognition", + "intent": user_message, + "complexity": "medium", + "requires_planning": True, + "parallel_tasks": [], + } + + async def route_to_space(self, space_name: str, role: str, task: str, + session_id: str, context: Dict = None) -> str: + """Route a task to the appropriate Space with the given Role.""" + space = self._spaces.get(space_name) + if not space: + space = self._spaces.get("god-core-space") + + if not space: + return f"Space '{space_name}' not available." + + return await space.execute( + task=task, + role=role, + session_id=session_id, + context=context or {}, + ) + + async def orchestrate(self, user_message: str, session_id: str, context: Dict = None) -> str: + """Main orchestration entry point.""" + task_id = str(uuid.uuid4())[:8] + ctx = self.context_manager.get(session_id) + + # Broadcast thinking state + if self.ws: + await self.ws.broadcast_to_room(f"chat:{session_id}", { + "type": "kernel_status", + "task_id": task_id, + "status": "analyzing", + "message": "🧠 Agent Kernel analyzing request...", + "timestamp": time.time(), + }) + + # 1. Classify intent β†’ determine Space + Role + intent = await self.classify_intent(user_message) + primary_space = intent.get("primary_space", "core") + role = intent.get("role", "cognition") + + # Update context + self.context_manager.update(session_id, { + "active_space": primary_space, + "current_role": role, + }) + self.context_manager.add_to_memory(session_id, { + "type": "user_message", + "content": user_message, + "space": primary_space, + "role": role, + }) + + # Broadcast space activation + if self.ws: + await self.ws.broadcast_to_room(f"chat:{session_id}", { + "type": "space_activated", + "task_id": task_id, + "space": primary_space, + "role": role, + "intent": intent.get("intent", ""), + "timestamp": time.time(), + }) + + # 2. Route to primary Space + try: + # Build memory context + mem_context = { + "short_term_memory": ctx["short_term_memory"][-5:], + "intent": intent, + **(context or {}), + } + + result = await self.route_to_space( + space_name=primary_space, + role=role, + task=user_message, + session_id=session_id, + context=mem_context, + ) + + # 3. Handle secondary spaces if needed + secondary_spaces = intent.get("secondary_spaces", []) + secondary_results = {} + for sec_space in secondary_spaces[:2]: # Max 2 secondary spaces + try: + sec_result = await self.route_to_space( + space_name=sec_space, + role="automation", + task=user_message, + session_id=session_id, + context={"primary_result": result, **mem_context}, + ) + secondary_results[sec_space] = sec_result + except Exception as e: + log.warning(f"Secondary space {sec_space} failed: {e}") + + # Combine results + final_result = result + if secondary_results: + secondary_text = "\n\n".join([f"[{k.upper()} SPACE]\n{v}" for k, v in secondary_results.items()]) + final_result = f"{result}\n\n{secondary_text}" + + # Store in memory + self.context_manager.add_to_memory(session_id, { + "type": "assistant_response", + "content": final_result, + "space": primary_space, + "role": role, + }) + + # Track task + self._task_history.append({ + "task_id": task_id, + "session_id": session_id, + "message": user_message, + "space": primary_space, + "role": role, + "result_length": len(final_result), + "timestamp": time.time(), + }) + + return final_result + + except Exception as e: + log.error(f"Orchestration error: {e}") + # Switch to Repair role + if self.ws: + await self.ws.broadcast_to_room(f"chat:{session_id}", { + "type": "space_activated", + "space": "debug", + "role": "repair", + "message": "πŸ”§ Switching to Repair role...", + }) + return f"⚠️ Error in {primary_space} Space: {str(e)}\n\nDebug Space activated. Please try again." + + def get_agent(self, name: str): + """Backward compatibility - get space by name.""" + return self._spaces.get(name) diff --git a/main_v12.py b/main_v12.py new file mode 100644 index 0000000000000000000000000000000000000000..ef40980e9c529db8c762bb518909af5156896fce --- /dev/null +++ b/main_v12.py @@ -0,0 +1,1314 @@ +""" +GOD AGENT OS v12 β€” TRUE AUTONOMOUS AGENT RUNTIME +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +Real execution: E2B sandbox + tool router + live streaming +NOT a chatbot β€” an actual autonomous agent OS like Manus/Devin +""" + +import asyncio +import hashlib +import json +import os +import time +import uuid +from contextlib import asynccontextmanager +from typing import AsyncGenerator, Dict, List, Optional, Any + +import httpx +import structlog +from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.gzip import GZipMiddleware +from fastapi.responses import StreamingResponse, JSONResponse +from slowapi import Limiter, _rate_limit_exceeded_handler +from slowapi.util import get_remote_address +from slowapi.errors import RateLimitExceeded + +structlog.configure( + processors=[ + structlog.processors.TimeStamper(fmt="iso"), + structlog.stdlib.add_log_level, + structlog.processors.StackInfoRenderer(), + structlog.dev.ConsoleRenderer(), + ] +) +log = structlog.get_logger() + +# ─── Environment ────────────────────────────────────────────────────────────── +E2B_API_KEY = os.environ.get("E2B_API_KEY", "") +GEMINI_KEY = os.environ.get("GEMINI_KEY", "") +SAMBANOVA_KEY = os.environ.get("SAMBANOVA_KEY", "") +GITHUB_KEY = os.environ.get("GITHUB_KEY", "") +GROQ_KEY = os.environ.get("GROQ_API_KEY", "") +OPENAI_KEY = os.environ.get("OPENAI_API_KEY", "") + +# ─── Rate Limiter ────────────────────────────────────────────────────────────── +limiter = Limiter(key_func=get_remote_address) + + +# ─── WebSocket Manager ───────────────────────────────────────────────────────── +class WebSocketManager: + def __init__(self): + self._rooms: Dict[str, set] = {} + self._conn_count = 0 + + async def connect(self, ws: WebSocket, room: str): + await ws.accept() + if room not in self._rooms: + self._rooms[room] = set() + self._rooms[room].add(ws) + self._conn_count += 1 + log.info("WS connected", room=room, total=self._conn_count) + + def disconnect(self, ws: WebSocket, room: str): + if room in self._rooms: + self._rooms[room].discard(ws) + self._conn_count = max(0, self._conn_count - 1) + + async def broadcast(self, room: str, data: dict): + if "ts" not in data: + data["ts"] = time.time() + dead = set() + for ws in list(self._rooms.get(room, [])): + try: + await ws.send_json(data) + except Exception: + dead.add(ws) + for ws in dead: + self._rooms.get(room, set()).discard(ws) + + async def emit_chat(self, session_id: str, event_type: str, data: dict): + event = { + "type": event_type, + "session_id": session_id, + "ts": time.time(), + "data": data, + } + await self.broadcast(f"chat:{session_id}", event) + + async def heartbeat_loop(self): + while True: + await asyncio.sleep(20) + for room in list(self._rooms.keys()): + await self.broadcast(room, {"type": "heartbeat", "ts": time.time()}) + + def stats(self): + return {"connections": self._conn_count, "rooms": len(self._rooms)} + + +# ─── AI Router β€” Multi-provider with streaming ──────────────────────────────── +class AIRouter: + """Multi-provider AI router: Gemini β†’ Sambanova β†’ GitHub β†’ Groq""" + + PROVIDERS = { + "gemini": { + "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent", + "key": lambda: os.environ.get("GEMINI_KEY", GEMINI_KEY), + "type": "gemini", + }, + "sambanova": { + "url": "https://api.sambanova.ai/v1/chat/completions", + "key": lambda: os.environ.get("SAMBANOVA_KEY", SAMBANOVA_KEY), + "model": "Meta-Llama-3.3-70B-Instruct", + "type": "openai", + }, + "github": { + "url": "https://models.inference.ai.azure.com/chat/completions", + "key": lambda: os.environ.get("GITHUB_KEY", GITHUB_KEY), + "model": "gpt-4o", + "type": "openai", + }, + "groq": { + "url": "https://api.groq.com/openai/v1/chat/completions", + "key": lambda: os.environ.get("GROQ_API_KEY", GROQ_KEY), + "model": "llama-3.3-70b-versatile", + "type": "openai", + }, + "openai": { + "url": "https://api.openai.com/v1/chat/completions", + "key": lambda: os.environ.get("OPENAI_API_KEY", OPENAI_KEY), + "model": "gpt-4o", + "type": "openai", + }, + } + + ORDER = ["gemini", "sambanova", "github", "groq", "openai"] + + def get_active_provider(self) -> Optional[str]: + for p in self.ORDER: + k = self.PROVIDERS[p]["key"]() + if k and len(k) > 10: + return p + return None + + def get_stats(self) -> Dict: + stats = {} + for p in self.ORDER: + k = self.PROVIDERS[p]["key"]() + stats[p] = {"available": bool(k and len(k) > 10), "key_set": bool(k)} + return stats + + async def stream_chat( + self, + messages: List[Dict], + session_id: str, + ws_manager: Optional[WebSocketManager] = None, + tools: Optional[List] = None, + temperature: float = 0.7, + max_tokens: int = 8192, + ) -> AsyncGenerator[str, None]: + """Stream tokens from AI provider.""" + provider = self.get_active_provider() + + if not provider: + # Demo mode β€” no API key + async for chunk in self._demo_stream(messages): + yield chunk + return + + cfg = self.PROVIDERS[provider] + key = cfg["key"]() + + if cfg["type"] == "gemini": + async for chunk in self._gemini_stream(cfg["url"], key, messages, max_tokens, tools): + yield chunk + else: + async for chunk in self._openai_stream( + cfg["url"], key, cfg.get("model", "gpt-4o"), + messages, max_tokens, tools, temperature + ): + yield chunk + + async def _gemini_stream( + self, url: str, key: str, messages: List[Dict], + max_tokens: int, tools: Optional[List] = None + ) -> AsyncGenerator[str, None]: + """Stream from Gemini API.""" + # Convert messages to Gemini format + contents = [] + system_text = "" + for m in messages: + role = m.get("role", "user") + content = m.get("content", "") + if role == "system": + system_text = content + continue + g_role = "user" if role == "user" else "model" + contents.append({"role": g_role, "parts": [{"text": content}]}) + + if not contents: + contents = [{"role": "user", "parts": [{"text": "Hello"}]}] + + body = { + "contents": contents, + "generationConfig": { + "maxOutputTokens": max_tokens, + "temperature": 0.7, + }, + } + if system_text: + body["systemInstruction"] = {"parts": [{"text": system_text}]} + + # Add tools if provided + if tools: + gemini_tools = [] + for tool in tools: + gemini_tools.append({ + "functionDeclarations": [{ + "name": tool["name"], + "description": tool.get("description", ""), + "parameters": tool.get("parameters", {}), + }] + }) + body["tools"] = gemini_tools + + stream_url = url if "?alt=sse" in url else url + "?alt=sse" + stream_url = stream_url.replace("streamGenerateContent?", "streamGenerateContent?") + f"&key={key}" + if "?alt=sse" not in stream_url: + stream_url = stream_url.replace(f"&key={key}", "") + f"?alt=sse&key={key}" + + # Use non-streaming endpoint for tool calls + final_url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key={key}" + + try: + async with httpx.AsyncClient(timeout=120.0) as client: + # Try SSE streaming first + sse_url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent?alt=sse&key={key}" + async with client.stream("POST", sse_url, json=body) as resp: + if resp.status_code != 200: + # Fallback to non-streaming + resp2 = await client.post(final_url, json=body) + if resp2.status_code == 200: + data = resp2.json() + candidates = data.get("candidates", []) + if candidates: + parts = candidates[0].get("content", {}).get("parts", []) + for part in parts: + if "text" in part: + yield part["text"] + elif "functionCall" in part: + yield json.dumps({"function_call": part["functionCall"]}) + return + + async for line in resp.aiter_lines(): + if not line.startswith("data:"): + continue + data_str = line[5:].strip() + if not data_str or data_str == "[DONE]": + continue + try: + data = json.loads(data_str) + candidates = data.get("candidates", []) + if candidates: + parts = candidates[0].get("content", {}).get("parts", []) + for part in parts: + if "text" in part: + yield part["text"] + elif "functionCall" in part: + yield json.dumps({"function_call": part["functionCall"]}) + except json.JSONDecodeError: + pass + except Exception as e: + log.error("Gemini stream error", error=str(e)) + yield f"[Gemini error: {str(e)[:100]}]" + + async def _openai_stream( + self, url: str, key: str, model: str, + messages: List[Dict], max_tokens: int, + tools: Optional[List] = None, temperature: float = 0.7 + ) -> AsyncGenerator[str, None]: + """Stream from OpenAI-compatible API.""" + payload: Dict[str, Any] = { + "model": model, + "messages": messages, + "stream": True, + "temperature": temperature, + "max_tokens": max_tokens, + } + if tools: + payload["tools"] = [{"type": "function", "function": t} for t in tools] + payload["tool_choice"] = "auto" + + headers = { + "Authorization": f"Bearer {key}", + "Content-Type": "application/json", + } + + try: + async with httpx.AsyncClient(timeout=120.0) as client: + async with client.stream("POST", url, json=payload, headers=headers) as resp: + if resp.status_code != 200: + error_text = await resp.aread() + log.error("OpenAI stream error", status=resp.status_code, body=error_text[:200]) + # Try next provider + return + + tool_calls_buffer = {} + + async for line in resp.aiter_lines(): + if not line.startswith("data:"): + continue + data_str = line[6:].strip() + if data_str == "[DONE]": + # Flush any buffered tool calls + if tool_calls_buffer: + yield json.dumps({"tool_calls": list(tool_calls_buffer.values())}) + return + try: + data = json.loads(data_str) + delta = data["choices"][0].get("delta", {}) + + # Text content + if "content" in delta and delta["content"]: + yield delta["content"] + + # Tool calls + if "tool_calls" in delta: + for tc in delta["tool_calls"]: + idx = tc.get("index", 0) + if idx not in tool_calls_buffer: + tool_calls_buffer[idx] = { + "id": tc.get("id", ""), + "type": "function", + "function": {"name": "", "arguments": ""} + } + if tc.get("id"): + tool_calls_buffer[idx]["id"] = tc["id"] + fn = tc.get("function", {}) + if fn.get("name"): + tool_calls_buffer[idx]["function"]["name"] += fn["name"] + if fn.get("arguments"): + tool_calls_buffer[idx]["function"]["arguments"] += fn["arguments"] + + # Check if complete + try: + args_str = tool_calls_buffer[idx]["function"]["arguments"] + if args_str: + json.loads(args_str) # Will raise if incomplete + # Complete! Emit tool call + yield json.dumps({"tool_call": tool_calls_buffer[idx]}) + del tool_calls_buffer[idx] + except json.JSONDecodeError: + pass # Still accumulating + + except (json.JSONDecodeError, KeyError, IndexError): + pass + except Exception as e: + log.error("OpenAI-compat stream error", error=str(e)) + + async def _demo_stream(self, messages: List[Dict]) -> AsyncGenerator[str, None]: + """Demo mode when no API keys are configured.""" + last_user = next((m["content"] for m in reversed(messages) if m.get("role") == "user"), "Hello") + response = ( + f"⚠️ **No AI API Keys Configured**\n\n" + f"I received: *{last_user[:100]}*\n\n" + f"To enable real AI responses, set one of these environment variables:\n" + f"- `GEMINI_KEY` β€” Google Gemini (recommended, free tier available)\n" + f"- `SAMBANOVA_KEY` β€” SambaNova (fast Llama models)\n" + f"- `GITHUB_KEY` β€” GitHub Models (GPT-4o)\n" + f"- `GROQ_API_KEY` β€” Groq (ultra-fast)\n" + f"- `OPENAI_API_KEY` β€” OpenAI GPT-4\n\n" + f"E2B sandbox execution is {'βœ… configured' if E2B_API_KEY else '❌ not configured (set E2B_API_KEY)'}." + ) + for word in response.split(): + yield word + " " + await asyncio.sleep(0.02) + + async def complete( + self, + messages: List[Dict], + tools: Optional[List] = None, + temperature: float = 0.7, + max_tokens: int = 8192, + ) -> str: + """Non-streaming completion.""" + full = "" + async for chunk in self.stream_chat(messages, "", tools=tools, temperature=temperature, max_tokens=max_tokens): + full += chunk + return full + + +# ─── Autonomous Agent β€” Manus-style execution loop ──────────────────────────── +class AutonomousAgent: + """ + Real autonomous agent with: + - Tool calling (E2B execution, file ops, shell) + - Multi-step reasoning loop + - Live streaming of thoughts + actions + - Self-repair on errors + """ + + SYSTEM_PROMPT = """You are GOD AGENT OS β€” an elite autonomous AI agent like Manus/Devin. +You EXECUTE tasks autonomously using real tools. You do NOT explain how to do things β€” you DO them. + +CRITICAL RULES: +1. ALWAYS use tools to actually execute code, create files, run commands +2. NEVER say "you can run this" or "try this command" β€” ACTUALLY RUN IT YOURSELF +3. For ANY code task β€” use execute_python or execute_shell to run the real code +4. For file operations β€” use write_file, read_file, delete_file tools +5. Return REAL output: actual stdout, stderr, exit codes, file contents, timestamps +6. If a step fails, self-repair and retry with a fix + +AUTONOMOUS EXECUTION PROTOCOL: +- Think step by step +- Use tools for every concrete action +- Show real terminal output +- Confirm actual results (real file sizes, real timestamps, real SHA256 hashes) +- Complete the task end-to-end without asking for clarification + +You have access to: Python execution, shell commands, file system, web search. +Always verify your work by reading back files, checking output, confirming operations.""" + + MAX_TOOL_ITERATIONS = 20 + + def __init__(self, ai_router: AIRouter, ws_manager: WebSocketManager, tool_router=None): + self.ai = ai_router + self.ws = ws_manager + self.tool_router = tool_router + + async def run( + self, + user_message: str, + session_id: str, + task_id: str = "", + ) -> AsyncGenerator[str, None]: + """ + Run autonomous agent loop with real-time streaming. + Yields SSE-compatible chunks. + """ + from tools.tool_router import ToolRouter, TOOL_DEFINITIONS + if not self.tool_router: + self.tool_router = ToolRouter(self.ws) + + messages = [ + {"role": "system", "content": self.SYSTEM_PROMPT}, + {"role": "user", "content": user_message}, + ] + + # Emit thinking start + await self.ws.emit_chat(session_id, "agent_thinking", { + "task_id": task_id, + "message": user_message[:100], + }) + + yield json.dumps({ + "type": "agent_start", + "data": {"task_id": task_id, "message": user_message[:100]}, + "session_id": session_id, + }) + "\n" + + iteration = 0 + full_response = "" + current_thought = "" + tool_results_context = [] + + while iteration < self.MAX_TOOL_ITERATIONS: + iteration += 1 + log.info("Agent iteration", iteration=iteration, session_id=session_id) + + # Emit iteration start + await self.ws.emit_chat(session_id, "agent_iteration", { + "iteration": iteration, + "task_id": task_id, + }) + + # Stream LLM response + current_chunk = "" + tool_call_json = "" + in_tool_call = False + has_tool_call = False + + yield json.dumps({ + "type": "thinking_start", + "data": {"iteration": iteration}, + "session_id": session_id, + }) + "\n" + + async for chunk in self.ai.stream_chat( + messages, + session_id, + tools=TOOL_DEFINITIONS, + temperature=0.2, # Lower for tool use + ): + # Check if this is a tool call JSON + if chunk.startswith('{"tool_call":') or chunk.startswith('{"tool_calls":'): + in_tool_call = True + has_tool_call = True + tool_call_json = chunk + continue + elif chunk.startswith('{"function_call":'): + # Gemini format + in_tool_call = True + has_tool_call = True + tool_call_json = chunk + continue + + # Regular text token + current_chunk += chunk + current_thought += chunk + full_response += chunk + + # Emit token to frontend (real-time streaming) + yield json.dumps({ + "type": "llm_chunk", + "data": {"chunk": chunk, "iteration": iteration}, + "session_id": session_id, + }) + "\n" + + await self.ws.emit_chat(session_id, "llm_chunk", { + "chunk": chunk, + "iteration": iteration, + "task_id": task_id, + }) + + # Process tool call if present + if has_tool_call and tool_call_json: + try: + tool_data = json.loads(tool_call_json) + + # Handle both single tool_call and tool_calls array + tool_calls = [] + if "tool_call" in tool_data: + tool_calls = [tool_data["tool_call"]] + elif "tool_calls" in tool_data: + tool_calls = tool_data["tool_calls"] + elif "function_call" in tool_data: + # Gemini format + fc = tool_data["function_call"] + tool_calls = [{ + "id": uuid.uuid4().hex[:8], + "type": "function", + "function": {"name": fc.get("name", ""), "arguments": json.dumps(fc.get("args", {}))} + }] + + for tc in tool_calls: + fn = tc.get("function", {}) + tool_name = fn.get("name", "") + try: + tool_args = json.loads(fn.get("arguments", "{}")) + except Exception: + tool_args = {} + + if not tool_name: + continue + + log.info("Executing tool", tool=tool_name, args=str(tool_args)[:100]) + + # Emit tool execution start + yield json.dumps({ + "type": "tool_executing", + "data": { + "tool": tool_name, + "args": {k: str(v)[:200] for k, v in tool_args.items()}, + "task_id": task_id, + }, + "session_id": session_id, + }) + "\n" + + await self.ws.emit_chat(session_id, "computer_use_step", { + "type": self._get_step_type(tool_name), + "title": f"{tool_name}: {str(tool_args)[:80]}", + "task_id": task_id, + "status": "running", + }) + + # ACTUALLY EXECUTE THE TOOL + tool_result = await self.tool_router.execute_tool( + tool_name, tool_args, session_id, task_id + ) + + formatted = self.tool_router.format_tool_result(tool_name, tool_result) + + # Emit tool result + yield json.dumps({ + "type": "tool_result", + "data": { + "tool": tool_name, + "result": formatted[:2000], + "raw": {k: str(v)[:500] for k, v in tool_result.items()}, + "success": tool_result.get("success", True), + "sandbox_id": tool_result.get("sandbox_id", "local"), + "task_id": task_id, + }, + "session_id": session_id, + }) + "\n" + + await self.ws.emit_chat(session_id, "computer_use_step", { + "type": self._get_step_type(tool_name), + "title": f"βœ… {tool_name} completed", + "detail": str(tool_result.get("stdout", tool_result.get("output", "")))[:200], + "task_id": task_id, + "status": "done", + }) + + # Add to context for next LLM call + tool_results_context.append({ + "tool": tool_name, + "args": tool_args, + "result": formatted[:3000], + "raw_result": tool_result, + }) + + # Add assistant message with tool call + result to messages + messages.append({ + "role": "assistant", + "content": current_thought or f"Executing {tool_name}...", + }) + messages.append({ + "role": "user", + "content": f"Tool execution result for {tool_name}:\n{formatted[:3000]}\n\nContinue with the task based on this result.", + }) + + except Exception as e: + log.error("Tool call parsing error", error=str(e), json=tool_call_json[:200]) + messages.append({ + "role": "assistant", + "content": current_thought, + }) + messages.append({ + "role": "user", + "content": f"Tool execution error: {str(e)}. Please continue.", + }) + + current_thought = "" + # Continue the loop for next iteration + continue + + else: + # No tool calls β€” LLM gave a final text response + # Add final response to messages + if current_thought.strip(): + messages.append({ + "role": "assistant", + "content": current_thought, + }) + + # Task complete + yield json.dumps({ + "type": "agent_complete", + "data": { + "task_id": task_id, + "result": full_response[:500], + "iterations": iteration, + "tools_called": len(tool_results_context), + }, + "session_id": session_id, + }) + "\n" + + await self.ws.emit_chat(session_id, "agent_complete", { + "task_id": task_id, + "iterations": iteration, + "tools_called": len(tool_results_context), + }) + + yield json.dumps({ + "type": "stream_end", + "data": {"full_response": full_response, "task_id": task_id}, + "session_id": session_id, + }) + "\n" + return + + # Max iterations reached + yield json.dumps({ + "type": "agent_complete", + "data": {"task_id": task_id, "result": "Max iterations reached", "iterations": iteration}, + "session_id": session_id, + }) + "\n" + yield json.dumps({ + "type": "stream_end", + "data": {"full_response": full_response}, + "session_id": session_id, + }) + "\n" + + def _get_step_type(self, tool_name: str) -> str: + mapping = { + "execute_python": "coding", + "execute_shell": "terminal", + "write_file": "file", + "read_file": "file", + "delete_file": "file", + "list_files": "file", + "web_search": "browsing", + "install_package": "terminal", + } + return mapping.get(tool_name, "executing") + + +# ─── Session/Task State Manager ─────────────────────────────────────────────── +class SessionManager: + def __init__(self): + self._sessions: Dict[str, Dict] = {} + self._computer_use: Dict[str, List] = {} + + def get_or_create(self, session_id: str) -> Dict: + if session_id not in self._sessions: + self._sessions[session_id] = { + "id": session_id, + "created_at": time.time(), + "last_active": time.time(), + "message_count": 0, + "task_ids": [], + "status": "active", + } + return self._sessions[session_id] + + def add_task(self, session_id: str, task_id: str): + sess = self.get_or_create(session_id) + sess["task_ids"].append(task_id) + sess["last_active"] = time.time() + + def add_computer_use_step(self, session_id: str, step_type: str, data: Dict): + if session_id not in self._computer_use: + self._computer_use[session_id] = [] + self._computer_use[session_id].append({ + "id": uuid.uuid4().hex[:8], + "type": step_type, + "data": data, + "ts": time.time(), + "status": data.get("status", "running"), + }) + # Keep last 200 steps + self._computer_use[session_id] = self._computer_use[session_id][-200:] + + def get_computer_use_steps(self, session_id: str) -> List: + return self._computer_use.get(session_id, []) + + def get_session(self, session_id: str) -> Optional[Dict]: + return self._sessions.get(session_id) + + def list_sessions(self) -> List[Dict]: + return list(self._sessions.values()) + + +# ─── App Factory ────────────────────────────────────────────────────────────── +ws_manager = WebSocketManager() +ai_router = AIRouter() +session_manager = SessionManager() +agent: Optional[AutonomousAgent] = None + + +@asynccontextmanager +async def lifespan(app: FastAPI): + global agent + log.info("πŸš€ GOD AGENT OS v12 starting β€” TRUE AUTONOMOUS RUNTIME") + log.info(f"E2B sandbox: {'βœ… configured' if E2B_API_KEY else '⚠️ not configured (local fallback)'}") + + ai_stats = ai_router.get_stats() + active = [p for p, s in ai_stats.items() if s["available"]] + log.info(f"AI providers: {active or ['demo mode']}") + + # Init agent + from tools.tool_router import ToolRouter + tool_router = ToolRouter(ws_manager) + agent = AutonomousAgent(ai_router, ws_manager, tool_router) + + # Start heartbeat + asyncio.create_task(ws_manager.heartbeat_loop()) + + # Init DB + try: + from memory.db import init_db + await init_db() + except Exception as e: + log.warning("DB init skipped", error=str(e)) + + log.info("βœ… GOD AGENT OS v12 ready β€” Real execution enabled") + yield + log.info("Shutting down GOD AGENT OS v12...") + + +app = FastAPI( + title="GOD AGENT OS v12", + description="True Autonomous Agent Runtime β€” Real E2B Execution + Live Streaming", + version="12.0.0", + docs_url="/api/docs", + redoc_url="/api/redoc", + lifespan=lifespan, +) + +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) +app.add_middleware(GZipMiddleware, minimum_size=1000) +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Share state +app.state.ws_manager = ws_manager +app.state.ai_router = ai_router +app.state.session_manager = session_manager + + +# ─── Health ──────────────────────────────────────────────────────────────────── +@app.get("/health") +@app.get("/api/v1/health") +async def health(): + ai_stats = ai_router.get_stats() + active_providers = [p for p, s in ai_stats.items() if s["available"]] + return { + "status": "healthy", + "version": "12.0.0", + "timestamp": time.time(), + "e2b": bool(E2B_API_KEY), + "ai_providers": active_providers, + "ws_connections": ws_manager.stats()["connections"], + "mode": "autonomous_agent", + "features": { + "real_execution": True, + "e2b_sandbox": bool(E2B_API_KEY), + "local_fallback": True, + "streaming": True, + "tool_calling": True, + "file_ops": True, + "shell_exec": True, + } + } + + +@app.get("/") +async def root(): + return { + "name": "GOD AGENT OS v12", + "version": "12.0.0", + "status": "operational", + "mode": "AUTONOMOUS_AGENT", + "docs": "/api/docs", + "health": "/health", + "execution": "real_e2b_sandbox" if E2B_API_KEY else "local_subprocess", + } + + +# ─── Chat (SSE streaming with real tool execution) ──────────────────────────── + +@app.post("/api/v1/chat") +async def chat(request: Request): + body = await request.json() + messages = body.get("messages", []) + stream = body.get("stream", True) + session_id = body.get("session_id") or uuid.uuid4().hex[:12] + + if not messages: + raise HTTPException(status_code=400, detail="messages required") + + # Get last user message + user_message = next( + (m["content"] for m in reversed(messages) if m.get("role") == "user"), "" + ) + + sess = session_manager.get_or_create(session_id) + sess["message_count"] = sess.get("message_count", 0) + 1 + + task_id = uuid.uuid4().hex[:12] + session_manager.add_task(session_id, task_id) + + if stream: + async def stream_gen(): + try: + async for chunk in agent.run(user_message, session_id, task_id): + yield f"data: {chunk}\n" + except Exception as e: + log.error("Stream error", error=str(e)) + yield f"data: {json.dumps({'type': 'error', 'data': {'error': str(e)}, 'session_id': session_id})}\n\n" + + return StreamingResponse( + stream_gen(), + media_type="text/event-stream", + headers={ + "Cache-Control": "no-cache", + "X-Accel-Buffering": "no", + "Connection": "keep-alive", + }, + ) + else: + # Non-streaming: collect full response + full = "" + async for chunk in agent.run(user_message, session_id, task_id): + try: + data = json.loads(chunk) + if data.get("type") == "llm_chunk": + full += data.get("data", {}).get("chunk", "") + elif data.get("type") == "stream_end": + full = data.get("data", {}).get("full_response", full) + break + except Exception: + pass + + return JSONResponse({ + "response": full, + "task_id": task_id, + "session_id": session_id, + "timestamp": time.time(), + }) + + +@app.post("/api/v1/chat/stream") +async def chat_stream(request: Request): + body = await request.json() + body["stream"] = True + # Rebuild request-like object + from fastapi import Request as FR + import io + new_body = json.dumps(body).encode() + # Patch the request body + async def receive(): + return {"type": "http.request", "body": new_body} + request._receive = receive + return await chat(request) + + +@app.post("/api/v1/orchestrate") +async def orchestrate(request: Request): + body = await request.json() + message = body.get("message", "") + session_id = body.get("session_id") or uuid.uuid4().hex[:12] + stream = body.get("stream", False) + + if not message: + raise HTTPException(status_code=400, detail="message required") + + task_id = uuid.uuid4().hex[:12] + session_manager.add_task(session_id, task_id) + + if stream: + async def stream_gen(): + async for chunk in agent.run(message, session_id, task_id): + yield f"data: {chunk}\n" + + return StreamingResponse( + stream_gen(), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}, + ) + + # Non-streaming + full = "" + tool_results = [] + async for chunk in agent.run(message, session_id, task_id): + try: + data = json.loads(chunk) + if data.get("type") == "llm_chunk": + full += data.get("data", {}).get("chunk", "") + elif data.get("type") == "tool_result": + tool_results.append(data.get("data", {})) + elif data.get("type") == "stream_end": + full = data.get("data", {}).get("full_response", full) + break + except Exception: + pass + + return { + "task_id": task_id, + "session_id": session_id, + "result": full, + "tool_results": tool_results, + "status": "complete", + "timestamp": time.time(), + } + + +# ─── Direct Tool Execution ───────────────────────────────────────────────────── +@app.post("/api/v1/execute") +async def execute_tool(request: Request): + """Direct tool execution endpoint.""" + body = await request.json() + tool_name = body.get("tool", "") + tool_args = body.get("args", {}) + session_id = body.get("session_id") or uuid.uuid4().hex[:12] + task_id = body.get("task_id") or uuid.uuid4().hex[:8] + + if not tool_name: + raise HTTPException(status_code=400, detail="tool name required") + + from tools.tool_router import ToolRouter + router = ToolRouter(ws_manager) + result = await router.execute_tool(tool_name, tool_args, session_id, task_id) + formatted = router.format_tool_result(tool_name, result) + + return { + "tool": tool_name, + "args": tool_args, + "result": result, + "formatted": formatted, + "session_id": session_id, + "task_id": task_id, + "timestamp": time.time(), + } + + +# ─── Sandbox Session Info ────────────────────────────────────────────────────── +@app.get("/api/v1/sandbox/{session_id}") +async def get_sandbox_info(session_id: str): + from sandbox.e2b_executor import get_executor + executor = get_executor() + info = executor.get_session_info(session_id) + return { + "session_id": session_id, + "sandbox": info, + "e2b_configured": bool(E2B_API_KEY), + } + + +@app.delete("/api/v1/sandbox/{session_id}") +async def close_sandbox(session_id: str): + from sandbox.e2b_executor import get_executor + executor = get_executor() + await executor.close_session(session_id) + return {"status": "closed", "session_id": session_id} + + +# ─── Computer Use Steps ──────────────────────────────────────────────────────── +@app.get("/api/v1/computer-use/{session_id}") +async def get_computer_use(session_id: str): + steps = session_manager.get_computer_use_steps(session_id) + return { + "session_id": session_id, + "steps": steps, + "count": len(steps), + "status": "complete" if steps and steps[-1].get("status") == "done" else "running" if steps else "idle", + } + + +# ─── WebSocket Endpoints ─────────────────────────────────────────────────────── +@app.websocket("/ws/{session_id}") +async def ws_endpoint(websocket: WebSocket, session_id: str): + await ws_manager.connect(websocket, f"chat:{session_id}") + session_manager.get_or_create(session_id) + + try: + while True: + data = await websocket.receive_json() + event_type = data.get("type", "") + + if event_type == "ping": + await websocket.send_json({"type": "pong", "ts": time.time()}) + + elif event_type == "message": + message = data.get("message", "") + task_id = uuid.uuid4().hex[:12] + session_manager.add_task(session_id, task_id) + + await ws_manager.emit_chat(session_id, "task_start", { + "task_id": task_id, + "message": message[:100], + }) + + # Run agent asynchronously + asyncio.create_task(_ws_run_agent(message, session_id, task_id)) + + elif event_type == "execute": + # Direct tool execution via WebSocket + tool_name = data.get("tool", "") + tool_args = data.get("args", {}) + task_id = data.get("task_id", uuid.uuid4().hex[:8]) + + if tool_name: + from tools.tool_router import ToolRouter + router = ToolRouter(ws_manager) + result = await router.execute_tool(tool_name, tool_args, session_id, task_id) + formatted = router.format_tool_result(tool_name, result) + await websocket.send_json({ + "type": "tool_result", + "tool": tool_name, + "result": result, + "formatted": formatted, + "task_id": task_id, + }) + + elif event_type == "stop": + await ws_manager.emit_chat(session_id, "task_stopped", {}) + + except WebSocketDisconnect: + ws_manager.disconnect(websocket, f"chat:{session_id}") + + +@app.websocket("/ws/computer-use/{session_id}") +async def ws_computer_use(websocket: WebSocket, session_id: str): + await websocket.accept() + last_count = 0 + try: + while True: + steps = session_manager.get_computer_use_steps(session_id) + if len(steps) > last_count: + for step in steps[last_count:]: + await websocket.send_json({ + "type": "computer_use_step", + "step": step, + "session_id": session_id, + }) + last_count = len(steps) + await asyncio.sleep(0.3) + except WebSocketDisconnect: + pass + except Exception: + pass + + +async def _ws_run_agent(message: str, session_id: str, task_id: str): + """Run agent and send results via WebSocket.""" + try: + async for chunk in agent.run(message, session_id, task_id): + try: + data = json.loads(chunk) + await ws_manager.emit_chat(session_id, data.get("type", "chunk"), data.get("data", {})) + except Exception: + pass + await ws_manager.emit_chat(session_id, "task_complete", {"task_id": task_id}) + except Exception as e: + await ws_manager.emit_chat(session_id, "task_error", {"task_id": task_id, "error": str(e)}) + + +# ─── AI Stats ────────────────────────────────────────────────────────────────── +@app.get("/api/v1/ai/stats") +async def ai_stats(): + return {"stats": ai_router.get_stats(), "active": ai_router.get_active_provider()} + + +@app.get("/api/v1/ai/pool-status") +async def ai_pool_status(): + return {"pools": ai_router.get_stats()} + + +@app.get("/api/v1/system/status") +async def system_status(): + ai_stats = ai_router.get_stats() + return { + "system": "god_agent_os_v12", + "status": "operational", + "timestamp": time.time(), + "version": "12.0.0", + "execution_mode": "e2b_sandbox" if E2B_API_KEY else "local_subprocess", + "ai_providers": {p: s["available"] for p, s in ai_stats.items()}, + "active_provider": ai_router.get_active_provider(), + "sessions": len(session_manager.list_sessions()), + "features": { + "real_execution": True, + "e2b_sandbox": bool(E2B_API_KEY), + "tool_calling": True, + "streaming": True, + "websocket": True, + "computer_use": True, + "self_repair": True, + } + } + + +# ─── Agents + Spaces (compatibility with frontend) ──────────────────────────── +AGENTS_LIST = [ + {"name": "chat", "status": "active", "role": "Conversation + Orchestration"}, + {"name": "coding", "status": "active", "role": "Code Generation + Review"}, + {"name": "sandbox", "status": "active", "role": f"Execution ({'E2B' if E2B_API_KEY else 'Local'})"}, + {"name": "planner", "status": "active", "role": "Task Planning"}, + {"name": "debug", "status": "active", "role": "Debugging + Error Analysis"}, + {"name": "file", "status": "active", "role": "File System Operations"}, + {"name": "git", "status": "active", "role": "Git + GitHub Operations"}, + {"name": "deploy", "status": "active", "role": "Deployment Automation"}, + {"name": "browser", "status": "active", "role": "Web Browsing + Research"}, + {"name": "memory", "status": "active", "role": "Long-term Memory"}, + {"name": "test", "status": "active", "role": "Test Generation + Running"}, + {"name": "vision", "status": "active", "role": "UI Generation + Vision"}, + {"name": "workflow", "status": "active", "role": "Workflow Automation"}, + {"name": "connector", "status": "active", "role": "External Integrations"}, + {"name": "reasoning", "status": "active", "role": "Deep Reasoning + Analysis"}, + {"name": "ui", "status": "active", "role": "UI/UX Generation"}, +] + + +@app.get("/api/v1/agents") +async def list_agents(): + return {"agents": AGENTS_LIST, "total": len(AGENTS_LIST)} + + +@app.post("/api/v1/agents/{agent_name}/run") +async def run_agent(agent_name: str, request: Request): + body = await request.json() + task = body.get("task", "") + session_id = body.get("session_id") or uuid.uuid4().hex[:12] + task_id = uuid.uuid4().hex[:12] + + # Route to autonomous agent + full = "" + async for chunk in agent.run(task, session_id, task_id): + try: + data = json.loads(chunk) + if data.get("type") == "llm_chunk": + full += data.get("data", {}).get("chunk", "") + elif data.get("type") == "stream_end": + break + except Exception: + pass + + return {"agent": agent_name, "task_id": task_id, "result": full, "status": "complete"} + + +SPACES_LIST = [ + {"id": "god-core", "name": "God Core", "role": "orchestration", "icon": "🧠", "status": "active"}, + {"id": "coding", "name": "Coding Worker", "role": "code_generation", "icon": "⚑", "status": "active"}, + {"id": "sandbox", "name": "Sandbox", "role": "execution", "icon": "πŸ”§", "status": "active", + "backend": "e2b" if E2B_API_KEY else "local"}, + {"id": "terminal", "name": "Terminal", "role": "shell", "icon": "πŸ–₯️", "status": "active"}, + {"id": "filesystem", "name": "FileSystem", "role": "files", "icon": "πŸ“", "status": "active"}, + {"id": "browser", "name": "Browser", "role": "research", "icon": "🌐", "status": "active"}, + {"id": "git", "name": "Git Worker", "role": "git", "icon": "πŸ”€", "status": "active"}, + {"id": "deploy", "name": "Deploy Worker", "role": "deployment", "icon": "πŸš€", "status": "active"}, + {"id": "memory", "name": "Memory", "role": "memory", "icon": "πŸ’Ύ", "status": "active"}, + {"id": "debug", "name": "Debug", "role": "debugging", "icon": "πŸ›", "status": "active"}, + {"id": "test", "name": "Testing", "role": "testing", "icon": "πŸ§ͺ", "status": "active"}, + {"id": "model-router", "name": "Model Router", "role": "ai_routing", "icon": "πŸ€–", "status": "active"}, +] + + +@app.get("/api/v1/spaces") +async def get_spaces(): + return { + "spaces": SPACES_LIST, + "total": len(SPACES_LIST), + "active": len(SPACES_LIST), + "execution_backend": "e2b" if E2B_API_KEY else "local_subprocess", + } + + +# ─── Memory (compatibility) ──────────────────────────────────────────────────── +@app.get("/api/v1/memory/") +async def get_memory(session_id: str = ""): + try: + from memory.db import get_history + history = await get_history(session_id=session_id, limit=50) + return {"memories": history, "total": len(history)} + except Exception: + return {"memories": [], "total": 0} + + +@app.post("/api/v1/memory/") +async def save_memory_entry(request: Request): + body = await request.json() + try: + from memory.db import save_memory + await save_memory( + content=body.get("content", ""), + memory_type=body.get("type", "general"), + session_id=body.get("session_id", ""), + key=body.get("key", ""), + ) + return {"status": "saved"} + except Exception as e: + return {"status": "error", "error": str(e)} + + +# ─── Tasks (compatibility) ───────────────────────────────────────────────────── +@app.get("/api/v1/tasks/") +async def get_tasks(): + try: + from memory.db import get_task + return {"tasks": [], "total": 0} + except Exception: + return {"tasks": [], "total": 0} + + +@app.post("/api/v1/chat/goal") +async def submit_goal(request: Request): + body = await request.json() + goal = body.get("goal", "") + session_id = body.get("session_id") or uuid.uuid4().hex[:12] + task_id = uuid.uuid4().hex[:12] + session_manager.add_task(session_id, task_id) + + return { + "task_id": task_id, + "goal": goal, + "status": "queued", + "session_id": session_id, + "ws_url": f"/ws/{session_id}", + "stream_url": f"/api/v1/chat", + } + + +# ─── GitHub (compatibility) ──────────────────────────────────────────────────── +@app.get("/api/v1/github/repos") +async def github_repos(): + return {"repos": [], "message": "Set GITHUB_TOKEN for GitHub integration"} + + +# ─── Connectors (compatibility) ──────────────────────────────────────────────── +@app.get("/api/v1/connectors") +async def get_connectors(): + connectors = [ + {"id": "e2b", "name": "E2B Sandbox", "type": "execution", + "connected": bool(E2B_API_KEY), "status": "active" if E2B_API_KEY else "needs_key"}, + {"id": "gemini", "name": "Google Gemini", "type": "ai", + "connected": bool(GEMINI_KEY), "status": "active" if GEMINI_KEY else "needs_key"}, + {"id": "github", "name": "GitHub Models", "type": "ai", + "connected": bool(GITHUB_KEY), "status": "active" if GITHUB_KEY else "needs_key"}, + {"id": "sambanova", "name": "SambaNova", "type": "ai", + "connected": bool(SAMBANOVA_KEY), "status": "active" if SAMBANOVA_KEY else "needs_key"}, + ] + return {"connectors": connectors, "total": len(connectors)} + + +if __name__ == "__main__": + import uvicorn + port = int(os.environ.get("PORT", 7860)) + uvicorn.run("main_v12:app", host="0.0.0.0", port=port, reload=False, workers=1) diff --git a/memory/__init__.py b/memory/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/memory/db.py b/memory/db.py new file mode 100644 index 0000000000000000000000000000000000000000..6fecd889cd5c43556171674ca8157677452162e5 --- /dev/null +++ b/memory/db.py @@ -0,0 +1,311 @@ +""" +Production SQLite Database β€” Async via aiosqlite +Handles tasks, memory, sessions, events +""" + +import aiosqlite +import os +import pathlib +import json +import time +from typing import Optional, List, Dict, Any +import structlog + +log = structlog.get_logger() + +import pathlib + +# Use /data for HuggingFace persistent storage, fallback to /tmp for local dev +_default_db = "/data/god_agent_os.db" if os.path.isdir("/data") else "/tmp/god_agent_os.db" +DB_PATH = os.environ.get("DB_PATH", _default_db) + +# Ensure the directory exists before SQLite tries to open the file +_db_dir = str(pathlib.Path(DB_PATH).parent) +os.makedirs(_db_dir, exist_ok=True) + + +async def get_db() -> aiosqlite.Connection: + db = await aiosqlite.connect(DB_PATH) + db.row_factory = aiosqlite.Row + await db.execute("PRAGMA journal_mode=WAL") + await db.execute("PRAGMA foreign_keys=ON") + return db + + +async def init_db(): + """Initialize all tables.""" + log.info("Initializing database", path=DB_PATH) + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("PRAGMA journal_mode=WAL") + await db.execute("PRAGMA foreign_keys=ON") + + # Tasks table + await db.execute(""" + CREATE TABLE IF NOT EXISTS tasks ( + id TEXT PRIMARY KEY, + session_id TEXT, + project_id TEXT, + goal TEXT NOT NULL, + status TEXT DEFAULT 'queued', + plan TEXT, + result TEXT, + error TEXT, + metadata TEXT DEFAULT '{}', + created_at REAL, + started_at REAL, + completed_at REAL, + retry_count INTEGER DEFAULT 0 + ) + """) + + # Task events table + await db.execute(""" + CREATE TABLE IF NOT EXISTS task_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id TEXT NOT NULL, + event_type TEXT NOT NULL, + data TEXT DEFAULT '{}', + timestamp REAL, + FOREIGN KEY (task_id) REFERENCES tasks(id) + ) + """) + + # Memory table + await db.execute(""" + CREATE TABLE IF NOT EXISTS memory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT, + project_id TEXT, + memory_type TEXT NOT NULL, + key TEXT, + content TEXT NOT NULL, + metadata TEXT DEFAULT '{}', + embedding TEXT, + created_at REAL, + updated_at REAL + ) + """) + + # Sessions table + await db.execute(""" + CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + project_id TEXT, + user_id TEXT, + metadata TEXT DEFAULT '{}', + created_at REAL, + last_active REAL + ) + """) + + # GitHub operations table + await db.execute(""" + CREATE TABLE IF NOT EXISTS github_ops ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id TEXT, + operation TEXT NOT NULL, + repo TEXT, + branch TEXT, + status TEXT DEFAULT 'pending', + result TEXT, + created_at REAL + ) + """) + + # Indexes + await db.execute("CREATE INDEX IF NOT EXISTS idx_tasks_session ON tasks(session_id)") + await db.execute("CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)") + await db.execute("CREATE INDEX IF NOT EXISTS idx_events_task ON task_events(task_id)") + await db.execute("CREATE INDEX IF NOT EXISTS idx_memory_session ON memory(session_id)") + await db.execute("CREATE INDEX IF NOT EXISTS idx_memory_project ON memory(project_id)") + await db.execute("CREATE INDEX IF NOT EXISTS idx_memory_type ON memory(memory_type)") + + await db.commit() + log.info("βœ… Database initialized") + + +# ─── Task CRUD ───────────────────────────────────────────────────────────────── + +async def create_task(task_id: str, goal: str, session_id: str = "", project_id: str = "", metadata: dict = {}): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(""" + INSERT INTO tasks (id, session_id, project_id, goal, status, metadata, created_at) + VALUES (?, ?, ?, ?, 'queued', ?, ?) + """, (task_id, session_id, project_id, goal, json.dumps(metadata), time.time())) + await db.commit() + + +async def update_task_status(task_id: str, status: str, **kwargs): + fields = ["status = ?"] + values = [status] + if status == "executing": + fields.append("started_at = ?") + values.append(time.time()) + if status in ("completed", "failed", "cancelled"): + fields.append("completed_at = ?") + values.append(time.time()) + for k, v in kwargs.items(): + if k in ("plan", "result", "error"): + fields.append(f"{k} = ?") + values.append(v if isinstance(v, str) else json.dumps(v)) + elif k == "retry_count": + fields.append("retry_count = ?") + values.append(v) + values.append(task_id) + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(f"UPDATE tasks SET {', '.join(fields)} WHERE id = ?", values) + await db.commit() + + +async def get_task(task_id: str) -> Optional[Dict]: + async with aiosqlite.connect(DB_PATH) as db: + db.row_factory = aiosqlite.Row + async with db.execute("SELECT * FROM tasks WHERE id = ?", (task_id,)) as cursor: + row = await cursor.fetchone() + if row: + d = dict(row) + d["metadata"] = json.loads(d.get("metadata") or "{}") + d["plan"] = json.loads(d["plan"]) if d.get("plan") else None + return d + return None + + +async def list_tasks(session_id: str = "", limit: int = 50) -> List[Dict]: + async with aiosqlite.connect(DB_PATH) as db: + db.row_factory = aiosqlite.Row + if session_id: + async with db.execute( + "SELECT * FROM tasks WHERE session_id = ? ORDER BY created_at DESC LIMIT ?", + (session_id, limit) + ) as cursor: + rows = await cursor.fetchall() + else: + async with db.execute( + "SELECT * FROM tasks ORDER BY created_at DESC LIMIT ?", (limit,) + ) as cursor: + rows = await cursor.fetchall() + return [dict(r) for r in rows] + + +async def save_task_event(task_id: str, event_type: str, data: dict = {}): + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(""" + INSERT INTO task_events (task_id, event_type, data, timestamp) + VALUES (?, ?, ?, ?) + """, (task_id, event_type, json.dumps(data), time.time())) + await db.commit() + + +async def get_task_events(task_id: str) -> List[Dict]: + async with aiosqlite.connect(DB_PATH) as db: + db.row_factory = aiosqlite.Row + async with db.execute( + "SELECT * FROM task_events WHERE task_id = ? ORDER BY timestamp ASC", (task_id,) + ) as cursor: + rows = await cursor.fetchall() + return [dict(r) for r in rows] + + +# ─── Memory CRUD ─────────────────────────────────────────────────────────────── + +async def save_memory( + content: str, + memory_type: str, + session_id: str = "", + project_id: str = "", + key: str = "", + metadata: dict = {} +): + now = time.time() + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(""" + INSERT INTO memory (session_id, project_id, memory_type, key, content, metadata, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, (session_id, project_id, memory_type, key, content, json.dumps(metadata), now, now)) + await db.commit() + + +async def search_memory(query: str, session_id: str = "", project_id: str = "", limit: int = 20) -> List[Dict]: + """Simple keyword search (upgrade to vector search in production).""" + async with aiosqlite.connect(DB_PATH) as db: + db.row_factory = aiosqlite.Row + q = f"%{query}%" + if session_id: + async with db.execute( + "SELECT * FROM memory WHERE session_id = ? AND content LIKE ? ORDER BY updated_at DESC LIMIT ?", + (session_id, q, limit) + ) as cursor: + rows = await cursor.fetchall() + elif project_id: + async with db.execute( + "SELECT * FROM memory WHERE project_id = ? AND content LIKE ? ORDER BY updated_at DESC LIMIT ?", + (project_id, q, limit) + ) as cursor: + rows = await cursor.fetchall() + else: + async with db.execute( + "SELECT * FROM memory WHERE content LIKE ? ORDER BY updated_at DESC LIMIT ?", + (q, limit) + ) as cursor: + rows = await cursor.fetchall() + return [dict(r) for r in rows] + + +async def get_project_memory(project_id: str, memory_type: str = "", limit: int = 100) -> List[Dict]: + async with aiosqlite.connect(DB_PATH) as db: + db.row_factory = aiosqlite.Row + if memory_type: + async with db.execute( + "SELECT * FROM memory WHERE project_id = ? AND memory_type = ? ORDER BY updated_at DESC LIMIT ?", + (project_id, memory_type, limit) + ) as cursor: + rows = await cursor.fetchall() + else: + async with db.execute( + "SELECT * FROM memory WHERE project_id = ? ORDER BY updated_at DESC LIMIT ?", + (project_id, limit) + ) as cursor: + rows = await cursor.fetchall() + return [dict(r) for r in rows] + + +async def get_history(session_id: str, limit: int = 50) -> List[Dict]: + async with aiosqlite.connect(DB_PATH) as db: + db.row_factory = aiosqlite.Row + async with db.execute( + "SELECT * FROM memory WHERE session_id = ? AND memory_type = 'conversation' ORDER BY created_at DESC LIMIT ?", + (session_id, limit) + ) as cursor: + rows = await cursor.fetchall() + return [dict(r) for r in rows] + + +async def list_sessions(limit: int = 50) -> List[Dict]: + """List all sessions from the sessions table.""" + async with aiosqlite.connect(DB_PATH) as db: + db.row_factory = aiosqlite.Row + async with db.execute( + "SELECT * FROM sessions ORDER BY last_active DESC LIMIT ?", (limit,) + ) as cursor: + rows = await cursor.fetchall() + return [dict(r) for r in rows] + + +async def upsert_session(session_id: str, project_id: str = "", user_id: str = "", metadata: dict = {}): + """Create or update a session record.""" + now = time.time() + async with aiosqlite.connect(DB_PATH) as db: + await db.execute(""" + INSERT INTO sessions (id, project_id, user_id, metadata, created_at, last_active) + VALUES (?, ?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET last_active = ?, metadata = ? + """, (session_id, project_id, user_id, json.dumps(metadata), now, now, now, json.dumps(metadata))) + await db.commit() + + +async def delete_session(session_id: str): + """Delete a session and its memories.""" + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("DELETE FROM sessions WHERE id = ?", (session_id,)) + await db.execute("DELETE FROM memory WHERE session_id = ?", (session_id,)) + await db.commit() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..e81f707673d506bf6d3ca3ba8140160015c3934a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +# God Agent OS β€” Phase 1 (stability-first) +fastapi==0.115.0 +uvicorn[standard]==0.30.6 +httpx[http2]==0.27.2 +pydantic>=2.8.2,<3 +python-multipart==0.0.9 +structlog==24.4.0 +python-dotenv==1.0.1 +# Real sandbox runtime +e2b==1.0.5 +e2b-code-interpreter==1.0.5 +# WS support +websockets==13.1 diff --git a/requirements_v12.txt b/requirements_v12.txt new file mode 100644 index 0000000000000000000000000000000000000000..47e293583a028bf2c8b441681eb493b2496bbfe0 --- /dev/null +++ b/requirements_v12.txt @@ -0,0 +1,32 @@ +# GOD AGENT OS v12 β€” Real Autonomous Agent Runtime +# Minimal dependencies β€” focused on actual execution + +# Core Framework +fastapi==0.115.0 +uvicorn[standard]==0.30.0 +websockets==13.0 +pydantic==2.8.0 +python-multipart==0.0.9 + +# HTTP + Async +httpx[http2]==0.27.0 +aiohttp==3.10.0 + +# Database +aiosqlite==0.20.0 +sqlalchemy[asyncio]==2.0.35 + +# E2B Sandbox (real code execution) +e2b==1.0.5 +e2b-code-interpreter==1.0.5 + +# Utilities +python-dotenv==1.0.1 +slowapi==0.1.9 +structlog==24.4.0 +rich==13.8.0 +psutil==6.0.0 + +# Git + GitHub +gitpython==3.1.43 +pygithub==2.3.0 diff --git a/requirements_v3.txt b/requirements_v3.txt new file mode 100644 index 0000000000000000000000000000000000000000..0161b1c91d3e4d62c60102c1f3ce89b133db4322 --- /dev/null +++ b/requirements_v3.txt @@ -0,0 +1,73 @@ +# πŸš€ GOD MODE+ v3 - Enhanced Backend Dependencies + +# Core Framework +fastapi==0.115.0 +uvicorn[standard]==0.30.0 +websockets==13.0 +pydantic==2.8.0 +pydantic-settings==2.3.0 + +# Authentication & Security +python-jose[cryptography]==3.3.0 +python-multipart==0.0.9 +passlib[bcrypt]==1.7.4 +cryptography==43.0.0 + +# HTTP & Async +aiohttp==3.10.0 +aiosqlite==0.20.0 +httpx==0.28.0 + +# Database & ORM +sqlalchemy[asyncio]==2.0.35 +alembic==1.13.2 + +# AI & LLM +openai==1.35.0 +anthropic==0.30.0 +groq==0.9.0 +together==1.1.0 + +# Advanced AI Frameworks +langchain==0.2.0 +langchain-core==0.2.0 +langchain-community==0.2.0 +langgraph==0.1.0 +langsmith==0.1.0 + +# Vector Database & Embeddings +pinecone-client==4.0.0 +weaviate-client==4.1.0 +sentence-transformers==3.0.0 + +# Code & Git +gitpython==3.1.43 +pygithub==2.3.0 + +# Utilities +python-dotenv==1.0.1 +slowapi==0.1.9 +structlog==24.4.0 +rich==13.8.0 +typer==0.12.3 +watchfiles==0.22.0 +psutil==6.0.0 + +# Async & Concurrency +asyncio-mqtt==0.16.2 +redis==5.1.0 + +# Monitoring & Observability +opentelemetry-api==1.25.0 +opentelemetry-sdk==1.25.0 +opentelemetry-exporter-jaeger==1.25.0 +prometheus-client==0.21.0 + +# Data Processing +pandas==2.2.0 +numpy==1.26.0 + +# Testing (optional, for dev) +pytest==8.0.0 +pytest-asyncio==0.24.0 +pytest-cov==5.0.0 diff --git a/sandbox/__init__.py b/sandbox/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/sandbox/e2b_executor.py b/sandbox/e2b_executor.py new file mode 100644 index 0000000000000000000000000000000000000000..028484733ca3f2d5d4a46e7b7d6e44a89d9f9c8c --- /dev/null +++ b/sandbox/e2b_executor.py @@ -0,0 +1,497 @@ +""" +E2B Sandbox Executor β€” Real code execution via E2B API +Provides actual Python/shell execution, file ops, stdout/stderr streaming +""" + +import asyncio +import os +import time +import uuid +from typing import AsyncGenerator, Dict, List, Optional + +import httpx +import structlog + +log = structlog.get_logger() + +E2B_API_KEY = os.environ.get("E2B_API_KEY", "") +E2B_BASE_URL = "https://api.e2b.dev" +E2B_SANDBOX_TEMPLATE = "base" # Python sandbox + + +class E2BSandboxSession: + """Represents a live E2B sandbox session.""" + + def __init__(self, sandbox_id: str, session_id: str): + self.sandbox_id = sandbox_id + self.session_id = session_id + self.created_at = time.time() + self.last_used = time.time() + self.files_created: List[str] = [] + + def touch(self): + self.last_used = time.time() + + def is_expired(self, ttl_seconds: int = 1800) -> bool: + return (time.time() - self.last_used) > ttl_seconds + + +class E2BExecutor: + """ + Real sandbox executor using E2B API. + Creates isolated sandbox environments for code execution. + """ + + def __init__(self): + self.api_key = E2B_API_KEY + self._sessions: Dict[str, E2BSandboxSession] = {} + self._client = httpx.AsyncClient( + timeout=120.0, + headers={ + "X-API-Key": self.api_key, + "Content-Type": "application/json", + } + ) + + async def get_or_create_sandbox(self, session_id: str) -> Optional[str]: + """Get existing or create new sandbox for session.""" + if not self.api_key: + log.warning("E2B_API_KEY not set β€” using local fallback") + return None + + # Reuse existing non-expired session + if session_id in self._sessions: + sess = self._sessions[session_id] + if not sess.is_expired(): + sess.touch() + return sess.sandbox_id + else: + # Try to close expired sandbox + await self._close_sandbox(sess.sandbox_id) + del self._sessions[session_id] + + # Create new sandbox + sandbox_id = await self._create_sandbox() + if sandbox_id: + self._sessions[session_id] = E2BSandboxSession(sandbox_id, session_id) + log.info("E2B sandbox created", sandbox_id=sandbox_id, session_id=session_id) + return sandbox_id + + async def _create_sandbox(self) -> Optional[str]: + """Create a new E2B sandbox.""" + try: + resp = await self._client.post( + f"{E2B_BASE_URL}/sandboxes", + json={ + "templateID": E2B_SANDBOX_TEMPLATE, + "metadata": {"source": "god-agent-os"}, + } + ) + if resp.status_code in (200, 201): + data = resp.json() + return data.get("sandboxID") or data.get("sandbox_id") or data.get("id") + else: + log.error("E2B create sandbox failed", status=resp.status_code, body=resp.text[:200]) + return None + except Exception as e: + log.error("E2B create sandbox exception", error=str(e)) + return None + + async def _close_sandbox(self, sandbox_id: str): + """Close/terminate a sandbox.""" + try: + await self._client.delete(f"{E2B_BASE_URL}/sandboxes/{sandbox_id}") + except Exception: + pass + + async def execute_code( + self, + code: str, + session_id: str, + language: str = "python", + timeout: int = 60, + ) -> Dict: + """Execute code in E2B sandbox and return real stdout/stderr.""" + sandbox_id = await self.get_or_create_sandbox(session_id) + + if not sandbox_id: + # Fallback: local subprocess execution + return await self._local_execute_code(code, language, timeout) + + try: + # E2B code execution API + if language == "python": + endpoint = f"{E2B_BASE_URL}/sandboxes/{sandbox_id}/code" + resp = await self._client.post( + endpoint, + json={ + "code": code, + "language": "python3", + "timeout": timeout, + }, + timeout=timeout + 10, + ) + else: + # Shell command + endpoint = f"{E2B_BASE_URL}/sandboxes/{sandbox_id}/processes" + resp = await self._client.post( + endpoint, + json={ + "cmd": code, + "timeout": timeout, + }, + timeout=timeout + 10, + ) + + if resp.status_code == 200: + data = resp.json() + stdout = data.get("stdout", "") or data.get("output", "") or "" + stderr = data.get("stderr", "") or "" + exit_code = data.get("exitCode", 0) or data.get("exit_code", 0) or 0 + + # Update session files + if session_id in self._sessions: + self._sessions[session_id].touch() + + return { + "stdout": stdout, + "stderr": stderr, + "exit_code": exit_code, + "sandbox_id": sandbox_id, + "success": exit_code == 0, + "execution_time_ms": data.get("duration", 0), + } + else: + log.error("E2B execute failed", status=resp.status_code, body=resp.text[:300]) + # Fallback to local + return await self._local_execute_code(code, language, timeout) + + except Exception as e: + log.error("E2B execute exception", error=str(e)) + return await self._local_execute_code(code, language, timeout) + + async def execute_shell( + self, + command: str, + session_id: str, + timeout: int = 60, + cwd: str = "/home/user", + ) -> Dict: + """Execute shell command in E2B sandbox.""" + sandbox_id = await self.get_or_create_sandbox(session_id) + + if not sandbox_id: + return await self._local_execute_shell(command, timeout, cwd) + + try: + resp = await self._client.post( + f"{E2B_BASE_URL}/sandboxes/{sandbox_id}/processes", + json={ + "cmd": command, + "cwd": cwd, + "timeout": timeout, + }, + timeout=timeout + 10, + ) + + if resp.status_code == 200: + data = resp.json() + stdout = data.get("stdout", "") or data.get("output", "") or "" + stderr = data.get("stderr", "") or "" + exit_code = data.get("exitCode", 0) or data.get("exit_code", 0) or 0 + + if session_id in self._sessions: + self._sessions[session_id].touch() + + return { + "stdout": stdout, + "stderr": stderr, + "exit_code": exit_code, + "sandbox_id": sandbox_id, + "success": exit_code == 0, + "command": command, + } + else: + return await self._local_execute_shell(command, timeout, cwd) + + except Exception as e: + log.error("E2B shell execute exception", error=str(e)) + return await self._local_execute_shell(command, timeout, cwd) + + async def write_file( + self, + path: str, + content: str, + session_id: str, + ) -> Dict: + """Write file in E2B sandbox filesystem.""" + sandbox_id = await self.get_or_create_sandbox(session_id) + + if not sandbox_id: + return await self._local_write_file(path, content) + + try: + resp = await self._client.post( + f"{E2B_BASE_URL}/sandboxes/{sandbox_id}/filesystem", + json={"path": path, "content": content}, + ) + + if resp.status_code in (200, 201): + if session_id in self._sessions: + self._sessions[session_id].files_created.append(path) + self._sessions[session_id].touch() + + return { + "success": True, + "path": path, + "size": len(content), + "sandbox_id": sandbox_id, + } + else: + return await self._local_write_file(path, content) + + except Exception as e: + log.error("E2B write file exception", error=str(e)) + return await self._local_write_file(path, content) + + async def read_file(self, path: str, session_id: str) -> Dict: + """Read file from E2B sandbox filesystem.""" + sandbox_id = await self.get_or_create_sandbox(session_id) + + if not sandbox_id: + return await self._local_read_file(path) + + try: + resp = await self._client.get( + f"{E2B_BASE_URL}/sandboxes/{sandbox_id}/filesystem", + params={"path": path}, + ) + + if resp.status_code == 200: + data = resp.json() + content = data.get("content", "") or resp.text + return { + "success": True, + "path": path, + "content": content, + "sandbox_id": sandbox_id, + } + else: + return {"success": False, "error": f"File not found: {path}"} + + except Exception as e: + return {"success": False, "error": str(e)} + + async def delete_file(self, path: str, session_id: str) -> Dict: + """Delete file from E2B sandbox.""" + sandbox_id = await self.get_or_create_sandbox(session_id) + + if not sandbox_id: + return await self._local_delete_file(path) + + try: + cmd = f"rm -f {path}" + result = await self.execute_shell(cmd, session_id) + success = result.get("exit_code", 1) == 0 + return { + "success": success, + "path": path, + "sandbox_id": sandbox_id, + } + except Exception as e: + return {"success": False, "error": str(e)} + + async def list_files(self, path: str, session_id: str) -> Dict: + """List files in E2B sandbox directory.""" + sandbox_id = await self.get_or_create_sandbox(session_id) + + if not sandbox_id: + return await self._local_list_files(path) + + try: + cmd = f"ls -la {path} 2>&1" + result = await self.execute_shell(cmd, session_id) + return { + "success": True, + "path": path, + "listing": result.get("stdout", ""), + "sandbox_id": sandbox_id, + } + except Exception as e: + return {"success": False, "error": str(e)} + + def get_session_info(self, session_id: str) -> Dict: + """Get sandbox session info.""" + if session_id in self._sessions: + sess = self._sessions[session_id] + return { + "sandbox_id": sess.sandbox_id, + "session_id": session_id, + "created_at": sess.created_at, + "last_used": sess.last_used, + "files_created": sess.files_created, + "active": not sess.is_expired(), + } + return {"session_id": session_id, "active": False} + + async def close_session(self, session_id: str): + """Close sandbox session.""" + if session_id in self._sessions: + sess = self._sessions[session_id] + await self._close_sandbox(sess.sandbox_id) + del self._sessions[session_id] + log.info("E2B session closed", session_id=session_id) + + # ─── Local Fallback (when E2B not available) ────────────────────────────── + + async def _local_execute_code(self, code: str, language: str, timeout: int) -> Dict: + """Execute code locally as fallback.""" + import tempfile + start = time.time() + + if language == "python": + with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as f: + f.write(code) + fname = f.name + try: + proc = await asyncio.create_subprocess_exec( + "python3", fname, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + duration_ms = int((time.time() - start) * 1000) + return { + "stdout": stdout.decode("utf-8", errors="replace"), + "stderr": stderr.decode("utf-8", errors="replace"), + "exit_code": proc.returncode, + "sandbox_id": "local", + "success": proc.returncode == 0, + "execution_time_ms": duration_ms, + } + except asyncio.TimeoutError: + return {"stdout": "", "stderr": f"Timeout after {timeout}s", "exit_code": -1, "sandbox_id": "local", "success": False} + finally: + try: + os.unlink(fname) + except Exception: + pass + else: + return await self._local_execute_shell(code, timeout) + + async def _local_execute_shell(self, command: str, timeout: int, cwd: str = "/tmp") -> Dict: + """Execute shell command locally as fallback.""" + # Safety check + blocked = ["rm -rf /", ":(){ :|:&", "mkfs", "shutdown", "reboot", "halt", "dd if=/dev/zero"] + for b in blocked: + if b in command: + return { + "stdout": "", "stderr": f"Blocked dangerous command", + "exit_code": 1, "sandbox_id": "local", "success": False, + "command": command, + } + + start = time.time() + try: + proc = await asyncio.create_subprocess_shell( + command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=cwd if os.path.exists(cwd) else "/tmp", + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + duration_ms = int((time.time() - start) * 1000) + return { + "stdout": stdout.decode("utf-8", errors="replace"), + "stderr": stderr.decode("utf-8", errors="replace"), + "exit_code": proc.returncode, + "sandbox_id": "local", + "success": proc.returncode == 0, + "execution_time_ms": duration_ms, + "command": command, + } + except asyncio.TimeoutError: + return { + "stdout": "", "stderr": f"Command timed out after {timeout}s", + "exit_code": -1, "sandbox_id": "local", "success": False, + "command": command, + } + except Exception as e: + return { + "stdout": "", "stderr": str(e), + "exit_code": -1, "sandbox_id": "local", "success": False, + "command": command, + } + + async def _local_write_file(self, path: str, content: str) -> Dict: + """Write file locally as fallback.""" + try: + os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) + with open(path, "w", encoding="utf-8") as f: + f.write(content) + return { + "success": True, + "path": path, + "size": len(content), + "sandbox_id": "local", + } + except Exception as e: + return {"success": False, "error": str(e), "sandbox_id": "local"} + + async def _local_read_file(self, path: str) -> Dict: + """Read file locally as fallback.""" + try: + with open(path, "r", encoding="utf-8") as f: + content = f.read() + return { + "success": True, + "path": path, + "content": content, + "sandbox_id": "local", + } + except FileNotFoundError: + return {"success": False, "error": f"File not found: {path}"} + except Exception as e: + return {"success": False, "error": str(e)} + + async def _local_delete_file(self, path: str) -> Dict: + """Delete file locally as fallback.""" + try: + os.unlink(path) + return {"success": True, "path": path, "sandbox_id": "local"} + except Exception as e: + return {"success": False, "error": str(e)} + + async def _local_list_files(self, path: str) -> Dict: + """List files locally as fallback.""" + try: + import subprocess + result = subprocess.run( + ["ls", "-la", path], + capture_output=True, text=True, timeout=5 + ) + return { + "success": True, + "path": path, + "listing": result.stdout, + "sandbox_id": "local", + } + except Exception as e: + return {"success": False, "error": str(e)} + + async def close(self): + """Cleanup all sessions.""" + for sid in list(self._sessions.keys()): + await self.close_session(sid) + await self._client.aclose() + + +# Global singleton +_executor: Optional[E2BExecutor] = None + + +def get_executor() -> E2BExecutor: + global _executor + if _executor is None: + _executor = E2BExecutor() + return _executor diff --git a/spaces/__init__.py b/spaces/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2dc77999e3f7ad8b275044b919b6961e4da56588 --- /dev/null +++ b/spaces/__init__.py @@ -0,0 +1,19 @@ +from .base_space import BaseSpace +from .catalog import SPACE_CATALOG, SPACE_INDEX +from .worker_space import WorkerSpace + + +def build_all_spaces(ws_manager=None, ai_router=None): + return { + spec["id"]: WorkerSpace(spec=spec, ws_manager=ws_manager, ai_router=ai_router) + for spec in SPACE_CATALOG + } + + +__all__ = [ + "BaseSpace", + "WorkerSpace", + "SPACE_CATALOG", + "SPACE_INDEX", + "build_all_spaces", +] diff --git a/spaces/base_space.py b/spaces/base_space.py new file mode 100644 index 0000000000000000000000000000000000000000..10aad202f7a0ca28e25f91449cef16542265ad68 --- /dev/null +++ b/spaces/base_space.py @@ -0,0 +1,91 @@ +""" +Base Space β€” Abstract interface for all Spaces in the Space-Role Architecture. +""" +from abc import ABC, abstractmethod +from typing import Any, Dict, Optional +import structlog + +log = structlog.get_logger() + + +class BaseSpace(ABC): + """ + Abstract base class for all Spaces. + Each Space provides a distinct domain of interaction with specific tools. + """ + + space_name: str = "base" + space_description: str = "Base Space" + available_roles: list = ["cognition", "automation", "execution"] + + def __init__(self, ws_manager=None, ai_router=None): + self.ws = ws_manager + self.ai_router = ai_router + self._tools: Dict[str, callable] = {} + self._initialized = False + log.info(f"πŸ“¦ {self.__class__.__name__} Space created") + + def register_tool(self, name: str, func: callable, description: str = ""): + """Register a tool in this Space.""" + self._tools[name] = {"func": func, "description": description} + + def get_tools(self) -> Dict[str, str]: + return {name: info["description"] for name, info in self._tools.items()} + + async def execute_tool(self, tool_name: str, **kwargs) -> Any: + """Execute a registered tool.""" + if tool_name not in self._tools: + raise ValueError(f"Tool '{tool_name}' not found in {self.space_name} Space") + return await self._tools[tool_name]["func"](**kwargs) + + def get_space_prompt(self, role: str, task: str, context: Dict) -> str: + """Build the system prompt for this Space + Role combination.""" + role_prompts = { + "cognition": "You are in COGNITION ROLE β€” analyze, plan, and think deeply.", + "automation": "You are in AUTOMATION ROLE β€” execute workflows and interact with systems.", + "execution": "You are in EXECUTION ROLE β€” write and run code, perform computational work.", + "repair": "You are in REPAIR ROLE β€” analyze errors, find root causes, implement fixes.", + "visual_intelligence": "You are in VISUAL INTELLIGENCE ROLE β€” interpret and generate visual content.", + } + + mem_context = "" + if context.get("short_term_memory"): + recent = context["short_term_memory"][-3:] + mem_context = "\n".join([f"- [{m['type']}]: {str(m.get('content',''))[:100]}" for m in recent]) + + return f"""You are GOD AGENT OS v9 β€” General Autonomous Agent OS. + +Active Space: {self.space_name.upper()} SPACE +{self.space_description} + +{role_prompts.get(role, role_prompts['cognition'])} + +Available Tools in this Space: +{chr(10).join([f'- {name}: {desc}' for name, desc in self.get_tools().items()])} + +Recent Context: +{mem_context or 'No previous context'} + +Be concise, accurate, and action-oriented. Return results directly.""" + + @abstractmethod + async def execute(self, task: str, role: str, session_id: str, context: Dict = None) -> str: + """Execute a task in this Space with the given Role.""" + pass + + async def stream_update(self, session_id: str, message: str, space: str = None): + """Send a streaming update to the client.""" + if self.ws: + await self.ws.broadcast_to_room(f"chat:{session_id}", { + "type": "space_update", + "space": space or self.space_name, + "message": message, + }) + + def get_info(self) -> Dict: + return { + "name": self.space_name, + "description": self.space_description, + "available_roles": self.available_roles, + "tools": list(self._tools.keys()), + } diff --git a/spaces/browser_space.py b/spaces/browser_space.py new file mode 100644 index 0000000000000000000000000000000000000000..c03c6e72c7a00922655b455ce9e6bb946ddf845a --- /dev/null +++ b/spaces/browser_space.py @@ -0,0 +1,104 @@ +""" +🌐 Browser Space β€” The Interface to the Web +Handles all internet-based research and interaction. +""" +import asyncio +import aiohttp +from typing import Dict +import structlog +from .base_space import BaseSpace + +log = structlog.get_logger() + + +class BrowserSpace(BaseSpace): + space_name = "browser" + space_description = "Web interface β€” research, navigation, data extraction from the internet." + available_roles = ["automation", "cognition"] + + def __init__(self, ws_manager=None, ai_router=None): + super().__init__(ws_manager, ai_router) + self.register_tool("web_search", self._web_search, "Search the web for information") + self.register_tool("fetch_url", self._fetch_url, "Fetch content from a URL") + self.register_tool("extract_data", self._extract_data, "Extract structured data from web pages") + + async def _web_search(self, query: str, **kwargs) -> str: + """Simulate web search via DuckDuckGo.""" + try: + encoded = query.replace(" ", "+") + url = f"https://api.duckduckgo.com/?q={encoded}&format=json&no_html=1&skip_disambig=1" + async with aiohttp.ClientSession() as session: + async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp: + data = await resp.json(content_type=None) + abstract = data.get("AbstractText", "") + related = [r.get("Text", "") for r in data.get("RelatedTopics", [])[:3] if r.get("Text")] + result = abstract or "No direct answer found." + if related: + result += "\n\nRelated:\n" + "\n".join([f"- {r}" for r in related]) + return result + except Exception as e: + return f"Search result for '{query}': Web search attempted. Error: {str(e)}" + + async def _fetch_url(self, url: str, **kwargs) -> str: + """Fetch content from a URL.""" + try: + async with aiohttp.ClientSession() as session: + headers = {"User-Agent": "Mozilla/5.0 (compatible; GodAgentOS/9.0)"} + async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=15)) as resp: + text = await resp.text() + # Basic text extraction + import re + text = re.sub(r']*>.*?', '', text, flags=re.DOTALL) + text = re.sub(r']*>.*?', '', text, flags=re.DOTALL) + text = re.sub(r'<[^>]+>', ' ', text) + text = re.sub(r'\s+', ' ', text).strip() + return text[:3000] + except Exception as e: + return f"Could not fetch {url}: {str(e)}" + + async def _extract_data(self, url: str, **kwargs) -> str: + content = await self._fetch_url(url) + return f"Extracted from {url}:\n{content[:1500]}" + + async def execute(self, task: str, role: str, session_id: str, context: Dict = None) -> str: + context = context or {} + + await self.stream_update(session_id, f"🌐 Browser Space activated β€” {role} role") + + # Try to perform web search + search_result = "" + try: + search_result = await self._web_search(task) + except Exception: + pass + + # Check if task has a URL + import re + urls = re.findall(r'https?://[^\s]+', task) + url_content = "" + if urls: + try: + url_content = await self._fetch_url(urls[0]) + except Exception: + pass + + system_prompt = self.get_space_prompt(role, task, context) + + enhanced_prompt = task + if search_result: + enhanced_prompt += f"\n\n[Web Search Results]\n{search_result}" + if url_content: + enhanced_prompt += f"\n\n[URL Content]\n{url_content[:1000]}" + + try: + response = await self.ai_router.complete( + prompt=enhanced_prompt, + system=system_prompt, + max_tokens=2048, + ) + return response.get("content", "Browser Space could not process the request.") + except Exception as e: + log.error(f"BrowserSpace error: {e}") + if search_result: + return f"🌐 Browser Space Results:\n\n{search_result}" + return f"Browser Space error: {str(e)}" diff --git a/spaces/catalog.py b/spaces/catalog.py new file mode 100644 index 0000000000000000000000000000000000000000..69580aa57e0875c1eecf02d2bdd7eb517c8de2af --- /dev/null +++ b/spaces/catalog.py @@ -0,0 +1,226 @@ +from __future__ import annotations + +SPACE_CATALOG = [ + { + "id": "god-core-space", + "name": "God Core Space", + "icon": "🧠", + "color": "#7c3aed", + "layer": "Core Cognitive Layer", + "description": "System brain for orchestration, planning, reasoning, workflow control, mission state, websocket events, and model routing.", + "responsibilities": ["orchestrator", "planner", "reasoning", "task graph", "workflow engine", "mission state", "memory routing", "websocket events", "llm routing"], + "roles": ["cognition", "automation"], + }, + { + "id": "coding-worker-space", + "name": "Coding Worker Space", + "icon": "πŸ”§", + "color": "#f59e0b", + "layer": "Execution Layer", + "description": "Code generation, file editing, refactoring, dependency handling, and code transformations.", + "responsibilities": ["code generation", "file editing", "refactoring", "dependency handling", "code transformations"], + "roles": ["execution", "cognition", "automation"], + }, + { + "id": "sandbox-worker-space", + "name": "Sandbox Worker Space", + "icon": "πŸ§ͺ", + "color": "#10b981", + "layer": "Execution Layer", + "description": "Isolated execution, runtime sandboxing, subprocesses, environment resets, and lifecycle management.", + "responsibilities": ["isolated execution", "docker runtime", "subprocesses", "environment resets", "runtime lifecycle"], + "roles": ["execution", "repair"], + }, + { + "id": "terminal-worker-space", + "name": "Terminal Worker Space", + "icon": "⌨️", + "color": "#14b8a6", + "layer": "Execution Layer", + "description": "Shell commands, package installs, build tools, and process monitoring.", + "responsibilities": ["shell commands", "package installs", "build tools", "process monitoring"], + "roles": ["execution", "automation"], + }, + { + "id": "filesystem-worker-space", + "name": "Filesystem Worker Space", + "icon": "πŸ—‚οΈ", + "color": "#22c55e", + "layer": "Execution Layer", + "description": "File writes, project trees, artifact management, and storage operations.", + "responsibilities": ["file writes", "project trees", "artifact management", "storage operations"], + "roles": ["execution", "automation"], + }, + { + "id": "browser-worker-space", + "name": "Browser Worker Space", + "icon": "🌐", + "color": "#3b82f6", + "layer": "Browser + UI Intelligence", + "description": "Playwright automation, navigation, screenshots, and interaction testing.", + "responsibilities": ["playwright", "browser automation", "navigation", "screenshots", "interaction testing"], + "roles": ["automation", "cognition"], + }, + { + "id": "vision-worker-space", + "name": "Vision Worker Space", + "icon": "πŸ‘οΈ", + "color": "#ec4899", + "layer": "Browser + UI Intelligence", + "description": "Screenshot analysis, OCR, layout detection, visual regression, and UI understanding.", + "responsibilities": ["screenshot analysis", "ocr", "layout detection", "visual regression", "ui understanding"], + "roles": ["visual_intelligence", "cognition"], + }, + { + "id": "ui-worker-space", + "name": "UI Worker Space", + "icon": "🎨", + "color": "#8b5cf6", + "layer": "Browser + UI Intelligence", + "description": "Frontend generation, design systems, responsive layouts, component consistency, and visual polish.", + "responsibilities": ["frontend generation", "design systems", "responsive layouts", "component consistency", "visual polish"], + "roles": ["visual_intelligence", "execution"], + }, + { + "id": "debug-worker-space", + "name": "Debug Worker Space", + "icon": "πŸ›", + "color": "#ef4444", + "layer": "Verification + Repair Layer", + "description": "Error analysis, traceback parsing, repair strategies, and retry planning.", + "responsibilities": ["error analysis", "traceback parsing", "repair strategies", "retry planning"], + "roles": ["repair", "cognition"], + }, + { + "id": "test-worker-space", + "name": "Test Worker Space", + "icon": "πŸ§ͺ", + "color": "#06b6d4", + "layer": "Verification + Repair Layer", + "description": "Run tests, assertions, integration checks, and regression testing.", + "responsibilities": ["run tests", "assertions", "integration checks", "regression testing"], + "roles": ["execution", "repair"], + }, + { + "id": "verification-worker-space", + "name": "Verification Worker Space", + "icon": "βœ…", + "color": "#84cc16", + "layer": "Verification + Repair Layer", + "description": "Validate outputs, compare expectations, quality scoring, and mission verification.", + "responsibilities": ["validate outputs", "compare expectations", "quality scoring", "mission verification"], + "roles": ["repair", "cognition"], + }, + { + "id": "git-worker-space", + "name": "Git Worker Space", + "icon": "🌳", + "color": "#f97316", + "layer": "Deployment Layer", + "description": "Commits, branching, diffs, merges, and repository workflow operations.", + "responsibilities": ["commits", "branching", "diffs", "merges"], + "roles": ["automation", "execution"], + }, + { + "id": "deploy-worker-space", + "name": "Deploy Worker Space", + "icon": "πŸš€", + "color": "#0ea5e9", + "layer": "Deployment Layer", + "description": "Vercel, Railway, Docker deploys, preview URLs, and CI/CD triggers.", + "responsibilities": ["vercel deploy", "railway deploy", "docker deploy", "preview urls", "ci/cd triggers"], + "roles": ["automation", "execution"], + }, + { + "id": "connector-worker-space", + "name": "Connector Worker Space", + "icon": "πŸ”Œ", + "color": "#6366f1", + "layer": "Deployment Layer", + "description": "GitHub, Supabase, APIs, and external integrations.", + "responsibilities": ["github", "supabase", "apis", "external integrations"], + "roles": ["automation", "cognition"], + }, + { + "id": "memory-worker-space", + "name": "Memory Worker Space", + "icon": "🧠", + "color": "#a855f7", + "layer": "Memory + Knowledge Layer", + "description": "Vector DB, execution history, learned fixes, project memory, and long-term state.", + "responsibilities": ["vector db", "execution history", "learned fixes", "project memory", "long-term state"], + "roles": ["cognition", "automation"], + }, + { + "id": "knowledge-worker-space", + "name": "Knowledge Worker Space", + "icon": "πŸ“š", + "color": "#4f46e5", + "layer": "Memory + Knowledge Layer", + "description": "Docs retrieval, semantic search, RAG pipelines, and indexed repositories.", + "responsibilities": ["docs retrieval", "semantic search", "rag pipelines", "indexed repositories"], + "roles": ["cognition", "automation"], + }, + { + "id": "workflow-worker-space", + "name": "Workflow Worker Space", + "icon": "🧭", + "color": "#0f766e", + "layer": "Coordination Layer", + "description": "DAG execution, task queues, retries, scheduling, and background jobs.", + "responsibilities": ["dag execution", "task queues", "retries", "scheduling", "background jobs"], + "roles": ["automation", "execution"], + }, + { + "id": "eventbus-space", + "name": "Eventbus Space", + "icon": "πŸ“‘", + "color": "#06b6d4", + "layer": "Coordination Layer", + "description": "Redis PubSub, NATS, RabbitMQ, and event streams.", + "responsibilities": ["redis pubsub", "nats", "rabbitmq", "event streams"], + "roles": ["automation", "execution"], + }, + { + "id": "observability-space", + "name": "Observability Space", + "icon": "πŸ“ˆ", + "color": "#22c55e", + "layer": "Monitoring Layer", + "description": "Logs, metrics, tracing, agent monitoring, and runtime analytics.", + "responsibilities": ["logs", "metrics", "tracing", "agent monitoring", "runtime analytics"], + "roles": ["cognition", "automation"], + }, + { + "id": "session-runtime-space", + "name": "Session Runtime Space", + "icon": "🧷", + "color": "#64748b", + "layer": "Session Layer", + "description": "User sessions, mission isolation, runtime persistence, and checkpointing.", + "responsibilities": ["user sessions", "mission isolation", "runtime persistence", "checkpointing"], + "roles": ["automation", "cognition"], + }, + { + "id": "model-router-space", + "name": "Model Router Space", + "icon": "πŸ›£οΈ", + "color": "#eab308", + "layer": "Infrastructure Layer", + "description": "GPT routing, Claude routing, fallback models, cost optimization, and model selection.", + "responsibilities": ["gpt routing", "claude routing", "fallback models", "cost optimization", "model selection"], + "roles": ["cognition", "automation"], + }, + { + "id": "auth-gateway-space", + "name": "Auth Gateway Space", + "icon": "πŸ”", + "color": "#9333ea", + "layer": "Infrastructure Layer", + "description": "Auth, API keys, rate limits, and permissions.", + "responsibilities": ["auth", "api keys", "rate limits", "permissions"], + "roles": ["automation", "repair"], + }, +] + +SPACE_INDEX = {item["id"]: item for item in SPACE_CATALOG} diff --git a/spaces/coding_space.py b/spaces/coding_space.py new file mode 100644 index 0000000000000000000000000000000000000000..d9a833c0a29429b796caaa2fe4f0eb8c8c9e0d1c --- /dev/null +++ b/spaces/coding_space.py @@ -0,0 +1,106 @@ +""" +πŸ”§ Coding Space β€” The Development Environment +Code generation, refactoring, analysis, and manipulation. +""" +import re +from typing import Dict +import structlog +from .base_space import BaseSpace + +log = structlog.get_logger() + +CODING_SYSTEM = """You are GOD AGENT OS v9 β€” Coding Space Expert. + +You excel at: +- Writing production-quality code in ANY language (Python, JS, TS, Go, Rust, Java, C++, etc.) +- Code review and refactoring +- Algorithm design and optimization +- Architecture patterns (MVC, microservices, event-driven) +- API design (REST, GraphQL, gRPC) +- Database schemas and queries +- Testing strategies (unit, integration, e2e) +- DevOps and CI/CD configurations + +Always write clean, well-documented, production-ready code. +Include error handling, type hints, and comments. +""" + + +class CodingSpace(BaseSpace): + space_name = "coding" + space_description = "Development environment β€” code generation, refactoring, and analysis." + available_roles = ["execution", "cognition", "automation"] + + def __init__(self, ws_manager=None, ai_router=None): + super().__init__(ws_manager, ai_router) + self.register_tool("generate_code", self._generate_code, "Generate code from requirements") + self.register_tool("review_code", self._review_code, "Review and suggest improvements") + self.register_tool("refactor", self._refactor, "Refactor existing code") + self.register_tool("generate_tests", self._generate_tests, "Generate test cases") + self.register_tool("generate_api", self._generate_api, "Generate REST API boilerplate") + + async def _generate_code(self, task: str, language: str = "python", **kwargs) -> str: + return f"Generating {language} code for: {task}" + + async def _review_code(self, code: str, **kwargs) -> str: + return f"Reviewing code..." + + async def _refactor(self, code: str, **kwargs) -> str: + return f"Refactoring code..." + + async def _generate_tests(self, code: str, **kwargs) -> str: + return f"Generating tests..." + + async def _generate_api(self, spec: str, **kwargs) -> str: + return f"Generating API..." + + async def execute(self, task: str, role: str, session_id: str, context: Dict = None) -> str: + context = context or {} + + await self.stream_update(session_id, f"πŸ”§ Coding Space activated β€” {role} role") + + # Detect language from task + lang_hints = { + "python": ["python", "py", "fastapi", "django", "flask", "pandas", "numpy"], + "typescript": ["typescript", "ts", "next.js", "nextjs", "react", "vue", "angular"], + "javascript": ["javascript", "js", "node", "express"], + "go": ["golang", "go lang"], + "rust": ["rust", "cargo"], + "java": ["java", "spring", "maven"], + } + + detected_lang = "python" + task_lower = task.lower() + for lang, hints in lang_hints.items(): + if any(h in task_lower for h in hints): + detected_lang = lang + break + + mem_context = "" + if context.get("short_term_memory"): + recent = context["short_term_memory"][-3:] + mem_context = "\n".join([f"- {m.get('content','')[:80]}" for m in recent]) + + enhanced_system = f"""{CODING_SYSTEM} + +Active Role: {role.upper()} +Detected Language: {detected_lang} +Recent Context: {mem_context or 'None'} + +Format all code in proper markdown code blocks with language tags. +Include: +1. The complete, working code +2. Brief explanation +3. Usage examples +4. Any important notes""" + + try: + response = await self.ai_router.complete( + prompt=task, + system=enhanced_system, + max_tokens=4096, + ) + return response.get("content", "Coding Space could not generate code.") + except Exception as e: + log.error(f"CodingSpace error: {e}") + return f"Coding Space error: {str(e)}" diff --git a/spaces/communication_space.py b/spaces/communication_space.py new file mode 100644 index 0000000000000000000000000000000000000000..b2f0db8718d249e697bef5126a7e8d0214cac3bd --- /dev/null +++ b/spaces/communication_space.py @@ -0,0 +1,106 @@ +""" +πŸ’¬ Communication Space β€” The Interaction Domain +Multi-channel messaging, email, notifications. +""" +from typing import Dict +import structlog +from .base_space import BaseSpace + +log = structlog.get_logger() + +COMM_SYSTEM = """You are GOD AGENT OS v9 β€” Communication Space Expert. + +You specialize in: +- Professional email drafting and templates +- Slack/Discord message formatting +- Technical documentation writing +- README and API documentation +- Meeting notes and summaries +- Project proposals and reports +- Code comments and docstrings +- User guides and tutorials +- Marketing copy and announcements +- Multilingual communication (Burmese, English, etc.) + +Writing principles: +- Clear, concise, and professional +- Appropriate tone for the audience +- Proper formatting (markdown, HTML) +- Action-oriented language +- Include call-to-action when relevant +""" + + +class CommunicationSpace(BaseSpace): + space_name = "communication" + space_description = "Interaction domain β€” chat, email, documentation, multi-channel messaging." + available_roles = ["automation", "cognition"] + + def __init__(self, ws_manager=None, ai_router=None): + super().__init__(ws_manager, ai_router) + self.register_tool("draft_email", self._draft_email, "Draft professional emails") + self.register_tool("write_docs", self._write_docs, "Write technical documentation") + self.register_tool("create_report", self._create_report, "Create structured reports") + self.register_tool("translate", self._translate, "Translate content between languages") + self.register_tool("summarize_thread", self._summarize_thread, "Summarize communication threads") + + async def _draft_email(self, subject: str, context: str, tone: str = "professional", **kwargs) -> str: + return f"Drafting email: {subject}" + + async def _write_docs(self, topic: str, format: str = "markdown", **kwargs) -> str: + return f"Writing docs for: {topic}" + + async def _create_report(self, data: str, **kwargs) -> str: + return f"Creating report from: {data[:50]}" + + async def _translate(self, text: str, target_lang: str = "english", **kwargs) -> str: + return f"Translating to {target_lang}: {text[:50]}" + + async def _summarize_thread(self, thread: str, **kwargs) -> str: + return f"Summarizing thread..." + + async def execute(self, task: str, role: str, session_id: str, context: Dict = None) -> str: + context = context or {} + + await self.stream_update(session_id, f"πŸ’¬ Communication Space activated β€” {role} role") + + # Detect communication type + task_lower = task.lower() + comm_type = "general" + if "email" in task_lower: + comm_type = "email" + elif "document" in task_lower or "docs" in task_lower or "readme" in task_lower: + comm_type = "documentation" + elif "report" in task_lower: + comm_type = "report" + elif "translate" in task_lower or "α€˜α€¬α€žα€¬" in task: + comm_type = "translation" + elif "summary" in task_lower or "summarize" in task_lower: + comm_type = "summary" + elif "slack" in task_lower or "discord" in task_lower: + comm_type = "instant_message" + + mem_context = "" + if context.get("short_term_memory"): + recent = context["short_term_memory"][-3:] + mem_context = "\n".join([f"- {m.get('content','')[:80]}" for m in recent]) + + enhanced_system = f"""{COMM_SYSTEM} + +Active Role: {role.upper()} +Communication Type: {comm_type} +Recent Context: {mem_context or 'None'} + +For Burmese language requests, respond in Burmese. +Format output appropriately for the communication type.""" + + try: + response = await self.ai_router.complete( + prompt=task, + system=enhanced_system, + max_tokens=3000, + ) + return response.get("content", "Communication Space could not process the request.") + except Exception as e: + log.error(f"CommunicationSpace error: {e}") + return f"Communication Space error: {str(e)}" diff --git a/spaces/core_space.py b/spaces/core_space.py new file mode 100644 index 0000000000000000000000000000000000000000..a8db5edd4f73ae79ad191abc9eb91c326ddc55b9 --- /dev/null +++ b/spaces/core_space.py @@ -0,0 +1,48 @@ +""" +🧠 Core Space β€” The Central Nervous System +Manages memory, planning, and overall orchestration. +""" +import json +from typing import Dict +import structlog +from .base_space import BaseSpace + +log = structlog.get_logger() + + +class CoreSpace(BaseSpace): + space_name = "core" + space_description = "Central nervous system β€” planning, memory, and orchestration." + available_roles = ["cognition", "automation"] + + def __init__(self, ws_manager=None, ai_router=None): + super().__init__(ws_manager, ai_router) + self.register_tool("plan", self._plan_task, "Break complex goals into executable steps") + self.register_tool("summarize", self._summarize, "Summarize information concisely") + self.register_tool("analyze", self._analyze, "Deep analysis of content or problems") + + async def _plan_task(self, task: str, **kwargs) -> str: + return f"Planning task: {task}" + + async def _summarize(self, content: str, **kwargs) -> str: + return f"Summary of: {content[:50]}..." + + async def _analyze(self, content: str, **kwargs) -> str: + return f"Analysis of: {content[:50]}..." + + async def execute(self, task: str, role: str, session_id: str, context: Dict = None) -> str: + context = context or {} + + system_prompt = self.get_space_prompt(role, task, context) + + try: + response = await self.ai_router.complete( + prompt=task, + system=system_prompt, + max_tokens=2048, + stream_callback=None, + ) + return response.get("content", "I couldn't process that request.") + except Exception as e: + log.error(f"CoreSpace error: {e}") + return f"Core Space error: {str(e)}" diff --git a/spaces/debug_space.py b/spaces/debug_space.py new file mode 100644 index 0000000000000000000000000000000000000000..ed514e37c5a3c0ee2bab8303c0c797ef0829124e --- /dev/null +++ b/spaces/debug_space.py @@ -0,0 +1,120 @@ +""" +πŸ› Debug Space β€” The Diagnostic Environment +Error analysis, log parsing, self-healing algorithms. +""" +import re +from typing import Dict +import structlog +from .base_space import BaseSpace + +log = structlog.get_logger() + +DEBUG_SYSTEM = """You are GOD AGENT OS v9 β€” Debug Space Expert (Repair Role). + +You specialize in: +- Stack trace analysis and root cause identification +- Error pattern recognition +- Self-healing code strategies +- Log analysis and anomaly detection +- Performance profiling and optimization +- Memory leak detection +- Race condition and concurrency bug analysis +- Security vulnerability scanning +- Dependency conflict resolution +- Code smell detection + +When analyzing errors: +1. Identify the root cause precisely +2. Explain WHY the error occurred +3. Provide the exact fix with code +4. Suggest preventive measures +5. Add proper error handling + +Be systematic and thorough. Debug like a senior engineer. +""" + + +class DebugSpace(BaseSpace): + space_name = "debug" + space_description = "Diagnostic environment β€” error analysis, log parsing, self-healing." + available_roles = ["repair", "cognition", "execution"] + + def __init__(self, ws_manager=None, ai_router=None): + super().__init__(ws_manager, ai_router) + self.register_tool("analyze_error", self._analyze_error, "Analyze error traces") + self.register_tool("parse_logs", self._parse_logs, "Parse and analyze log files") + self.register_tool("suggest_fix", self._suggest_fix, "Suggest code fixes") + self.register_tool("self_heal", self._self_heal, "Auto-generate healing strategies") + + async def _analyze_error(self, error: str, **kwargs) -> str: + return f"Analyzing error: {error[:100]}" + + async def _parse_logs(self, logs: str, **kwargs) -> str: + # Extract error patterns + error_lines = [l for l in logs.split('\n') if any( + kw in l.lower() for kw in ['error', 'exception', 'fatal', 'critical', 'warn'] + )] + return "\n".join(error_lines[:20]) if error_lines else "No errors found in logs" + + async def _suggest_fix(self, error: str, code: str = "", **kwargs) -> str: + return f"Suggesting fix for: {error[:100]}" + + async def _self_heal(self, error: str, **kwargs) -> str: + return f"Self-healing strategy for: {error[:100]}" + + def _detect_error_type(self, task: str) -> str: + task_lower = task.lower() + if "typeerror" in task_lower or "type error" in task_lower: + return "TypeError" + elif "syntaxerror" in task_lower: + return "SyntaxError" + elif "importerror" in task_lower or "modulenot" in task_lower: + return "ImportError" + elif "attributeerror" in task_lower: + return "AttributeError" + elif "keyerror" in task_lower: + return "KeyError" + elif "indexerror" in task_lower: + return "IndexError" + elif "valueerror" in task_lower: + return "ValueError" + elif "connectionerror" in task_lower or "timeout" in task_lower: + return "NetworkError" + elif "permissionerror" in task_lower: + return "PermissionError" + return "Unknown Error" + + async def execute(self, task: str, role: str, session_id: str, context: Dict = None) -> str: + context = context or {} + + await self.stream_update(session_id, f"πŸ› Debug Space activated β€” {role} role") + + error_type = self._detect_error_type(task) + + mem_context = "" + if context.get("short_term_memory"): + recent = context["short_term_memory"][-3:] + mem_context = "\n".join([f"- {m.get('content','')[:80]}" for m in recent]) + + enhanced_system = f"""{DEBUG_SYSTEM} + +Active Role: {role.upper()} +Detected Error Type: {error_type} +Recent Context: {mem_context or 'None'} + +Provide: +1. Root cause analysis +2. Step-by-step fix with code +3. Prevention strategy +4. Testing recommendation""" + + try: + response = await self.ai_router.complete( + prompt=task, + system=enhanced_system, + max_tokens=3000, + ) + return response.get("content", "Debug Space could not analyze the error.") + except Exception as e: + log.error(f"DebugSpace error: {e}") + return f"Debug Space error: {str(e)}" diff --git a/spaces/deploy_space.py b/spaces/deploy_space.py new file mode 100644 index 0000000000000000000000000000000000000000..84fa220750d30237821d5ebced02d18096ad2b5b --- /dev/null +++ b/spaces/deploy_space.py @@ -0,0 +1,109 @@ +""" +πŸš€ Deploy Space β€” The Infrastructure Domain +Cloud deployments, CI/CD, containerization. +""" +from typing import Dict +import structlog +from .base_space import BaseSpace + +log = structlog.get_logger() + +DEPLOY_SYSTEM = """You are GOD AGENT OS v9 β€” Deploy Space Expert. + +You specialize in: +- Vercel deployments (Next.js, React, API routes) +- Hugging Face Spaces (Gradio, Streamlit, Docker) +- Docker containerization and Docker Compose +- GitHub Actions CI/CD pipelines +- AWS deployments (EC2, Lambda, ECS, S3) +- GCP deployments (Cloud Run, App Engine) +- Kubernetes manifests and Helm charts +- Environment variable management +- Domain configuration and SSL +- CDN setup (Cloudflare, AWS CloudFront) +- Database migrations and deployment strategies +- Blue-green and canary deployments +- Monitoring setup (Prometheus, Grafana) + +Always provide: +1. Complete configuration files +2. Step-by-step deployment commands +3. Environment variable templates (.env.example) +4. Troubleshooting tips for common issues +""" + + +class DeploySpace(BaseSpace): + space_name = "deploy" + space_description = "Infrastructure domain β€” cloud deployments, CI/CD, containerization." + available_roles = ["automation", "execution", "cognition"] + + def __init__(self, ws_manager=None, ai_router=None): + super().__init__(ws_manager, ai_router) + self.register_tool("gen_dockerfile", self._gen_dockerfile, "Generate Dockerfile") + self.register_tool("gen_github_actions", self._gen_github_actions, "Generate GitHub Actions workflow") + self.register_tool("gen_vercel_config", self._gen_vercel_config, "Generate Vercel configuration") + self.register_tool("gen_hf_config", self._gen_hf_config, "Generate HuggingFace Space config") + self.register_tool("gen_k8s_manifest", self._gen_k8s_manifest, "Generate Kubernetes manifests") + + async def _gen_dockerfile(self, app_type: str = "python", **kwargs) -> str: + return f"Generating Dockerfile for {app_type}" + + async def _gen_github_actions(self, workflow_type: str = "deploy", **kwargs) -> str: + return f"Generating GitHub Actions for {workflow_type}" + + async def _gen_vercel_config(self, **kwargs) -> str: + return "Generating vercel.json" + + async def _gen_hf_config(self, **kwargs) -> str: + return "Generating HF Space README.md" + + async def _gen_k8s_manifest(self, app_name: str = "app", **kwargs) -> str: + return f"Generating K8s manifest for {app_name}" + + async def execute(self, task: str, role: str, session_id: str, context: Dict = None) -> str: + context = context or {} + + await self.stream_update(session_id, f"πŸš€ Deploy Space activated β€” {role} role") + + # Detect deployment target + task_lower = task.lower() + targets = [] + if "vercel" in task_lower: + targets.append("Vercel") + if "huggingface" in task_lower or "hf" in task_lower or "hugging face" in task_lower: + targets.append("HuggingFace Spaces") + if "docker" in task_lower: + targets.append("Docker") + if "github actions" in task_lower or "ci/cd" in task_lower: + targets.append("GitHub Actions") + if "aws" in task_lower or "lambda" in task_lower: + targets.append("AWS") + if "kubernetes" in task_lower or "k8s" in task_lower: + targets.append("Kubernetes") + + targets_str = ", ".join(targets) if targets else "general deployment" + + mem_context = "" + if context.get("short_term_memory"): + recent = context["short_term_memory"][-2:] + mem_context = "\n".join([f"- {m.get('content','')[:80]}" for m in recent]) + + enhanced_system = f"""{DEPLOY_SYSTEM} + +Active Role: {role.upper()} +Deployment Targets: {targets_str} +Recent Context: {mem_context or 'None'} + +Provide complete, copy-paste ready configurations.""" + + try: + response = await self.ai_router.complete( + prompt=task, + system=enhanced_system, + max_tokens=4096, + ) + return response.get("content", "Deploy Space could not generate configuration.") + except Exception as e: + log.error(f"DeploySpace error: {e}") + return f"Deploy Space error: {str(e)}" diff --git a/spaces/sandbox_space.py b/spaces/sandbox_space.py new file mode 100644 index 0000000000000000000000000000000000000000..22c4e76d978749d3a6cbea215ab4b4fe90b00921 --- /dev/null +++ b/spaces/sandbox_space.py @@ -0,0 +1,178 @@ +""" +πŸ’» Sandbox Space β€” Secure Code Execution Environment +Where code is run and tested safely. +""" +import asyncio +import os +import subprocess +import tempfile +import re +from typing import Dict +import structlog +from .base_space import BaseSpace + +log = structlog.get_logger() + +ALLOWED_LANGUAGES = {"python", "javascript", "bash", "sh"} + + +class SandboxSpace(BaseSpace): + space_name = "sandbox" + space_description = "Secure execution environment β€” run Python, JavaScript, shell scripts safely." + available_roles = ["execution", "cognition"] + + def __init__(self, ws_manager=None, ai_router=None): + super().__init__(ws_manager, ai_router) + self.workspace = "/tmp/god_sandbox" + os.makedirs(self.workspace, exist_ok=True) + self.register_tool("run_python", self._run_python, "Execute Python code") + self.register_tool("run_shell", self._run_shell, "Execute shell commands") + self.register_tool("run_javascript", self._run_javascript, "Execute JavaScript with Node.js") + + async def _run_python(self, code: str, timeout: int = 30, **kwargs) -> str: + """Run Python code safely.""" + try: + with tempfile.NamedTemporaryFile(suffix=".py", dir=self.workspace, + mode='w', delete=False) as f: + f.write(code) + fname = f.name + + proc = await asyncio.create_subprocess_exec( + "python3", fname, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=self.workspace, + ) + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + output = stdout.decode("utf-8", errors="replace") + errors = stderr.decode("utf-8", errors="replace") + + result = "" + if output: + result += f"Output:\n{output}" + if errors: + result += f"\nErrors:\n{errors}" + return result or "Code executed successfully (no output)" + except asyncio.TimeoutError: + proc.kill() + return "⚠️ Execution timed out (30s limit)" + except Exception as e: + return f"❌ Python execution error: {str(e)}" + finally: + try: + os.unlink(fname) + except Exception: + pass + + async def _run_shell(self, command: str, timeout: int = 30, **kwargs) -> str: + """Run shell command safely.""" + # Basic security: block dangerous commands + dangerous = ["rm -rf /", "dd if=", "mkfs", ":(){ :|:& };:", "> /dev/sda"] + for d in dangerous: + if d in command: + return f"⚠️ Command blocked for safety: {d}" + + try: + proc = await asyncio.create_subprocess_shell( + command, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=self.workspace, + ) + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + output = stdout.decode("utf-8", errors="replace") + errors = stderr.decode("utf-8", errors="replace") + result = output + (f"\n[stderr]: {errors}" if errors else "") + return result or f"Command completed (exit code: {proc.returncode})" + except asyncio.TimeoutError: + proc.kill() + return "⚠️ Command timed out (30s limit)" + except Exception as e: + return f"❌ Shell error: {str(e)}" + + async def _run_javascript(self, code: str, timeout: int = 30, **kwargs) -> str: + """Run JavaScript with Node.js.""" + try: + with tempfile.NamedTemporaryFile(suffix=".js", dir=self.workspace, + mode='w', delete=False) as f: + f.write(code) + fname = f.name + + proc = await asyncio.create_subprocess_exec( + "node", fname, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd=self.workspace, + ) + try: + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=timeout) + output = stdout.decode("utf-8", errors="replace") + errors = stderr.decode("utf-8", errors="replace") + result = output + (f"\nErrors:\n{errors}" if errors else "") + return result or "JS executed successfully" + except asyncio.TimeoutError: + proc.kill() + return "⚠️ JS execution timed out" + except Exception as e: + return f"❌ JS error: {str(e)}" + finally: + try: + os.unlink(fname) + except Exception: + pass + + def _extract_code(self, text: str, language: str = "python") -> str: + """Extract code from markdown code blocks.""" + patterns = [ + rf"```{language}\n(.*?)```", + r"```\n(.*?)```", + r"`(.*?)`", + ] + for pattern in patterns: + match = re.search(pattern, text, re.DOTALL) + if match: + return match.group(1).strip() + return text + + async def execute(self, task: str, role: str, session_id: str, context: Dict = None) -> str: + context = context or {} + + await self.stream_update(session_id, f"πŸ’» Sandbox Space activated β€” {role} role") + + system_prompt = self.get_space_prompt(role, task, context) + + # Ask AI to generate and optionally execute code + code_prompt = f"""{task} + +If this requires code execution, generate the code and I will run it. +Format code in ```python, ```javascript, or ```bash blocks. +After the code, explain what it does.""" + + try: + response = await self.ai_router.complete( + prompt=code_prompt, + system=system_prompt, + max_tokens=2048, + ) + ai_response = response.get("content", "") + + # Try to extract and execute Python code + code_blocks = re.findall(r'```(?:python)?\n(.*?)```', ai_response, re.DOTALL) + + execution_results = [] + for code in code_blocks[:2]: # Execute max 2 code blocks + if code.strip(): + result = await self._run_python(code.strip()) + execution_results.append(f"```\n{result}\n```") + + final = ai_response + if execution_results: + final += "\n\n**Execution Results:**\n" + "\n".join(execution_results) + + return final + + except Exception as e: + log.error(f"SandboxSpace error: {e}") + return f"Sandbox Space error: {str(e)}" diff --git a/spaces/vision_space.py b/spaces/vision_space.py new file mode 100644 index 0000000000000000000000000000000000000000..5c20379e4df96b23dc52bfbee548c9758550efff --- /dev/null +++ b/spaces/vision_space.py @@ -0,0 +1,80 @@ +""" +πŸ‘οΈ Vision Space β€” Visual Processing Domain +Image understanding, UI generation, OCR, visual analysis. +""" +from typing import Dict +import structlog +from .base_space import BaseSpace + +log = structlog.get_logger() + +VISION_SYSTEM = """You are GOD AGENT OS v9 β€” Vision Space Expert. + +You specialize in: +- UI/UX design and code generation (React, Next.js, Tailwind) +- Visual layout descriptions and wireframing +- Image analysis and description +- Design-to-code conversion +- CSS and styling +- Responsive design patterns +- Component library creation (shadcn/ui, Radix, MUI) +- Color theory and design systems +- Accessibility (WCAG) guidelines +- Animation and interaction design (Framer Motion, CSS animations) + +When creating UI components, always use modern frameworks and produce clean, production-ready code. +""" + + +class VisionSpace(BaseSpace): + space_name = "vision" + space_description = "Visual processing β€” UI generation, image analysis, design-to-code." + available_roles = ["visual_intelligence", "execution", "cognition"] + + def __init__(self, ws_manager=None, ai_router=None): + super().__init__(ws_manager, ai_router) + self.register_tool("generate_ui", self._generate_ui, "Generate UI components from descriptions") + self.register_tool("analyze_design", self._analyze_design, "Analyze design requirements") + self.register_tool("design_system", self._design_system, "Create design system tokens") + + async def _generate_ui(self, description: str, framework: str = "react", **kwargs) -> str: + return f"Generating {framework} UI for: {description}" + + async def _analyze_design(self, description: str, **kwargs) -> str: + return f"Analyzing design: {description}" + + async def _design_system(self, brand: str, **kwargs) -> str: + return f"Creating design system for: {brand}" + + async def execute(self, task: str, role: str, session_id: str, context: Dict = None) -> str: + context = context or {} + + await self.stream_update(session_id, f"πŸ‘οΈ Vision Space activated β€” {role} role") + + mem_context = "" + if context.get("short_term_memory"): + recent = context["short_term_memory"][-2:] + mem_context = "\n".join([f"- {m.get('content','')[:80]}" for m in recent]) + + enhanced_system = f"""{VISION_SYSTEM} + +Active Role: {role.upper()} +Recent Context: {mem_context or 'None'} + +For UI generation tasks: +- Use React + TypeScript + Tailwind CSS by default +- Create complete, self-contained components +- Include all necessary imports +- Add PropTypes or TypeScript interfaces +- Make it responsive and accessible""" + + try: + response = await self.ai_router.complete( + prompt=task, + system=enhanced_system, + max_tokens=4096, + ) + return response.get("content", "Vision Space could not process the request.") + except Exception as e: + log.error(f"VisionSpace error: {e}") + return f"Vision Space error: {str(e)}" diff --git a/spaces/worker_space.py b/spaces/worker_space.py new file mode 100644 index 0000000000000000000000000000000000000000..691beceebbd12e76c70900f000f46c1ecdd9b5fd --- /dev/null +++ b/spaces/worker_space.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from typing import Any, Dict, List +import structlog + +from .base_space import BaseSpace + +log = structlog.get_logger() + + +class WorkerSpace(BaseSpace): + available_roles = ["cognition", "automation", "execution", "repair", "visual_intelligence"] + + def __init__(self, spec: Dict[str, Any], ws_manager=None, ai_router=None): + self.spec = spec + self.space_name = spec["id"] + self.space_description = spec["description"] + self.available_roles = spec.get("roles", self.available_roles) + super().__init__(ws_manager, ai_router) + self._register_default_tools() + + def _register_default_tools(self): + for responsibility in self.spec.get("responsibilities", []): + tool_name = responsibility.lower().replace(" ", "_").replace("/", "_") + self.register_tool(tool_name, self._generic_tool, responsibility) + + async def _generic_tool(self, **kwargs) -> str: + return f"{self.spec['name']} executed with {kwargs}" + + def _build_specialized_prompt(self, role: str, task: str, context: Dict[str, Any]) -> str: + responsibilities = ", ".join(self.spec.get("responsibilities", [])) + layer = self.spec.get("layer", "") + return f"""You are {self.spec['name']} inside GOD AGENT OS v10. + +Layer: {layer} +Space ID: {self.spec['id']} +Description: {self.spec['description']} +Responsibilities: {responsibilities} +Active Role: {role} + +Rules: +- Stay inside this space's domain responsibilities. +- Produce concrete, production-ready output. +- When a task spans multiple domains, explain how this space contributes and what should happen next. +- Prefer structured bullets for plans, commands, patches, interfaces, contracts, and validation criteria. +- Be concise but specific. +""" + + async def execute(self, task: str, role: str, session_id: str, context: Dict = None) -> str: + context = context or {} + await self.stream_update(session_id, f"{self.spec['icon']} {self.spec['name']} activated β€” {role} role", space=self.space_name) + + if not self.ai_router: + responsibilities = "\n".join(f"- {item}" for item in self.spec.get("responsibilities", [])) + return f"{self.spec['name']} is offline.\n\nResponsibilities:\n{responsibilities}" + + system_prompt = self._build_specialized_prompt(role, task, context) + try: + response = await self.ai_router.complete(prompt=task, system=system_prompt, max_tokens=2048) + if isinstance(response, dict): + return response.get("content", "") or f"{self.spec['name']} completed the task." + return str(response) + except Exception as exc: + log.error("worker_space_execute_failed", space=self.space_name, error=str(exc)) + responsibilities = ", ".join(self.spec.get("responsibilities", [])) + return ( + f"{self.spec['name']} error: {exc}\n\n" + f"Primary responsibilities: {responsibilities}" + ) diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tools/executor.py b/tools/executor.py new file mode 100644 index 0000000000000000000000000000000000000000..2ae0b80afbd92030c28334a35653c342c2599eb4 --- /dev/null +++ b/tools/executor.py @@ -0,0 +1,176 @@ +""" +Tool Executor β€” Routes tool calls to the right implementation +Supports: code, shell, file, browser, github, memory, search, test, none +""" + +import asyncio +import os +import subprocess +import tempfile +import time +from typing import Any, List, Optional + +import structlog + +from api.websocket_manager import WebSocketManager + +log = structlog.get_logger() + + +class ToolExecutor: + def __init__(self, ws_manager: WebSocketManager): + self.ws = ws_manager + + async def run( + self, + tool: str, + task: str, + goal: str = "", + previous: List = [], + task_id: str = "", + session_id: str = "", + ) -> str: + tool = (tool or "none").lower().strip() + + dispatch = { + "code": self._tool_code, + "shell": self._tool_shell, + "file": self._tool_file, + "github": self._tool_github, + "memory": self._tool_memory, + "search": self._tool_search, + "test": self._tool_test, + "browser": self._tool_browser, + "none": self._tool_none, + } + + fn = dispatch.get(tool, self._tool_none) + return await fn(task=task, goal=goal, previous=previous, task_id=task_id, session_id=session_id) + + # ─── Code Tool ───────────────────────────────────────────────────────────── + async def _tool_code(self, task, goal, previous, task_id, session_id) -> str: + """Generate code using LLM.""" + from core.agent import AgentCore + agent = AgentCore(self.ws) + messages = [ + {"role": "system", "content": "You are an expert software engineer. Write clean, production-quality code. Return only the code with minimal explanation."}, + {"role": "user", "content": f"Task: {task}\nGoal: {goal}\n\nWrite the code to accomplish this."}, + ] + result = await agent.llm_stream(messages, task_id=task_id, session_id=session_id) + return result or f"# Code for: {task}" + + # ─── Shell Tool ──────────────────────────────────────────────────────────── + async def _tool_shell(self, task, goal, previous, task_id, session_id) -> str: + """Execute shell commands safely in a temp workspace.""" + # Extract command from task description + from core.agent import AgentCore + agent = AgentCore(self.ws) + messages = [ + {"role": "system", "content": "Extract the shell command to run. Return ONLY the command, nothing else."}, + {"role": "user", "content": f"Task: {task}"}, + ] + cmd = await agent.llm_stream(messages, task_id=task_id, session_id=session_id) + cmd = cmd.strip().strip("`").strip() + + # Safety: block dangerous commands + blocked = ["rm -rf /", ":(){ :|:& };:", "mkfs", "dd if=", "shutdown", "reboot", "halt"] + for b in blocked: + if b in cmd: + return f"❌ Blocked dangerous command: {cmd}" + + try: + await self.ws.emit(task_id, "step_progress", { + "action": "shell_exec", + "command": cmd[:200], + }, session_id=session_id) + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE, + cwd="/tmp", + ) + stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=30) + output = stdout.decode()[:2000] + (stderr.decode()[:500] if stderr else "") + return output or "Command executed (no output)" + except asyncio.TimeoutError: + return "⚠️ Command timed out after 30s" + except Exception as e: + return f"❌ Shell error: {str(e)}" + + # ─── File Tool ───────────────────────────────────────────────────────────── + async def _tool_file(self, task, goal, previous, task_id, session_id) -> str: + """Create or modify files.""" + from core.agent import AgentCore + agent = AgentCore(self.ws) + messages = [ + {"role": "system", "content": "Generate file content. Respond with JSON: {\"filename\": \"...\", \"content\": \"...\"}"}, + {"role": "user", "content": f"Task: {task}\nGoal: {goal}"}, + ] + raw = await agent.llm_stream(messages, task_id=task_id, session_id=session_id) + try: + import json + start = raw.find("{") + end = raw.rfind("}") + 1 + data = json.loads(raw[start:end]) + filename = data.get("filename", "output.txt") + content = data.get("content", raw) + path = f"/tmp/workspace/{filename}" + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w") as f: + f.write(content) + await self.ws.emit(task_id, "step_progress", { + "action": "file_written", + "filename": filename, + "size": len(content), + }, session_id=session_id) + return f"βœ… File written: {filename} ({len(content)} chars)" + except Exception as e: + return f"File task result: {raw[:500]}" + + # ─── GitHub Tool ─────────────────────────────────────────────────────────── + async def _tool_github(self, task, goal, previous, task_id, session_id) -> str: + """Perform GitHub operations.""" + return f"GitHub: {task}\n(Set GITHUB_TOKEN to enable real GitHub operations)" + + # ─── Memory Tool ─────────────────────────────────────────────────────────── + async def _tool_memory(self, task, goal, previous, task_id, session_id) -> str: + """Save/retrieve from memory.""" + from memory.db import save_memory, search_memory + results = await search_memory(task[:50], session_id=session_id) + if results: + return "\n".join([r["content"][:300] for r in results[:3]]) + return "No relevant memories found" + + # ─── Search Tool ─────────────────────────────────────────────────────────── + async def _tool_search(self, task, goal, previous, task_id, session_id) -> str: + """Web search using available APIs.""" + return f"Search result for: {task}\n(Integrate search API for real results)" + + # ─── Test Tool ───────────────────────────────────────────────────────────── + async def _tool_test(self, task, goal, previous, task_id, session_id) -> str: + """Generate and run tests.""" + from core.agent import AgentCore + agent = AgentCore(self.ws) + messages = [ + {"role": "system", "content": "Write test cases for the given task. Use pytest format."}, + {"role": "user", "content": f"Write tests for: {task}\nContext: {goal}"}, + ] + result = await agent.llm_stream(messages, task_id=task_id, session_id=session_id) + return result or f"# Tests for: {task}" + + # ─── Browser Tool ────────────────────────────────────────────────────────── + async def _tool_browser(self, task, goal, previous, task_id, session_id) -> str: + """Browser automation (stub β€” extend with playwright).""" + return f"Browser task: {task}\n(Install playwright for real browser automation)" + + # ─── None Tool ───────────────────────────────────────────────────────────── + async def _tool_none(self, task, goal, previous, task_id, session_id) -> str: + """Use LLM directly without tools.""" + from core.agent import AgentCore + agent = AgentCore(self.ws) + messages = [ + {"role": "system", "content": "You are an expert engineer. Complete the task thoroughly."}, + {"role": "user", "content": f"Task: {task}\nGoal context: {goal}"}, + ] + result = await agent.llm_stream(messages, task_id=task_id, session_id=session_id) + return result or f"Completed: {task}" diff --git a/tools/tool_router.py b/tools/tool_router.py new file mode 100644 index 0000000000000000000000000000000000000000..32082ede60e30cf1302917692689864cf4dd2fe9 --- /dev/null +++ b/tools/tool_router.py @@ -0,0 +1,377 @@ +""" +Real Tool Router β€” Autonomous Agent Function Calling +Routes LLM intent to real execution: E2B sandbox, file ops, shell, git, etc. +""" + +import asyncio +import hashlib +import json +import os +import time +import uuid +from typing import Any, AsyncGenerator, Dict, List, Optional, Tuple + +import httpx +import structlog + +log = structlog.get_logger() + +# ─── Tool Definitions (for LLM function calling) ────────────────────────────── + +TOOL_DEFINITIONS = [ + { + "name": "execute_python", + "description": "Execute Python code in a real sandbox and return actual stdout/stderr output. Use this for ANY Python code execution, calculations, data processing, etc.", + "parameters": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "The Python code to execute" + }, + "description": { + "type": "string", + "description": "What this code does" + } + }, + "required": ["code"] + } + }, + { + "name": "execute_shell", + "description": "Execute a shell/terminal command and return real output. Use for file operations, system commands, installing packages, etc.", + "parameters": { + "type": "object", + "properties": { + "command": { + "type": "string", + "description": "The shell command to execute" + }, + "cwd": { + "type": "string", + "description": "Working directory (default: /tmp)" + } + }, + "required": ["command"] + } + }, + { + "name": "write_file", + "description": "Create or overwrite a file with the given content. Returns confirmation with file size.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "File path to write" + }, + "content": { + "type": "string", + "description": "File content" + } + }, + "required": ["path", "content"] + } + }, + { + "name": "read_file", + "description": "Read the contents of a file from the sandbox filesystem.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "File path to read" + } + }, + "required": ["path"] + } + }, + { + "name": "delete_file", + "description": "Delete a file from the sandbox filesystem.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "File path to delete" + } + }, + "required": ["path"] + } + }, + { + "name": "list_files", + "description": "List files and directories in a given path.", + "parameters": { + "type": "object", + "properties": { + "path": { + "type": "string", + "description": "Directory path to list" + } + }, + "required": ["path"] + } + }, + { + "name": "web_search", + "description": "Search the web for information. Returns relevant results.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Search query" + } + }, + "required": ["query"] + } + }, + { + "name": "install_package", + "description": "Install a Python package using pip.", + "parameters": { + "type": "object", + "properties": { + "package": { + "type": "string", + "description": "Package name (e.g., 'requests', 'numpy')" + } + }, + "required": ["package"] + } + }, +] + +# Tool name β†’ display info +TOOL_DISPLAY = { + "execute_python": {"icon": "🐍", "label": "Python Execution"}, + "execute_shell": {"icon": "πŸ’»", "label": "Terminal"}, + "write_file": {"icon": "πŸ“", "label": "Write File"}, + "read_file": {"icon": "πŸ“–", "label": "Read File"}, + "delete_file": {"icon": "πŸ—‘οΈ", "label": "Delete File"}, + "list_files": {"icon": "πŸ“", "label": "List Files"}, + "web_search": {"icon": "πŸ”", "label": "Web Search"}, + "install_package": {"icon": "πŸ“¦", "label": "Install Package"}, +} + + +class ToolRouter: + """ + Routes tool calls to real execution engines. + Integrates with E2B for sandboxed execution. + """ + + def __init__(self, ws_manager=None): + self.ws = ws_manager + from sandbox.e2b_executor import get_executor + self.executor = get_executor() + + async def execute_tool( + self, + tool_name: str, + tool_args: Dict[str, Any], + session_id: str, + task_id: str = "", + ) -> Dict[str, Any]: + """Execute a tool and return real results.""" + display = TOOL_DISPLAY.get(tool_name, {"icon": "βš™οΈ", "label": tool_name}) + start_time = time.time() + + # Emit tool start event + if self.ws: + await self.ws.emit_chat(session_id, "tool_start", { + "tool": tool_name, + "icon": display["icon"], + "label": display["label"], + "args": {k: str(v)[:200] for k, v in tool_args.items()}, + "task_id": task_id, + }) + + log.info("Tool executing", tool=tool_name, session_id=session_id) + + try: + if tool_name == "execute_python": + result = await self._execute_python(tool_args, session_id) + elif tool_name == "execute_shell": + result = await self._execute_shell(tool_args, session_id) + elif tool_name == "write_file": + result = await self._write_file(tool_args, session_id) + elif tool_name == "read_file": + result = await self._read_file(tool_args, session_id) + elif tool_name == "delete_file": + result = await self._delete_file(tool_args, session_id) + elif tool_name == "list_files": + result = await self._list_files(tool_args, session_id) + elif tool_name == "web_search": + result = await self._web_search(tool_args, session_id) + elif tool_name == "install_package": + result = await self._install_package(tool_args, session_id) + else: + result = {"error": f"Unknown tool: {tool_name}", "success": False} + + except Exception as e: + log.error("Tool execution error", tool=tool_name, error=str(e)) + result = {"error": str(e), "success": False} + + duration_ms = int((time.time() - start_time) * 1000) + result["_duration_ms"] = duration_ms + result["_tool"] = tool_name + + # Emit tool complete event + if self.ws: + await self.ws.emit_chat(session_id, "tool_complete", { + "tool": tool_name, + "icon": display["icon"], + "label": display["label"], + "success": result.get("success", True), + "duration_ms": duration_ms, + "task_id": task_id, + "output_preview": str(result.get("stdout", result.get("content", result.get("output", ""))))[:200], + }) + + return result + + async def _execute_python(self, args: Dict, session_id: str) -> Dict: + code = args.get("code", "") + result = await self.executor.execute_code(code, session_id, language="python") + return result + + async def _execute_shell(self, args: Dict, session_id: str) -> Dict: + command = args.get("command", "") + cwd = args.get("cwd", "/tmp") + + # Safety check + blocked = ["rm -rf /", ":(){ :|:&", "mkfs", "shutdown", "reboot", "halt"] + for b in blocked: + if b in command: + return {"stdout": "", "stderr": "β›” Blocked dangerous command", "exit_code": 1, "success": False} + + result = await self.executor.execute_shell(command, session_id, cwd=cwd) + return result + + async def _write_file(self, args: Dict, session_id: str) -> Dict: + path = args.get("path", "/tmp/output.txt") + content = args.get("content", "") + + # Normalize path + if not path.startswith("/"): + path = f"/tmp/workspace/{path}" + + result = await self.executor.write_file(path, content, session_id) + if result.get("success"): + result["output"] = f"βœ… File written: {path} ({len(content)} chars, {len(content.splitlines())} lines)" + return result + + async def _read_file(self, args: Dict, session_id: str) -> Dict: + path = args.get("path", "") + if not path.startswith("/"): + path = f"/tmp/workspace/{path}" + return await self.executor.read_file(path, session_id) + + async def _delete_file(self, args: Dict, session_id: str) -> Dict: + path = args.get("path", "") + if not path.startswith("/"): + path = f"/tmp/workspace/{path}" + result = await self.executor.delete_file(path, session_id) + if result.get("success"): + result["output"] = f"βœ… File deleted: {path}" + return result + + async def _list_files(self, args: Dict, session_id: str) -> Dict: + path = args.get("path", "/tmp") + return await self.executor.list_files(path, session_id) + + async def _web_search(self, args: Dict, session_id: str) -> Dict: + query = args.get("query", "") + try: + async with httpx.AsyncClient(timeout=15.0) as client: + # DuckDuckGo instant answers API + resp = await client.get( + "https://api.duckduckgo.com/", + params={"q": query, "format": "json", "no_html": "1"}, + ) + if resp.status_code == 200: + data = resp.json() + abstract = data.get("AbstractText", "") + related = [r.get("Text", "") for r in data.get("RelatedTopics", [])[:5] if "Text" in r] + result_text = abstract or "\n".join(related) or f"Search completed for: {query}" + return { + "success": True, + "query": query, + "output": result_text, + "source": "duckduckgo", + } + except Exception as e: + log.warning("Web search failed", error=str(e)) + + return { + "success": True, + "query": query, + "output": f"Web search for '{query}' β€” integrate a search API for full results", + } + + async def _install_package(self, args: Dict, session_id: str) -> Dict: + package = args.get("package", "") + if not package or not package.replace("-", "").replace("_", "").replace(".", "").isalnum(): + return {"success": False, "error": "Invalid package name"} + + result = await self.executor.execute_shell( + f"pip install {package} -q", + session_id, + timeout=120, + ) + if result.get("exit_code", 1) == 0: + result["output"] = f"βœ… Package installed: {package}" + return result + + def format_tool_result(self, tool_name: str, result: Dict) -> str: + """Format tool result for inclusion in LLM context.""" + if not result.get("success", True) and result.get("error"): + return f"❌ Tool Error: {result['error']}" + + # Python/Shell execution + stdout = result.get("stdout", "") + stderr = result.get("stderr", "") + exit_code = result.get("exit_code", 0) + sandbox_id = result.get("sandbox_id", "unknown") + duration_ms = result.get("_duration_ms", 0) + + if tool_name in ("execute_python", "execute_shell"): + parts = [] + if stdout: + parts.append(f"**stdout:**\n```\n{stdout[:3000]}\n```") + if stderr and exit_code != 0: + parts.append(f"**stderr:**\n```\n{stderr[:1000]}\n```") + parts.append(f"**Exit code:** {exit_code} | **Sandbox:** `{sandbox_id}` | **Time:** {duration_ms}ms") + return "\n".join(parts) if parts else f"Executed (exit: {exit_code})" + + elif tool_name == "write_file": + if result.get("success"): + return f"βœ… **File created:** `{result.get('path', '')}` ({result.get('size', 0)} bytes) on sandbox `{sandbox_id}`" + return f"❌ Write failed: {result.get('error', '')}" + + elif tool_name == "read_file": + if result.get("success"): + content = result.get("content", "") + return f"**File contents of `{result.get('path', '')}`:**\n```\n{content[:3000]}\n```" + return f"❌ Read failed: {result.get('error', '')}" + + elif tool_name == "delete_file": + return result.get("output", f"Delete operation completed for {result.get('path', '')}") + + elif tool_name == "list_files": + listing = result.get("listing", "") + return f"**Directory listing of `{result.get('path', '')}`:**\n```\n{listing}\n```" + + elif tool_name == "web_search": + return f"**Search results for '{result.get('query', '')}':**\n{result.get('output', '')}" + + elif tool_name == "install_package": + if result.get("exit_code", 1) == 0: + return f"βœ… Package installed successfully" + return f"Package install output:\n```\n{stdout[:500]}\n```" + + return result.get("output", str(result)[:500]) diff --git a/worker_spaces/__init__.py b/worker_spaces/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8d95b054b75726823220dc1b63f54967bff330be --- /dev/null +++ b/worker_spaces/__init__.py @@ -0,0 +1 @@ +WORKER_SPACES = ['god-core-space', 'coding-worker-space', 'sandbox-worker-space', 'terminal-worker-space', 'filesystem-worker-space', 'browser-worker-space', 'vision-worker-space', 'ui-worker-space', 'debug-worker-space', 'test-worker-space', 'verification-worker-space', 'git-worker-space', 'deploy-worker-space', 'connector-worker-space', 'memory-worker-space', 'knowledge-worker-space', 'workflow-worker-space', 'eventbus-space', 'observability-space', 'session-runtime-space', 'model-router-space', 'auth-gateway-space'] diff --git a/worker_spaces/auth_gateway_space/__init__.py b/worker_spaces/auth_gateway_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..40fdff2384147ed7c380fd5e680a36881c0aa20d --- /dev/null +++ b/worker_spaces/auth_gateway_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'auth-gateway-space' diff --git a/worker_spaces/auth_gateway_space/spec.py b/worker_spaces/auth_gateway_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..13aeb75c1614fedd0b85f34f56aa8ecda33d5bb8 --- /dev/null +++ b/worker_spaces/auth_gateway_space/spec.py @@ -0,0 +1,18 @@ +SPACE_SPEC = { + "id": "auth-gateway-space", + "name": "Auth Gateway Space", + "icon": "πŸ”", + "color": "#9333ea", + "layer": "Infrastructure Layer", + "description": "Auth, API keys, rate limits, and permissions.", + "responsibilities": [ + "auth", + "api keys", + "rate limits", + "permissions" + ], + "roles": [ + "automation", + "repair" + ] +} diff --git a/worker_spaces/browser_worker_space/__init__.py b/worker_spaces/browser_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c1ade85b757bd3602b7165138cd7a0185fdea5dd --- /dev/null +++ b/worker_spaces/browser_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'browser-worker-space' diff --git a/worker_spaces/browser_worker_space/spec.py b/worker_spaces/browser_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..7505d3c937866c6fa3aa60c1be59b5993449a85f --- /dev/null +++ b/worker_spaces/browser_worker_space/spec.py @@ -0,0 +1,19 @@ +SPACE_SPEC = { + "id": "browser-worker-space", + "name": "Browser Worker Space", + "icon": "🌐", + "color": "#3b82f6", + "layer": "Browser + UI Intelligence", + "description": "Playwright automation, navigation, screenshots, and interaction testing.", + "responsibilities": [ + "playwright", + "browser automation", + "navigation", + "screenshots", + "interaction testing" + ], + "roles": [ + "automation", + "cognition" + ] +} diff --git a/worker_spaces/coding_worker_space/__init__.py b/worker_spaces/coding_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..82a3268659db1767713230d05acb6636b48c2389 --- /dev/null +++ b/worker_spaces/coding_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'coding-worker-space' diff --git a/worker_spaces/coding_worker_space/spec.py b/worker_spaces/coding_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..099e5fc5a5c02aa9f66ab85b07ddb156cd2e0b65 --- /dev/null +++ b/worker_spaces/coding_worker_space/spec.py @@ -0,0 +1,20 @@ +SPACE_SPEC = { + "id": "coding-worker-space", + "name": "Coding Worker Space", + "icon": "πŸ”§", + "color": "#f59e0b", + "layer": "Execution Layer", + "description": "Code generation, file editing, refactoring, dependency handling, and code transformations.", + "responsibilities": [ + "code generation", + "file editing", + "refactoring", + "dependency handling", + "code transformations" + ], + "roles": [ + "execution", + "cognition", + "automation" + ] +} diff --git a/worker_spaces/connector_worker_space/__init__.py b/worker_spaces/connector_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c15fe26eb245a383963905fdcdd05e3e2d693fc6 --- /dev/null +++ b/worker_spaces/connector_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'connector-worker-space' diff --git a/worker_spaces/connector_worker_space/spec.py b/worker_spaces/connector_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..5c4d4fe011850dfe5dc227a2bcce93e2c1c408a6 --- /dev/null +++ b/worker_spaces/connector_worker_space/spec.py @@ -0,0 +1,18 @@ +SPACE_SPEC = { + "id": "connector-worker-space", + "name": "Connector Worker Space", + "icon": "πŸ”Œ", + "color": "#6366f1", + "layer": "Deployment Layer", + "description": "GitHub, Supabase, APIs, and external integrations.", + "responsibilities": [ + "github", + "supabase", + "apis", + "external integrations" + ], + "roles": [ + "automation", + "cognition" + ] +} diff --git a/worker_spaces/debug_worker_space/__init__.py b/worker_spaces/debug_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2b1a338d8f1e7201f79a01996db11b9f864e29b6 --- /dev/null +++ b/worker_spaces/debug_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'debug-worker-space' diff --git a/worker_spaces/debug_worker_space/spec.py b/worker_spaces/debug_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..3464fd50957b70df8f88c3119b9aa379a2b43c5d --- /dev/null +++ b/worker_spaces/debug_worker_space/spec.py @@ -0,0 +1,18 @@ +SPACE_SPEC = { + "id": "debug-worker-space", + "name": "Debug Worker Space", + "icon": "πŸ›", + "color": "#ef4444", + "layer": "Verification + Repair Layer", + "description": "Error analysis, traceback parsing, repair strategies, and retry planning.", + "responsibilities": [ + "error analysis", + "traceback parsing", + "repair strategies", + "retry planning" + ], + "roles": [ + "repair", + "cognition" + ] +} diff --git a/worker_spaces/deploy_worker_space/__init__.py b/worker_spaces/deploy_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..f6ae9b9622bcf2ae5136a7fe8cc38862c132fc14 --- /dev/null +++ b/worker_spaces/deploy_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'deploy-worker-space' diff --git a/worker_spaces/deploy_worker_space/spec.py b/worker_spaces/deploy_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..b38aff923d5793edb8633e80aa907d3a7ae18497 --- /dev/null +++ b/worker_spaces/deploy_worker_space/spec.py @@ -0,0 +1,19 @@ +SPACE_SPEC = { + "id": "deploy-worker-space", + "name": "Deploy Worker Space", + "icon": "πŸš€", + "color": "#0ea5e9", + "layer": "Deployment Layer", + "description": "Vercel, Railway, Docker deploys, preview URLs, and CI/CD triggers.", + "responsibilities": [ + "vercel deploy", + "railway deploy", + "docker deploy", + "preview urls", + "ci/cd triggers" + ], + "roles": [ + "automation", + "execution" + ] +} diff --git a/worker_spaces/eventbus_space/__init__.py b/worker_spaces/eventbus_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..efbfcdfd16ab422cfd343b121cc4c5c7dadc8e5b --- /dev/null +++ b/worker_spaces/eventbus_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'eventbus-space' diff --git a/worker_spaces/eventbus_space/spec.py b/worker_spaces/eventbus_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..403aab21e05fd12fbdc8fd1d960cbcb183053eb0 --- /dev/null +++ b/worker_spaces/eventbus_space/spec.py @@ -0,0 +1,18 @@ +SPACE_SPEC = { + "id": "eventbus-space", + "name": "Eventbus Space", + "icon": "πŸ“‘", + "color": "#06b6d4", + "layer": "Coordination Layer", + "description": "Redis PubSub, NATS, RabbitMQ, and event streams.", + "responsibilities": [ + "redis pubsub", + "nats", + "rabbitmq", + "event streams" + ], + "roles": [ + "automation", + "execution" + ] +} diff --git a/worker_spaces/filesystem_worker_space/__init__.py b/worker_spaces/filesystem_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a6e007f3052b84178d7a3afbc0c17a8efba04e43 --- /dev/null +++ b/worker_spaces/filesystem_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'filesystem-worker-space' diff --git a/worker_spaces/filesystem_worker_space/spec.py b/worker_spaces/filesystem_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..b286172fef925236514a6134df367192a7d98e8e --- /dev/null +++ b/worker_spaces/filesystem_worker_space/spec.py @@ -0,0 +1,18 @@ +SPACE_SPEC = { + "id": "filesystem-worker-space", + "name": "Filesystem Worker Space", + "icon": "πŸ—‚οΈ", + "color": "#22c55e", + "layer": "Execution Layer", + "description": "File writes, project trees, artifact management, and storage operations.", + "responsibilities": [ + "file writes", + "project trees", + "artifact management", + "storage operations" + ], + "roles": [ + "execution", + "automation" + ] +} diff --git a/worker_spaces/git_worker_space/__init__.py b/worker_spaces/git_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2f59130d0f5f3889f3e9fb212adbcb53bac7efb3 --- /dev/null +++ b/worker_spaces/git_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'git-worker-space' diff --git a/worker_spaces/git_worker_space/spec.py b/worker_spaces/git_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..869f45edeb0688eb21557358c7c3a8204ffe7c7b --- /dev/null +++ b/worker_spaces/git_worker_space/spec.py @@ -0,0 +1,18 @@ +SPACE_SPEC = { + "id": "git-worker-space", + "name": "Git Worker Space", + "icon": "🌳", + "color": "#f97316", + "layer": "Deployment Layer", + "description": "Commits, branching, diffs, merges, and repository workflow operations.", + "responsibilities": [ + "commits", + "branching", + "diffs", + "merges" + ], + "roles": [ + "automation", + "execution" + ] +} diff --git a/worker_spaces/god_core_space/__init__.py b/worker_spaces/god_core_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..641e91e9ddf7a40026510dfadffb6e2294324776 --- /dev/null +++ b/worker_spaces/god_core_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'god-core-space' diff --git a/worker_spaces/god_core_space/spec.py b/worker_spaces/god_core_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..9f43ff3ee65de9048986e57e3daaf924fa91c11d --- /dev/null +++ b/worker_spaces/god_core_space/spec.py @@ -0,0 +1,23 @@ +SPACE_SPEC = { + "id": "god-core-space", + "name": "God Core Space", + "icon": "🧠", + "color": "#7c3aed", + "layer": "Core Cognitive Layer", + "description": "System brain for orchestration, planning, reasoning, workflow control, mission state, websocket events, and model routing.", + "responsibilities": [ + "orchestrator", + "planner", + "reasoning", + "task graph", + "workflow engine", + "mission state", + "memory routing", + "websocket events", + "llm routing" + ], + "roles": [ + "cognition", + "automation" + ] +} diff --git a/worker_spaces/knowledge_worker_space/__init__.py b/worker_spaces/knowledge_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..18248007a882e95da1e9454852cfed315f0cf183 --- /dev/null +++ b/worker_spaces/knowledge_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'knowledge-worker-space' diff --git a/worker_spaces/knowledge_worker_space/spec.py b/worker_spaces/knowledge_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..e83ca7f4632774b15af6051c55859406b8f1b6dd --- /dev/null +++ b/worker_spaces/knowledge_worker_space/spec.py @@ -0,0 +1,18 @@ +SPACE_SPEC = { + "id": "knowledge-worker-space", + "name": "Knowledge Worker Space", + "icon": "πŸ“š", + "color": "#4f46e5", + "layer": "Memory + Knowledge Layer", + "description": "Docs retrieval, semantic search, RAG pipelines, and indexed repositories.", + "responsibilities": [ + "docs retrieval", + "semantic search", + "rag pipelines", + "indexed repositories" + ], + "roles": [ + "cognition", + "automation" + ] +} diff --git a/worker_spaces/memory_worker_space/__init__.py b/worker_spaces/memory_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ab32e16ed27b4973e325686e43c5ebdec4800915 --- /dev/null +++ b/worker_spaces/memory_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'memory-worker-space' diff --git a/worker_spaces/memory_worker_space/spec.py b/worker_spaces/memory_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..b392610d8db451c543b94d75e8ab2eda6e4ce316 --- /dev/null +++ b/worker_spaces/memory_worker_space/spec.py @@ -0,0 +1,19 @@ +SPACE_SPEC = { + "id": "memory-worker-space", + "name": "Memory Worker Space", + "icon": "🧠", + "color": "#a855f7", + "layer": "Memory + Knowledge Layer", + "description": "Vector DB, execution history, learned fixes, project memory, and long-term state.", + "responsibilities": [ + "vector db", + "execution history", + "learned fixes", + "project memory", + "long-term state" + ], + "roles": [ + "cognition", + "automation" + ] +} diff --git a/worker_spaces/model_router_space/__init__.py b/worker_spaces/model_router_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..c359382910a7ff37a595bd92dd7ebcd3a139cad7 --- /dev/null +++ b/worker_spaces/model_router_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'model-router-space' diff --git a/worker_spaces/model_router_space/spec.py b/worker_spaces/model_router_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..cc28053fcd5927132569c0d4f084a29a1624d777 --- /dev/null +++ b/worker_spaces/model_router_space/spec.py @@ -0,0 +1,19 @@ +SPACE_SPEC = { + "id": "model-router-space", + "name": "Model Router Space", + "icon": "πŸ›£οΈ", + "color": "#eab308", + "layer": "Infrastructure Layer", + "description": "GPT routing, Claude routing, fallback models, cost optimization, and model selection.", + "responsibilities": [ + "gpt routing", + "claude routing", + "fallback models", + "cost optimization", + "model selection" + ], + "roles": [ + "cognition", + "automation" + ] +} diff --git a/worker_spaces/observability_space/__init__.py b/worker_spaces/observability_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..cc973f7c98d8e29353f61937662cfc7ed0f098be --- /dev/null +++ b/worker_spaces/observability_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'observability-space' diff --git a/worker_spaces/observability_space/spec.py b/worker_spaces/observability_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..82d1439082f8cbff07139bd572fe0eb04faef1e1 --- /dev/null +++ b/worker_spaces/observability_space/spec.py @@ -0,0 +1,19 @@ +SPACE_SPEC = { + "id": "observability-space", + "name": "Observability Space", + "icon": "πŸ“ˆ", + "color": "#22c55e", + "layer": "Monitoring Layer", + "description": "Logs, metrics, tracing, agent monitoring, and runtime analytics.", + "responsibilities": [ + "logs", + "metrics", + "tracing", + "agent monitoring", + "runtime analytics" + ], + "roles": [ + "cognition", + "automation" + ] +} diff --git a/worker_spaces/sandbox_worker_space/__init__.py b/worker_spaces/sandbox_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3477d1c1b32610a160f814ed50af57d1094ef806 --- /dev/null +++ b/worker_spaces/sandbox_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'sandbox-worker-space' diff --git a/worker_spaces/sandbox_worker_space/spec.py b/worker_spaces/sandbox_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..5b41f384261280954d8325efeee03fb39dee20a1 --- /dev/null +++ b/worker_spaces/sandbox_worker_space/spec.py @@ -0,0 +1,19 @@ +SPACE_SPEC = { + "id": "sandbox-worker-space", + "name": "Sandbox Worker Space", + "icon": "πŸ§ͺ", + "color": "#10b981", + "layer": "Execution Layer", + "description": "Isolated execution, runtime sandboxing, subprocesses, environment resets, and lifecycle management.", + "responsibilities": [ + "isolated execution", + "docker runtime", + "subprocesses", + "environment resets", + "runtime lifecycle" + ], + "roles": [ + "execution", + "repair" + ] +} diff --git a/worker_spaces/session_runtime_space/__init__.py b/worker_spaces/session_runtime_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..535b64c8a274d467e607ec29e610ff2ecfe2be2a --- /dev/null +++ b/worker_spaces/session_runtime_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'session-runtime-space' diff --git a/worker_spaces/session_runtime_space/spec.py b/worker_spaces/session_runtime_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..aaa8be3b0cefdb45180d23ce7f0a027f2f37ebe5 --- /dev/null +++ b/worker_spaces/session_runtime_space/spec.py @@ -0,0 +1,18 @@ +SPACE_SPEC = { + "id": "session-runtime-space", + "name": "Session Runtime Space", + "icon": "🧷", + "color": "#64748b", + "layer": "Session Layer", + "description": "User sessions, mission isolation, runtime persistence, and checkpointing.", + "responsibilities": [ + "user sessions", + "mission isolation", + "runtime persistence", + "checkpointing" + ], + "roles": [ + "automation", + "cognition" + ] +} diff --git a/worker_spaces/terminal_worker_space/__init__.py b/worker_spaces/terminal_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..875a68566900428203ff00244941739d08cdbb41 --- /dev/null +++ b/worker_spaces/terminal_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'terminal-worker-space' diff --git a/worker_spaces/terminal_worker_space/spec.py b/worker_spaces/terminal_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..a8747cdff54a7c294e270f8b020fa43215f3d3b1 --- /dev/null +++ b/worker_spaces/terminal_worker_space/spec.py @@ -0,0 +1,18 @@ +SPACE_SPEC = { + "id": "terminal-worker-space", + "name": "Terminal Worker Space", + "icon": "⌨️", + "color": "#14b8a6", + "layer": "Execution Layer", + "description": "Shell commands, package installs, build tools, and process monitoring.", + "responsibilities": [ + "shell commands", + "package installs", + "build tools", + "process monitoring" + ], + "roles": [ + "execution", + "automation" + ] +} diff --git a/worker_spaces/test_worker_space/__init__.py b/worker_spaces/test_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8f97fdd7eb73cc6b6642ca30e265a2be1d480b2d --- /dev/null +++ b/worker_spaces/test_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'test-worker-space' diff --git a/worker_spaces/test_worker_space/spec.py b/worker_spaces/test_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..fc36822dfbb5ec317f4c29b3029ce7db6308d09f --- /dev/null +++ b/worker_spaces/test_worker_space/spec.py @@ -0,0 +1,18 @@ +SPACE_SPEC = { + "id": "test-worker-space", + "name": "Test Worker Space", + "icon": "πŸ§ͺ", + "color": "#06b6d4", + "layer": "Verification + Repair Layer", + "description": "Run tests, assertions, integration checks, and regression testing.", + "responsibilities": [ + "run tests", + "assertions", + "integration checks", + "regression testing" + ], + "roles": [ + "execution", + "repair" + ] +} diff --git a/worker_spaces/ui_worker_space/__init__.py b/worker_spaces/ui_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2055f9d5bb3dd8931de5a24b952e3cb8df7d952a --- /dev/null +++ b/worker_spaces/ui_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'ui-worker-space' diff --git a/worker_spaces/ui_worker_space/spec.py b/worker_spaces/ui_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..c15eebcea9cf8309dd73ce9126061ff7642e0644 --- /dev/null +++ b/worker_spaces/ui_worker_space/spec.py @@ -0,0 +1,19 @@ +SPACE_SPEC = { + "id": "ui-worker-space", + "name": "UI Worker Space", + "icon": "🎨", + "color": "#8b5cf6", + "layer": "Browser + UI Intelligence", + "description": "Frontend generation, design systems, responsive layouts, component consistency, and visual polish.", + "responsibilities": [ + "frontend generation", + "design systems", + "responsive layouts", + "component consistency", + "visual polish" + ], + "roles": [ + "visual_intelligence", + "execution" + ] +} diff --git a/worker_spaces/verification_worker_space/__init__.py b/worker_spaces/verification_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d82e92a01bf079067cfdd47b8a70b8a740db0e97 --- /dev/null +++ b/worker_spaces/verification_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'verification-worker-space' diff --git a/worker_spaces/verification_worker_space/spec.py b/worker_spaces/verification_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..b4c3034773f8d0a6fd1aeaa1f2b6c21725e62d22 --- /dev/null +++ b/worker_spaces/verification_worker_space/spec.py @@ -0,0 +1,18 @@ +SPACE_SPEC = { + "id": "verification-worker-space", + "name": "Verification Worker Space", + "icon": "βœ…", + "color": "#84cc16", + "layer": "Verification + Repair Layer", + "description": "Validate outputs, compare expectations, quality scoring, and mission verification.", + "responsibilities": [ + "validate outputs", + "compare expectations", + "quality scoring", + "mission verification" + ], + "roles": [ + "repair", + "cognition" + ] +} diff --git a/worker_spaces/vision_worker_space/__init__.py b/worker_spaces/vision_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..2589fd9802388f103df0caad3ec06cb412e94204 --- /dev/null +++ b/worker_spaces/vision_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'vision-worker-space' diff --git a/worker_spaces/vision_worker_space/spec.py b/worker_spaces/vision_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..7e09c19b95abb159abedb45ab449febb7307735f --- /dev/null +++ b/worker_spaces/vision_worker_space/spec.py @@ -0,0 +1,19 @@ +SPACE_SPEC = { + "id": "vision-worker-space", + "name": "Vision Worker Space", + "icon": "πŸ‘οΈ", + "color": "#ec4899", + "layer": "Browser + UI Intelligence", + "description": "Screenshot analysis, OCR, layout detection, visual regression, and UI understanding.", + "responsibilities": [ + "screenshot analysis", + "ocr", + "layout detection", + "visual regression", + "ui understanding" + ], + "roles": [ + "visual_intelligence", + "cognition" + ] +} diff --git a/worker_spaces/workflow_worker_space/__init__.py b/worker_spaces/workflow_worker_space/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6246ac9a23aec72e3b9570d7b67d66aa77409206 --- /dev/null +++ b/worker_spaces/workflow_worker_space/__init__.py @@ -0,0 +1 @@ +SPACE_ID = 'workflow-worker-space' diff --git a/worker_spaces/workflow_worker_space/spec.py b/worker_spaces/workflow_worker_space/spec.py new file mode 100644 index 0000000000000000000000000000000000000000..091f7a2e0ca426c2b6f9964601b469e06af3b05d --- /dev/null +++ b/worker_spaces/workflow_worker_space/spec.py @@ -0,0 +1,19 @@ +SPACE_SPEC = { + "id": "workflow-worker-space", + "name": "Workflow Worker Space", + "icon": "🧭", + "color": "#0f766e", + "layer": "Coordination Layer", + "description": "DAG execution, task queues, retries, scheduling, and background jobs.", + "responsibilities": [ + "dag execution", + "task queues", + "retries", + "scheduling", + "background jobs" + ], + "roles": [ + "automation", + "execution" + ] +}