cesjavi commited on
Commit
11d89a2
·
0 Parent(s):

final: mobile fixes and quality relaxation

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +15 -0
  2. .gemini/antigravity/brain/8453b74f-68a6-47ae-887d-1123cb011afb/scratch/verify_supabase.py +10 -0
  3. .gitattributes +35 -0
  4. .gitignore +13 -0
  5. Dockerfile +40 -0
  6. README.md +143 -0
  7. ROADMAP.md +61 -0
  8. SPEC.md +137 -0
  9. VERSION +1 -0
  10. backend/.env.example +19 -0
  11. backend/Dockerfile +32 -0
  12. backend/agents/agent_factory.py +44 -0
  13. backend/agents/amd_agent.py +33 -0
  14. backend/agents/base.py +162 -0
  15. backend/agents/gemini_agent.py +37 -0
  16. backend/agents/groq_agent.py +98 -0
  17. backend/agents/local_agent.py +48 -0
  18. backend/agents/openai_agent.py +28 -0
  19. backend/agents_debug.json +1 -0
  20. backend/api/index.py +1 -0
  21. backend/main.py +102 -0
  22. backend/project_debug.json +1 -0
  23. backend/requirements.txt +19 -0
  24. backend/routers/__init__.py +1 -0
  25. backend/routers/agent_runner.py +151 -0
  26. backend/routers/monitoring.py +70 -0
  27. backend/routers/orchestrator.py +169 -0
  28. backend/scratch/check_db.py +22 -0
  29. backend/scratch/create_comparison_project.py +61 -0
  30. backend/scratch/find_user.py +24 -0
  31. backend/scratch/fix_logs_rls.py +33 -0
  32. backend/services/agent_runner_service.py +210 -0
  33. backend/services/audit_service.py +31 -0
  34. backend/services/config.py +101 -0
  35. backend/services/orchestrator_service.py +773 -0
  36. backend/services/output_quality.py +312 -0
  37. backend/services/project_service.py +35 -0
  38. backend/services/semantic_backprop.py +104 -0
  39. backend/services/supabase_service.py +13 -0
  40. backend/services/task_queue.py +47 -0
  41. backend/tools/browser.py +111 -0
  42. backend/tools/decomposer.py +20 -0
  43. backend/tools/file_generator.py +61 -0
  44. backend/tools/registry.py +260 -0
  45. backend/tools/sandbox.py +39 -0
  46. backend/tools/sre.py +72 -0
  47. backend/tools/visuals.py +48 -0
  48. backend/worker.py +53 -0
  49. database/agent_ownership.sql +42 -0
  50. database/default_agents.sql +36 -0
.dockerignore ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gemini
3
+ .vercel
4
+
5
+ backend/.env
6
+ backend/venv
7
+ backend/__pycache__
8
+ backend/**/__pycache__
9
+ backend/**/*.pyc
10
+
11
+ frontend/.env
12
+ frontend/node_modules
13
+ frontend/dist
14
+
15
+ *.log
.gemini/antigravity/brain/8453b74f-68a6-47ae-887d-1123cb011afb/scratch/verify_supabase.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ sys.path.append(os.path.join(os.getcwd(), 'backend'))
4
+
5
+ try:
6
+ from backend.services.supabase_service import supabase
7
+ res = supabase.table("agents").select("count").execute()
8
+ print(f"Connection successful! Agents count: {res.data}")
9
+ except Exception as e:
10
+ print(f"Error connecting to Supabase: {e}")
.gitattributes ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ backend/.env
2
+ /backend/venv
3
+ /backend/__pycache__
4
+ frontend/.env
5
+ node_modules/
6
+ dist/
7
+ __pycache__/
8
+ *.pyc
9
+
10
+ .vercel
11
+ frontend/ios
12
+ frontend/android
13
+ .DS_Store
Dockerfile ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:22-slim AS frontend-build
2
+
3
+ WORKDIR /app/frontend
4
+
5
+ COPY frontend/package*.json ./
6
+ RUN npm ci
7
+
8
+ COPY frontend ./
9
+
10
+ ARG VITE_API_URL=""
11
+ ARG VITE_SUPABASE_URL=""
12
+ ARG VITE_SUPABASE_ANON_KEY=""
13
+ ARG VITE_SENTRY_DSN=""
14
+
15
+ ENV VITE_API_URL=$VITE_API_URL
16
+ ENV VITE_SUPABASE_URL=$VITE_SUPABASE_URL
17
+ ENV VITE_SUPABASE_ANON_KEY=$VITE_SUPABASE_ANON_KEY
18
+ ENV VITE_SENTRY_DSN=$VITE_SENTRY_DSN
19
+
20
+ RUN npm run build
21
+
22
+ FROM python:3.11-slim
23
+
24
+ ENV PORT=7860
25
+ ENV PYTHONUNBUFFERED=1
26
+
27
+ WORKDIR /app
28
+
29
+ COPY VERSION VERSION
30
+ COPY backend/requirements.txt backend/requirements.txt
31
+ RUN pip install --no-cache-dir -r backend/requirements.txt
32
+
33
+ COPY backend backend
34
+ COPY --from=frontend-build /app/frontend/dist frontend/dist
35
+
36
+ WORKDIR /app/backend
37
+
38
+ EXPOSE 7860
39
+
40
+ CMD ["sh", "-c", "uvicorn main:app --host 0.0.0.0 --port ${PORT:-7860}"]
README.md ADDED
@@ -0,0 +1,143 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Aubm
3
+ sdk: docker
4
+ app_port: 7860
5
+ license: mit
6
+ short_description: Automated Business Machines
7
+ ---
8
+
9
+ # 🤖 Aubm
10
+
11
+ ### Enterprise-Grade AI Agent Orchestration & Collaboration Platform
12
+
13
+ Aubm (Automated Unified Business Machines) is a sophisticated platform designed to orchestrate multiple autonomous AI agents to complete complex projects. Featuring **Human-in-the-Loop** supervision, **Dynamic DAG** task execution, and **Semantic RAG** context injection.
14
+
15
+ ---
16
+
17
+ ## 🚀 Key Features
18
+
19
+ - **Multi-Provider Support**: Seamless integration with OpenAI, AMD (inference.do-ai.run), Groq, Gemini, Qwen, Ollama, and OpenRouter.
20
+ - **Autonomous Orchestration**: Intelligent task prioritization and execution based on dependencies (DAG).
21
+ - **Human-in-the-Loop**: Approval-based workflow ensuring quality and safety.
22
+ - **Semantic Backpropagation**: Context from completed tasks is automatically injected into subsequent tasks.
23
+ - **Real-time Monitoring**: SSE-powered live logs and progress tracking.
24
+ - **Project Wizard**: AI-driven project creation and task decomposition.
25
+ - **Operational Safety**: Automatic recovery of stale runs and comprehensive health monitoring.
26
+
27
+ ---
28
+
29
+ ## 🛠️ Tech Stack
30
+
31
+ - **Frontend**: React + Vite + TypeScript (Styled with Vanilla CSS for maximum performance)
32
+ - **Backend**: FastAPI (Python 3.10+)
33
+ - **Database**: Supabase (Postgres + Auth + Real-time)
34
+ - **Deployment**: Optimized for Vercel (Serverless Backend + Static Frontend)
35
+
36
+ ---
37
+
38
+ ## 🏗️ Project Structure
39
+
40
+ ```bash
41
+ aubm/
42
+ ├── backend/ # FastAPI Application & AI Core
43
+ │ ├── agents/ # LLM Provider Implementations
44
+ │ ├── routers/ # API Endpoints (Runner, Orchestrator)
45
+ │ ├── services/ # Business Logic (Queue, RAG, Guards)
46
+ │ └── main.py # App Entrypoint
47
+ ├── frontend/ # React Application
48
+ │ ├── src/ # Components, Hooks, Context, Services
49
+ │ └── vite.config.ts # Vite Configuration
50
+ └── database/ # Supabase Schema & Migrations
51
+ ```
52
+
53
+ ---
54
+
55
+ ## ⚙️ Getting Started
56
+
57
+ ### 1. Database Setup (Supabase)
58
+ 1. Create a new project in [Supabase](https://supabase.com).
59
+ 2. Go to the **SQL Editor** and execute the content of `backend/schema.sql`.
60
+ 3. Enable **Auth** with your preferred providers (Email/Password by default).
61
+
62
+ ### 2. Backend Installation
63
+ ```bash
64
+ cd backend
65
+ python -m venv venv
66
+ source venv/bin/activate # or venv\Scripts\activate on Windows
67
+ pip install -r requirements.txt
68
+ ```
69
+
70
+ Create a `.env` file in `/backend`:
71
+ ```env
72
+ SUPABASE_URL=your_project_url
73
+ SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
74
+ OPENAI_API_KEY=optional_key
75
+ GROQ_API_KEY=optional_key
76
+ TAVILY_API_KEY=optional_key
77
+ # See SPEC.md for all available providers
78
+ ```
79
+
80
+ Run the server:
81
+ ```bash
82
+ uvicorn main:app --reload --port 8000
83
+ ```
84
+
85
+ ### 3. Frontend Installation
86
+ ```bash
87
+ cd frontend
88
+ npm install
89
+ ```
90
+
91
+ Create a `.env` file in `/frontend`:
92
+ ```env
93
+ VITE_API_URL=http://localhost:8000
94
+ VITE_SUPABASE_URL=your_project_url
95
+ VITE_SUPABASE_ANON_KEY=your_anon_key
96
+ ```
97
+
98
+ Run the development server:
99
+ ```bash
100
+ npm run dev
101
+ ```
102
+
103
+ ---
104
+
105
+ ## 📈 Operational Modes
106
+
107
+ - **Embedded Worker**: Runs the task queue within the FastAPI process (set `TASK_QUEUE_EMBEDDED_WORKER=true`).
108
+ - **Standalone Worker**: For high-load environments, run the worker in a separate process:
109
+ ```bash
110
+ cd backend
111
+ python worker.py
112
+ ```
113
+
114
+ ---
115
+
116
+ ## Hugging Face Spaces
117
+
118
+ This repository is ready to deploy as a Docker Space. Create a Hugging Face Space with SDK `Docker`, then push this repo to the Space remote.
119
+
120
+ Configure these Space secrets or variables:
121
+
122
+ ```env
123
+ SUPABASE_URL=your_project_url
124
+ SUPABASE_SERVICE_ROLE_KEY=your_service_role_key
125
+ SUPABASE_ANON_KEY=your_anon_key
126
+ GROQ_API_KEY=optional_key
127
+ OPENAI_API_KEY=optional_key
128
+ GEMINI_API_KEY=optional_key
129
+ AMD_API_KEY=optional_key
130
+ TAVILY_API_KEY=optional_key
131
+ TASK_QUEUE_EMBEDDED_WORKER=true
132
+ ```
133
+
134
+ `VITE_API_URL` can stay empty on Spaces because the frontend calls the FastAPI backend on the same origin.
135
+
136
+ ---
137
+
138
+ ## 📄 Documentation
139
+
140
+ For detailed technical architecture, refer to:
141
+ - [SPEC.md](./SPEC.md) - Deep technical specifications.
142
+ - [ROADMAP.md](./ROADMAP.md) - Future development goals.
143
+ - [docs/](./docs/) - Extended guides and manuals.
ROADMAP.md ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Aubm Roadmap
2
+
3
+ This document outlines the strategic evolution of Aubm, moving from a robust orchestration core to an enterprise-ready multi-agent operating layer.
4
+
5
+ ## Phase 1: Core Foundation (Completed)
6
+ - [x] Autonomous Agent Execution: Multi-provider support (OpenAI, Groq, Gemini, etc.).
7
+ - [x] Project Orchestration: Intelligent task scheduling and dependency management (DAG).
8
+ - [x] Human-in-the-Loop: Approval and rejection workflows for agent outputs.
9
+ - [x] Semantic RAG: Contextual memory injection across project tasks.
10
+ - [x] Real-time Logs: Streaming agent thoughts and actions via SSE.
11
+ - [x] Cost Control: Token-based budgeting and execution blocking.
12
+
13
+ ## Phase 2: Advanced Collaboration and Tools (Completed)
14
+ - [x] Multi-Agent Debates: Allow agents to cross-verify each other's outputs before human review.
15
+ - [x] Extended Toolbelt:
16
+ - [x] Web Browser Tool (via Playwright) for live data fetching.
17
+ - [x] Code Sandbox for executing and testing generated snippets.
18
+ - [x] File Generation (Excel, Word, and advanced PDF layouts).
19
+ - [x] Collaborative Editing: Real-time collaborative output refining for humans.
20
+ - [x] Mobile Experience: Capacitor-based mobile app for project monitoring (initialized).
21
+
22
+ ## Phase 3: Intelligence and Scale (Completed)
23
+ - [x] Fine-tuning Loop: Feedback loop (Like/Dislike) implemented for data collection.
24
+ - [x] Recursive Project Decomposition: Agents that can spawn sub-tasks and manage them.
25
+ - [x] Enterprise Security:
26
+ - [x] SSO Integration (Google, GitHub via Supabase).
27
+ - [x] Advanced RLS for granular team permissions.
28
+ - [x] Audit logs for every LLM interaction.
29
+ - [x] Agent Marketplace: Community-driven agent templates and specialized skill sets.
30
+
31
+ ## Phase 4: Autonomy and Beyond (Completed)
32
+ - [x] Self-Healing Infrastructure: Agents that can monitor health and apply safe patches.
33
+ - [x] Voice Interaction: Control navigation and hear project/task status updates via browser voice APIs.
34
+ - [x] VR/AR Dashboard: Spatial DAG viewer scaffold for layered project/task visualization.
35
+
36
+ ## Phase 5: Production Operations (Completed)
37
+ - [x] Operations Monitoring: Backend health summary endpoint and frontend monitoring dashboard with Supabase fallback.
38
+ - [x] Deployment Hardening: Dockerized backend/runtime profile and production CORS configuration.
39
+ - [x] Error Tracking: Sentry-compatible error reporting hooks for backend and frontend.
40
+ - [x] Performance Budgeting: Frontend code splitting and bundle-size targets.
41
+
42
+ ## Phase 6: Distributed Scale and Intelligence (In Progress)
43
+ - [x] Recursive Project Decomposition: Agents that can automatically break down goals.
44
+ - [x] Numerical Consistency (Semantic Backprop): Enforce absolute figures across tasks.
45
+ - [x] Visual Tooling: Integrated support for charts and AI illustrations.
46
+ - [x] Vercel Deployment: Monorepo serverless configuration.
47
+ - [x] Heuristic Output Guardrails: Prompt hardening, reviewer checks, and final-report filtering for placeholders, unsupported claims, and low-quality sections.
48
+ - [ ] Asynchronous Task Queue: Dedicated background workers (`worker.py`).
49
+ - [ ] Vectorized Long-term Memory: Cross-project semantic retrieval.
50
+ - [ ] Self-Optimizing Agents: Meta-prompting loops based on human feedback.
51
+
52
+ ## Phase 7: Structured Evidence and Entity Integrity (Next)
53
+ - [ ] Strict JSON Task Schemas: Enforce structured outputs per task type instead of free-form text.
54
+ - [ ] Mandatory `source_url` per Claim: Require evidence links for competitor, pricing, release, benchmark, and market claims.
55
+ - [ ] Entity Normalization Layer: Canonicalize entity names, merge aliases, and separate direct competitors from adjacent tools before final reporting.
56
+ - [ ] Semantic Deduplication: Collapse equivalent claims written differently across tasks.
57
+ - [ ] Evidence-Aware Final Report: Build the final report from normalized entities and validated claims only.
58
+
59
+ ---
60
+
61
+ *Last updated: May 6, 2026*
SPEC.md ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🛠️ Aubm — Technical Specification
2
+
3
+ > **Target Stack**: FastAPI (Python) + React/TypeScript (Vite) + Supabase (Postgres + Auth)
4
+
5
+ This document provides a comprehensive technical blueprint for recreating Aubm.
6
+
7
+ ---
8
+
9
+ ## 1. System Architecture
10
+
11
+ Aubm follows a decoupled architecture with a centralized database (Supabase) acting as the source of truth and coordination layer.
12
+
13
+ ### Directory Structure
14
+ ```
15
+ aubm/
16
+ ├── backend/ # Python 3.10+
17
+ │ ├── main.py # Application entrypoint & CRUD API
18
+ │ ├── worker.py # Standalone task queue worker
19
+ │ ├── schema.sql # Full DDL for Supabase
20
+ │ ├── agents/ # Provider-specific implementations
21
+ │ │ ├── base.py # Abstract BaseAgent class
22
+ │ │ ├── agent_factory.py # Factory for creating agent instances
23
+ │ │ └── {provider}_agent.py
24
+ │ ├── routers/ # Functional endpoint grouping
25
+ │ │ ├── agent_runner.py # Task execution logic
26
+ │ │ └── orchestrator.py # Multi-task project flow
27
+ │ └── services/ # Core business logic
28
+ │ ├── config.py # Configuration management
29
+ │ ├── task_queue.py # Background processing loop
30
+ │ └── semantic_backprop.py # RAG context builder
31
+ ├── frontend/ # React + Vite + TS
32
+ │ ├── src/
33
+ │ │ ├── components/ # UI Modular components
34
+ │ │ ├── services/ # API communication layer
35
+ │ │ ├── context/ # Auth & Global state
36
+ │ │ └── i18n/ # Multi-language support
37
+ │ └── vite.config.ts
38
+ └── database/ # Migrations & Seed data
39
+ ```
40
+
41
+ ---
42
+
43
+ ## 2. Database Schema (Supabase/Postgres)
44
+
45
+ ### Core Tables
46
+
47
+ | Table | Purpose | Key Columns |
48
+ |-------|---------|-------------|
49
+ | `profiles` | User extensions | `id (uuid)`, `role`, `full_name`, `avatar_url` |
50
+ | `projects` | Project containers | `id`, `name`, `description`, `context`, `owner_id`, `status` |
51
+ | `agents` | AI Identities | `id`, `name`, `role`, `api_provider`, `model`, `system_prompt` |
52
+ | `tasks` | Units of work | `id`, `project_id`, `assigned_agent_id`, `status`, `output_data` |
53
+ | `task_runs` | Execution history | `id`, `task_id`, `agent_id`, `status`, `error_message` |
54
+ | `agent_logs` | Execution traces | `id`, `task_id`, `action`, `content`, `metadata` |
55
+ | `app_config` | Global settings | `key`, `value` (JSONB) |
56
+
57
+ ### Status Enums
58
+ - **Tasks**: `todo`, `in_progress`, `awaiting_approval`, `done`, `failed`, `cancelled`.
59
+ - **Task Runs**: `queued`, `running`, `completed`, `failed`, `cancelled`.
60
+ - **Profiles**: `user`, `manager`, `admin`.
61
+
62
+ ---
63
+
64
+ ## 3. Backend Logic
65
+
66
+ ### Agent Execution Flow
67
+ 1. **Request**: `POST /tasks/{id}/run`
68
+ 2. **Initialization**: Fetch task, agent, and project data.
69
+ 3. **Context Building**: `semantic_backprop` fetches outputs from previous tasks in the same project.
70
+ 4. **Agent Factory**: Instantiates the correct `BaseAgent` subclass (e.g., `GroqAgent`).
71
+ 5. **Execution**:
72
+ - LLM call with dynamic prompt.
73
+ - Real-time logging to `agent_logs` via SSE.
74
+ 6. **Guardrails**:
75
+ - `output_cleaner`: Strips markdown artifacts.
76
+ - `language_guard`: Ensures output matches `app_config["output_language"]`.
77
+ 7. **Persistence**: Updates `task.output_data` and sets status to `awaiting_approval`.
78
+
79
+ ### Orchestration Engine
80
+ - Processes a project's task list as a Directed Acyclic Graph (DAG).
81
+ - Respects `is_critical` and `priority` fields.
82
+ - Auto-assigns available agents from the `agents` pool if no agent is pre-assigned.
83
+
84
+ ### Tool System (Phase 2)
85
+ - **Tool Registry**: A central registry where tools are defined and permissioned.
86
+ - **Browser Tool**: Uses Playwright for headless browsing and content extraction.
87
+ - **Sandbox Tool**: Executes code in a restricted environment.
88
+ - **Integration**: Tools are exposed to agents via the OpenAI function-calling/tool-calling schema.
89
+
90
+ ---
91
+
92
+ ## 4. Frontend Design System
93
+
94
+ - **Styling**: Vanilla CSS with modern variables (HSL colors, glassmorphism).
95
+ - **Icons**: Lucide React.
96
+ - **State Management**: React Context + Hooks.
97
+ - **Features**:
98
+ - Kanban Board for task management.
99
+ - Real-time streaming console for agent thoughts.
100
+ - Interactive Project Wizard for quick setup.
101
+ - Analytics dashboard for project performance.
102
+
103
+ ---
104
+
105
+ ## 5. Deployment Guide
106
+
107
+ ### Vercel Integration
108
+ The project is designed to run seamlessly on Vercel:
109
+ - **Frontend**: Standard Vite build.
110
+ - **Backend**: Python Serverless Functions.
111
+ - **Database**: External Supabase instance.
112
+
113
+ ### Local Setup
114
+ 1. **DB**: Apply `schema.sql` to Supabase.
115
+ 2. **Backend**: `pip install -r requirements.txt` & `uvicorn main:app`.
116
+ 3. **Frontend**: `npm install` & `npm run dev`.
117
+
118
+ ---
119
+
120
+ ## 6. Key Dependencies
121
+
122
+ ### Backend
123
+ - `fastapi`, `supabase`, `openai`, `groq`, `google-genai`, `playwright`, `folium`.
124
+
125
+ ### Frontend
126
+ - `react`, `lucide-react`, `framer-motion` (for animations), `i18next`.
127
+
128
+ ---
129
+
130
+ ## 7. Security (RLS)
131
+ - **Projects**: Only visible to owner or if `is_public=true`.
132
+ - **Config**: Only writable by users with `role='admin'`.
133
+ - **Agents**: Writable by `manager` or `admin`.
134
+ - **Tasks**: Protected by project-level RLS.
135
+
136
+ ---
137
+ *End of Specification*
VERSION ADDED
@@ -0,0 +1 @@
 
 
1
+ 0.7.0
backend/.env.example ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Supabase Configuration
2
+ SUPABASE_URL=https://your-project-id.supabase.co
3
+ SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
4
+
5
+ # AI Provider Keys
6
+ OPENAI_API_KEY=your-openai-key
7
+ GROQ_API_KEY=your-groq-key
8
+ GEMINI_API_KEY=your-gemini-key
9
+ ANTHROPIC_API_KEY=your-anthropic-key
10
+ TAVILY_API_KEY=your-tavily-key
11
+
12
+ # App Settings
13
+ PORT=8000
14
+ ALLOWED_ORIGINS=http://localhost:5173,https://your-app.vercel.app
15
+ TASK_QUEUE_EMBEDDED_WORKER=true
16
+ OUTPUT_LANGUAGE=en
17
+
18
+ # Error Tracking
19
+ SENTRY_DSN=your-sentry-dsn
backend/Dockerfile ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Build stage for backend
2
+ FROM python:3.11-slim
3
+
4
+ # Set environment variables
5
+ ENV PYTHONDONTWRITEBYTECODE=1
6
+ ENV PYTHONUNBUFFERED=1
7
+
8
+ # Set work directory
9
+ WORKDIR /app
10
+
11
+ # Install system dependencies
12
+ RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ build-essential \
14
+ libpq-dev \
15
+ curl \
16
+ && rm -rf /var/lib/apt/lists/*
17
+
18
+ # Install python dependencies
19
+ COPY requirements.txt .
20
+ RUN pip install --no-cache-dir -r requirements.txt
21
+
22
+ # Install Playwright browsers and their dependencies
23
+ RUN playwright install --with-deps chromium
24
+
25
+ # Copy project
26
+ COPY . .
27
+
28
+ # Expose port
29
+ EXPOSE 8000
30
+
31
+ # Run the application
32
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
backend/agents/agent_factory.py ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Type
2
+ from .base import BaseAgent
3
+ from .openai_agent import OpenAIAgent
4
+ from .amd_agent import AMDAgent
5
+ from .groq_agent import GroqAgent
6
+ from .gemini_agent import GeminiAgent
7
+ from .local_agent import LocalAgent
8
+ from services.config import settings
9
+
10
+ # Map of providers to their respective classes
11
+ PROVIDER_MAP: Dict[str, Type[BaseAgent]] = {
12
+ "openai": OpenAIAgent,
13
+ "amd": AMDAgent,
14
+ "groq": GroqAgent,
15
+ "gemini": GeminiAgent,
16
+ "local": LocalAgent,
17
+ "ollama": LocalAgent
18
+ }
19
+
20
+ class AgentFactory:
21
+ @staticmethod
22
+ def get_agent(provider: str, name: str, role: str, model: str, system_prompt: str = None) -> BaseAgent:
23
+ """
24
+ Instantiates the appropriate agent based on the provider string.
25
+ Includes a fallback to Groq if OpenAI is requested but no key is provided.
26
+ """
27
+ provider = provider.lower()
28
+
29
+ # Groq Redirection Logic
30
+ if provider == "openai" and not settings.OPENAI_API_KEY:
31
+ # Check if we have a Groq key before redirecting
32
+ if settings.GROQ_API_KEY:
33
+ provider = "groq"
34
+ model = "llama-3.3-70b-versatile" # Robust fallback model
35
+ else:
36
+ # If neither is available, let it fail with the original provider
37
+ pass
38
+
39
+ agent_class = PROVIDER_MAP.get(provider)
40
+
41
+ if not agent_class:
42
+ raise ValueError(f"Unsupported agent provider: {provider}")
43
+
44
+ return agent_class(name=name, role=role, model=model, system_prompt=system_prompt)
backend/agents/amd_agent.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .base import BaseAgent
2
+ from typing import Dict, Any, List
3
+ import openai
4
+ from services.config import settings, config_service
5
+
6
+ class AMDAgent(BaseAgent):
7
+ """
8
+ Agent implementation for AMD Inference (inference.do-ai.run).
9
+ Compatible with OpenAI's API format.
10
+ """
11
+ def __init__(self, name: str, role: str, model: str = "gpt-4o", system_prompt: str = None):
12
+ super().__init__(name, role, model, system_prompt)
13
+
14
+ self.provider_config = config_service.get_provider_config("amd")
15
+ api_key = self.provider_config.get("api_key") or settings.AMD_API_KEY
16
+
17
+ self.client = openai.AsyncOpenAI(
18
+ api_key=api_key,
19
+ base_url=self.provider_config.get("base_url", "https://inference.do-ai.run/v1")
20
+ )
21
+ self.temperature = self.provider_config.get("temperature", 0.7)
22
+ self.max_tokens = self.provider_config.get("max_tokens", 4096)
23
+
24
+ async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
25
+ return await self._run_openai_compatible(
26
+ provider="amd",
27
+ create_fn=self.client.chat.completions.create,
28
+ task_description=task_description,
29
+ context=context,
30
+ use_tools=use_tools,
31
+ extra_context=extra_context,
32
+ response_format={"type": "json_object"}
33
+ )
backend/agents/base.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, Any, List, Optional
3
+ import json
4
+
5
+ class BaseAgent(ABC):
6
+ def __init__(self, name: str, role: str, model: str, system_prompt: Optional[str] = None):
7
+ self.name = name
8
+ self.role = role
9
+ self.model = model
10
+ self.system_prompt = system_prompt or f"You are {name}, acting as a {role}."
11
+
12
+ @abstractmethod
13
+ async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
14
+ """
15
+ Executes a task given its description and previous context.
16
+ Returns a dictionary containing the output data.
17
+ """
18
+ pass
19
+
20
+ def _format_context(self, context: List[Dict[str, Any]]) -> str:
21
+ """Helper to format previous task outputs for the current agent."""
22
+ if not context:
23
+ return "No previous context available."
24
+
25
+ formatted = "Previous tasks context:\n"
26
+ for item in context:
27
+ formatted += f"- Task: {item.get('title')}\n Output: {json.dumps(item.get('output_data', {}))}\n"
28
+ return formatted
29
+
30
+ def _build_json_prompt(self, task_description: str, context: List[Dict[str, Any]], extra_context: str = "") -> str:
31
+ return f"""
32
+ Task: {task_description}
33
+
34
+ {self._format_context(context)}
35
+
36
+ {extra_context}
37
+
38
+ Please provide your output as a JSON object.
39
+ """
40
+
41
+ def _build_chat_messages(self, task_description: str, context: List[Dict[str, Any]], extra_context: str = "") -> List[Dict[str, Any]]:
42
+ return [
43
+ {"role": "system", "content": self.system_prompt},
44
+ {"role": "user", "content": self._build_json_prompt(task_description, context, extra_context)}
45
+ ]
46
+
47
+ def _parse_json_output(self, content: str) -> Any:
48
+ """Parse strict JSON first, then tolerate fenced or prefixed JSON."""
49
+ if not content:
50
+ return {}
51
+
52
+ try:
53
+ return json.loads(content)
54
+ except json.JSONDecodeError:
55
+ pass
56
+
57
+ try:
58
+ if "```json" in content:
59
+ clean = content.split("```json", 1)[1].split("```", 1)[0].strip()
60
+ elif "```" in content:
61
+ clean = content.split("```", 1)[1].split("```", 1)[0].strip()
62
+ else:
63
+ object_start, array_start = content.find("{"), content.find("[")
64
+ starts = [index for index in (object_start, array_start) if index != -1]
65
+ start = min(starts) if starts else -1
66
+ if start == array_start:
67
+ end = content.rfind("]")
68
+ else:
69
+ end = content.rfind("}")
70
+ clean = content[start:end + 1] if start != -1 and end != -1 else content
71
+ return json.loads(clean)
72
+ except Exception:
73
+ return {"raw_text": content}
74
+
75
+ def _parse_tool_arguments(self, arguments: str | None) -> Dict[str, Any]:
76
+ parsed = self._parse_json_output(arguments or "{}")
77
+ return parsed if isinstance(parsed, dict) else {}
78
+
79
+ async def _append_tool_results(self, messages: List[Dict[str, Any]], tool_calls: Any, tool_registry: Any) -> None:
80
+ for tool_call in tool_calls or []:
81
+ tool_name = tool_call.function.name
82
+ tool_args = self._parse_tool_arguments(tool_call.function.arguments)
83
+ tool_result = await tool_registry.call_tool(tool_name, tool_args)
84
+
85
+ messages.append({
86
+ "tool_call_id": tool_call.id,
87
+ "role": "tool",
88
+ "name": tool_name,
89
+ "content": str(tool_result),
90
+ })
91
+
92
+ async def _run_openai_compatible(
93
+ self,
94
+ provider: str,
95
+ create_fn,
96
+ task_description: str,
97
+ context: List[Dict[str, Any]],
98
+ use_tools: bool = False,
99
+ extra_context: str = "",
100
+ **extra_kwargs
101
+ ) -> Dict[str, Any]:
102
+ """
103
+ Unified runner for OpenAI-compatible APIs (OpenAI, Groq, etc.)
104
+ """
105
+ from tools.registry import tool_registry
106
+
107
+ messages = self._build_chat_messages(task_description, context, extra_context)
108
+
109
+ is_reasoning_model = "gpt-oss-" in self.model or self.model.startswith("o1-") or self.model.startswith("o3-")
110
+
111
+ kwargs = {
112
+ "model": self.model,
113
+ "messages": messages,
114
+ **extra_kwargs
115
+ }
116
+
117
+ # Handle temperature/max_tokens based on model type
118
+ if is_reasoning_model:
119
+ # Reasoning models prefer temperature 1.0 or none
120
+ kwargs["temperature"] = extra_kwargs.get("temperature", 1.0)
121
+ # Use max_completion_tokens if provided, otherwise default to max_tokens logic but renamed
122
+ if "max_completion_tokens" not in kwargs:
123
+ kwargs["max_completion_tokens"] = getattr(self, "max_tokens", 4096)
124
+ # Standard max_tokens is often forbidden in reasoning models
125
+ kwargs.pop("max_tokens", None)
126
+ else:
127
+ kwargs["temperature"] = getattr(self, "temperature", 0.7)
128
+ kwargs["max_tokens"] = getattr(self, "max_tokens", 4096)
129
+
130
+ if use_tools:
131
+ # Note: Many reasoning models don't support tools yet, but we'll include if requested
132
+ kwargs["tools"] = tool_registry.get_tool_definitions()
133
+ kwargs["tool_choice"] = "auto"
134
+
135
+ response = await create_fn(**kwargs)
136
+ message = response.choices[0].message
137
+
138
+ # Handle tool calls
139
+ if message.tool_calls:
140
+ messages.append(message)
141
+ await self._append_tool_results(messages, message.tool_calls, tool_registry)
142
+
143
+ # Second call after tool execution
144
+ # Remove tools from second call to force a final answer
145
+ kwargs.pop("tools", None)
146
+ kwargs.pop("tool_choice", None)
147
+
148
+ final_response = await create_fn(**kwargs)
149
+ content = final_response.choices[0].message.content
150
+ else:
151
+ content = message.content
152
+
153
+ return self._result(provider, content or "")
154
+
155
+ def _result(self, provider: str, content: str) -> Dict[str, Any]:
156
+ return {
157
+ "agent_name": self.name,
158
+ "provider": provider,
159
+ "model": self.model,
160
+ "raw_output": content,
161
+ "data": self._parse_json_output(content)
162
+ }
backend/agents/gemini_agent.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .base import BaseAgent
2
+ from typing import Dict, Any, List
3
+ from google import genai
4
+ from services.config import settings, config_service
5
+
6
+ class GeminiAgent(BaseAgent):
7
+ """
8
+ Agent implementation for Google Gemini using the new google-genai SDK.
9
+ """
10
+ def __init__(self, name: str, role: str, model: str = "gemini-2.0-flash", system_prompt: str = None):
11
+ super().__init__(name, role, model, system_prompt)
12
+
13
+ # Load dynamic config
14
+ self.provider_config = config_service.get_provider_config("gemini")
15
+ api_key = self.provider_config.get("api_key") or settings.GEMINI_API_KEY
16
+
17
+ self.client = genai.Client(api_key=api_key)
18
+ self.temperature = self.provider_config.get("temperature", 0.7)
19
+
20
+ async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
21
+ full_prompt = f"""
22
+ System Instruction: {self.system_prompt}
23
+
24
+ {self._build_json_prompt(task_description, context, extra_context)}
25
+ """
26
+
27
+ # Gemini 2.0 Flash is very fast.
28
+ response = await self.client.aio.models.generate(
29
+ model=self.model,
30
+ contents=full_prompt,
31
+ config={
32
+ "temperature": self.temperature,
33
+ "response_mime_type": "application/json",
34
+ }
35
+ )
36
+
37
+ return self._result("gemini", response.text or "")
backend/agents/groq_agent.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from .base import BaseAgent
3
+ from typing import Dict, Any, List
4
+ import groq
5
+ import json
6
+ from services.config import settings, config_service
7
+ from tools.registry import tool_registry
8
+
9
+ logger = logging.getLogger("uvicorn")
10
+
11
+ GROQ_ROTATION_POOL = [
12
+ "llama-3.3-70b-versatile",
13
+ "openai/gpt-oss-120b",
14
+ "meta-llama/llama-4-scout-17b-16e-instruct",
15
+ "qwen/qwen3-32b",
16
+ "openai/gpt-oss-20b",
17
+ "groq/compound",
18
+ "llama-3.1-8b-instant"
19
+ ]
20
+
21
+ class GroqAgent(BaseAgent):
22
+ """
23
+ Agent implementation for Groq with automatic model rotation for rate limits.
24
+ """
25
+ def __init__(self, name: str, role: str, model: str = "llama-3.3-70b-versatile", system_prompt: str = None):
26
+ # Auto-migrate decommissioned models
27
+ if "llama-3.1-70b" in model:
28
+ model = "llama-3.3-70b-versatile"
29
+
30
+ super().__init__(name, role, model, system_prompt)
31
+
32
+ # Load dynamic config
33
+ self.provider_config = config_service.get_provider_config("groq")
34
+ api_key = self.provider_config.get("api_key") or settings.GROQ_API_KEY
35
+
36
+ self.client = groq.AsyncGroq(api_key=api_key)
37
+ self.temperature = self.provider_config.get("temperature", 0.7)
38
+ self.max_tokens = self.provider_config.get("max_tokens", 4096)
39
+ self.reasoning_effort = self.provider_config.get("reasoning_effort", "medium")
40
+
41
+ def _format_context(self, context: List[Dict[str, Any]]) -> str:
42
+ """Extremely aggressive truncation for Groq TPM limits."""
43
+ if not context:
44
+ return "No previous context available."
45
+
46
+ # Only take the last 3 tasks to save tokens
47
+ recent_context = context[-3:]
48
+
49
+ formatted = "Previous tasks context (EXTREMELY TRUNCATED for Groq):\n"
50
+ for item in recent_context:
51
+ output_raw = json.dumps(item.get('output_data', {}))
52
+ # 800 chars is roughly 200 tokens.
53
+ if len(output_raw) > 800:
54
+ output_raw = output_raw[:800] + "... [TRUNCATED]"
55
+
56
+ formatted += f"- Task: {item.get('title')}\n Output: {output_raw}\n"
57
+ return formatted
58
+
59
+ async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
60
+ # Very limited semantic context
61
+ if len(extra_context) > 1000:
62
+ extra_context = extra_context[:1000] + "... [TRUNCATED]"
63
+
64
+ try:
65
+ return await self._execute_run(task_description, context, use_tools, extra_context)
66
+ except groq.RateLimitError as e:
67
+ logger.warning(f"Rate limit reached for {self.model} (429). Attempting model rotation...")
68
+
69
+ # Find current model index in pool
70
+ try:
71
+ current_idx = GROQ_ROTATION_POOL.index(self.model)
72
+ except ValueError:
73
+ current_idx = -1
74
+
75
+ # Try the next model in the pool
76
+ next_idx = (current_idx + 1) % len(GROQ_ROTATION_POOL)
77
+ fallback_model = GROQ_ROTATION_POOL[next_idx]
78
+
79
+ logger.info(f"Rotating from {self.model} to {fallback_model}")
80
+ self.model = fallback_model
81
+
82
+ # Retry once with fallback model
83
+ return await self._execute_run(task_description, context, use_tools, extra_context)
84
+
85
+ async def _execute_run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
86
+ extra_kwargs = {}
87
+ if "gpt-oss-" in self.model:
88
+ extra_kwargs["reasoning_effort"] = self.reasoning_effort
89
+
90
+ return await self._run_openai_compatible(
91
+ provider="groq",
92
+ create_fn=self.client.chat.completions.create,
93
+ task_description=task_description,
94
+ context=context,
95
+ use_tools=use_tools,
96
+ extra_context=extra_context,
97
+ **extra_kwargs
98
+ )
backend/agents/local_agent.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .base import BaseAgent
2
+ from typing import Dict, Any, List
3
+ import httpx
4
+ from services.config import config_service
5
+
6
+ class LocalAgent(BaseAgent):
7
+ """
8
+ Agent implementation for Local LLMs (Ollama).
9
+ """
10
+ def __init__(self, name: str, role: str, model: str = "llama3.1:8b", system_prompt: str = None):
11
+ super().__init__(name, role, model, system_prompt)
12
+
13
+ # Load dynamic config
14
+ self.provider_config = config_service.get_provider_config("ollama")
15
+ self.base_url = self.provider_config.get("base_url", "http://localhost:11434")
16
+ self.temperature = self.provider_config.get("temperature", 0.7)
17
+
18
+ async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
19
+ full_prompt = f"""
20
+ System Instructions: {self.system_prompt}
21
+
22
+ {self._build_json_prompt(task_description, context, extra_context)}
23
+ """
24
+
25
+ async with httpx.AsyncClient(timeout=60.0) as client:
26
+ try:
27
+ response = await client.post(
28
+ f"{self.base_url}/api/generate",
29
+ json={
30
+ "model": self.model,
31
+ "prompt": full_prompt,
32
+ "stream": False,
33
+ "format": "json",
34
+ "options": {
35
+ "temperature": self.temperature
36
+ }
37
+ }
38
+ )
39
+ response.raise_for_status()
40
+ result = response.json()
41
+ return self._result("local", result.get("response", "{}"))
42
+ except Exception as e:
43
+ return {
44
+ "agent_name": self.name,
45
+ "provider": "local",
46
+ "status": "error",
47
+ "error": f"Ollama connection failed: {str(e)}"
48
+ }
backend/agents/openai_agent.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .base import BaseAgent
2
+ from typing import Dict, Any, List
3
+ import openai
4
+ from services.config import settings, config_service
5
+ from tools.registry import tool_registry
6
+
7
+ class OpenAIAgent(BaseAgent):
8
+ def __init__(self, name: str, role: str, model: str = "gpt-4o", system_prompt: str = None):
9
+ super().__init__(name, role, model, system_prompt)
10
+
11
+ # Load dynamic config
12
+ self.provider_config = config_service.get_provider_config("openai")
13
+ api_key = self.provider_config.get("api_key") or settings.OPENAI_API_KEY
14
+
15
+ self.client = openai.AsyncOpenAI(api_key=api_key)
16
+ self.temperature = self.provider_config.get("temperature", 0.7)
17
+ self.max_tokens = self.provider_config.get("max_tokens", 4096)
18
+
19
+ async def run(self, task_description: str, context: List[Dict[str, Any]], use_tools: bool = False, extra_context: str = "") -> Dict[str, Any]:
20
+ return await self._run_openai_compatible(
21
+ provider="openai",
22
+ create_fn=self.client.chat.completions.create,
23
+ task_description=task_description,
24
+ context=context,
25
+ use_tools=use_tools,
26
+ extra_context=extra_context,
27
+ response_format={"type": "json_object"}
28
+ )
backend/agents_debug.json ADDED
@@ -0,0 +1 @@
 
 
1
+ [{"id": "297ef087-89af-4e3b-8d80-c5c5c7499e3b", "name": "GPT-4o", "role": "General Intelligence", "api_provider": "openai", "model": "gpt-4o", "system_prompt": "You are a highly capable AI assistant.", "created_at": "2026-05-04T14:32:03.072888+00:00", "updated_at": "2026-05-04T14:32:03.072888+00:00", "user_id": null}, {"id": "64aebf0c-625a-4c6f-895d-a36274c4f9fd", "name": "AMD-4o", "role": "Performance Specialist", "api_provider": "amd", "model": "gpt-4o", "system_prompt": "You are a high-performance agent running on AMD infrastructure.", "created_at": "2026-05-04T14:32:03.072888+00:00", "updated_at": "2026-05-04T14:32:03.072888+00:00", "user_id": null}, {"id": "f7cc5a82-1e7d-4f21-9855-922fb82cd6f9", "name": "Llama-3-70B", "role": "Fast Logic", "api_provider": "groq", "model": "llama3-70b-8192", "system_prompt": "You are a fast and efficient reasoning agent.", "created_at": "2026-05-04T14:32:03.072888+00:00", "updated_at": "2026-05-04T14:32:03.072888+00:00", "user_id": null}, {"id": "1d988b3c-38c1-4132-85e1-82e7a4bc4f8a", "name": "Growth Hacker", "role": "Marketing Expert", "api_provider": "openai", "model": "gpt-4o", "system_prompt": "You are a Growth Hacker focused on low-cost, high-impact strategies.", "created_at": "2026-05-04T15:50:52.628388+00:00", "updated_at": "2026-05-04T15:50:52.628388+00:00", "user_id": "483025be-ca4b-4de3-aa80-9eb39dbd3578"}, {"id": "7b056c90-f4a6-4629-81b1-f4316b76d091", "name": "Growth Hacker", "role": "Marketing Expert", "api_provider": "openai", "model": "gpt-4o", "system_prompt": "You are a Growth Hacker focused on low-cost, high-impact strategies.", "created_at": "2026-05-04T16:22:10.993123+00:00", "updated_at": "2026-05-04T16:22:10.993123+00:00", "user_id": "483025be-ca4b-4de3-aa80-9eb39dbd3578"}, {"id": "138d7d29-b2c0-4ffb-b89f-1a0965dca6b6", "name": "Planner", "role": "Project Planner", "api_provider": "openai", "model": "gpt-4o", "system_prompt": "You decompose goals into clear, ordered implementation tasks.", "created_at": "2026-05-04T18:06:05.280562+00:00", "updated_at": "2026-05-04T18:06:05.280562+00:00", "user_id": "483025be-ca4b-4de3-aa80-9eb39dbd3578"}, {"id": "edfc99cf-a70c-42e0-a2f6-4508bb6aac33", "name": "Builder", "role": "Implementation Agent", "api_provider": "openai", "model": "gpt-4o", "system_prompt": "You implement practical, production-oriented solutions with concise output.", "created_at": "2026-05-04T18:06:05.280562+00:00", "updated_at": "2026-05-04T18:06:05.280562+00:00", "user_id": "483025be-ca4b-4de3-aa80-9eb39dbd3578"}, {"id": "5ef6c6f3-fc4a-40ba-abe8-7630fefcece2", "name": "Reviewer", "role": "Quality Reviewer", "api_provider": "openai", "model": "gpt-4o", "system_prompt": "You review outputs for correctness, security, completeness, and missing tests.", "created_at": "2026-05-04T18:06:05.280562+00:00", "updated_at": "2026-05-04T18:06:05.280562+00:00", "user_id": "483025be-ca4b-4de3-aa80-9eb39dbd3578"}]
backend/api/index.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from main import app
backend/main.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import FileResponse, Response
4
+ import os
5
+ import json
6
+ from pathlib import Path
7
+ from dotenv import load_dotenv
8
+ import sentry_sdk
9
+
10
+
11
+ def _load_app_version() -> str:
12
+ version_file = Path(__file__).resolve().parent.parent / "VERSION"
13
+ if version_file.exists():
14
+ value = version_file.read_text(encoding="utf-8").strip()
15
+ if value:
16
+ return value
17
+ return os.getenv("APP_VERSION", "0.7.0")
18
+
19
+
20
+ # Load environment variables
21
+ load_dotenv()
22
+ FRONTEND_DIST = Path(__file__).resolve().parent.parent / "frontend" / "dist"
23
+ APP_VERSION = _load_app_version()
24
+
25
+ # Sentry Initialization
26
+ SENTRY_DSN = os.getenv("SENTRY_DSN")
27
+ if SENTRY_DSN:
28
+ sentry_sdk.init(
29
+ dsn=SENTRY_DSN,
30
+ traces_sample_rate=1.0,
31
+ profiles_sample_rate=1.0,
32
+ )
33
+
34
+ app = FastAPI(
35
+ title="Aubm API",
36
+ description="Enterprise-Grade AI Agent Orchestration & Collaboration Platform",
37
+ version=APP_VERSION
38
+ )
39
+
40
+ # CORS Configuration
41
+ allowed_origins = os.getenv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173").split(",")
42
+ app.add_middleware(
43
+ CORSMiddleware,
44
+ allow_origins=allowed_origins if allowed_origins != ["*"] else ["*"],
45
+ allow_origin_regex=os.getenv("ALLOWED_ORIGIN_REGEX"),
46
+ allow_credentials=True,
47
+ allow_methods=["*"],
48
+ allow_headers=["*"],
49
+ )
50
+
51
+ @app.get("/")
52
+ async def root():
53
+ index_path = FRONTEND_DIST / "index.html"
54
+ if index_path.exists():
55
+ return FileResponse(index_path)
56
+
57
+ return {
58
+ "status": "online",
59
+ "message": "Aubm API is operational",
60
+ "version": APP_VERSION
61
+ }
62
+
63
+ # Placeholder for routers
64
+ from routers import agent_runner, orchestrator, monitoring
65
+
66
+ app.include_router(agent_runner.router, prefix="/tasks", tags=["Tasks"])
67
+ app.include_router(orchestrator.router, prefix="/orchestrator", tags=["Orchestration"])
68
+ app.include_router(monitoring.router, prefix="/monitoring", tags=["Monitoring"])
69
+
70
+ @app.get("/runtime-config.js", include_in_schema=False)
71
+ async def runtime_config():
72
+ config = {
73
+ "apiUrl": os.getenv("VITE_API_URL", ""),
74
+ "supabaseUrl": os.getenv("VITE_SUPABASE_URL", os.getenv("SUPABASE_URL", "")),
75
+ "supabaseAnonKey": os.getenv("VITE_SUPABASE_ANON_KEY", os.getenv("SUPABASE_ANON_KEY", "")),
76
+ "sentryDsn": os.getenv("VITE_SENTRY_DSN", os.getenv("SENTRY_DSN", "")),
77
+ "appVersion": APP_VERSION,
78
+ }
79
+ return Response(
80
+ content=f"window.__AUBM_CONFIG__ = {json.dumps(config)};",
81
+ media_type="application/javascript",
82
+ )
83
+
84
+ @app.get("/{path:path}", include_in_schema=False)
85
+ async def serve_frontend(path: str):
86
+ if not FRONTEND_DIST.exists():
87
+ return await root()
88
+
89
+ requested_path = FRONTEND_DIST / path
90
+ if requested_path.is_file():
91
+ return FileResponse(requested_path)
92
+
93
+ index_path = FRONTEND_DIST / "index.html"
94
+ if index_path.exists():
95
+ return FileResponse(index_path)
96
+
97
+ return await root()
98
+
99
+ if __name__ == "__main__":
100
+ import uvicorn
101
+ from services.config import settings
102
+ uvicorn.run("main:app", host="0.0.0.0", port=settings.PORT, reload=True)
backend/project_debug.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"id": "53f39562-09aa-447b-b251-3844e1415d4c", "name": "Commercial analysis", "description": "I want to evaluate differents applications similar to this app", "context": "search in internet", "owner_id": "483025be-ca4b-4de3-aa80-9eb39dbd3578", "status": "active", "is_public": true, "created_at": "2026-05-04T16:38:03.872534+00:00", "updated_at": "2026-05-04T16:38:03.872534+00:00", "team_id": null}
backend/requirements.txt ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ supabase
4
+ openai
5
+ groq
6
+ google-genai
7
+ playwright
8
+ folium
9
+ python-dotenv
10
+ pydantic
11
+ pydantic-settings
12
+ httpx
13
+ jinja2
14
+ python-multipart
15
+ reportlab
16
+ pandas
17
+ openpyxl
18
+ psutil
19
+ sentry-sdk[fastapi]
backend/routers/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Routers package
backend/routers/agent_runner.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, BackgroundTasks
2
+ from services.supabase_service import supabase
3
+ from services.agent_runner_service import AgentRunnerService
4
+ from services.output_quality import report_text_from_output
5
+ import logging
6
+
7
+ router = APIRouter()
8
+ logger = logging.getLogger("uvicorn")
9
+
10
+
11
+ def _assert_task_quality(task: dict):
12
+ output_data = task.get("output_data") or {}
13
+ if not isinstance(output_data, dict):
14
+ raise HTTPException(status_code=400, detail="Task output is missing or malformed.")
15
+ if output_data.get("error"):
16
+ raise HTTPException(status_code=400, detail=f"Task execution failed: {output_data['error']}")
17
+ rendered = report_text_from_output(output_data).strip()
18
+ if not rendered or rendered in ("{}", "[]"):
19
+ raise HTTPException(status_code=400, detail="Task has no usable output to approve.")
20
+ quality_review = output_data.get("quality_review")
21
+ if not quality_review:
22
+ raise HTTPException(status_code=400, detail="Task output is missing quality validation.")
23
+ if quality_review.get("approved"):
24
+ return
25
+ reasons = quality_review.get("fail_reasons") or ["Task output failed quality validation."]
26
+ raise HTTPException(status_code=400, detail=f"Task output failed quality review: {'; '.join(reasons)}")
27
+
28
+ def update_task_status(task_id: str, status: str):
29
+ result = (
30
+ supabase.table("tasks")
31
+ .update({"status": status})
32
+ .eq("id", task_id)
33
+ .execute()
34
+ )
35
+ if not result.data:
36
+ raise HTTPException(status_code=404, detail="Task not found or status was not updated")
37
+
38
+ task_data = result.data[0]
39
+
40
+ project_id = task_data.get("project_id")
41
+ if project_id:
42
+ task_result = (
43
+ supabase.table("tasks")
44
+ .select("id,status")
45
+ .eq("project_id", project_id)
46
+ .execute()
47
+ )
48
+ tasks = task_result.data or []
49
+ if status == "done" and tasks and all(task.get("status") == "done" for task in tasks):
50
+ supabase.table("projects").update({"status": "completed"}).eq("id", project_id).execute()
51
+ elif status != "done":
52
+ supabase.table("projects").update({"status": "active"}).eq("id", project_id).execute()
53
+
54
+ return task_data
55
+
56
+ @router.post("/{task_id}/run")
57
+ async def run_task(task_id: str, background_tasks: BackgroundTasks):
58
+ """
59
+ Triggers the execution of a specific task.
60
+ """
61
+ # 1. Fetch task data
62
+ task_res = supabase.table("tasks").select("*, project:projects(*)").eq("id", task_id).single().execute()
63
+ if not task_res.data:
64
+ raise HTTPException(status_code=404, detail="Task not found")
65
+
66
+ task = task_res.data
67
+
68
+ # 2. Check if agent is assigned
69
+ agent_id = task.get("assigned_agent_id")
70
+ if not agent_id:
71
+ raise HTTPException(status_code=400, detail="No agent assigned to this task")
72
+
73
+ # 3. Fetch agent data
74
+ agent_res = supabase.table("agents").select("*").eq("id", agent_id).single().execute()
75
+ if not agent_res.data:
76
+ raise HTTPException(status_code=404, detail="Assigned agent not found")
77
+
78
+ agent_data = agent_res.data
79
+
80
+ # 4. Update task status to in_progress
81
+ supabase.table("tasks").update({"status": "in_progress"}).eq("id", task_id).execute()
82
+
83
+ # 5. Run in background
84
+ background_tasks.add_task(AgentRunnerService.execute_agent_logic, task, agent_data)
85
+
86
+ return {"message": "Task execution started", "task_id": task_id}
87
+
88
+ @router.post("/{task_id}/approve")
89
+ async def approve_task(task_id: str):
90
+ task_res = supabase.table("tasks").select("*").eq("id", task_id).single().execute()
91
+ if not task_res.data:
92
+ raise HTTPException(status_code=404, detail="Task not found")
93
+ _assert_task_quality(task_res.data)
94
+ task = update_task_status(task_id, "done")
95
+ return {"message": "Task approved", "task": task}
96
+
97
+ @router.post("/{task_id}/reject")
98
+ async def reject_task(task_id: str):
99
+ task = update_task_status(task_id, "todo")
100
+ return {"message": "Task rejected", "task": task}
101
+ @router.post("/project/{project_id}/approve-all")
102
+ async def approve_all_tasks(project_id: str):
103
+ """
104
+ Approves all tasks in a project that are awaiting approval.
105
+ """
106
+ waiting_tasks = (
107
+ supabase.table("tasks")
108
+ .select("*")
109
+ .eq("project_id", project_id)
110
+ .eq("status", "awaiting_approval")
111
+ .execute()
112
+ .data
113
+ or []
114
+ )
115
+ blocked = []
116
+ approvable_ids = []
117
+ for task in waiting_tasks:
118
+ try:
119
+ _assert_task_quality(task)
120
+ approvable_ids.append(task["id"])
121
+ except HTTPException:
122
+ blocked.append(task["title"])
123
+
124
+ # 1. Update tasks
125
+ result_data = []
126
+ if approvable_ids:
127
+ result = (
128
+ supabase.table("tasks")
129
+ .update({"status": "done"})
130
+ .eq("project_id", project_id)
131
+ .in_("id", approvable_ids)
132
+ .execute()
133
+ )
134
+ result_data = result.data or []
135
+
136
+ # 2. Check if all tasks in project are now done
137
+ task_result = (
138
+ supabase.table("tasks")
139
+ .select("status")
140
+ .eq("project_id", project_id)
141
+ .execute()
142
+ )
143
+ tasks = task_result.data or []
144
+ if tasks and all(task.get("status") == "done" for task in tasks):
145
+ supabase.table("projects").update({"status": "completed"}).eq("id", project_id).execute()
146
+
147
+ return {
148
+ "message": f"Approved {len(result_data)} tasks",
149
+ "count": len(result_data),
150
+ "blocked": blocked
151
+ }
backend/routers/monitoring.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime, timezone
2
+ from fastapi import APIRouter
3
+ from services.supabase_service import supabase
4
+
5
+ router = APIRouter()
6
+
7
+
8
+ def _count_table(table_name: str) -> int:
9
+ response = supabase.table(table_name).select("id", count="exact").limit(1).execute()
10
+ return response.count or 0
11
+
12
+
13
+ @router.get("/summary")
14
+ async def monitoring_summary():
15
+ """
16
+ Lightweight operational summary for dashboards and uptime checks.
17
+ """
18
+ checks = {
19
+ "api": "ok",
20
+ "database": "ok",
21
+ }
22
+
23
+ counts = {
24
+ "projects": 0,
25
+ "tasks": 0,
26
+ "agents": 0,
27
+ "task_runs": 0,
28
+ "failed_tasks": 0,
29
+ "pending_reviews": 0,
30
+ }
31
+
32
+ try:
33
+ counts["projects"] = _count_table("projects")
34
+ counts["tasks"] = _count_table("tasks")
35
+ counts["agents"] = _count_table("agents")
36
+ counts["task_runs"] = _count_table("task_runs")
37
+ counts["failed_tasks"] = (
38
+ supabase.table("tasks")
39
+ .select("id", count="exact")
40
+ .eq("status", "failed")
41
+ .limit(1)
42
+ .execute()
43
+ .count
44
+ or 0
45
+ )
46
+ counts["pending_reviews"] = (
47
+ supabase.table("tasks")
48
+ .select("id", count="exact")
49
+ .eq("status", "awaiting_approval")
50
+ .limit(1)
51
+ .execute()
52
+ .count
53
+ or 0
54
+ )
55
+ except Exception as exc:
56
+ checks["database"] = "error"
57
+ return {
58
+ "status": "degraded",
59
+ "checks": checks,
60
+ "counts": counts,
61
+ "error": str(exc),
62
+ "timestamp": datetime.now(timezone.utc).isoformat(),
63
+ }
64
+
65
+ return {
66
+ "status": "ok",
67
+ "checks": checks,
68
+ "counts": counts,
69
+ "timestamp": datetime.now(timezone.utc).isoformat(),
70
+ }
backend/routers/orchestrator.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, BackgroundTasks, HTTPException
2
+ from fastapi.responses import Response
3
+ from services.orchestrator_service import orchestrator_service
4
+ from pydantic import BaseModel
5
+ from io import BytesIO
6
+ from reportlab.lib.pagesizes import letter
7
+ from reportlab.lib.styles import getSampleStyleSheet
8
+ from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle
9
+ from reportlab.graphics.shapes import Drawing
10
+ from reportlab.graphics.charts.barcharts import VerticalBarChart
11
+ from reportlab.graphics.charts.piecharts import Pie
12
+ from reportlab.lib import colors
13
+ from reportlab.lib.units import inch
14
+ import re
15
+
16
+ router = APIRouter()
17
+
18
+ def _safe_filename(value: str) -> str:
19
+ return re.sub(r"[^a-zA-Z0-9_-]+", "_", value).strip("_").lower() or "report"
20
+
21
+ def _bar_chart(title: str, rows: list[dict]) -> Drawing:
22
+ drawing = Drawing(460, 180)
23
+ chart = VerticalBarChart()
24
+ chart.x = 40
25
+ chart.y = 35
26
+ chart.height = 110
27
+ chart.width = 380
28
+ chart.data = [[row["value"] for row in rows]]
29
+ chart.categoryAxis.categoryNames = [row["label"] for row in rows]
30
+ chart.valueAxis.valueMin = 0
31
+ chart.valueAxis.valueMax = max([row["value"] for row in rows] + [1])
32
+ chart.valueAxis.valueStep = max(1, round(chart.valueAxis.valueMax / 4))
33
+ chart.bars[0].fillColor = colors.HexColor("#14b8a6")
34
+ drawing.add(chart)
35
+ return drawing
36
+
37
+ def _pie_chart(rows: list[dict]) -> Drawing:
38
+ drawing = Drawing(460, 180)
39
+ pie = Pie()
40
+ pie.x = 150
41
+ pie.y = 20
42
+ pie.width = 140
43
+ pie.height = 140
44
+ pie.data = [row["value"] for row in rows]
45
+ pie.labels = [row["label"] for row in rows]
46
+ pie.slices.strokeWidth = 0.5
47
+ palette = [colors.HexColor("#22c55e"), colors.HexColor("#facc15"), colors.HexColor("#ef4444")]
48
+ for index, color in enumerate(palette[:len(rows)]):
49
+ pie.slices[index].fillColor = color
50
+ drawing.add(pie)
51
+ return drawing
52
+
53
+ def _report_pdf_bytes(title: str, content: str, charts: dict | None = None) -> bytes:
54
+ buffer = BytesIO()
55
+ doc = SimpleDocTemplate(
56
+ buffer,
57
+ pagesize=letter,
58
+ rightMargin=0.7 * inch,
59
+ leftMargin=0.7 * inch,
60
+ topMargin=0.7 * inch,
61
+ bottomMargin=0.7 * inch,
62
+ )
63
+ styles = getSampleStyleSheet()
64
+ story = [Paragraph(title, styles["Title"]), Spacer(1, 0.2 * inch)]
65
+ if charts:
66
+ story.append(Paragraph("Project Execution Summary", styles["Heading2"]))
67
+ story.append(Spacer(1, 0.1 * inch))
68
+
69
+ # Summary Table instead of charts
70
+ table_data = [["Metric / Category", "Value"]]
71
+
72
+ # Tasks Status
73
+ status_counts = {row["label"]: row["value"] for row in charts.get("status", [])}
74
+ for label, val in status_counts.items():
75
+ table_data.append([f"Tasks: {label}", str(val)])
76
+
77
+ # Categories
78
+ for cat in charts.get("categories", []):
79
+ table_data.append([f"Type: {cat['label']}", str(cat['value'])])
80
+
81
+ table = Table(table_data, colWidths=[3.5*inch, 1.5*inch])
82
+ table.setStyle(TableStyle([
83
+ ('BACKGROUND', (0,0), (-1,0), colors.HexColor("#6e59ff")),
84
+ ('TEXTCOLOR', (0,0), (-1,0), colors.whitesmoke),
85
+ ('ALIGN', (0,0), (-1,-1), 'LEFT'),
86
+ ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
87
+ ('BOTTOMPADDING', (0,0), (-1,0), 10),
88
+ ('BACKGROUND', (0,1), (-1,-1), colors.HexColor("#f8fafc")),
89
+ ('GRID', (0,0), (-1,-1), 0.5, colors.grey),
90
+ ('FONTSIZE', (0,0), (-1,-1), 9),
91
+ ]))
92
+ story.append(table)
93
+ story.append(Spacer(1, 0.3 * inch))
94
+
95
+ for raw_line in content.splitlines():
96
+ line = raw_line.strip()
97
+ if not line:
98
+ story.append(Spacer(1, 0.1 * inch))
99
+ continue
100
+ if line.startswith("# "):
101
+ story.append(Paragraph(line[2:], styles["Title"]))
102
+ elif line.startswith("## "):
103
+ story.append(Paragraph(line[3:], styles["Heading2"]))
104
+ elif line.startswith("### "):
105
+ story.append(Paragraph(line[4:], styles["Heading3"]))
106
+ elif line.startswith("- "):
107
+ story.append(Paragraph(f"• {line[2:]}", styles["BodyText"]))
108
+ else:
109
+ story.append(Paragraph(line, styles["BodyText"]))
110
+
111
+ doc.build(story)
112
+ return buffer.getvalue()
113
+
114
+ class DebateRequest(BaseModel):
115
+
116
+ task_id: str
117
+ agent_a_id: str
118
+ agent_b_id: str
119
+
120
+ @router.post("/debate")
121
+ async def start_debate(request: DebateRequest, background_tasks: BackgroundTasks):
122
+ """
123
+ Starts a debate between two agents for a specific task.
124
+ """
125
+ background_tasks.add_task(
126
+ orchestrator_service.run_debate,
127
+ request.task_id,
128
+ request.agent_a_id,
129
+ request.agent_b_id
130
+ )
131
+ return {"message": "Debate started in background"}
132
+
133
+
134
+ @router.post("/projects/{project_id}/run")
135
+ async def run_project_orchestrator(project_id: str, background_tasks: BackgroundTasks):
136
+ """
137
+ Runs all queued tasks for a project in priority order.
138
+ """
139
+ background_tasks.add_task(orchestrator_service.run_project, project_id)
140
+ return {"message": "Project orchestrator started", "project_id": project_id}
141
+
142
+ @router.get("/projects/{project_id}/final-report")
143
+ async def get_project_final_report(project_id: str, variant: str = "full"):
144
+ """
145
+ Builds a consolidated report from all approved task outputs.
146
+ """
147
+ try:
148
+ return await orchestrator_service.build_final_report(project_id, variant)
149
+ except ValueError as exc:
150
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
151
+
152
+ @router.get("/projects/{project_id}/final-report.pdf")
153
+ async def download_project_final_report_pdf(project_id: str, variant: str = "full"):
154
+ """
155
+ Downloads the selected report variant as a PDF.
156
+ """
157
+ try:
158
+ result = await orchestrator_service.build_final_report(project_id, variant)
159
+ except ValueError as exc:
160
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
161
+
162
+ title = f"{result['project_name']} - {result['variant']} report"
163
+ pdf = _report_pdf_bytes(title, result["report"], result.get("charts"))
164
+ filename = f"{_safe_filename(result['project_name'])}_{_safe_filename(result['variant'])}.pdf"
165
+ return Response(
166
+ content=pdf,
167
+ media_type="application/pdf",
168
+ headers={"Content-Disposition": f'attachment; filename="{filename}"'}
169
+ )
backend/scratch/check_db.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from supabase import create_client
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ supabase_url = os.getenv("SUPABASE_URL")
8
+ supabase_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
9
+ supabase = create_client(supabase_url, supabase_key)
10
+
11
+ def check_logs():
12
+ try:
13
+ res = supabase.table("agent_logs").select("*").order("created_at", desc=True).limit(20).execute()
14
+ print(f"Total logs retrieved: {len(res.data)}")
15
+ for log in res.data:
16
+ print(f"[{log['created_at']}] {log['action']}: {log['content'][:50]}...")
17
+
18
+ except Exception as e:
19
+ print(f"Error accessing agent_logs: {e}")
20
+
21
+ if __name__ == "__main__":
22
+ check_logs()
backend/scratch/create_comparison_project.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from supabase import create_client
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ supabase_url = os.getenv("SUPABASE_URL")
8
+ supabase_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
9
+ supabase = create_client(supabase_url, supabase_key)
10
+
11
+ def create_project():
12
+ try:
13
+ # 1. Try to get owner_id from existing projects first
14
+ existing_projects = supabase.table("projects").select("owner_id").limit(1).execute()
15
+ user_id = None
16
+ if existing_projects.data and existing_projects.data[0].get("owner_id"):
17
+ user_id = existing_projects.data[0]["owner_id"]
18
+
19
+ if not user_id:
20
+ # Fallback to profiles
21
+ users = supabase.table("profiles").select("id").limit(1).execute()
22
+ if users.data:
23
+ user_id = users.data[0]["id"]
24
+
25
+ if not user_id:
26
+ print("No valid owner_id found in projects or profiles. The project will be created without owner and might not be visible.")
27
+ else:
28
+ print(f"Using owner_id: {user_id}")
29
+
30
+ # 2. Create Project
31
+ project_data = {
32
+ "name": "Aubm Competitor Analysis",
33
+ "description": "Deep dive into the multi-agent orchestration market to identify Aubm's unique value proposition and feature gaps.",
34
+ "status": "active",
35
+ "context": "Focus on developer experience, visual observability, and the 'Agent Debate' mechanism as key differentiators."
36
+ }
37
+ if user_id:
38
+ project_data["owner_id"] = user_id
39
+
40
+ project_res = supabase.table("projects").insert(project_data).execute()
41
+ project_id = project_res.data[0]["id"]
42
+ print(f"Created Project: {project_id}")
43
+
44
+ # 3. Create Tasks
45
+ tasks = [
46
+ {"title": "Identify Top 5 Competitors", "description": "Research and list 5 similar multi-agent orchestration platforms (e.g., CrewAI, AutoGen, LangGraph, PydanticAI).", "status": "todo", "project_id": project_id},
47
+ {"title": "Feature Comparison Matrix", "description": "Create a detailed matrix comparing Aubm's core features (Project Decomposition, Agent Debate, Real-time Console) against identified competitors.", "status": "todo", "project_id": project_id},
48
+ {"title": "Pricing Model Analysis", "description": "Analyze how competitors charge (SaaS, Open Source, API usage) and recommend a competitive strategy for Aubm.", "status": "todo", "project_id": project_id},
49
+ {"title": "UI/UX Aesthetic Audit", "description": "Evaluate the visual aesthetics and ease of use of competitors compared to Aubm's premium dashboard. Look for glassmorphism and animations.", "status": "todo", "project_id": project_id},
50
+ {"title": "Technical Architecture Deep-Dive", "description": "Investigate the underlying tech stacks (Python vs TS, Vector DBs used, Orchestration logic) of top competitors.", "status": "todo", "project_id": project_id},
51
+ {"title": "SWOT Analysis & Strategy Report", "description": "Compile all findings into a comprehensive report with a SWOT analysis and strategic recommendations for the next 6 months.", "status": "todo", "project_id": project_id}
52
+ ]
53
+
54
+ supabase.table("tasks").insert(tasks).execute()
55
+ print(f"Added {len(tasks)} tasks to the project.")
56
+
57
+ except Exception as e:
58
+ print(f"Error: {e}")
59
+
60
+ if __name__ == "__main__":
61
+ create_project()
backend/scratch/find_user.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from supabase import create_client
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ supabase_url = os.getenv("SUPABASE_URL")
8
+ supabase_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
9
+ supabase = create_client(supabase_url, supabase_key)
10
+
11
+ def check_users():
12
+ # Try different tables where users might be
13
+ tables = ["profiles", "users", "team_members"]
14
+ for table in tables:
15
+ try:
16
+ res = supabase.table(table).select("id").limit(1).execute()
17
+ print(f"Table {table} count: {len(res.data)}")
18
+ if res.data:
19
+ print(f"Sample ID: {res.data[0]['id']}")
20
+ except Exception as e:
21
+ print(f"Error checking {table}: {e}")
22
+
23
+ if __name__ == "__main__":
24
+ check_users()
backend/scratch/fix_logs_rls.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from supabase import create_client
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ supabase_url = os.getenv("SUPABASE_URL")
8
+ supabase_key = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
9
+ supabase = create_client(supabase_url, supabase_key)
10
+
11
+ sql = """
12
+ ALTER TABLE agent_logs ENABLE ROW LEVEL SECURITY;
13
+ DROP POLICY IF EXISTS "Enable read access for all users" ON agent_logs;
14
+ CREATE POLICY "Enable read access for all users" ON agent_logs FOR SELECT USING (true);
15
+ """
16
+
17
+ # Note: This assumes an 'exec_sql' RPC exists, which is common in many setups.
18
+ # If not, I'll have to find another way.
19
+ try:
20
+ # Actually, let's try a different approach if RPC fails.
21
+ # We can try to use the REST API to check if it works.
22
+ print("Attempting to set RLS policy...")
23
+ # Since I don't have direct SQL access via the client without RPC,
24
+ # I'll assume the user might need to do this in the dashboard or I'll try to find an RPC.
25
+
26
+ # Let's check if the client can read with anon key.
27
+ anon_key = os.getenv("SUPABASE_ANON_KEY")
28
+ anon_s = create_client(supabase_url, anon_key)
29
+ res = anon_s.table("agent_logs").select("*").limit(1).execute()
30
+ print(f"Anon read test: {'Success' if not res.data else 'Empty/Restricted'}")
31
+
32
+ except Exception as e:
33
+ print(f"Error: {e}")
backend/services/agent_runner_service.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from datetime import datetime, timezone
3
+ from services.supabase_service import supabase
4
+ from services.audit_service import audit_service
5
+ from agents.agent_factory import AgentFactory
6
+ from services.semantic_backprop import semantic_backprop
7
+ from services.output_quality import build_quality_instructions, validate_output
8
+
9
+ logger = logging.getLogger("agent_runner_service")
10
+
11
+ class AgentRunnerService:
12
+ @staticmethod
13
+ async def run_agent_task(
14
+ task: dict,
15
+ agent_data: dict,
16
+ *,
17
+ include_semantic_context: bool = False,
18
+ start_action: str = "execution_start",
19
+ start_content: str | None = None,
20
+ complete_action: str = "execution_complete",
21
+ complete_content: str = "Agent successfully completed the task and produced output.",
22
+ update_task: bool = True
23
+ ) -> tuple[dict, str]:
24
+ task_id = task["id"]
25
+ project_id = task["project_id"]
26
+ run_id = None
27
+
28
+ if update_task:
29
+ supabase.table("tasks").update({"status": "in_progress"}).eq("id", task_id).execute()
30
+
31
+ try:
32
+ run_res = supabase.table("task_runs").insert({
33
+ "task_id": task_id,
34
+ "agent_id": agent_data["id"],
35
+ "status": "running"
36
+ }).execute()
37
+ run_id = run_res.data[0]["id"]
38
+
39
+ agent = AgentFactory.get_agent(
40
+ provider=agent_data["api_provider"],
41
+ name=agent_data["name"],
42
+ role=agent_data["role"],
43
+ model=agent_data["model"],
44
+ system_prompt=agent_data.get("system_prompt")
45
+ )
46
+
47
+ context_res = supabase.table("tasks").select("title, output_data") \
48
+ .eq("project_id", project_id) \
49
+ .eq("status", "done") \
50
+ .execute()
51
+ context = context_res.data if context_res.data else []
52
+
53
+ project_data = task.get("project")
54
+ if not isinstance(project_data, dict):
55
+ project_res = (
56
+ supabase.table("projects")
57
+ .select("name,description,context")
58
+ .eq("id", project_id)
59
+ .single()
60
+ .execute()
61
+ )
62
+ project_data = project_res.data if project_res and project_res.data else {}
63
+ quality_task = {**task, "project": project_data}
64
+
65
+ extra_context = ""
66
+ if include_semantic_context:
67
+ extra_context = await semantic_backprop.get_project_context(project_id, task_id)
68
+
69
+ import time
70
+ import hashlib
71
+
72
+ # Simple in-memory cache for the session (could be persistent later)
73
+ if not hasattr(AgentRunnerService, "_task_cache"):
74
+ AgentRunnerService._task_cache = {}
75
+
76
+ # 1. Create a cache key based on task, agent (model + system prompt), and context
77
+ cache_input = f"{task['id']}-{agent_data['model']}-{agent_data.get('system_prompt', '')}-{task.get('description')}-{str(context)}-{extra_context}"
78
+ cache_key = hashlib.md5(cache_input.encode()).hexdigest()
79
+
80
+ # 2. Check Cache
81
+ if cache_key in AgentRunnerService._task_cache:
82
+ logger.info(f"Cache hit for task {task_id}. Skipping LLM call.")
83
+ cached_result = AgentRunnerService._task_cache[cache_key]
84
+
85
+ # Still log the "start" for UI consistency
86
+ agent_name = agent_data.get('name', 'Agent')
87
+ log_msg = start_content or f"Agent {agent_name} resuming task"
88
+ supabase.table("agent_logs").insert({
89
+ "task_id": task_id,
90
+ "run_id": run_id,
91
+ "action": start_action,
92
+ "content": f"[CACHE HIT] {log_msg}"
93
+ }).execute()
94
+
95
+ if update_task:
96
+ supabase.table("tasks").update({
97
+ "status": "awaiting_approval",
98
+ "output_data": cached_result
99
+ }).eq("id", task_id).execute()
100
+
101
+ return cached_result, run_id
102
+
103
+ # 3. Log Start
104
+ supabase.table("agent_logs").insert({
105
+ "task_id": task_id,
106
+ "run_id": run_id,
107
+ "action": start_action,
108
+ "content": start_content or f"Agent {agent_data['name']} starting task: {task['title']}"
109
+ }).execute()
110
+
111
+ # 4. Execute Run with timing
112
+ start_time = time.time()
113
+ task_instructions = task.get("description") or task["title"]
114
+ task_instructions = f"{task_instructions}\n\n{build_quality_instructions(quality_task)}"
115
+ result = await agent.run(task_instructions, context, extra_context=extra_context)
116
+ duration = time.time() - start_time
117
+
118
+ if result.get("status") == "error":
119
+ raise RuntimeError(result.get("error") or "Agent returned an error result.")
120
+
121
+ # 5. Security Sanitization (Defense in Depth)
122
+ raw_out = str(result.get("raw_output", ""))
123
+ suspicious_patterns = ["rm -rf", "mkfs", "dd if=", "curl", "wget", "chmod 777", "> /dev/sda"]
124
+ for pattern in suspicious_patterns:
125
+ if pattern in raw_out:
126
+ logger.warning(f"SECURITY: Suspicious pattern '{pattern}' detected in agent output for task {task_id}.")
127
+ result["security_warning"] = f"Output sanitized: suspicious pattern '{pattern}' detected."
128
+ # We don't block yet, but we flag it.
129
+
130
+ quality_review = validate_output(quality_task, result)
131
+ result["quality_review"] = quality_review
132
+
133
+ # 6. Save to Cache
134
+ AgentRunnerService._task_cache[cache_key] = result
135
+
136
+ if update_task:
137
+ supabase.table("tasks").update({
138
+ "status": "awaiting_approval",
139
+ "output_data": result
140
+ }).eq("id", task_id).execute()
141
+
142
+ # 7. Update Run Status
143
+ supabase.table("task_runs").update({
144
+ "status": "completed",
145
+ "finished_at": datetime.now(timezone.utc).isoformat(),
146
+ "duration_seconds": round(duration, 2)
147
+ }).eq("id", run_id).execute()
148
+
149
+ # 8. Log Completion with Metrics
150
+ supabase.table("agent_logs").insert({
151
+ "task_id": task_id,
152
+ "run_id": run_id,
153
+ "action": complete_action,
154
+ "content": f"{complete_content} (Execution time: {duration:.2f}s)"
155
+ }).execute()
156
+
157
+ if not quality_review["approved"]:
158
+ supabase.table("agent_logs").insert({
159
+ "task_id": task_id,
160
+ "run_id": run_id,
161
+ "action": "quality_review_failed",
162
+ "content": f"Quality review failed: {', '.join(quality_review['fail_reasons'])}"
163
+ }).execute()
164
+
165
+ return result, run_id
166
+
167
+ except Exception as e:
168
+ logger.error(f"Error executing task {task_id}: {str(e)}")
169
+ if run_id:
170
+ supabase.table("task_runs").update({
171
+ "status": "failed",
172
+ "finished_at": datetime.now(timezone.utc).isoformat()
173
+ }).eq("id", run_id).execute()
174
+
175
+ if update_task:
176
+ supabase.table("tasks").update({
177
+ "status": "failed",
178
+ "output_data": {"error": str(e)}
179
+ }).eq("id", task_id).execute()
180
+
181
+ # LOG ERROR TO AGENT CONSOLE
182
+ supabase.table("agent_logs").insert({
183
+ "task_id": task_id,
184
+ "run_id": run_id,
185
+ "action": "execution_failed",
186
+ "content": f"ERROR: {str(e)}"
187
+ }).execute()
188
+
189
+ raise e
190
+
191
+ @staticmethod
192
+ async def execute_agent_logic(task: dict, agent_data: dict):
193
+ task_id = task["id"]
194
+ try:
195
+ await AgentRunnerService.run_agent_task(
196
+ task,
197
+ agent_data,
198
+ include_semantic_context=True
199
+ )
200
+
201
+ await audit_service.log_action(
202
+ user_id=None,
203
+ action="agent_task_completed",
204
+ agent_id=agent_data["id"],
205
+ task_id=task_id,
206
+ metadata={"model": agent_data["model"]}
207
+ )
208
+
209
+ except Exception:
210
+ raise
backend/services/audit_service.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from services.supabase_service import supabase
2
+ from typing import Dict, Any, Optional
3
+ import logging
4
+
5
+ logger = logging.getLogger("uvicorn")
6
+
7
+ class AuditService:
8
+ @staticmethod
9
+ async def log_action(
10
+ user_id: Optional[str],
11
+ action: str,
12
+ agent_id: Optional[str] = None,
13
+ task_id: Optional[str] = None,
14
+ metadata: Optional[Dict[str, Any]] = None
15
+ ):
16
+ """
17
+ Records an action in the audit_logs table.
18
+ """
19
+ try:
20
+ data = {
21
+ "user_id": user_id,
22
+ "action": action,
23
+ "agent_id": agent_id,
24
+ "task_id": task_id,
25
+ "metadata": metadata or {}
26
+ }
27
+ supabase.table("audit_logs").insert(data).execute()
28
+ except Exception as e:
29
+ logger.error(f"AuditService error: {str(e)}")
30
+
31
+ audit_service = AuditService()
backend/services/config.py ADDED
@@ -0,0 +1,101 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from pydantic_settings import BaseSettings
3
+ from typing import Optional, Dict, Any
4
+ from supabase import create_client, Client
5
+
6
+ class Settings(BaseSettings):
7
+ # Supabase
8
+ SUPABASE_URL: str = ""
9
+ SUPABASE_SERVICE_ROLE_KEY: str = ""
10
+
11
+ # AI Providers
12
+ OPENAI_API_KEY: Optional[str] = None
13
+ GROQ_API_KEY: Optional[str] = None
14
+ GEMINI_API_KEY: Optional[str] = None
15
+ ANTHROPIC_API_KEY: Optional[str] = None
16
+ AMD_API_KEY: Optional[str] = None
17
+ TAVILY_API_KEY: Optional[str] = None
18
+
19
+ # App Config
20
+ TASK_QUEUE_EMBEDDED_WORKER: bool = True
21
+ OUTPUT_LANGUAGE: str = "en"
22
+ PORT: int = 8000
23
+ SENTRY_DSN: Optional[str] = None
24
+
25
+ model_config = {
26
+ "env_file": ".env",
27
+ "extra": "ignore"
28
+ }
29
+
30
+ settings = Settings()
31
+
32
+ class ConfigService:
33
+ """
34
+ Manages application-wide settings stored in Supabase with local fallback defaults.
35
+ Borrowed from AgentCollab for enhanced flexibility.
36
+ """
37
+ _cache: Dict[str, Any] = {}
38
+ _supabase: Client = None
39
+
40
+ @classmethod
41
+ def _get_supabase(cls):
42
+ if not cls._supabase:
43
+ if not settings.SUPABASE_URL or not settings.SUPABASE_SERVICE_ROLE_KEY:
44
+ return None
45
+ cls._supabase = create_client(settings.SUPABASE_URL, settings.SUPABASE_SERVICE_ROLE_KEY)
46
+ return cls._supabase
47
+
48
+ # Defaults used when DB has no config entry for a provider
49
+ _DEFAULTS: Dict[str, Any] = {
50
+ "groq": {"enabled": True, "default_model": "llama-3.3-70b-versatile", "temperature": 0.7, "max_tokens": 4096},
51
+ "openai": {"enabled": True, "default_model": "gpt-4o", "temperature": 0.7, "max_tokens": 4096},
52
+ "openrouter": {"enabled": True, "default_model": "google/gemini-2.0-flash", "temperature": 0.7, "max_tokens": 8192},
53
+ "gemini": {"enabled": True, "default_model": "gemini-2.0-flash", "temperature": 0.7, "max_tokens": 8192},
54
+ "amd": {"enabled": True, "default_model": "gpt-4o", "temperature": 0.7, "max_tokens": 4096, "base_url": "https://inference.do-ai.run/v1"},
55
+ "ollama": {"enabled": True, "default_model": "llama3.1:8b", "temperature": 0.7, "base_url": "http://localhost:11434"},
56
+ }
57
+
58
+ @classmethod
59
+ def get_provider_config(cls, provider: str) -> Dict[str, Any]:
60
+ """Returns config for a provider from cache, DB, then defaults."""
61
+ cache_key = f"provider:{provider}"
62
+ if cache_key in cls._cache:
63
+ return cls._cache[cache_key]
64
+
65
+ db = cls._get_supabase()
66
+ if db:
67
+ try:
68
+ resp = db.table("app_config").select("*").eq("key", provider).execute()
69
+ if resp.data and len(resp.data) > 0:
70
+ cls._cache[cache_key] = resp.data[0]["value"]
71
+ return cls._cache[cache_key]
72
+ except Exception:
73
+ pass # Fall through to defaults
74
+
75
+ result = cls._DEFAULTS.get(provider, {})
76
+ cls._cache[cache_key] = result
77
+ return result
78
+
79
+ @classmethod
80
+ def get_global_setting(cls, key: str, default: Any = None) -> Any:
81
+ cache_key = f"global:{key}"
82
+ if cache_key in cls._cache:
83
+ return cls._cache[cache_key]
84
+
85
+ db = cls._get_supabase()
86
+ if db:
87
+ try:
88
+ resp = db.table("app_config").select("*").eq("key", key).execute()
89
+ if resp.data and len(resp.data) > 0:
90
+ cls._cache[cache_key] = resp.data[0]["value"]
91
+ return cls._cache[cache_key]
92
+ except Exception:
93
+ pass
94
+
95
+ return default
96
+
97
+ @classmethod
98
+ def invalidate_cache(cls) -> None:
99
+ cls._cache.clear()
100
+
101
+ config_service = ConfigService()
backend/services/orchestrator_service.py ADDED
@@ -0,0 +1,773 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from services.supabase_service import supabase
2
+ from agents.agent_factory import AgentFactory
3
+ import json
4
+ import logging
5
+ import re
6
+ from services.config import settings
7
+ from services.agent_runner_service import AgentRunnerService
8
+ from services.output_quality import clean_report_text, dedupe_lines, filter_report_sections, validate_output
9
+
10
+ logger = logging.getLogger("uvicorn")
11
+
12
+ NOISY_REPORT_KEYS = {
13
+ "raw_text",
14
+ "sampleBackendCode",
15
+ "sampleUploadSnippet",
16
+ "sampleSearchEndpoint",
17
+ "sampleRedisCartHelper",
18
+ "sampleWebhookHandler",
19
+ "sampleStateMachine",
20
+ "repositoryStructure",
21
+ "wireframes",
22
+ "dataModel",
23
+ "userStories",
24
+ }
25
+
26
+ def _humanize_key(key: str) -> str:
27
+ return key.replace("_", " ").replace("-", " ").strip().title()
28
+
29
+ def _format_value_for_report(value, level: int = 0) -> list[str]:
30
+ if value is None:
31
+ return ["Not specified."]
32
+
33
+ if isinstance(value, (str, int, float, bool)):
34
+ return [str(value)]
35
+
36
+ if isinstance(value, list):
37
+ lines: list[str] = []
38
+ for item in value:
39
+ if isinstance(item, dict):
40
+ item_lines = _format_value_for_report(item, level + 1)
41
+ if item_lines:
42
+ lines.append(f"- {item_lines[0]}")
43
+ lines.extend(f" {line}" for line in item_lines[1:])
44
+ elif isinstance(item, list):
45
+ nested = _format_value_for_report(item, level + 1)
46
+ lines.extend(f"- {line}" for line in nested)
47
+ else:
48
+ lines.append(f"- {item}")
49
+ return lines or ["No items."]
50
+
51
+ if isinstance(value, dict):
52
+ lines: list[str] = []
53
+ for key, item in value.items():
54
+ if str(key) in NOISY_REPORT_KEYS:
55
+ continue
56
+ title = _humanize_key(str(key))
57
+ if isinstance(item, dict):
58
+ lines.append(f"{title}:")
59
+ lines.extend(f" {line}" for line in _format_value_for_report(item, level + 1))
60
+ elif isinstance(item, list):
61
+ lines.append(f"{title}:")
62
+ lines.extend(f" {line}" for line in _format_value_for_report(item, level + 1))
63
+ else:
64
+ lines.append(f"{title}: {item}")
65
+ return lines or ["No details."]
66
+
67
+ return [str(value)]
68
+
69
+
70
+ def _extract_json_payload(text: str):
71
+ stripped = text.strip()
72
+ if stripped.startswith("```"):
73
+ stripped = stripped.strip("`")
74
+ if stripped.lower().startswith("json"):
75
+ stripped = stripped[4:].strip()
76
+ try:
77
+ return json.loads(stripped)
78
+ except Exception:
79
+ match = re.search(r"```json\s*(.*?)\s*```", text, re.IGNORECASE | re.DOTALL)
80
+ if match:
81
+ try:
82
+ return json.loads(match.group(1))
83
+ except Exception:
84
+ return None
85
+ return None
86
+
87
+ def _format_output_for_report(output_data) -> str:
88
+ if not output_data:
89
+ return "No approved output was saved for this task."
90
+
91
+ if isinstance(output_data, dict):
92
+ primary = (
93
+ output_data.get("data")
94
+ or output_data.get("final")
95
+ or output_data.get("raw_output")
96
+ or output_data
97
+ )
98
+ else:
99
+ primary = output_data
100
+
101
+ if isinstance(primary, str):
102
+ parsed = _extract_json_payload(primary)
103
+ if parsed is not None:
104
+ return clean_report_text(dedupe_lines("\n".join(_format_value_for_report(parsed))))
105
+ return clean_report_text(dedupe_lines(primary))
106
+
107
+ return clean_report_text(dedupe_lines("\n".join(_format_value_for_report(primary))))
108
+
109
+
110
+ def _is_empty_curated_text(text: str) -> bool:
111
+ normalized = (text or "").strip().lower()
112
+ return normalized in {
113
+ "",
114
+ "no approved output was saved for this task.",
115
+ "{}",
116
+ "[]",
117
+ }
118
+
119
+
120
+ def _format_conclusion_payload(data: dict) -> str:
121
+ conclusion = data.get("strategicConclusion") or data.get("conclusion") or data.get("content") or ""
122
+ next_steps = data.get("nextSteps") or data.get("next_steps") or []
123
+
124
+ lines: list[str] = []
125
+ if isinstance(conclusion, str) and conclusion.strip():
126
+ lines.append(conclusion.strip())
127
+
128
+ if isinstance(next_steps, list) and next_steps:
129
+ lines.append("")
130
+ lines.append("Next steps:")
131
+ for step in next_steps[:5]:
132
+ if isinstance(step, str) and step.strip():
133
+ lines.append(f"- {step.strip()}")
134
+
135
+ return "\n".join(lines).strip() or "\n".join(_format_value_for_report(data))
136
+
137
+
138
+ def _has_usable_output(output_data) -> bool:
139
+ if not output_data:
140
+ return False
141
+ if isinstance(output_data, dict):
142
+ if output_data.get("error"):
143
+ return False
144
+ primary = output_data.get("data")
145
+ if primary in (None, "", [], {}):
146
+ return False
147
+ return True
148
+
149
+ def _output_text(output_data) -> str:
150
+ return _format_output_for_report(output_data).lower()
151
+
152
+ def _build_report_charts(tasks: list[dict]) -> dict:
153
+ total = len(tasks)
154
+ done = sum(1 for task in tasks if task.get("status") == "done")
155
+ failed = sum(1 for task in tasks if task.get("status") == "failed")
156
+ pending = max(total - done - failed, 0)
157
+
158
+ priority_counts: dict[str, int] = {}
159
+ for task in tasks:
160
+ priority = str(task.get("priority") if task.get("priority") is not None else 0)
161
+ priority_counts[priority] = priority_counts.get(priority, 0) + 1
162
+
163
+ categories = {
164
+ "Market": ("market", "competitor", "customer", "segment", "demand"),
165
+ "Product": ("product", "mvp", "feature", "design", "scope"),
166
+ "Revenue": ("revenue", "price", "pricing", "margin", "commission"),
167
+ "Operations": ("operation", "process", "logistic", "support", "fulfillment"),
168
+ "Risk": ("risk", "threat", "failure", "weak", "mitigation")
169
+ }
170
+ category_counts = {name: 0 for name in categories}
171
+ risk_mentions = 0
172
+
173
+ for task in tasks:
174
+ text = f"{task.get('title', '')} {task.get('description', '')} {_output_text(task.get('output_data'))}"
175
+ risk_mentions += sum(text.count(term) for term in categories["Risk"])
176
+ for category, terms in categories.items():
177
+ if any(term in text for term in terms):
178
+ category_counts[category] += 1
179
+
180
+ opportunity_score = 85 if total and done == total else round((done / total) * 85) if total else 0
181
+ risk_score = min(95, 35 + risk_mentions * 3)
182
+ readiness_score = round((done / total) * 100) if total else 0
183
+
184
+ return {
185
+ "status": [
186
+ {"label": "Approved", "value": done},
187
+ {"label": "Pending", "value": pending},
188
+ {"label": "Failed", "value": failed}
189
+ ],
190
+ "priorities": [
191
+ {"label": f"Priority {key}", "value": value}
192
+ for key, value in sorted(priority_counts.items(), key=lambda item: int(item[0]) if item[0].isdigit() else 0, reverse=True)
193
+ ],
194
+ "categories": [
195
+ {"label": label, "value": value}
196
+ for label, value in category_counts.items()
197
+ ],
198
+ "scores": [
199
+ {"label": "Readiness", "value": readiness_score},
200
+ {"label": "Opportunity", "value": opportunity_score},
201
+ {"label": "Risk", "value": risk_score}
202
+ ]
203
+ }
204
+
205
+ REPORT_VARIANTS = {
206
+ "full": {
207
+ "title": "Final Report",
208
+ "agent_terms": [],
209
+ "fallback_heading": "Approved Work Summary",
210
+ "prompt": ""
211
+ },
212
+ "brief": {
213
+ "title": "Short Brief",
214
+ "agent_terms": ["brief", "summary", "writer"],
215
+ "fallback_heading": "Short Brief",
216
+ "prompt": (
217
+ "Create a concise executive brief from the approved project work. "
218
+ "Use plain English, no JSON, no code blocks. Include: objective, main findings, recommended next steps, and key risks. "
219
+ "Keep it short and decision-oriented. Do not invent entities, metrics, or placeholders."
220
+ )
221
+ },
222
+ "pessimistic": {
223
+ "title": "Pessimistic Analysis",
224
+ "agent_terms": ["pessimistic", "risk", "critic", "reviewer"],
225
+ "fallback_heading": "Pessimistic Analysis",
226
+ "prompt": (
227
+ "Create a skeptical, downside-focused analysis from the approved project work. "
228
+ "Use plain English, no JSON, no code blocks. Focus on what can fail, weak assumptions, operational risks, market risks, "
229
+ "financial risks, execution gaps, and mitigation priorities. Do not invent entities, metrics, or placeholders."
230
+ )
231
+ }
232
+ }
233
+
234
+ class OrchestratorService:
235
+ """
236
+ Handles complex multi-agent workflows like Debates and Peer Reviews.
237
+ """
238
+
239
+ async def run_debate(self, task_id: str, agent_a_id: str, agent_b_id: str):
240
+ """
241
+ Executes a debate between two agents for a specific task.
242
+ """
243
+ try:
244
+ # 1. Fetch task and agents
245
+ task = supabase.table("tasks").select("*").eq("id", task_id).single().execute().data
246
+ agent_a_data = supabase.table("agents").select("*").eq("id", agent_a_id).single().execute().data
247
+ agent_b_data = supabase.table("agents").select("*").eq("id", agent_b_id).single().execute().data
248
+
249
+ if not task or not agent_a_data or not agent_b_data:
250
+ raise ValueError("Task or agents not found for debate.")
251
+
252
+ # Update status to in_progress
253
+ supabase.table("tasks").update({"status": "in_progress"}).eq("id", task_id).execute()
254
+
255
+ # 2. Agent A generates initial response
256
+ initial_res, _ = await AgentRunnerService.run_agent_task(
257
+ task,
258
+ agent_a_data,
259
+ start_action="debate_initial_start",
260
+ start_content=f"Debate Step 1: {agent_a_data['name']} generating initial proposal.",
261
+ complete_action="debate_initial_complete",
262
+ update_task=False
263
+ )
264
+
265
+ # 3. Agent B reviews and critiques
266
+ # We temporarily modify the task description for this run
267
+ task_critique = task.copy()
268
+ task_critique["description"] = f"Review the following output for the task: '{task['description']}'. Provide constructive critique and identify errors.\n\nOutput: {json.dumps(initial_res['data'])}"
269
+
270
+ critique_res, _ = await AgentRunnerService.run_agent_task(
271
+ task_critique,
272
+ agent_b_data,
273
+ start_action="debate_critique_start",
274
+ start_content=f"Debate Step 2: {agent_b_data['name']} critiquing the proposal.",
275
+ complete_action="debate_critique_complete",
276
+ update_task=False
277
+ )
278
+
279
+ # 4. Agent A refines based on critique
280
+ task_refinement = task.copy()
281
+ task_refinement["description"] = f"Refine your initial output for the task: '{task['description']}' based on this critique: {json.dumps(critique_res['data'])}"
282
+
283
+ final_res, _ = await AgentRunnerService.run_agent_task(
284
+ task_refinement,
285
+ agent_a_data,
286
+ start_action="debate_refinement_start",
287
+ start_content=f"Debate Step 3: {agent_a_data['name']} refining proposal based on feedback.",
288
+ complete_action="debate_refinement_complete",
289
+ update_task=False
290
+ )
291
+
292
+ # 5. Save consolidated result and mark for approval
293
+ consolidated_output = {
294
+ "agent_name": agent_a_data["name"],
295
+ "provider": agent_a_data["api_provider"],
296
+ "model": agent_a_data["model"],
297
+ "is_debate": True,
298
+ "data": final_res["data"],
299
+ "debate_history": {
300
+ "initial": initial_res["data"],
301
+ "critique": critique_res["data"],
302
+ "final": final_res["data"]
303
+ }
304
+ }
305
+
306
+ supabase.table("tasks").update({
307
+ "status": "awaiting_approval",
308
+ "output_data": consolidated_output
309
+ }).eq("id", task_id).execute()
310
+
311
+ logger.info(f"Debate completed for task {task_id}")
312
+
313
+ except Exception as e:
314
+ logger.error(f"Debate failed: {str(e)}")
315
+ supabase.table("tasks").update({
316
+ "status": "failed",
317
+ "output_data": {"error": str(e)}
318
+ }).eq("id", task_id).execute()
319
+
320
+ # LOG ERROR TO AGENT CONSOLE
321
+ supabase.table("agent_logs").insert({
322
+ "task_id": task_id,
323
+ "action": "debate_failed",
324
+ "content": f"DEBATE ERROR: {str(e)}"
325
+ }).execute()
326
+
327
+ async def run_project(self, project_id: str):
328
+ """
329
+ Runs queued tasks in a project sequentially. Unassigned tasks are assigned
330
+ to the first available project-owner or global agent.
331
+ """
332
+ project = supabase.table("projects").select("*").eq("id", project_id).single().execute().data
333
+ if not project:
334
+ raise ValueError(f"Project not found: {project_id}")
335
+
336
+ owner_id = project.get("owner_id")
337
+ tasks = (
338
+ supabase.table("tasks")
339
+ .select("*")
340
+ .eq("project_id", project_id)
341
+ .eq("status", "todo")
342
+ .order("priority", desc=True)
343
+ .order("created_at", desc=False)
344
+ .execute()
345
+ .data
346
+ or []
347
+ )
348
+
349
+ # Check if ANY tasks exist for this project (regardless of status) to avoid re-decomposing
350
+ all_tasks_res = supabase.table("tasks").select("id", count="exact").eq("project_id", project_id).limit(1).execute()
351
+ has_any_tasks = all_tasks_res.count > 0 if all_tasks_res.count is not None else len(all_tasks_res.data) > 0
352
+
353
+ # Automatic Decomposition: Only if no tasks exist AT ALL
354
+ if not has_any_tasks:
355
+ logger.info(f"No tasks found for project {project_id}. Triggering auto-decomposition.")
356
+ await self.decompose_project(project_id)
357
+ # Re-fetch tasks after decomposition
358
+ tasks = (
359
+ supabase.table("tasks")
360
+ .select("*")
361
+ .eq("project_id", project_id)
362
+ .in_("status", ["todo", "failed"])
363
+ .order("priority", desc=True)
364
+ .order("created_at", desc=False)
365
+ .execute()
366
+ .data
367
+ or []
368
+ )
369
+
370
+ agents = supabase.table("agents").select("*").execute().data or []
371
+ available_agents = [
372
+ agent for agent in agents
373
+ if agent.get("user_id") in (None, owner_id)
374
+ ]
375
+
376
+ completed = 0
377
+ failed = 0
378
+
379
+ for task in tasks:
380
+ try:
381
+ agent_data = self._resolve_agent(task, available_agents)
382
+ if not agent_data:
383
+ raise ValueError("No available agent for task")
384
+
385
+ if not task.get("assigned_agent_id"):
386
+ supabase.table("tasks").update({
387
+ "assigned_agent_id": agent_data["id"]
388
+ }).eq("id", task["id"]).execute()
389
+ task["assigned_agent_id"] = agent_data["id"]
390
+
391
+ await self._run_task(task, agent_data)
392
+ completed += 1
393
+ except Exception as exc:
394
+ failed += 1
395
+ logger.error(f"Project orchestration task failed: {str(exc)}")
396
+ supabase.table("tasks").update({
397
+ "status": "failed",
398
+ "output_data": {"error": str(exc)}
399
+ }).eq("id", task["id"]).execute()
400
+
401
+ return {
402
+ "project_id": project_id,
403
+ "queued_tasks": len(tasks),
404
+ "completed": completed,
405
+ "failed": failed,
406
+ }
407
+
408
+ def _select_report_agent(self, project: dict, variant: str):
409
+ config = REPORT_VARIANTS.get(variant, REPORT_VARIANTS["full"])
410
+ terms = config["agent_terms"]
411
+ if not terms:
412
+ return None
413
+
414
+ owner_id = project.get("owner_id")
415
+ agents = supabase.table("agents").select("*").execute().data or []
416
+ available_agents = [
417
+ agent for agent in agents
418
+ if agent.get("user_id") in (None, owner_id)
419
+ ]
420
+
421
+ return next(
422
+ (
423
+ agent for agent in available_agents
424
+ if any(term in f"{agent.get('name', '')} {agent.get('role', '')}".lower() for term in terms)
425
+ ),
426
+ available_agents[0] if available_agents else None
427
+ )
428
+
429
+ async def _generate_report_variant_with_agent(self, project: dict, report: str, variant: str):
430
+ agent_data = self._select_report_agent(project, variant)
431
+ if not agent_data:
432
+ return None
433
+
434
+ config = REPORT_VARIANTS[variant]
435
+ agent = AgentFactory.get_agent(
436
+ provider=agent_data["api_provider"],
437
+ name=agent_data["name"],
438
+ role=agent_data["role"],
439
+ model=agent_data["model"],
440
+ system_prompt=agent_data.get("system_prompt")
441
+ )
442
+ result = await agent.run(f"{config['prompt']}\n\nApproved project material:\n{report}", [])
443
+ if result.get("status") == "error":
444
+ raise RuntimeError(result.get("error") or "Report agent returned an error.")
445
+
446
+ data = result.get("data")
447
+ if isinstance(data, dict):
448
+ for key in ("brief", "analysis", "report", "summary", "content"):
449
+ if isinstance(data.get(key), str):
450
+ return data[key]
451
+ return "\n".join(_format_value_for_report(data))
452
+ if isinstance(data, str):
453
+ return data
454
+ return result.get("raw_output")
455
+
456
+ def _build_fallback_variant(self, project: dict, tasks: list[dict], variant: str):
457
+ config = REPORT_VARIANTS[variant]
458
+ lines = [
459
+ f"# {config['title']}: {project['name']}",
460
+ "",
461
+ "## Project Brief",
462
+ project.get("description") or "No project description provided.",
463
+ "",
464
+ f"## {config['fallback_heading']}"
465
+ ]
466
+
467
+ if variant == "brief":
468
+ lines.extend([
469
+ f"All {len(tasks)} approved tasks have been consolidated.",
470
+ "The project is ready for decision review based on the approved task outputs.",
471
+ "",
472
+ "Recommended next steps:",
473
+ "- Validate the highest-impact assumptions with real users or customers.",
474
+ "- Prioritize the smallest launch scope that proves demand.",
475
+ "- Convert approved outputs into an execution backlog with owners and dates."
476
+ ])
477
+ return "\n".join(lines)
478
+
479
+ if variant == "pessimistic":
480
+ lines.extend([
481
+ "This project can still fail even with all tasks approved.",
482
+ "",
483
+ "Primary downside risks:",
484
+ "- Approved task outputs may be internally consistent but unvalidated by the market.",
485
+ "- Revenue, conversion, operational, and adoption assumptions may be too optimistic.",
486
+ "- Execution scope can expand faster than the team can deliver.",
487
+ "- Competitors can respond with pricing, distribution, or trust advantages.",
488
+ "",
489
+ "Mitigation priorities:",
490
+ "- Validate demand before building broad feature scope.",
491
+ "- Stress-test unit economics and support costs.",
492
+ "- Define kill criteria before committing more resources."
493
+ ])
494
+ return "\n".join(lines)
495
+
496
+ return None
497
+
498
+ def _quality_approved_tasks(self, tasks: list[dict], project: dict) -> tuple[list[dict], list[dict]]:
499
+ approved: list[dict] = []
500
+ excluded: list[dict] = []
501
+ for task in tasks:
502
+ output_data = task.get("output_data") or {}
503
+ if not _has_usable_output(output_data):
504
+ excluded.append({
505
+ "title": task.get("title", "Untitled task"),
506
+ "reasons": ["Task has no usable approved output."]
507
+ })
508
+ continue
509
+ task_with_project = {**task, "project": project}
510
+ quality_review = output_data.get("quality_review") if isinstance(output_data, dict) else None
511
+ if not quality_review and isinstance(output_data, dict):
512
+ quality_review = validate_output(task_with_project, output_data)
513
+ if quality_review and not quality_review.get("approved", False):
514
+ excluded.append({
515
+ "title": task.get("title", "Untitled task"),
516
+ "reasons": quality_review.get("fail_reasons") or ["Failed quality review."]
517
+ })
518
+ continue
519
+ approved.append(task)
520
+ return approved, excluded
521
+
522
+ def _curate_task_output(self, output_data) -> tuple[str, list[str]]:
523
+ text = _format_output_for_report(output_data)
524
+ text = clean_report_text(dedupe_lines(text))
525
+ text, excluded_lines = filter_report_sections(text)
526
+ return text or "No approved output was saved for this task.", excluded_lines
527
+
528
+ async def build_final_report(self, project_id: str, variant: str = "full"):
529
+ variant = variant if variant in REPORT_VARIANTS else "full"
530
+ project = supabase.table("projects").select("*").eq("id", project_id).single().execute().data
531
+ if not project:
532
+ raise ValueError(f"Project not found: {project_id}")
533
+
534
+ tasks = (
535
+ supabase.table("tasks")
536
+ .select("title,description,status,priority,output_data,created_at")
537
+ .eq("project_id", project_id)
538
+ .order("priority", desc=True)
539
+ .order("created_at", desc=False)
540
+ .execute()
541
+ .data
542
+ or []
543
+ )
544
+
545
+ if not tasks:
546
+ raise ValueError("Project has no tasks to summarize.")
547
+
548
+ incomplete = [task for task in tasks if task.get("status") != "done"]
549
+ if incomplete:
550
+ raise ValueError(f"Final report is available after all tasks are approved. Pending tasks: {len(incomplete)}")
551
+
552
+ curated_tasks, excluded_tasks = self._quality_approved_tasks(tasks, project)
553
+ if not curated_tasks:
554
+ raise ValueError("No approved task outputs passed quality validation for final reporting.")
555
+
556
+ # 0. Header and Description
557
+ report_title = REPORT_VARIANTS[variant]["title"]
558
+ lines = [
559
+ f"# {report_title}: {project['name']}",
560
+ "",
561
+ "## Project Overview",
562
+ project.get("description") or "No description provided.",
563
+ ""
564
+ ]
565
+
566
+ # Add Context if exists
567
+ if project.get("context"):
568
+ lines.extend(["## Context", project["context"], ""])
569
+
570
+ lines.extend(["## Execution Summary", ""])
571
+
572
+ # We will add the tabular summary later in the UI or via charts,
573
+ # but for the text report, we include the approved work summary.
574
+ lines.extend(["## Approved Work Summary", ""])
575
+
576
+ report_exclusions: list[str] = []
577
+ kept_task_count = 0
578
+ for task in curated_tasks:
579
+ curated_text, excluded_lines = self._curate_task_output(task.get("output_data"))
580
+ report_exclusions.extend(excluded_lines)
581
+ if _is_empty_curated_text(curated_text):
582
+ excluded_tasks.append({
583
+ "title": task.get("title", "Untitled task"),
584
+ "reasons": ["Task output became empty after quality filtering."]
585
+ })
586
+ continue
587
+ kept_task_count += 1
588
+ lines.extend([
589
+ f"### {kept_task_count}. {task['title']}",
590
+ task.get("description") or "No task description provided.",
591
+ "",
592
+ curated_text,
593
+ ""
594
+ ])
595
+
596
+ if excluded_tasks or report_exclusions:
597
+ lines.extend(["## Excluded Content", ""])
598
+ for excluded in excluded_tasks:
599
+ lines.append(f"- Excluded task output: {excluded['title']} ({'; '.join(excluded['reasons'])})")
600
+ for excluded_line in list(dict.fromkeys(report_exclusions))[:10]:
601
+ if excluded_line:
602
+ lines.append(f"- {excluded_line}")
603
+ lines.append("")
604
+
605
+ # Final Conclusion Generation
606
+ conclusion = (
607
+ "Based on the approved task outputs, the project has successfully established a foundational framework. "
608
+ "The key findings suggest a viable path forward by focusing on the identified entry wedge and "
609
+ "mitigating primary risks through phased execution."
610
+ )
611
+
612
+ if variant == "full":
613
+ try:
614
+ # Use the 'Brief Writer' or any available agent to summarize a conclusion
615
+ agent_data = self._select_report_agent(project, "brief")
616
+ if agent_data:
617
+ agent = AgentFactory.get_agent(
618
+ provider=agent_data["api_provider"],
619
+ name=agent_data["name"],
620
+ role=agent_data["role"],
621
+ model=agent_data["model"],
622
+ system_prompt="You write a 2-3 sentence strategic conclusion and 3 actionable next steps for a project report. Never introduce placeholders or unsupported facts."
623
+ )
624
+ report_so_far = "\n".join(lines)
625
+ res = await agent.run(f"Based on this project report, write a final strategic conclusion and 3 next steps:\n\n{report_so_far}", [])
626
+ if res.get("status") != "error":
627
+ data = res.get("data")
628
+ if isinstance(data, str):
629
+ conclusion = data
630
+ elif isinstance(data, dict):
631
+ conclusion = _format_conclusion_payload(data)
632
+ except Exception as exc:
633
+ logger.warning(f"Failed to generate dynamic conclusion: {exc}")
634
+
635
+ lines.extend([
636
+ "## Strategic Conclusion",
637
+ conclusion,
638
+ "",
639
+ "## Completion Status",
640
+ f"{len(tasks)} tasks reached done status. {kept_task_count} task outputs were included in the final report. {len(excluded_tasks)} task outputs were excluded from the final report."
641
+ ])
642
+
643
+ supabase.table("projects").update({"status": "completed"}).eq("id", project_id).execute()
644
+ report = "\n".join(lines)
645
+
646
+ if variant != "full":
647
+ try:
648
+ generated = await self._generate_report_variant_with_agent(project, report, variant)
649
+ report = generated or self._build_fallback_variant(project, tasks, variant) or report
650
+ except Exception as exc:
651
+ logger.warning(f"Report variant generation failed: {exc}")
652
+ report = self._build_fallback_variant(project, tasks, variant) or report
653
+
654
+ return {
655
+ "project_id": project_id,
656
+ "project_name": project["name"],
657
+ "task_count": kept_task_count,
658
+ "variant": variant,
659
+ "report": clean_report_text(dedupe_lines(report)),
660
+ "charts": _build_report_charts(curated_tasks)
661
+ }
662
+
663
+ async def decompose_project(self, project_id: str):
664
+ """
665
+ Uses a Planner agent to decompose a project into discrete tasks.
666
+ """
667
+ project = supabase.table("projects").select("*").eq("id", project_id).single().execute().data
668
+ owner_id = project.get("owner_id")
669
+
670
+ # Find a Planner agent, prioritizing Groq as requested
671
+ agents = supabase.table("agents").select("*").execute().data or []
672
+
673
+ # 1. Try to find an existing Groq Planner
674
+ planner_agent_data = next(
675
+ (a for a in agents if "Planner" in a["name"] and a.get("api_provider") == "groq"),
676
+ None
677
+ )
678
+
679
+ # 2. If not found, try any Planner
680
+ if not planner_agent_data:
681
+ planner_agent_data = next(
682
+ (a for a in agents if "Planner" in a["name"] and a.get("user_id") in (None, owner_id)),
683
+ next((a for a in agents if a.get("user_id") in (None, owner_id)), None)
684
+ )
685
+
686
+ # 3. If still no agent, or it's OpenAI but we want Groq, create a temporary one
687
+ if not planner_agent_data or (planner_agent_data.get("api_provider") == "openai" and not settings.OPENAI_API_KEY):
688
+ logger.info("Using default Groq Planner for decomposition.")
689
+ planner = AgentFactory.get_agent(
690
+ provider="groq",
691
+ name="System Planner",
692
+ role="Project Decomposer",
693
+ model="llama-3.3-70b-versatile",
694
+ system_prompt="You decompose goals into clear, ordered implementation tasks."
695
+ )
696
+ else:
697
+ planner = AgentFactory.get_agent(
698
+ provider=planner_agent_data["api_provider"],
699
+ name=planner_agent_data["name"],
700
+ role=planner_agent_data["role"],
701
+ model=planner_agent_data["model"],
702
+ system_prompt=planner_agent_data.get("system_prompt")
703
+ )
704
+
705
+ prompt = f"""Decompose the following project into 3-5 clear, actionable implementation tasks.
706
+ Project Name: {project['name']}
707
+ Description: {project['description']}
708
+ Context: {project.get('context', 'None')}
709
+
710
+ ### Output Requirements:
711
+ You MUST return a valid JSON array of objects. Each object represents a task.
712
+ Do not include any conversational text, markdown formatting outside of the JSON, or explanations.
713
+
714
+ ### JSON Schema:
715
+ [
716
+ {{
717
+ "title": "string (The name of the task)",
718
+ "description": "string (Detailed instructions for the agent)",
719
+ "priority": "integer (1-5, where 5 is highest priority)"
720
+ }}
721
+ ]
722
+
723
+ IMPORTANT: Return a flat array. Do not wrap it in a parent 'tasks' object.
724
+ Do not use placeholder names or generic filler tasks. Every task title must be concrete and directly relevant to the stated project.
725
+ """
726
+
727
+ try:
728
+ result = await planner.run(prompt, [])
729
+ tasks_data = result.get("data")
730
+
731
+ # Handle common LLM wrapping patterns
732
+ if isinstance(tasks_data, dict):
733
+ if "tasks" in tasks_data and isinstance(tasks_data["tasks"], list):
734
+ tasks_data = tasks_data["tasks"]
735
+ else:
736
+ tasks_data = [tasks_data]
737
+
738
+ if not isinstance(tasks_data, list):
739
+ raise ValueError(f"Agent returned invalid format: {type(tasks_data)}. Expected list or dict.")
740
+
741
+ # Filter out invalid tasks
742
+ valid_tasks = [
743
+ t for t in tasks_data
744
+ if isinstance(t, dict) and t.get("title")
745
+ ]
746
+
747
+ if not valid_tasks:
748
+ raise ValueError("No valid tasks extracted from agent output.")
749
+
750
+ # Insert tasks
751
+ from .project_service import project_service
752
+ await project_service.add_tasks_to_project(project_id, valid_tasks)
753
+ logger.info(f"Auto-decomposed project {project_id} into {len(valid_tasks)} tasks.")
754
+ except Exception as e:
755
+ logger.error(f"Project decomposition failed: {e}")
756
+
757
+ def _resolve_agent(self, task: dict, available_agents: list[dict]):
758
+ assigned_agent_id = task.get("assigned_agent_id")
759
+ if assigned_agent_id:
760
+ return next((agent for agent in available_agents if agent["id"] == assigned_agent_id), None)
761
+ return available_agents[0] if available_agents else None
762
+
763
+ async def _run_task(self, task: dict, agent_data: dict):
764
+ await AgentRunnerService.run_agent_task(
765
+ task,
766
+ agent_data,
767
+ start_action="orchestrator_execution_start",
768
+ start_content=f"Orchestrator assigned {agent_data['name']} to task: {task['title']}",
769
+ complete_action="orchestrator_execution_complete",
770
+ complete_content="Task completed and is awaiting approval."
771
+ )
772
+
773
+ orchestrator_service = OrchestratorService()
backend/services/output_quality.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import re
3
+ from collections import OrderedDict
4
+ from typing import Any
5
+
6
+ PLACEHOLDER_PATTERNS = [
7
+ r"\bCompetitor\s+[A-Z]\b",
8
+ r"\bDashboard\s+[A-Z]\b",
9
+ r"\bProduct\s+[A-Z]\b",
10
+ r"\bCompany\s+[A-Z]\b",
11
+ r"\bOur Company\b",
12
+ ]
13
+
14
+ GENERIC_FILLER_PATTERNS = [
15
+ r"\bsustainable products?\b",
16
+ r"\bdigital marketing\b",
17
+ r"\bcustomer segments?\b",
18
+ r"\bdemographics\b",
19
+ r"\bpsychographics\b",
20
+ r"\bdistribution channels?\b",
21
+ ]
22
+
23
+ SENSITIVE_FACT_PATTERNS = [
24
+ r"\bmarket share\b",
25
+ r"\brevenue\b",
26
+ r"\barr\b",
27
+ r"\bpricing\b",
28
+ r"\bprice\b",
29
+ r"\blatest release version\b",
30
+ r"\bprofit\b",
31
+ ]
32
+
33
+ RAW_DUMP_PATTERNS = [
34
+ r"```(?:json)?",
35
+ r'"raw_text"\s*:',
36
+ r'"projectoverview"\s*:',
37
+ r'"projectoverview"\s*:',
38
+ r'"userstories"\s*:',
39
+ r'"datamodel"\s*:',
40
+ ]
41
+
42
+ LATAM_HINTS = [
43
+ "mercadolibre",
44
+ "mercado libre",
45
+ "latam",
46
+ "latin america",
47
+ "argentina",
48
+ "mexico",
49
+ "brazil",
50
+ "brasil",
51
+ "chile",
52
+ "colombia",
53
+ "peru",
54
+ "uruguay",
55
+ ]
56
+
57
+ SEA_HINTS = [
58
+ "indonesia",
59
+ "yogyakarta",
60
+ "bali",
61
+ "southeast asia",
62
+ "tokopedia",
63
+ "shopee",
64
+ "jakarta",
65
+ ]
66
+
67
+ STRICT_TASK_PATTERNS = [
68
+ r"\bresearch\b",
69
+ r"\banaly[sz]e\b",
70
+ r"\banalysis\b",
71
+ r"\bcompetitor\b",
72
+ r"\bpricing\b",
73
+ r"\bmarket\b",
74
+ r"\baudit\b",
75
+ r"\breport\b",
76
+ r"\bcompare\b",
77
+ ]
78
+
79
+
80
+ def _stringify_payload(value: Any) -> str:
81
+ if value is None:
82
+ return ""
83
+ if isinstance(value, str):
84
+ return value
85
+ try:
86
+ return json.dumps(value, ensure_ascii=True)
87
+ except Exception:
88
+ return str(value)
89
+
90
+
91
+ def build_quality_instructions(task: dict) -> str:
92
+ project_text = _project_text(task)
93
+ task_text = f"{task.get('title', '')}\n{task.get('description', '')}\n{project_text}".lower()
94
+ strict_mode = any(re.search(pattern, task_text, re.IGNORECASE) for pattern in STRICT_TASK_PATTERNS)
95
+
96
+ base = [
97
+ "Output quality rules:",
98
+ "- Never use placeholder names like Competitor A, Dashboard B, Product C, or Our Company.",
99
+ "- If a real named entity cannot be identified with confidence, return unknown instead of inventing one.",
100
+ "- Keep the output strictly within the requested scope.",
101
+ "- Stay aligned with the project's stated geography, competitors, and market context. Do not switch regions or industries unless the task explicitly requires it.",
102
+ "- Do not include generic filler sections that were not requested.",
103
+ "- Use clean UTF-8/ASCII friendly text. Do not output corrupted characters.",
104
+ "- Do not return raw JSON dumps, code blocks, repository scaffolds, or intermediate planning artifacts unless the task explicitly asks for them.",
105
+ ]
106
+
107
+ if strict_mode:
108
+ base.extend(
109
+ [
110
+ "- Return structured JSON where possible.",
111
+ "- For factual claims about competitors, products, pricing, versions, revenue, market share, or benchmarks, include source_url when available.",
112
+ "- Do not invent pricing, release versions, market share, revenue, ARR impact, or benchmarks.",
113
+ "- If a sensitive fact cannot be verified, omit it or mark it unknown.",
114
+ ]
115
+ )
116
+
117
+ return "\n".join(base)
118
+
119
+
120
+ def _project_text(task: dict) -> str:
121
+ project = task.get("project")
122
+ if isinstance(project, dict):
123
+ return "\n".join(
124
+ str(project.get(key, "") or "")
125
+ for key in ("name", "description", "context")
126
+ )
127
+ return str(task.get("project_context") or "")
128
+
129
+
130
+ def _contains_any(text: str, terms: list[str]) -> bool:
131
+ lowered = text.lower()
132
+ return any(term in lowered for term in terms)
133
+
134
+
135
+ def _looks_like_raw_dump(text: str) -> bool:
136
+ # Extremely relaxed check: Only flag as raw dump if it contains internal system keys
137
+ # that indicate it's a raw unformatted API response rather than a report.
138
+ internal_keys = [r'"raw_text"\s*:', r'"internal_status"\s*:', r'"debug_info"\s*:']
139
+ if any(re.search(pattern, text, re.IGNORECASE) for pattern in internal_keys):
140
+ return True
141
+
142
+ return False
143
+
144
+
145
+ def _is_context_drift(task_text: str, output_text: str) -> bool:
146
+ task_lower = task_text.lower()
147
+ output_lower = output_text.lower()
148
+
149
+ if _contains_any(task_lower, LATAM_HINTS) and _contains_any(output_lower, SEA_HINTS):
150
+ return True
151
+
152
+ return False
153
+
154
+
155
+ def validate_output(task: dict, result: dict) -> dict:
156
+ raw_text = _stringify_payload(result.get("raw_output"))
157
+ data_text = _stringify_payload(result.get("data"))
158
+ combined = "\n".join(part for part in [raw_text, data_text] if part).strip()
159
+ task_text = "\n".join(
160
+ [
161
+ str(task.get("title", "") or ""),
162
+ str(task.get("description", "") or ""),
163
+ _project_text(task),
164
+ ]
165
+ )
166
+
167
+ fail_reasons: list[str] = []
168
+ must_fix: list[str] = []
169
+ placeholder_entities: list[str] = []
170
+ unsupported_claims: list[str] = []
171
+ duplicate_claims: list[str] = []
172
+ encoding_issues: list[str] = []
173
+
174
+ if not combined:
175
+ fail_reasons.append("Empty output.")
176
+
177
+ for pattern in PLACEHOLDER_PATTERNS:
178
+ matches = re.findall(pattern, combined, re.IGNORECASE)
179
+ placeholder_entities.extend(matches)
180
+
181
+ if placeholder_entities:
182
+ # We don't add to fail_reasons anymore, just let the score reduction handle it
183
+ pass
184
+
185
+ if "■" in combined:
186
+ encoding_issues.append("Found corrupted character '■'.")
187
+
188
+ if encoding_issues:
189
+ fail_reasons.append("Output contains encoding corruption.")
190
+ must_fix.append("Remove corrupted characters and normalize text encoding.")
191
+
192
+ if _looks_like_raw_dump(combined):
193
+ fail_reasons.append("Output contains raw JSON/code dump instead of a usable task result.")
194
+ must_fix.append("Convert intermediate JSON/code output into the requested final artifact.")
195
+
196
+ if _is_context_drift(task_text, combined):
197
+ fail_reasons.append("Output drifted away from the project's stated geography or market context.")
198
+ must_fix.append("Regenerate the output using the project's explicit region, competitor set, and business context.")
199
+
200
+ for pattern in GENERIC_FILLER_PATTERNS:
201
+ if re.search(pattern, combined, re.IGNORECASE):
202
+ unsupported_claims.append(pattern.replace("\\b", "").replace("?", ""))
203
+
204
+ if unsupported_claims:
205
+ fail_reasons.append("Output contains generic filler outside the likely project scope.")
206
+ must_fix.append("Remove generic business-analysis filler not tied to the requested task.")
207
+
208
+ has_source_url = bool(re.search(r"https?://", combined, re.IGNORECASE))
209
+ for pattern in SENSITIVE_FACT_PATTERNS:
210
+ if re.search(pattern, combined, re.IGNORECASE) and not has_source_url:
211
+ unsupported_claims.append(f"Sensitive fact without source: {pattern}")
212
+
213
+ if any(item.startswith("Sensitive fact without source:") for item in unsupported_claims):
214
+ # We don't add to fail_reasons anymore, just let the score reduction handle it
215
+ pass
216
+
217
+ normalized_lines = []
218
+ seen_lines: set[str] = set()
219
+ for line in combined.splitlines():
220
+ normalized = re.sub(r"\s+", " ", line).strip().lower()
221
+ if len(normalized) < 20:
222
+ continue
223
+ if normalized in seen_lines:
224
+ duplicate_claims.append(line.strip())
225
+ else:
226
+ seen_lines.add(normalized)
227
+ normalized_lines.append(normalized)
228
+
229
+ if duplicate_claims:
230
+ # Just let the score reduction handle it
231
+ pass
232
+
233
+ score = 100
234
+ if placeholder_entities:
235
+ score = min(score, 20)
236
+ if _looks_like_raw_dump(combined):
237
+ score = min(score, 20)
238
+ if _is_context_drift(task_text, combined):
239
+ score = min(score, 20)
240
+ if any(item.startswith("Sensitive fact without source:") for item in unsupported_claims):
241
+ score = min(score, 30)
242
+ if duplicate_claims:
243
+ score = min(score, 50)
244
+ if unsupported_claims and not any(item.startswith("Sensitive fact without source:") for item in unsupported_claims):
245
+ score = min(score, 60)
246
+ if encoding_issues:
247
+ score = min(score, 60)
248
+ if not combined:
249
+ score = 0
250
+
251
+ approved = score >= 20
252
+ return {
253
+ "approved": approved,
254
+ "score": score,
255
+ "fail_reasons": fail_reasons,
256
+ "must_fix": must_fix,
257
+ "duplicate_claims": list(OrderedDict.fromkeys(duplicate_claims))[:10],
258
+ "unsupported_claims": list(OrderedDict.fromkeys(unsupported_claims))[:10],
259
+ "placeholder_entities": list(OrderedDict.fromkeys(placeholder_entities))[:10],
260
+ "encoding_issues": encoding_issues,
261
+ }
262
+
263
+
264
+ def report_text_from_output(output_data: Any) -> str:
265
+ if not output_data:
266
+ return ""
267
+ if isinstance(output_data, dict):
268
+ primary = output_data.get("data") or output_data.get("final") or output_data.get("raw_output") or output_data
269
+ else:
270
+ primary = output_data
271
+ return _stringify_payload(primary)
272
+
273
+
274
+ def clean_report_text(text: str) -> str:
275
+ cleaned = text.replace("■", "-").replace("\u25A0", "-")
276
+ cleaned = re.sub(r"[ \t]+", " ", cleaned)
277
+ cleaned = re.sub(r"\n{3,}", "\n\n", cleaned)
278
+ return cleaned.strip()
279
+
280
+
281
+ def dedupe_lines(text: str) -> str:
282
+ lines = text.splitlines()
283
+ kept: list[str] = []
284
+ seen: set[str] = set()
285
+ for line in lines:
286
+ normalized = re.sub(r"\s+", " ", line).strip().lower()
287
+ if normalized and len(normalized) > 15 and normalized in seen:
288
+ continue
289
+ if normalized:
290
+ seen.add(normalized)
291
+ kept.append(line)
292
+ return "\n".join(kept).strip()
293
+
294
+
295
+ def filter_report_sections(text: str) -> tuple[str, list[str]]:
296
+ excluded: list[str] = []
297
+ kept_lines: list[str] = []
298
+ for line in text.splitlines():
299
+ lowered = line.lower()
300
+ if any(re.search(pattern, lowered, re.IGNORECASE) for pattern in PLACEHOLDER_PATTERNS):
301
+ excluded.append("Removed placeholder content.")
302
+ continue
303
+ if any(re.search(pattern, lowered, re.IGNORECASE) for pattern in GENERIC_FILLER_PATTERNS):
304
+ excluded.append("Removed generic filler outside the requested scope.")
305
+ continue
306
+ if _looks_like_raw_dump(line):
307
+ excluded.append("Removed raw JSON/code dump content.")
308
+ continue
309
+ kept_lines.append(line)
310
+ return "\n".join(kept_lines).strip(), excluded
311
+
312
+
backend/services/project_service.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from services.supabase_service import supabase
2
+ from typing import List, Dict, Any
3
+ import logging
4
+
5
+ logger = logging.getLogger("uvicorn")
6
+
7
+ class ProjectService:
8
+ """
9
+ Handles the creation and management of projects and their constituent tasks.
10
+ """
11
+
12
+ @staticmethod
13
+ async def create_project(title: str, description: str, user_id: str) -> Dict[str, Any]:
14
+ res = supabase.table("projects").insert({
15
+ "title": title,
16
+ "description": description,
17
+ "user_id": user_id,
18
+ "status": "active"
19
+ }).execute()
20
+ return res.data[0]
21
+
22
+ @staticmethod
23
+ async def add_tasks_to_project(project_id: str, tasks: List[Dict[str, Any]]):
24
+ """
25
+ Adds a list of tasks to a project.
26
+ tasks: [{"title": "...", "description": "...", "assigned_agent_id": "..."}]
27
+ """
28
+ formatted_tasks = [
29
+ {**task, "project_id": project_id, "status": "todo"}
30
+ for task in tasks
31
+ ]
32
+ supabase.table("tasks").insert(formatted_tasks).execute()
33
+ logger.info(f"Added {len(tasks)} tasks to project {project_id}")
34
+
35
+ project_service = ProjectService()
backend/services/semantic_backprop.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import re
2
+ import logging
3
+ from typing import List, Dict, Any
4
+ from services.supabase_service import supabase
5
+
6
+ logger = logging.getLogger("uvicorn")
7
+
8
+ class SemanticBackpropService:
9
+ """
10
+ Ensures numerical consistency across agent tasks by extracting 'Canonical Numbers'
11
+ from previous task outputs.
12
+ """
13
+
14
+ @staticmethod
15
+ async def get_project_context(project_id: str, current_task_id: str) -> str:
16
+ """
17
+ Fetches and extracts canonical figures from all completed sibling tasks.
18
+ """
19
+ try:
20
+ resp = supabase.table("tasks") \
21
+ .select("title, output_data") \
22
+ .eq("project_id", project_id) \
23
+ .eq("status", "done") \
24
+ .neq("id", current_task_id) \
25
+ .execute()
26
+
27
+ if not resp.data:
28
+ return ""
29
+
30
+ canonical_blocks = []
31
+ topic_blocks = []
32
+
33
+ for task in resp.data:
34
+ output = task.get("output_data") or {}
35
+ # Handle different output formats (raw string or dict with 'result')
36
+ result_text = ""
37
+ if isinstance(output, dict):
38
+ result_text = output.get("result", "") or output.get("raw_output", "")
39
+ elif isinstance(output, str):
40
+ result_text = output
41
+
42
+ if not result_text:
43
+ continue
44
+
45
+ # Extract financial and numerical lines
46
+ lines = result_text.splitlines()
47
+ financial_lines = []
48
+
49
+ # Keywords that often indicate a 'canonical' number
50
+ keywords = [
51
+ "$", "%", "USD", "MRR", "ARR", "ROI", "cost", "budget",
52
+ "revenue", "price", "fee", "estimate", "total", "quota"
53
+ ]
54
+
55
+ for line in lines:
56
+ if any(k.lower() in line.lower() for k in keywords):
57
+ if len(line.strip()) > 5: # Ignore very short lines
58
+ financial_lines.append(line.strip())
59
+
60
+ if financial_lines:
61
+ # De-duplicate similar lines
62
+ seen = set()
63
+ unique_fin = []
64
+ for fl in financial_lines:
65
+ key = fl[:50]
66
+ if key not in seen:
67
+ seen.add(key)
68
+ unique_fin.append(fl)
69
+
70
+ canonical_blocks.append(
71
+ f"Source Task: **{task['title']}**\n" +
72
+ "\n".join(f" • {fl}" for fl in unique_fin[:8])
73
+ )
74
+
75
+ # Also track what topics were covered to avoid repetition
76
+ topic_blocks.append(f"- **{task['title']}**: (Covered in previous step)")
77
+
78
+ if not canonical_blocks and not topic_blocks:
79
+ return ""
80
+
81
+ context = "\n---\n"
82
+ if canonical_blocks:
83
+ context += (
84
+ "### ⚠️ CANONICAL FIGURES — PREVIOUSLY ESTABLISHED\n"
85
+ "> **MANDATORY RULE**: The following numbers and figures were established by agents\n"
86
+ "> responsible for those domains. You MUST use these exact values if you reference them.\n"
87
+ "> DO NOT re-calculate or propose alternative values for these specific items.\n\n"
88
+ )
89
+ context += "\n\n".join(canonical_blocks) + "\n\n"
90
+
91
+ if topic_blocks:
92
+ context += (
93
+ "### 📋 PREVIOUSLY COVERED TOPICS\n"
94
+ "> Do not repeat the analysis of these topics. Focus only on your specific task.\n"
95
+ )
96
+ context += "\n".join(topic_blocks) + "\n"
97
+
98
+ return context
99
+
100
+ except Exception as e:
101
+ logger.error(f"Semantic Backprop failed: {e}")
102
+ return ""
103
+
104
+ semantic_backprop = SemanticBackpropService()
backend/services/supabase_service.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from supabase import create_client, Client
2
+ from services.config import settings
3
+
4
+ def get_supabase_client() -> Client:
5
+ """
6
+ Initializes and returns a Supabase client.
7
+ """
8
+ if not settings.SUPABASE_URL or not settings.SUPABASE_SERVICE_ROLE_KEY:
9
+ raise ValueError("SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY must be set in environment.")
10
+
11
+ return create_client(settings.SUPABASE_URL, settings.SUPABASE_SERVICE_ROLE_KEY)
12
+
13
+ supabase: Client = get_supabase_client()
backend/services/task_queue.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import Optional, List
3
+ from .supabase_service import supabase
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ class TaskQueueService:
8
+ @staticmethod
9
+ async def queue_task(task_id: str):
10
+ """
11
+ Marks a task as 'queued' in the database.
12
+ """
13
+ try:
14
+ result = supabase.table("tasks").update({"status": "queued"}).eq("id", task_id).execute()
15
+ return result
16
+ except Exception as e:
17
+ logger.error(f"Error queueing task {task_id}: {e}")
18
+ return None
19
+
20
+ @staticmethod
21
+ async def get_next_queued_task():
22
+ """
23
+ Fetches the next available task from the queue.
24
+ """
25
+ try:
26
+ # Fetch one task that is in 'queued' status, ordered by priority and created_at
27
+ result = supabase.table("tasks") \
28
+ .select("*") \
29
+ .eq("status", "queued") \
30
+ .order("priority", desc=True) \
31
+ .order("created_at") \
32
+ .limit(1) \
33
+ .execute()
34
+
35
+ if result.data:
36
+ return result.data[0]
37
+ return None
38
+ except Exception as e:
39
+ logger.error(f"Error fetching next queued task: {e}")
40
+ return None
41
+
42
+ @staticmethod
43
+ async def mark_in_progress(task_id: str):
44
+ """
45
+ Marks a task as 'in_progress'.
46
+ """
47
+ return supabase.table("tasks").update({"status": "in_progress"}).eq("id", task_id).execute()
backend/tools/browser.py ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from typing import Any
3
+
4
+ import httpx
5
+ from playwright.async_api import async_playwright
6
+
7
+ from services.config import settings
8
+
9
+ logger = logging.getLogger("uvicorn")
10
+
11
+
12
+ class BrowserTool:
13
+ """
14
+ Tools for live web search and direct URL extraction.
15
+ """
16
+
17
+ def __init__(self) -> None:
18
+ self.tavily_api_key = settings.TAVILY_API_KEY
19
+
20
+ async def search_and_extract(self, url: str) -> str:
21
+ """
22
+ Navigates to a URL and returns the page text content.
23
+ """
24
+ logger.info("BrowserTool: Navigating to %s", url)
25
+ async with async_playwright() as playwright:
26
+ browser = await playwright.chromium.launch(headless=True)
27
+ page = await browser.new_page()
28
+ try:
29
+ await page.goto(url, wait_until="networkidle", timeout=30000)
30
+ title = await page.title()
31
+ content = await page.inner_text("body")
32
+ combined = f"Title: {title}\nURL: {url}\n\n{content}".strip()
33
+ return combined[:12000]
34
+ except Exception as exc:
35
+ logger.error("BrowserTool extract error for %s: %s", url, exc)
36
+ return f"Error accessing {url}: {exc}"
37
+ finally:
38
+ await browser.close()
39
+
40
+ async def web_search(self, query: str, topic: str = "general", max_results: int = 5) -> str:
41
+ """
42
+ Searches the public web with Tavily and returns LLM-friendly results.
43
+ """
44
+ if not self.tavily_api_key:
45
+ return (
46
+ "Web search is unavailable: TAVILY_API_KEY is not configured. "
47
+ "Add it to the backend environment to enable internet search."
48
+ )
49
+
50
+ payload = {
51
+ "query": query,
52
+ "topic": topic if topic in {"general", "news", "finance"} else "general",
53
+ "search_depth": "advanced",
54
+ "max_results": max(1, min(max_results, 10)),
55
+ "include_answer": "advanced",
56
+ "include_raw_content": False,
57
+ "include_images": False,
58
+ }
59
+
60
+ headers = {
61
+ "Authorization": f"Bearer {self.tavily_api_key}",
62
+ "Content-Type": "application/json",
63
+ }
64
+
65
+ try:
66
+ async with httpx.AsyncClient(timeout=45.0) as client:
67
+ response = await client.post(
68
+ "https://api.tavily.com/search",
69
+ headers=headers,
70
+ json=payload,
71
+ )
72
+ response.raise_for_status()
73
+ except httpx.HTTPStatusError as exc:
74
+ detail = exc.response.text[:500] if exc.response is not None else str(exc)
75
+ logger.error("Tavily HTTP error: %s", detail)
76
+ return f"Tavily search failed with status {exc.response.status_code}: {detail}"
77
+ except Exception as exc:
78
+ logger.error("Tavily request error: %s", exc)
79
+ return f"Tavily search failed: {exc}"
80
+
81
+ data = response.json()
82
+ return self._format_tavily_results(query, data)
83
+
84
+ def _format_tavily_results(self, query: str, data: dict[str, Any]) -> str:
85
+ answer = data.get("answer")
86
+ results = data.get("results") or []
87
+
88
+ lines = [f"Search query: {query}"]
89
+ if answer:
90
+ lines.extend(["", "Answer:", str(answer).strip()])
91
+
92
+ if not results:
93
+ lines.extend(["", "No search results returned."])
94
+ return "\n".join(lines)
95
+
96
+ lines.extend(["", "Sources:"])
97
+ for index, result in enumerate(results, start=1):
98
+ title = result.get("title") or "Untitled"
99
+ url = result.get("url") or ""
100
+ snippet = (result.get("content") or "").strip()
101
+ score = result.get("score")
102
+
103
+ lines.append(f"{index}. {title}")
104
+ if url:
105
+ lines.append(f" URL: {url}")
106
+ if score is not None:
107
+ lines.append(f" Score: {score}")
108
+ if snippet:
109
+ lines.append(f" Snippet: {snippet[:900]}")
110
+
111
+ return "\n".join(lines)[:12000]
backend/tools/decomposer.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from services.project_service import project_service
2
+ from typing import List, Dict, Any
3
+ import logging
4
+
5
+ logger = logging.getLogger("uvicorn")
6
+
7
+ class DecompositionTool:
8
+ """
9
+ A tool that allows agents to break down complex goals into actionable tasks.
10
+ """
11
+ async def create_subtasks(self, project_id: str, tasks: List[Dict[str, Any]]) -> str:
12
+ """
13
+ Takes a list of task definitions and adds them to the database for the given project.
14
+ """
15
+ logger.info(f"DecompositionTool: Creating {len(tasks)} subtasks for project {project_id}")
16
+ try:
17
+ await project_service.add_tasks_to_project(project_id, tasks)
18
+ return f"Successfully created {len(tasks)} subtasks."
19
+ except Exception as e:
20
+ return f"Failed to create subtasks: {str(e)}"
backend/tools/file_generator.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import pandas as pd
3
+ from reportlab.lib.pagesizes import letter
4
+ from reportlab.pdfgen import canvas
5
+ from datetime import datetime
6
+ import logging
7
+
8
+ logger = logging.getLogger("uvicorn")
9
+
10
+ class FileGeneratorTool:
11
+ """
12
+ A tool that allows agents to generate PDF and Excel files.
13
+ """
14
+ def __init__(self):
15
+ self.output_dir = "outputs"
16
+ os.makedirs(self.output_dir, exist_ok=True)
17
+
18
+ def _generate_filename(self, extension: str) -> str:
19
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
20
+ return os.path.join(self.output_dir, f"report_{timestamp}.{extension}")
21
+
22
+ async def generate_pdf(self, title: str, content: str) -> str:
23
+ """
24
+ Generates a PDF document with the provided title and content.
25
+ """
26
+ filename = self._generate_filename("pdf")
27
+ logger.info(f"FileGenerator: Generating PDF {filename}")
28
+
29
+ try:
30
+ c = canvas.Canvas(filename, pagesize=letter)
31
+ width, height = letter
32
+
33
+ # Title
34
+ c.setFont("Helvetica-Bold", 16)
35
+ c.drawString(72, height - 72, title)
36
+
37
+ # Content (very basic wrapping/split)
38
+ c.setFont("Helvetica", 12)
39
+ text_object = c.beginText(72, height - 100)
40
+ for line in content.split('\n'):
41
+ text_object.textLine(line)
42
+ c.drawText(text_object)
43
+
44
+ c.save()
45
+ return f"PDF generated successfully: {filename}"
46
+ except Exception as e:
47
+ return f"Failed to generate PDF: {str(e)}"
48
+
49
+ async def generate_excel(self, data: list) -> str:
50
+ """
51
+ Generates an Excel file from a list of dictionaries.
52
+ """
53
+ filename = self._generate_filename("xlsx")
54
+ logger.info(f"FileGenerator: Generating Excel {filename}")
55
+
56
+ try:
57
+ df = pd.DataFrame(data)
58
+ df.to_excel(filename, index=False)
59
+ return f"Excel generated successfully: {filename}"
60
+ except Exception as e:
61
+ return f"Failed to generate Excel: {str(e)}"
backend/tools/registry.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .file_generator import FileGeneratorTool
2
+ from .decomposer import DecompositionTool
3
+ from .sre import SRETool
4
+ from .browser import BrowserTool
5
+ from .sandbox import CodeSandboxTool
6
+ from .visuals import VisualsTool
7
+ from typing import Any, Dict, List
8
+
9
+ class ToolRegistry:
10
+ def __init__(self):
11
+ self.browser = BrowserTool()
12
+ self.sandbox = CodeSandboxTool()
13
+ self.file_gen = FileGeneratorTool()
14
+ self.decomposer = DecompositionTool()
15
+ self.sre = SRETool()
16
+ self.visuals = VisualsTool()
17
+ self.tools = {
18
+ "web_search": {
19
+ "func": self.browser.web_search,
20
+ "description": "Searches the public web using Tavily and returns summarized results with source URLs."
21
+ },
22
+ "extract_url": {
23
+ "func": self.browser.search_and_extract,
24
+ "description": "Extracts text content from a specific URL."
25
+ },
26
+ "execute_python": {
27
+ "func": self.sandbox.execute_python,
28
+ "description": "Executes Python code and returns the output."
29
+ },
30
+ "generate_pdf": {
31
+ "func": self.file_gen.generate_pdf,
32
+ "description": "Generates a PDF document."
33
+ },
34
+ "generate_excel": {
35
+ "func": self.file_gen.generate_excel,
36
+ "description": "Generates an Excel spreadsheet from structured data."
37
+ },
38
+ "create_subtasks": {
39
+ "func": self.decomposer.create_subtasks,
40
+ "description": "Breaks down a goal into a list of actionable tasks."
41
+ },
42
+ "generate_chart": {
43
+ "func": self.visuals.generate_chart,
44
+ "description": "Generates a chart image (bar, line, pie) from a JSON config."
45
+ },
46
+ "generate_illustration": {
47
+ "func": self.visuals.generate_illustration,
48
+ "description": "Generates an AI illustration or drawing based on a text prompt."
49
+ },
50
+ "get_system_health": {
51
+ "func": self.sre.get_system_health,
52
+ "description": "Returns system health metrics (CPU, Memory, Disk)."
53
+ },
54
+ "check_service_status": {
55
+ "func": self.sre.check_service_status,
56
+ "description": "Checks if a specific service or process is running."
57
+ },
58
+ "run_patch_command": {
59
+ "func": self.sre.run_patch_command,
60
+ "description": "Executes a safe system patch command (e.g., git pull, npm install)."
61
+ }
62
+ }
63
+
64
+ def get_tool_definitions(self) -> List[Dict[str, Any]]:
65
+ """
66
+ Returns OpenAI-style tool definitions.
67
+ """
68
+ return [
69
+ {
70
+ "type": "function",
71
+ "function": {
72
+ "name": "web_search",
73
+ "description": "Search the public web for information using Tavily. Use this when the task requires current external information.",
74
+ "parameters": {
75
+ "type": "object",
76
+ "properties": {
77
+ "query": {"type": "string", "description": "The search query"},
78
+ "topic": {
79
+ "type": "string",
80
+ "enum": ["general", "news", "finance"],
81
+ "description": "The search category. Use news for recent events and finance for market/company financial queries."
82
+ },
83
+ "max_results": {
84
+ "type": "integer",
85
+ "description": "Maximum number of results to return. Keep this small to control context size.",
86
+ "default": 5
87
+ }
88
+ },
89
+ "required": ["query"]
90
+ }
91
+ }
92
+ },
93
+ {
94
+ "type": "function",
95
+ "function": {
96
+ "name": "extract_url",
97
+ "description": "Extract text content from a URL",
98
+ "parameters": {
99
+ "type": "object",
100
+ "properties": {
101
+ "url": {"type": "string", "description": "The URL to extract from"}
102
+ },
103
+ "required": ["url"]
104
+ }
105
+ }
106
+ },
107
+ {
108
+ "type": "function",
109
+ "function": {
110
+ "name": "execute_python",
111
+ "description": "Execute Python code to perform calculations, data analysis, or logic verification.",
112
+ "parameters": {
113
+ "type": "object",
114
+ "properties": {
115
+ "code": {"type": "string", "description": "The Python code to execute"}
116
+ },
117
+ "required": ["code"]
118
+ }
119
+ }
120
+ },
121
+ {
122
+ "type": "function",
123
+ "function": {
124
+ "name": "generate_pdf",
125
+ "description": "Create a professional PDF report",
126
+ "parameters": {
127
+ "type": "object",
128
+ "properties": {
129
+ "title": {"type": "string", "description": "The title of the report"},
130
+ "content": {"type": "string", "description": "The text content of the report"}
131
+ },
132
+ "required": ["title", "content"]
133
+ }
134
+ }
135
+ },
136
+ {
137
+ "type": "function",
138
+ "function": {
139
+ "name": "generate_excel",
140
+ "description": "Create an Excel spreadsheet from data",
141
+ "parameters": {
142
+ "type": "object",
143
+ "properties": {
144
+ "data": {
145
+ "type": "array",
146
+ "items": {"type": "object"},
147
+ "description": "List of rows as objects"
148
+ }
149
+ },
150
+ "required": ["data"]
151
+ }
152
+ }
153
+ },
154
+ {
155
+ "type": "function",
156
+ "function": {
157
+ "name": "create_subtasks",
158
+ "description": "Break down a complex goal into smaller, actionable tasks for other agents.",
159
+ "parameters": {
160
+ "type": "object",
161
+ "properties": {
162
+ "project_id": {"type": "string", "description": "The current project UUID"},
163
+ "tasks": {
164
+ "type": "array",
165
+ "items": {
166
+ "type": "object",
167
+ "properties": {
168
+ "title": {"type": "string", "description": "Clear title of the subtask"},
169
+ "description": {"type": "string", "description": "Detailed instructions for the next agent"},
170
+ "assigned_agent_id": {"type": "string", "description": "The UUID of the agent to handle this task"}
171
+ },
172
+ "required": ["title", "description", "assigned_agent_id"]
173
+ }
174
+ }
175
+ },
176
+ "required": ["project_id", "tasks"]
177
+ }
178
+ }
179
+ },
180
+ {
181
+ "type": "function",
182
+ "function": {
183
+ "name": "generate_chart",
184
+ "description": "Generate a visual chart image (bar, line, pie, etc.) using QuickChart.io.",
185
+ "parameters": {
186
+ "type": "object",
187
+ "properties": {
188
+ "chart_type": {"type": "string", "enum": ["bar", "line", "pie", "doughnut"], "description": "Type of chart"},
189
+ "chart_config": {"type": "string", "description": "The JSON configuration for QuickChart (e.g., {type: 'bar', data: {...}})"}
190
+ },
191
+ "required": ["chart_type", "chart_config"]
192
+ }
193
+ }
194
+ },
195
+ {
196
+ "type": "function",
197
+ "function": {
198
+ "name": "generate_illustration",
199
+ "description": "Generate an AI illustration or drawing based on a prompt using Pollinations.ai.",
200
+ "parameters": {
201
+ "type": "object",
202
+ "properties": {
203
+ "prompt": {"type": "string", "description": "Detailed description of the illustration to generate"}
204
+ },
205
+ "required": ["prompt"]
206
+ }
207
+ }
208
+ },
209
+ {
210
+ "type": "function",
211
+ "function": {
212
+ "name": "get_system_health",
213
+ "description": "Monitor server vital signs like CPU usage, memory availability, and disk space.",
214
+ "parameters": {
215
+ "type": "object",
216
+ "properties": {}
217
+ }
218
+ }
219
+ },
220
+ {
221
+ "type": "function",
222
+ "function": {
223
+ "name": "check_service_status",
224
+ "description": "Verify if a critical service or process is currently active on the host.",
225
+ "parameters": {
226
+ "type": "object",
227
+ "properties": {
228
+ "service_name": {"type": "string", "description": "The name of the process or service to check"}
229
+ },
230
+ "required": ["service_name"]
231
+ }
232
+ }
233
+ },
234
+ {
235
+ "type": "function",
236
+ "function": {
237
+ "name": "run_patch_command",
238
+ "description": "Apply a system patch or update. Limited to safe commands like 'git pull' or 'npm install'.",
239
+ "parameters": {
240
+ "type": "object",
241
+ "properties": {
242
+ "command": {"type": "string", "description": "The restricted command to execute"}
243
+ },
244
+ "required": ["command"]
245
+ }
246
+ }
247
+ }
248
+ ]
249
+
250
+ async def call_tool(self, name: str, arguments: Dict[str, Any]) -> Any:
251
+ """
252
+ Executes a tool by name with provided arguments.
253
+ """
254
+ if name not in self.tools:
255
+ raise ValueError(f"Tool {name} not found")
256
+
257
+ func = self.tools[name]["func"]
258
+ return await func(**arguments)
259
+
260
+ tool_registry = ToolRegistry()
backend/tools/sandbox.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import io
3
+ import contextlib
4
+ import logging
5
+
6
+ logger = logging.getLogger("uvicorn")
7
+
8
+ class CodeSandboxTool:
9
+ """
10
+ A tool that allows agents to execute Python code and see the output.
11
+ """
12
+ async def execute_python(self, code: str) -> str:
13
+ """
14
+ Executes the provided Python code and returns the stdout/stderr.
15
+ """
16
+ logger.info("CodeSandboxTool: Executing Python code")
17
+
18
+ # Capture stdout and stderr
19
+ stdout = io.StringIO()
20
+ stderr = io.StringIO()
21
+
22
+ try:
23
+ with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
24
+ # Using a fresh globals dictionary for each execution
25
+ exec_globals = {}
26
+ exec(code, exec_globals)
27
+
28
+ output = stdout.getvalue()
29
+ errors = stderr.getvalue()
30
+
31
+ if errors:
32
+ return f"Output:\n{output}\nErrors:\n{errors}"
33
+ return output if output else "Execution successful (no output)."
34
+
35
+ except Exception as e:
36
+ return f"Execution failed: {str(e)}"
37
+ finally:
38
+ stdout.close()
39
+ stderr.close()
backend/tools/sre.py ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import psutil
2
+ import os
3
+ import platform
4
+ from typing import Dict, Any
5
+ import logging
6
+
7
+ logger = logging.getLogger("uvicorn")
8
+
9
+ class SRETool:
10
+ """
11
+ A toolset for Site Reliability Engineering (SRE) agents to monitor and manage system health.
12
+ """
13
+
14
+ async def get_system_health(self) -> Dict[str, Any]:
15
+ """
16
+ Returns real-time system health metrics (CPU, RAM, Disk).
17
+ """
18
+ logger.info("SRETool: Gathering system health metrics")
19
+ return {
20
+ "cpu_percent": psutil.cpu_percent(interval=1),
21
+ "memory": {
22
+ "total": psutil.virtual_memory().total,
23
+ "available": psutil.virtual_memory().available,
24
+ "percent": psutil.virtual_memory().percent
25
+ },
26
+ "disk": {
27
+ "total": psutil.disk_usage('/').total,
28
+ "used": psutil.disk_usage('/').used,
29
+ "percent": psutil.disk_usage('/').percent
30
+ },
31
+ "os": platform.system(),
32
+ "uptime": self._get_uptime()
33
+ }
34
+
35
+ async def check_service_status(self, service_name: str) -> str:
36
+ """
37
+ Checks if a specific service/process is running.
38
+ """
39
+ logger.info(f"SRETool: Checking status of {service_name}")
40
+ for proc in psutil.process_iter(['name']):
41
+ if service_name.lower() in proc.info['name'].lower():
42
+ return f"Service '{service_name}' is RUNNING."
43
+ return f"Service '{service_name}' is NOT running."
44
+
45
+ def _get_uptime(self) -> str:
46
+ # Simple uptime calculation
47
+ import time
48
+ boot_time = psutil.boot_time()
49
+ uptime_seconds = time.time() - boot_time
50
+ return f"{int(uptime_seconds // 3600)}h {int((uptime_seconds % 3600) // 60)}m"
51
+
52
+ async def run_patch_command(self, command: str) -> str:
53
+ """
54
+ Executes a restricted set of patching commands.
55
+ """
56
+ logger.warning(f"SRETool: Attempting to run patch command: {command}")
57
+
58
+ # Restricted whitelist for security
59
+ whitelist = ["npm install", "pip install", "git pull", "npm audit fix"]
60
+
61
+ is_safe = any(command.startswith(safe) for safe in whitelist)
62
+ if not is_safe:
63
+ return f"Command '{command}' is not in the safety whitelist. Patch rejected."
64
+
65
+ try:
66
+ import subprocess
67
+ result = subprocess.run(command, shell=True, capture_output=True, text=True, timeout=60)
68
+ if result.returncode == 0:
69
+ return f"Patch successful: {result.stdout}"
70
+ return f"Patch failed: {result.stderr}"
71
+ except Exception as e:
72
+ return f"Error executing patch: {str(e)}"
backend/tools/visuals.py ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import urllib.parse
2
+ import json
3
+ import logging
4
+
5
+ logger = logging.getLogger("uvicorn")
6
+
7
+ class VisualsTool:
8
+ """
9
+ Provides visual generation capabilities like charts and illustrations.
10
+ """
11
+
12
+ async def generate_chart(self, chart_type: str, chart_config: str) -> str:
13
+ """
14
+ Generates a chart using QuickChart.io.
15
+ chart_type: 'bar', 'line', 'pie', 'doughnut'
16
+ chart_config: A JSON string containing the QuickChart configuration.
17
+ """
18
+ try:
19
+ # If chart_config is already a dict, convert to JSON
20
+ if isinstance(chart_config, dict):
21
+ config_str = json.dumps(chart_config)
22
+ else:
23
+ # Try to parse it to validate
24
+ config_str = chart_config
25
+ json.loads(config_str)
26
+
27
+ encoded_config = urllib.parse.quote(config_str)
28
+ url = f"https://quickchart.io/chart?c={encoded_config}"
29
+
30
+ logger.info(f"Generated chart URL: {url}")
31
+ return f"Chart generated successfully: {url}. You should include this URL in your markdown output as an image: ![Chart]({url})"
32
+ except Exception as e:
33
+ logger.error(f"Failed to generate chart: {e}")
34
+ return f"Error generating chart: {str(e)}. Please ensure your chart_config is a valid JSON string."
35
+
36
+ async def generate_illustration(self, prompt: str) -> str:
37
+ """
38
+ Generates an illustration using Pollinations.ai.
39
+ """
40
+ try:
41
+ encoded_prompt = urllib.parse.quote(prompt)
42
+ url = f"https://pollinations.ai/p/{encoded_prompt}?width=1024&height=1024&seed=42&model=flux"
43
+
44
+ logger.info(f"Generated illustration URL: {url}")
45
+ return f"Illustration generated successfully: {url}. You should include this URL in your markdown output as an image: ![Illustration]({url})"
46
+ except Exception as e:
47
+ logger.error(f"Failed to generate illustration: {e}")
48
+ return f"Error generating illustration: {str(e)}"
backend/worker.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import logging
3
+ import signal
4
+ from services.task_queue import TaskQueueService
5
+ from services.supabase_service import supabase
6
+ from services.agent_runner_service import AgentRunnerService
7
+
8
+ logging.basicConfig(level=logging.INFO)
9
+ logger = logging.getLogger("worker")
10
+
11
+ class AubmWorker:
12
+ def __init__(self):
13
+ self.running = True
14
+
15
+ async def start(self):
16
+ logger.info("Aubm Background Worker started.")
17
+ while self.running:
18
+ task = await TaskQueueService.get_next_queued_task()
19
+
20
+ if task:
21
+ task_id = task['id']
22
+ logger.info(f"Processing task: {task_id}")
23
+
24
+ try:
25
+ # Fetch agent data for this task
26
+ agent_res = supabase.table("agents").select("*").eq("id", task["assigned_agent_id"]).single().execute()
27
+ if agent_res.data:
28
+ await AgentRunnerService.execute_agent_logic(task, agent_res.data)
29
+ logger.info(f"Task {task_id} completed successfully.")
30
+ else:
31
+ logger.error(f"No agent found for task {task_id}")
32
+ except Exception as e:
33
+ logger.error(f"Failed to process task {task_id}: {e}")
34
+ else:
35
+ # No tasks, sleep for a bit
36
+ await asyncio.sleep(5)
37
+
38
+ def stop(self):
39
+ logger.info("Stopping worker...")
40
+ self.running = False
41
+
42
+ async def main():
43
+ worker = AubmWorker()
44
+
45
+ # Handle shutdown signals
46
+ loop = asyncio.get_running_loop()
47
+ for sig in (signal.SIGINT, signal.SIGTERM):
48
+ loop.add_signal_handler(sig, worker.stop)
49
+
50
+ await worker.start()
51
+
52
+ if __name__ == "__main__":
53
+ asyncio.run(main())
database/agent_ownership.sql ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Agent ownership and marketplace deploy policies
2
+ -- Apply this migration to existing Supabase projects after schema.sql.
3
+
4
+ ALTER TABLE public.agents
5
+ ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES auth.users ON DELETE CASCADE;
6
+
7
+ ALTER TABLE public.agents ENABLE ROW LEVEL SECURITY;
8
+
9
+ DO $$
10
+ BEGIN
11
+ IF NOT EXISTS (
12
+ SELECT 1 FROM pg_policies
13
+ WHERE schemaname = 'public'
14
+ AND tablename = 'agents'
15
+ AND policyname = 'Users can create own agents'
16
+ ) THEN
17
+ CREATE POLICY "Users can create own agents" ON public.agents
18
+ FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id);
19
+ END IF;
20
+
21
+ IF NOT EXISTS (
22
+ SELECT 1 FROM pg_policies
23
+ WHERE schemaname = 'public'
24
+ AND tablename = 'agents'
25
+ AND policyname = 'Users can update own agents'
26
+ ) THEN
27
+ CREATE POLICY "Users can update own agents" ON public.agents
28
+ FOR UPDATE TO authenticated USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id);
29
+ END IF;
30
+
31
+ IF NOT EXISTS (
32
+ SELECT 1 FROM pg_policies
33
+ WHERE schemaname = 'public'
34
+ AND tablename = 'agents'
35
+ AND policyname = 'Users can delete own agents'
36
+ ) THEN
37
+ CREATE POLICY "Users can delete own agents" ON public.agents
38
+ FOR DELETE TO authenticated USING (auth.uid() = user_id);
39
+ END IF;
40
+ END $$;
41
+
42
+ NOTIFY pgrst, 'reload schema';
database/default_agents.sql ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Global default agents
2
+ -- These agents are readable by authenticated users and usable by the backend
3
+ -- orchestrator as fallback agents. They are not owned by a specific user.
4
+
5
+ INSERT INTO public.agents (name, role, api_provider, model, system_prompt)
6
+ SELECT
7
+ 'Planner',
8
+ 'Project Planner',
9
+ 'openai',
10
+ 'gpt-4o',
11
+ 'You decompose goals into clear, ordered implementation tasks.'
12
+ WHERE NOT EXISTS (
13
+ SELECT 1 FROM public.agents WHERE user_id IS NULL AND name = 'Planner'
14
+ );
15
+
16
+ INSERT INTO public.agents (name, role, api_provider, model, system_prompt)
17
+ SELECT
18
+ 'Builder',
19
+ 'Implementation Agent',
20
+ 'openai',
21
+ 'gpt-4o',
22
+ 'You implement practical, production-oriented solutions with concise output.'
23
+ WHERE NOT EXISTS (
24
+ SELECT 1 FROM public.agents WHERE user_id IS NULL AND name = 'Builder'
25
+ );
26
+
27
+ INSERT INTO public.agents (name, role, api_provider, model, system_prompt)
28
+ SELECT
29
+ 'Reviewer',
30
+ 'Quality Reviewer',
31
+ 'openai',
32
+ 'gpt-4o',
33
+ 'You review outputs for correctness, security, completeness, and missing tests.'
34
+ WHERE NOT EXISTS (
35
+ SELECT 1 FROM public.agents WHERE user_id IS NULL AND name = 'Reviewer'
36
+ );