Update Hugging Face Space
Browse files- .gitignore +0 -0
- AUDIT.md +45 -0
- OPERATING_GUIDE.md +280 -0
- ROADMAP.md +53 -0
- SPEC.md +137 -0
- TASKS.md +101 -0
- backend/agents/amd_agent.py +9 -17
- backend/agents/base.py +63 -0
- backend/agents/groq_agent.py +72 -31
- backend/agents/openai_agent.py +9 -35
- backend/main.py +3 -2
- backend/routers/agent_runner.py +30 -4
- backend/routers/orchestrator.py +29 -13
- backend/services/agent_runner_service.py +81 -13
- backend/services/config.py +15 -13
- backend/services/orchestrator_service.py +142 -34
- docker-compose.yml +26 -0
- frontend/src/App.tsx +12 -13
- frontend/src/components/AgentConsole.tsx +110 -0
- frontend/src/components/Dashboard.tsx +30 -9
- frontend/src/components/DebateView.tsx +163 -5
- frontend/src/components/ProjectDetail.tsx +102 -13
- frontend/src/components/SplashScreen.tsx +117 -0
- frontend/src/index.css +30 -12
- frontend/src/services/llmConfig.ts +17 -1
- vercel.json +27 -0
.gitignore
CHANGED
|
Binary files a/.gitignore and b/.gitignore differ
|
|
|
AUDIT.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🛡️ Aubm System Stability Audit Report
|
| 2 |
+
|
| 3 |
+
**Date**: May 4, 2026
|
| 4 |
+
**Status**: Stable / Production-Ready (Phase 4 Initialized)
|
| 5 |
+
|
| 6 |
+
## 🏗️ Architecture Overview
|
| 7 |
+
The system follows a modular micro-service pattern using **FastAPI** (Python) and **React 18** (Vite).
|
| 8 |
+
- **Backend**: Highly decoupled agent-provider pattern with a centralized `ToolRegistry`.
|
| 9 |
+
- **Frontend**: Glassmorphic UI with real-time SSE logging and mobile readiness (Capacitor).
|
| 10 |
+
|
| 11 |
+
## 🔒 Security & Governance
|
| 12 |
+
- [x] **Authentication**: Supabase Auth with SSO (Google/GitHub) support.
|
| 13 |
+
- [x] **Authorization**: Advanced RLS (Row Level Security) with team-based isolation and role-based access control (Admin/Editor/Viewer).
|
| 14 |
+
- [x] **Auditing**: Every agent action and LLM call is recorded in `audit_logs` for compliance.
|
| 15 |
+
|
| 16 |
+
## 🤖 Agent Capabilities
|
| 17 |
+
| Tool | Stability | Notes |
|
| 18 |
+
| :--- | :--- | :--- |
|
| 19 |
+
| **BrowserTool** | High | Integrated with Playwright for reliable web research. |
|
| 20 |
+
| **CodeSandbox** | High | Isolated Python execution for logical verification. |
|
| 21 |
+
| **FileGenerator** | High | Professional PDF/Excel generation (ReportLab/Pandas). |
|
| 22 |
+
| **Decomposer** | High | Enables recursive agent autonomy (project planning). |
|
| 23 |
+
| **SRE Tool** | High | System health monitoring and whitelisted autonomous patching. |
|
| 24 |
+
|
| 25 |
+
## 📊 Database Health
|
| 26 |
+
- Schema is partitioned across 4 main upgrade files (`schema.sql`, `phase3_updates.sql`, `marketplace.sql`, `enterprise_security.sql`).
|
| 27 |
+
- All tables include proper foreign key constraints and RLS policies.
|
| 28 |
+
- **Seeding**: Initial agent experts and project templates are pre-loaded.
|
| 29 |
+
|
| 30 |
+
## 🚀 Autonomous Reliability (Phase 4)
|
| 31 |
+
- **Self-Healing**: The SRE agent can now detect service failures and apply whitelisted patches (e.g., `git pull`, `npm install`).
|
| 32 |
+
- **Safety**: Whitelist prevents destructive commands, ensuring the agent cannot harm the host OS.
|
| 33 |
+
- **Next-Gen Interfaces**: Voice control and the spatial DAG viewer are scaffolded in the frontend for hands-free status checks and immersive task-flow inspection.
|
| 34 |
+
|
| 35 |
+
## Operations Readiness (Phase 5)
|
| 36 |
+
- **Monitoring Endpoint**: `GET /monitoring/summary` reports API/database health and core workflow counts.
|
| 37 |
+
- **Operations Dashboard**: The frontend includes a monitoring view with backend-first status checks and Supabase fallback metrics.
|
| 38 |
+
|
| 39 |
+
## 💡 Recommendations for Next Steps
|
| 40 |
+
1. **Production Deployment**: Finalize Dockerization for isolated backend deployment.
|
| 41 |
+
2. **Monitoring**: Integrate Sentry or Datadog for real-time error tracking.
|
| 42 |
+
3. **Intelligence**: Begin fine-tuning local models (Ollama) using the captured `task_feedback` data.
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
**Verdict**: The system is robust, secure, and ready for autonomous project orchestration at scale.
|
OPERATING_GUIDE.md
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Aubm Operating Guide
|
| 2 |
+
|
| 3 |
+
## What Aubm Does
|
| 4 |
+
|
| 5 |
+
Aubm is an AI agent orchestration platform. Users sign in with Supabase Auth, deploy or configure agents, assign them to tasks, run autonomous executions, review outputs, and monitor system health from a React dashboard.
|
| 6 |
+
|
| 7 |
+
The application has three main layers:
|
| 8 |
+
|
| 9 |
+
- `frontend/`: React + Vite dashboard for authentication, marketplace, debates, voice control, spatial task visualization, and monitoring.
|
| 10 |
+
- `backend/`: FastAPI API for task execution, multi-agent debate orchestration, tool calling, and monitoring.
|
| 11 |
+
- `database/`: Supabase SQL schema, seed data, RLS policies, marketplace tables, audit logs, teams, and migrations.
|
| 12 |
+
|
| 13 |
+
## Core Runtime Flow
|
| 14 |
+
|
| 15 |
+
1. A user signs in through Supabase Auth.
|
| 16 |
+
2. The frontend reads templates, agents, projects, and tasks from Supabase.
|
| 17 |
+
3. A user deploys an agent from the marketplace into `public.agents`.
|
| 18 |
+
4. A task references an assigned agent through `tasks.assigned_agent_id`.
|
| 19 |
+
5. `POST /tasks/{task_id}/run` starts backend execution.
|
| 20 |
+
6. The backend loads the task, assigned agent, and previous completed task outputs.
|
| 21 |
+
7. `AgentFactory` creates the right provider implementation, currently `OpenAIAgent` or `AMDAgent`.
|
| 22 |
+
8. The agent produces JSON output.
|
| 23 |
+
9. The backend writes output to `tasks.output_data`, moves the task to `awaiting_approval`, records `task_runs`, `agent_logs`, and `audit_logs`.
|
| 24 |
+
10. A human reviews, edits, approves, or gives feedback through the frontend.
|
| 25 |
+
|
| 26 |
+
## Main Features
|
| 27 |
+
|
| 28 |
+
### Dashboard
|
| 29 |
+
|
| 30 |
+
Shows project cards and high-level workflow progress. It is currently a static dashboard scaffold, ready to be connected to live project data.
|
| 31 |
+
|
| 32 |
+
### Agent Marketplace
|
| 33 |
+
|
| 34 |
+
Reads `agent_templates` from Supabase and deploys selected templates into `agents`.
|
| 35 |
+
|
| 36 |
+
Required database support:
|
| 37 |
+
|
| 38 |
+
- `agents.user_id`
|
| 39 |
+
- Insert policy allowing authenticated users to create agents where `auth.uid() = user_id`
|
| 40 |
+
|
| 41 |
+
Apply:
|
| 42 |
+
|
| 43 |
+
```sql
|
| 44 |
+
-- database/agent_ownership.sql
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
### Custom Agents
|
| 48 |
+
|
| 49 |
+
The `Agents` screen lets users create custom agents directly.
|
| 50 |
+
|
| 51 |
+
Each agent has:
|
| 52 |
+
|
| 53 |
+
- Name
|
| 54 |
+
- Role
|
| 55 |
+
- LLM provider
|
| 56 |
+
- Model
|
| 57 |
+
- System prompt
|
| 58 |
+
|
| 59 |
+
The currently wired backend providers are:
|
| 60 |
+
|
| 61 |
+
- `openai`
|
| 62 |
+
- `amd`
|
| 63 |
+
|
| 64 |
+
Settings stores the frontend default provider/model in browser local storage. Provider API keys are never stored in the frontend; they must stay in `backend/.env`.
|
| 65 |
+
|
| 66 |
+
### Agent Debate
|
| 67 |
+
|
| 68 |
+
Uses the backend endpoint:
|
| 69 |
+
|
| 70 |
+
```text
|
| 71 |
+
POST /orchestrator/debate
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
The flow is:
|
| 75 |
+
|
| 76 |
+
1. Agent A generates an initial answer.
|
| 77 |
+
2. Agent B critiques the answer.
|
| 78 |
+
3. Agent A refines the output.
|
| 79 |
+
4. The final debate result is saved to `tasks.output_data`.
|
| 80 |
+
|
| 81 |
+
### Voice Control
|
| 82 |
+
|
| 83 |
+
Uses browser Web Speech APIs.
|
| 84 |
+
|
| 85 |
+
Supported commands include:
|
| 86 |
+
|
| 87 |
+
- `dashboard`
|
| 88 |
+
- `marketplace`
|
| 89 |
+
- `debate`
|
| 90 |
+
- `settings`
|
| 91 |
+
- `new project`
|
| 92 |
+
- `status`
|
| 93 |
+
|
| 94 |
+
The `status` command reads project/task counts from Supabase and speaks the result.
|
| 95 |
+
|
| 96 |
+
### Spatial View
|
| 97 |
+
|
| 98 |
+
Shows a layered task DAG-style visualization. It reads recent tasks from Supabase and falls back to demo nodes if no tasks are available.
|
| 99 |
+
|
| 100 |
+
### Monitoring
|
| 101 |
+
|
| 102 |
+
Uses:
|
| 103 |
+
|
| 104 |
+
```text
|
| 105 |
+
GET /monitoring/summary
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
The endpoint reports:
|
| 109 |
+
|
| 110 |
+
- API status
|
| 111 |
+
- Database status
|
| 112 |
+
- Project count
|
| 113 |
+
- Task count
|
| 114 |
+
- Agent count
|
| 115 |
+
- Task run count
|
| 116 |
+
- Failed tasks
|
| 117 |
+
- Tasks awaiting approval
|
| 118 |
+
|
| 119 |
+
If the backend endpoint is unavailable, the frontend falls back to direct Supabase count queries.
|
| 120 |
+
|
| 121 |
+
## Backend Setup
|
| 122 |
+
|
| 123 |
+
From `backend/`:
|
| 124 |
+
|
| 125 |
+
```powershell
|
| 126 |
+
python -m venv venv
|
| 127 |
+
.\venv\Scripts\activate
|
| 128 |
+
pip install -r requirements.txt
|
| 129 |
+
uvicorn main:app --reload --port 8000
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
Required `backend/.env` values:
|
| 133 |
+
|
| 134 |
+
```env
|
| 135 |
+
SUPABASE_URL=...
|
| 136 |
+
SUPABASE_SERVICE_ROLE_KEY=...
|
| 137 |
+
OPENAI_API_KEY=...
|
| 138 |
+
AMD_API_KEY=...
|
| 139 |
+
```
|
| 140 |
+
|
| 141 |
+
Optional provider keys:
|
| 142 |
+
|
| 143 |
+
```env
|
| 144 |
+
GROQ_API_KEY=...
|
| 145 |
+
GEMINI_API_KEY=...
|
| 146 |
+
ANTHROPIC_API_KEY=...
|
| 147 |
+
```
|
| 148 |
+
|
| 149 |
+
## Frontend Setup
|
| 150 |
+
|
| 151 |
+
From `frontend/`:
|
| 152 |
+
|
| 153 |
+
```powershell
|
| 154 |
+
npm install
|
| 155 |
+
npm run dev
|
| 156 |
+
```
|
| 157 |
+
|
| 158 |
+
Required `frontend/.env` values:
|
| 159 |
+
|
| 160 |
+
```env
|
| 161 |
+
VITE_SUPABASE_URL=...
|
| 162 |
+
VITE_SUPABASE_ANON_KEY=...
|
| 163 |
+
VITE_API_URL=http://127.0.0.1:8000
|
| 164 |
+
```
|
| 165 |
+
|
| 166 |
+
Build check:
|
| 167 |
+
|
| 168 |
+
```powershell
|
| 169 |
+
npm run build
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
## Database Setup Order
|
| 173 |
+
|
| 174 |
+
Apply SQL files in this order for a fresh Supabase project:
|
| 175 |
+
|
| 176 |
+
1. `database/schema.sql`
|
| 177 |
+
2. `database/seed.sql`
|
| 178 |
+
3. `database/phase3_updates.sql`
|
| 179 |
+
4. `database/marketplace.sql`
|
| 180 |
+
5. `database/enterprise_security.sql`
|
| 181 |
+
6. `database/agent_ownership.sql`
|
| 182 |
+
7. `database/task_owner_policies.sql`
|
| 183 |
+
8. `database/default_agents.sql`
|
| 184 |
+
|
| 185 |
+
For an existing Supabase project where marketplace deploy fails with missing `user_id`, apply only:
|
| 186 |
+
|
| 187 |
+
```sql
|
| 188 |
+
-- database/agent_ownership.sql
|
| 189 |
+
```
|
| 190 |
+
|
| 191 |
+
Then reload the frontend with a hard refresh.
|
| 192 |
+
|
| 193 |
+
## Important Tables
|
| 194 |
+
|
| 195 |
+
- `profiles`: User metadata and role.
|
| 196 |
+
- `projects`: Project containers.
|
| 197 |
+
- `agents`: Deployed AI agents.
|
| 198 |
+
- `agent_templates`: Marketplace templates.
|
| 199 |
+
- `tasks`: Work units assigned to agents.
|
| 200 |
+
- `task_runs`: Execution history.
|
| 201 |
+
- `agent_logs`: Agent execution traces.
|
| 202 |
+
- `audit_logs`: Governance and compliance trail.
|
| 203 |
+
- `task_feedback`: Like/dislike feedback for future tuning.
|
| 204 |
+
- `teams` and `team_members`: Enterprise team permissions.
|
| 205 |
+
|
| 206 |
+
## Tool System
|
| 207 |
+
|
| 208 |
+
The backend exposes tools to agents through `tools/registry.py`.
|
| 209 |
+
|
| 210 |
+
Available tools include:
|
| 211 |
+
|
| 212 |
+
- Web extraction with Playwright.
|
| 213 |
+
- Python code execution.
|
| 214 |
+
- PDF generation.
|
| 215 |
+
- Excel generation.
|
| 216 |
+
- Project decomposition.
|
| 217 |
+
- System health checks.
|
| 218 |
+
- Restricted patch commands.
|
| 219 |
+
|
| 220 |
+
## Current Roadmap State
|
| 221 |
+
|
| 222 |
+
Completed:
|
| 223 |
+
|
| 224 |
+
- Core backend and frontend foundation.
|
| 225 |
+
- Supabase auth and schema.
|
| 226 |
+
- Agent execution.
|
| 227 |
+
- Multi-agent debate.
|
| 228 |
+
- Marketplace.
|
| 229 |
+
- Voice control.
|
| 230 |
+
- Spatial task viewer.
|
| 231 |
+
- Operations monitoring.
|
| 232 |
+
|
| 233 |
+
In progress:
|
| 234 |
+
|
| 235 |
+
- Production operations hardening.
|
| 236 |
+
- Error tracking.
|
| 237 |
+
- Docker/runtime packaging.
|
| 238 |
+
- Frontend bundle splitting.
|
| 239 |
+
- Production CORS allowlist.
|
| 240 |
+
|
| 241 |
+
## Common Errors
|
| 242 |
+
|
| 243 |
+
### `403 Forbidden` on `POST /rest/v1/agents`
|
| 244 |
+
|
| 245 |
+
Cause: RLS policy does not allow insert.
|
| 246 |
+
|
| 247 |
+
Fix: Apply `database/agent_ownership.sql`.
|
| 248 |
+
|
| 249 |
+
### `Could not find the 'user_id' column of 'agents' in the schema cache`
|
| 250 |
+
|
| 251 |
+
Cause: `agents.user_id` is missing or PostgREST schema cache has not reloaded.
|
| 252 |
+
|
| 253 |
+
Fix:
|
| 254 |
+
|
| 255 |
+
```sql
|
| 256 |
+
ALTER TABLE public.agents
|
| 257 |
+
ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES auth.users ON DELETE CASCADE;
|
| 258 |
+
|
| 259 |
+
NOTIFY pgrst, 'reload schema';
|
| 260 |
+
```
|
| 261 |
+
|
| 262 |
+
Then hard refresh the frontend.
|
| 263 |
+
|
| 264 |
+
### `OTS parsing error`
|
| 265 |
+
|
| 266 |
+
Cause: A CSS URL was incorrectly used as a font file.
|
| 267 |
+
|
| 268 |
+
Fix: Use Google Fonts through `@import`, already applied in `frontend/src/styles/variables.css`.
|
| 269 |
+
|
| 270 |
+
### Frontend chunk-size warning
|
| 271 |
+
|
| 272 |
+
Vite currently warns that the JS chunk is larger than 500 KB. This is not a runtime error. The Phase 5 roadmap includes bundle splitting.
|
| 273 |
+
|
| 274 |
+
## Development Rules
|
| 275 |
+
|
| 276 |
+
- Keep frontend display text in English.
|
| 277 |
+
- Keep documentation in English.
|
| 278 |
+
- Keep database migrations idempotent when possible.
|
| 279 |
+
- Never commit real secrets from `.env`.
|
| 280 |
+
- Prefer applying database changes through separate migration files instead of editing only `schema.sql`.
|
ROADMAP.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🗺️ AgentCollab Roadmap
|
| 2 |
+
|
| 3 |
+
This document outlines the strategic evolution of Aubm, moving from a robust orchestration core to an enterprise ecosystem.
|
| 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 & 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, more 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 & 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 & 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 & 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 |
+
- [ ] **Asynchronous Task Queue**: Dedicated background workers (worker.py).
|
| 48 |
+
- [ ] **Vectorized Long-term Memory**: Cross-project semantic retrieval.
|
| 49 |
+
- [ ] **Self-Optimizing Agents**: Meta-prompting loops based on human feedback.
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
*Last updated: May 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*
|
TASKS.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project Tasks: Aubm Implementation
|
| 2 |
+
|
| 3 |
+
This file tracks the granular implementation steps for the Aubm platform, following the [ROADMAP.md](./ROADMAP.md) and [SPEC.md](./SPEC.md).
|
| 4 |
+
|
| 5 |
+
## Phase 1: Core Foundation
|
| 6 |
+
|
| 7 |
+
### 1.1 Project Initialization
|
| 8 |
+
- [x] Create directory structure (`backend/`, `frontend/`, `database/`)
|
| 9 |
+
- [x] Initialize Python virtual environment in `backend/`
|
| 10 |
+
- [x] Initialize Vite + React + TS project in `frontend/`
|
| 11 |
+
- [x] Create `backend/requirements.txt` with core dependencies
|
| 12 |
+
- [x] Create `frontend/package.json` and install dependencies
|
| 13 |
+
|
| 14 |
+
### 1.2 Database & Schema
|
| 15 |
+
- [x] Create `database/schema.sql` based on SPEC.md
|
| 16 |
+
- [x] Set up Supabase project
|
| 17 |
+
- [x] Implement initial seed data for `agents` and `app_config`
|
| 18 |
+
|
| 19 |
+
### 1.3 Backend Core
|
| 20 |
+
- [x] Implement `backend/main.py` entrypoint
|
| 21 |
+
- [x] Create `backend/services/config.py` for environment management
|
| 22 |
+
- [x] Implement `backend/agents/base.py`
|
| 23 |
+
- [x] Implement first agent providers (`OpenAIAgent`, `AMDAgent`)
|
| 24 |
+
- [x] Implement `backend/routers/agent_runner.py` for task execution
|
| 25 |
+
|
| 26 |
+
### 1.4 Frontend Core
|
| 27 |
+
- [x] Set up CSS design system
|
| 28 |
+
- [x] Implement Supabase Auth integration
|
| 29 |
+
- [x] Create app layout with sidebar and header
|
| 30 |
+
- [x] Build project dashboard view
|
| 31 |
+
|
| 32 |
+
## Phase 2: Advanced Collaboration & Tools
|
| 33 |
+
|
| 34 |
+
### 2.1 Extended Toolbelt
|
| 35 |
+
- [x] Implement `BrowserTool` using Playwright
|
| 36 |
+
- [x] Create `ToolRegistry` for agent access
|
| 37 |
+
- [x] Implement `CodeSandboxTool`
|
| 38 |
+
- [x] Add file generation capabilities
|
| 39 |
+
|
| 40 |
+
### 2.2 Multi-Agent Features
|
| 41 |
+
- [x] Implement debate logic
|
| 42 |
+
- [x] Create peer review status/dashboard for tasks
|
| 43 |
+
|
| 44 |
+
### 2.3 Real-Time Collaboration
|
| 45 |
+
- [x] Implement collaborative output editor
|
| 46 |
+
- [ ] Real-time cursor/presence indicators
|
| 47 |
+
|
| 48 |
+
### 2.4 Mobile Experience
|
| 49 |
+
- [x] Initialize Capacitor in the frontend project
|
| 50 |
+
- [ ] Add Android/iOS platform scaffolding
|
| 51 |
+
|
| 52 |
+
## Phase 3: Intelligence & Scale
|
| 53 |
+
|
| 54 |
+
### 3.1 Advanced Analytics & Security
|
| 55 |
+
- [x] Implement audit logs for LLM interaction tracking
|
| 56 |
+
- [x] Add feedback loop for fine-tuning data collection
|
| 57 |
+
- [x] Implement SSO integration through Supabase
|
| 58 |
+
- [x] Implement granular RLS for project teams
|
| 59 |
+
|
| 60 |
+
### 3.2 Recursive Autonomy
|
| 61 |
+
- [x] Implement project decomposition
|
| 62 |
+
- [x] Create agent marketplace schema and gallery UI
|
| 63 |
+
|
| 64 |
+
## Phase 4: Autonomy & Beyond
|
| 65 |
+
|
| 66 |
+
### 4.1 System Self-Healing
|
| 67 |
+
- [x] Implement health check agents
|
| 68 |
+
- [x] Create restricted autonomous patching logic
|
| 69 |
+
|
| 70 |
+
### 4.2 Next-Gen Interfaces
|
| 71 |
+
- [x] Voice control integration
|
| 72 |
+
- [x] Scaffolding for VR/AR project viewer
|
| 73 |
+
|
| 74 |
+
## Phase 5: Production Operations
|
| 75 |
+
|
| 76 |
+
### 5.1 Observability
|
| 77 |
+
- [x] Add backend monitoring summary endpoint
|
| 78 |
+
- [x] Add frontend operations monitoring dashboard
|
| 79 |
+
- [x] Add external error tracking integration
|
| 80 |
+
|
| 81 |
+
### 5.2 Deployment Hardening
|
| 82 |
+
- [x] Add Dockerfile and production server command
|
| 83 |
+
- [x] Replace wildcard CORS with environment-driven allowlist
|
| 84 |
+
- [x] Add frontend bundle splitting/performance budget
|
| 85 |
+
|
| 86 |
+
## Phase 6: Distributed Scale & Intelligence
|
| 87 |
+
|
| 88 |
+
### 6.1 Asynchronous Workers
|
| 89 |
+
- [/] Implement `backend/worker.py` for task consumption
|
| 90 |
+
- [/] Implement `backend/services/task_queue.py` using a lightweight polling or webhook mechanism
|
| 91 |
+
|
| 92 |
+
### 6.2 Advanced Memory
|
| 93 |
+
- [ ] Set up pgvector extension in Supabase
|
| 94 |
+
- [ ] Implement semantic retrieval service for cross-project context
|
| 95 |
+
|
| 96 |
+
### 6.3 Self-Optimization
|
| 97 |
+
- [ ] Create agent "reflection" router to analyze task history
|
| 98 |
+
- [ ] Implement automated system prompt refinement logic
|
| 99 |
+
|
| 100 |
+
---
|
| 101 |
+
*Legend: Pending | In Progress | Completed*
|
backend/agents/amd_agent.py
CHANGED
|
@@ -22,20 +22,12 @@ class AMDAgent(BaseAgent):
|
|
| 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 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
return self._result("amd", response.choices[0].message.content or "")
|
| 35 |
-
except Exception as e:
|
| 36 |
-
return {
|
| 37 |
-
"agent_name": self.name,
|
| 38 |
-
"provider": "amd",
|
| 39 |
-
"status": "error",
|
| 40 |
-
"error": str(e)
|
| 41 |
-
}
|
|
|
|
| 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
CHANGED
|
@@ -89,6 +89,69 @@ Please provide your output as a JSON object.
|
|
| 89 |
"content": str(tool_result),
|
| 90 |
})
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
def _result(self, provider: str, content: str) -> Dict[str, Any]:
|
| 93 |
return {
|
| 94 |
"agent_name": self.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,
|
backend/agents/groq_agent.py
CHANGED
|
@@ -2,16 +2,31 @@ import logging
|
|
| 2 |
from .base import BaseAgent
|
| 3 |
from typing import Dict, Any, List
|
| 4 |
import groq
|
|
|
|
| 5 |
from services.config import settings, config_service
|
| 6 |
from tools.registry import tool_registry
|
| 7 |
|
| 8 |
logger = logging.getLogger("uvicorn")
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
class GroqAgent(BaseAgent):
|
| 11 |
"""
|
| 12 |
-
Agent implementation for Groq.
|
| 13 |
"""
|
| 14 |
def __init__(self, name: str, role: str, model: str = "llama-3.3-70b-versatile", system_prompt: str = None):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
super().__init__(name, role, model, system_prompt)
|
| 16 |
|
| 17 |
# Load dynamic config
|
|
@@ -21,37 +36,63 @@ class GroqAgent(BaseAgent):
|
|
| 21 |
self.client = groq.AsyncGroq(api_key=api_key)
|
| 22 |
self.temperature = self.provider_config.get("temperature", 0.7)
|
| 23 |
self.max_tokens = self.provider_config.get("max_tokens", 4096)
|
|
|
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
"model": self.model,
|
| 30 |
-
"messages": messages,
|
| 31 |
-
"temperature": self.temperature,
|
| 32 |
-
"max_tokens": self.max_tokens
|
| 33 |
-
}
|
| 34 |
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
| 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/openai_agent.py
CHANGED
|
@@ -17,38 +17,12 @@ class OpenAIAgent(BaseAgent):
|
|
| 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 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
"
|
| 28 |
-
|
| 29 |
-
if use_tools:
|
| 30 |
-
request_kwargs["tools"] = tool_registry.get_tool_definitions()
|
| 31 |
-
request_kwargs["tool_choice"] = "auto"
|
| 32 |
-
|
| 33 |
-
response = await self.client.chat.completions.create(**request_kwargs)
|
| 34 |
-
|
| 35 |
-
message = response.choices[0].message
|
| 36 |
-
|
| 37 |
-
# Handle tool calls
|
| 38 |
-
if message.tool_calls:
|
| 39 |
-
messages.append(message)
|
| 40 |
-
await self._append_tool_results(messages, message.tool_calls, tool_registry)
|
| 41 |
-
|
| 42 |
-
# Second call after tool execution
|
| 43 |
-
final_response = await self.client.chat.completions.create(
|
| 44 |
-
model=self.model,
|
| 45 |
-
messages=messages,
|
| 46 |
-
response_format={"type": "json_object"},
|
| 47 |
-
temperature=self.temperature,
|
| 48 |
-
max_tokens=self.max_tokens
|
| 49 |
-
)
|
| 50 |
-
content = final_response.choices[0].message.content
|
| 51 |
-
else:
|
| 52 |
-
content = message.content
|
| 53 |
-
|
| 54 |
-
return self._result("openai", content or "")
|
|
|
|
| 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/main.py
CHANGED
|
@@ -27,10 +27,11 @@ app = FastAPI(
|
|
| 27 |
)
|
| 28 |
|
| 29 |
# CORS Configuration
|
| 30 |
-
allowed_origins = os.getenv("ALLOWED_ORIGINS", "
|
| 31 |
app.add_middleware(
|
| 32 |
CORSMiddleware,
|
| 33 |
-
allow_origins=allowed_origins,
|
|
|
|
| 34 |
allow_credentials=True,
|
| 35 |
allow_methods=["*"],
|
| 36 |
allow_headers=["*"],
|
|
|
|
| 27 |
)
|
| 28 |
|
| 29 |
# CORS Configuration
|
| 30 |
+
allowed_origins = os.getenv("ALLOWED_ORIGINS", "http://localhost:5173,http://localhost:3000,http://127.0.0.1:5173").split(",")
|
| 31 |
app.add_middleware(
|
| 32 |
CORSMiddleware,
|
| 33 |
+
allow_origins=allowed_origins if allowed_origins != ["*"] else ["*"],
|
| 34 |
+
allow_origin_regex=os.getenv("ALLOWED_ORIGIN_REGEX"),
|
| 35 |
allow_credentials=True,
|
| 36 |
allow_methods=["*"],
|
| 37 |
allow_headers=["*"],
|
backend/routers/agent_runner.py
CHANGED
|
@@ -11,14 +11,14 @@ def update_task_status(task_id: str, status: str):
|
|
| 11 |
supabase.table("tasks")
|
| 12 |
.update({"status": status})
|
| 13 |
.eq("id", task_id)
|
| 14 |
-
.select("id,project_id,status")
|
| 15 |
-
.single()
|
| 16 |
.execute()
|
| 17 |
)
|
| 18 |
if not result.data:
|
| 19 |
raise HTTPException(status_code=404, detail="Task not found or status was not updated")
|
| 20 |
|
| 21 |
-
|
|
|
|
|
|
|
| 22 |
if project_id:
|
| 23 |
task_result = (
|
| 24 |
supabase.table("tasks")
|
|
@@ -32,7 +32,7 @@ def update_task_status(task_id: str, status: str):
|
|
| 32 |
elif status != "done":
|
| 33 |
supabase.table("projects").update({"status": "active"}).eq("id", project_id).execute()
|
| 34 |
|
| 35 |
-
return
|
| 36 |
|
| 37 |
@router.post("/{task_id}/run")
|
| 38 |
async def run_task(task_id: str, background_tasks: BackgroundTasks):
|
|
@@ -75,3 +75,29 @@ async def approve_task(task_id: str):
|
|
| 75 |
async def reject_task(task_id: str):
|
| 76 |
task = update_task_status(task_id, "todo")
|
| 77 |
return {"message": "Task rejected", "task": task}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
supabase.table("tasks")
|
| 12 |
.update({"status": status})
|
| 13 |
.eq("id", task_id)
|
|
|
|
|
|
|
| 14 |
.execute()
|
| 15 |
)
|
| 16 |
if not result.data:
|
| 17 |
raise HTTPException(status_code=404, detail="Task not found or status was not updated")
|
| 18 |
|
| 19 |
+
task_data = result.data[0]
|
| 20 |
+
|
| 21 |
+
project_id = task_data.get("project_id")
|
| 22 |
if project_id:
|
| 23 |
task_result = (
|
| 24 |
supabase.table("tasks")
|
|
|
|
| 32 |
elif status != "done":
|
| 33 |
supabase.table("projects").update({"status": "active"}).eq("id", project_id).execute()
|
| 34 |
|
| 35 |
+
return task_data
|
| 36 |
|
| 37 |
@router.post("/{task_id}/run")
|
| 38 |
async def run_task(task_id: str, background_tasks: BackgroundTasks):
|
|
|
|
| 75 |
async def reject_task(task_id: str):
|
| 76 |
task = update_task_status(task_id, "todo")
|
| 77 |
return {"message": "Task rejected", "task": task}
|
| 78 |
+
@router.post("/project/{project_id}/approve-all")
|
| 79 |
+
async def approve_all_tasks(project_id: str):
|
| 80 |
+
"""
|
| 81 |
+
Approves all tasks in a project that are awaiting approval.
|
| 82 |
+
"""
|
| 83 |
+
# 1. Update tasks
|
| 84 |
+
result = (
|
| 85 |
+
supabase.table("tasks")
|
| 86 |
+
.update({"status": "done"})
|
| 87 |
+
.eq("project_id", project_id)
|
| 88 |
+
.eq("status", "awaiting_approval")
|
| 89 |
+
.execute()
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
# 2. Check if all tasks in project are now done
|
| 93 |
+
task_result = (
|
| 94 |
+
supabase.table("tasks")
|
| 95 |
+
.select("status")
|
| 96 |
+
.eq("project_id", project_id)
|
| 97 |
+
.execute()
|
| 98 |
+
)
|
| 99 |
+
tasks = task_result.data or []
|
| 100 |
+
if tasks and all(task.get("status") == "done" for task in tasks):
|
| 101 |
+
supabase.table("projects").update({"status": "completed"}).eq("id", project_id).execute()
|
| 102 |
+
|
| 103 |
+
return {"message": f"Approved {len(result.data)} tasks", "count": len(result.data)}
|
backend/routers/orchestrator.py
CHANGED
|
@@ -5,7 +5,7 @@ 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
|
| 9 |
from reportlab.graphics.shapes import Drawing
|
| 10 |
from reportlab.graphics.charts.barcharts import VerticalBarChart
|
| 11 |
from reportlab.graphics.charts.piecharts import Pie
|
|
@@ -63,18 +63,34 @@ def _report_pdf_bytes(title: str, content: str, charts: dict | None = None) -> b
|
|
| 63 |
styles = getSampleStyleSheet()
|
| 64 |
story = [Paragraph(title, styles["Title"]), Spacer(1, 0.2 * inch)]
|
| 65 |
if charts:
|
| 66 |
-
story.
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
|
| 79 |
for raw_line in content.splitlines():
|
| 80 |
line = raw_line.strip()
|
|
|
|
| 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
|
|
|
|
| 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()
|
backend/services/agent_runner_service.py
CHANGED
|
@@ -17,13 +17,15 @@ class AgentRunnerService:
|
|
| 17 |
start_action: str = "execution_start",
|
| 18 |
start_content: str | None = None,
|
| 19 |
complete_action: str = "execution_complete",
|
| 20 |
-
complete_content: str = "Agent successfully completed the task and produced output."
|
|
|
|
| 21 |
) -> tuple[dict, str]:
|
| 22 |
task_id = task["id"]
|
| 23 |
project_id = task["project_id"]
|
| 24 |
run_id = None
|
| 25 |
|
| 26 |
-
|
|
|
|
| 27 |
|
| 28 |
try:
|
| 29 |
run_res = supabase.table("task_runs").insert({
|
|
@@ -51,6 +53,41 @@ class AgentRunnerService:
|
|
| 51 |
if include_semantic_context:
|
| 52 |
extra_context = await semantic_backprop.get_project_context(project_id, task_id)
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
supabase.table("agent_logs").insert({
|
| 55 |
"task_id": task_id,
|
| 56 |
"run_id": run_id,
|
|
@@ -58,25 +95,45 @@ class AgentRunnerService:
|
|
| 58 |
"content": start_content or f"Agent {agent_data['name']} starting task: {task['title']}"
|
| 59 |
}).execute()
|
| 60 |
|
|
|
|
|
|
|
| 61 |
result = await agent.run(task.get("description") or task["title"], context, extra_context=extra_context)
|
|
|
|
|
|
|
| 62 |
if result.get("status") == "error":
|
| 63 |
raise RuntimeError(result.get("error") or "Agent returned an error result.")
|
| 64 |
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
supabase.table("task_runs").update({
|
| 71 |
"status": "completed",
|
| 72 |
-
"finished_at": datetime.now(timezone.utc).isoformat()
|
|
|
|
| 73 |
}).eq("id", run_id).execute()
|
| 74 |
|
|
|
|
| 75 |
supabase.table("agent_logs").insert({
|
| 76 |
"task_id": task_id,
|
| 77 |
"run_id": run_id,
|
| 78 |
"action": complete_action,
|
| 79 |
-
"content": complete_content
|
| 80 |
}).execute()
|
| 81 |
|
| 82 |
return result, run_id
|
|
@@ -88,10 +145,21 @@ class AgentRunnerService:
|
|
| 88 |
"status": "failed",
|
| 89 |
"finished_at": datetime.now(timezone.utc).isoformat()
|
| 90 |
}).eq("id", run_id).execute()
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
"
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
raise e
|
| 96 |
|
| 97 |
@staticmethod
|
|
|
|
| 17 |
start_action: str = "execution_start",
|
| 18 |
start_content: str | None = None,
|
| 19 |
complete_action: str = "execution_complete",
|
| 20 |
+
complete_content: str = "Agent successfully completed the task and produced output.",
|
| 21 |
+
update_task: bool = True
|
| 22 |
) -> tuple[dict, str]:
|
| 23 |
task_id = task["id"]
|
| 24 |
project_id = task["project_id"]
|
| 25 |
run_id = None
|
| 26 |
|
| 27 |
+
if update_task:
|
| 28 |
+
supabase.table("tasks").update({"status": "in_progress"}).eq("id", task_id).execute()
|
| 29 |
|
| 30 |
try:
|
| 31 |
run_res = supabase.table("task_runs").insert({
|
|
|
|
| 53 |
if include_semantic_context:
|
| 54 |
extra_context = await semantic_backprop.get_project_context(project_id, task_id)
|
| 55 |
|
| 56 |
+
import time
|
| 57 |
+
import hashlib
|
| 58 |
+
|
| 59 |
+
# Simple in-memory cache for the session (could be persistent later)
|
| 60 |
+
if not hasattr(AgentRunnerService, "_task_cache"):
|
| 61 |
+
AgentRunnerService._task_cache = {}
|
| 62 |
+
|
| 63 |
+
# 1. Create a cache key based on task, agent (model + system prompt), and context
|
| 64 |
+
cache_input = f"{task['id']}-{agent_data['model']}-{agent_data.get('system_prompt', '')}-{task.get('description')}-{str(context)}-{extra_context}"
|
| 65 |
+
cache_key = hashlib.md5(cache_input.encode()).hexdigest()
|
| 66 |
+
|
| 67 |
+
# 2. Check Cache
|
| 68 |
+
if cache_key in AgentRunnerService._task_cache:
|
| 69 |
+
logger.info(f"Cache hit for task {task_id}. Skipping LLM call.")
|
| 70 |
+
cached_result = AgentRunnerService._task_cache[cache_key]
|
| 71 |
+
|
| 72 |
+
# Still log the "start" for UI consistency
|
| 73 |
+
agent_name = agent_data.get('name', 'Agent')
|
| 74 |
+
log_msg = start_content or f"Agent {agent_name} resuming task"
|
| 75 |
+
supabase.table("agent_logs").insert({
|
| 76 |
+
"task_id": task_id,
|
| 77 |
+
"run_id": run_id,
|
| 78 |
+
"action": start_action,
|
| 79 |
+
"content": f"[CACHE HIT] {log_msg}"
|
| 80 |
+
}).execute()
|
| 81 |
+
|
| 82 |
+
if update_task:
|
| 83 |
+
supabase.table("tasks").update({
|
| 84 |
+
"status": "awaiting_approval",
|
| 85 |
+
"output_data": cached_result
|
| 86 |
+
}).eq("id", task_id).execute()
|
| 87 |
+
|
| 88 |
+
return cached_result, run_id
|
| 89 |
+
|
| 90 |
+
# 3. Log Start
|
| 91 |
supabase.table("agent_logs").insert({
|
| 92 |
"task_id": task_id,
|
| 93 |
"run_id": run_id,
|
|
|
|
| 95 |
"content": start_content or f"Agent {agent_data['name']} starting task: {task['title']}"
|
| 96 |
}).execute()
|
| 97 |
|
| 98 |
+
# 4. Execute Run with timing
|
| 99 |
+
start_time = time.time()
|
| 100 |
result = await agent.run(task.get("description") or task["title"], context, extra_context=extra_context)
|
| 101 |
+
duration = time.time() - start_time
|
| 102 |
+
|
| 103 |
if result.get("status") == "error":
|
| 104 |
raise RuntimeError(result.get("error") or "Agent returned an error result.")
|
| 105 |
|
| 106 |
+
# 5. Security Sanitization (Defense in Depth)
|
| 107 |
+
raw_out = str(result.get("raw_output", ""))
|
| 108 |
+
suspicious_patterns = ["rm -rf", "mkfs", "dd if=", "curl", "wget", "chmod 777", "> /dev/sda"]
|
| 109 |
+
for pattern in suspicious_patterns:
|
| 110 |
+
if pattern in raw_out:
|
| 111 |
+
logger.warning(f"SECURITY: Suspicious pattern '{pattern}' detected in agent output for task {task_id}.")
|
| 112 |
+
result["security_warning"] = f"Output sanitized: suspicious pattern '{pattern}' detected."
|
| 113 |
+
# We don't block yet, but we flag it.
|
| 114 |
+
|
| 115 |
+
# 6. Save to Cache
|
| 116 |
+
AgentRunnerService._task_cache[cache_key] = result
|
| 117 |
+
|
| 118 |
+
if update_task:
|
| 119 |
+
supabase.table("tasks").update({
|
| 120 |
+
"status": "awaiting_approval",
|
| 121 |
+
"output_data": result
|
| 122 |
+
}).eq("id", task_id).execute()
|
| 123 |
+
|
| 124 |
+
# 7. Update Run Status
|
| 125 |
supabase.table("task_runs").update({
|
| 126 |
"status": "completed",
|
| 127 |
+
"finished_at": datetime.now(timezone.utc).isoformat(),
|
| 128 |
+
"duration_seconds": round(duration, 2)
|
| 129 |
}).eq("id", run_id).execute()
|
| 130 |
|
| 131 |
+
# 8. Log Completion with Metrics
|
| 132 |
supabase.table("agent_logs").insert({
|
| 133 |
"task_id": task_id,
|
| 134 |
"run_id": run_id,
|
| 135 |
"action": complete_action,
|
| 136 |
+
"content": f"{complete_content} (Execution time: {duration:.2f}s)"
|
| 137 |
}).execute()
|
| 138 |
|
| 139 |
return result, run_id
|
|
|
|
| 145 |
"status": "failed",
|
| 146 |
"finished_at": datetime.now(timezone.utc).isoformat()
|
| 147 |
}).eq("id", run_id).execute()
|
| 148 |
+
|
| 149 |
+
if update_task:
|
| 150 |
+
supabase.table("tasks").update({
|
| 151 |
+
"status": "failed",
|
| 152 |
+
"output_data": {"error": str(e)}
|
| 153 |
+
}).eq("id", task_id).execute()
|
| 154 |
+
|
| 155 |
+
# LOG ERROR TO AGENT CONSOLE
|
| 156 |
+
supabase.table("agent_logs").insert({
|
| 157 |
+
"task_id": task_id,
|
| 158 |
+
"run_id": run_id,
|
| 159 |
+
"action": "execution_failed",
|
| 160 |
+
"content": f"ERROR: {str(e)}"
|
| 161 |
+
}).execute()
|
| 162 |
+
|
| 163 |
raise e
|
| 164 |
|
| 165 |
@staticmethod
|
backend/services/config.py
CHANGED
|
@@ -5,24 +5,26 @@ 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] =
|
| 13 |
-
GROQ_API_KEY: Optional[str] =
|
| 14 |
-
GEMINI_API_KEY: Optional[str] =
|
| 15 |
-
ANTHROPIC_API_KEY: Optional[str] =
|
| 16 |
-
AMD_API_KEY: Optional[str] =
|
| 17 |
|
| 18 |
# App Config
|
| 19 |
-
TASK_QUEUE_EMBEDDED_WORKER: bool =
|
| 20 |
-
OUTPUT_LANGUAGE: str =
|
| 21 |
-
PORT: int =
|
| 22 |
-
SENTRY_DSN: Optional[str] =
|
| 23 |
|
| 24 |
-
|
| 25 |
-
env_file
|
|
|
|
|
|
|
| 26 |
|
| 27 |
settings = Settings()
|
| 28 |
|
|
|
|
| 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 |
|
| 18 |
# App Config
|
| 19 |
+
TASK_QUEUE_EMBEDDED_WORKER: bool = True
|
| 20 |
+
OUTPUT_LANGUAGE: str = "en"
|
| 21 |
+
PORT: int = 8000
|
| 22 |
+
SENTRY_DSN: Optional[str] = None
|
| 23 |
|
| 24 |
+
model_config = {
|
| 25 |
+
"env_file": ".env",
|
| 26 |
+
"extra": "ignore"
|
| 27 |
+
}
|
| 28 |
|
| 29 |
settings = Settings()
|
| 30 |
|
backend/services/orchestrator_service.py
CHANGED
|
@@ -164,37 +164,86 @@ class OrchestratorService:
|
|
| 164 |
try:
|
| 165 |
# 1. Fetch task and agents
|
| 166 |
task = supabase.table("tasks").select("*").eq("id", task_id).single().execute().data
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
# 2. Agent A generates initial response
|
| 171 |
-
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 173 |
|
| 174 |
# 3. Agent B reviews and critiques
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
# 4. Agent A refines based on critique
|
| 180 |
-
|
| 181 |
-
|
| 182 |
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
"initial": initial_res["data"],
|
| 188 |
"critique": critique_res["data"],
|
| 189 |
"final": final_res["data"]
|
| 190 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
}).eq("id", task_id).execute()
|
| 192 |
|
| 193 |
logger.info(f"Debate completed for task {task_id}")
|
| 194 |
|
| 195 |
except Exception as e:
|
| 196 |
logger.error(f"Debate failed: {str(e)}")
|
| 197 |
-
supabase.table("tasks").update({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
|
| 199 |
async def run_project(self, project_id: str):
|
| 200 |
"""
|
|
@@ -218,8 +267,12 @@ class OrchestratorService:
|
|
| 218 |
or []
|
| 219 |
)
|
| 220 |
|
| 221 |
-
#
|
| 222 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
logger.info(f"No tasks found for project {project_id}. Triggering auto-decomposition.")
|
| 224 |
await self.decompose_project(project_id)
|
| 225 |
# Re-fetch tasks after decomposition
|
|
@@ -387,29 +440,68 @@ class OrchestratorService:
|
|
| 387 |
if incomplete:
|
| 388 |
raise ValueError(f"Final report is available after all tasks are approved. Pending tasks: {len(incomplete)}")
|
| 389 |
|
|
|
|
| 390 |
report_title = REPORT_VARIANTS[variant]["title"]
|
| 391 |
lines = [
|
| 392 |
f"# {report_title}: {project['name']}",
|
| 393 |
"",
|
| 394 |
-
"## Project
|
| 395 |
-
project.get("description") or "No
|
|
|
|
| 396 |
]
|
| 397 |
|
|
|
|
| 398 |
if project.get("context"):
|
| 399 |
-
lines.extend(["
|
| 400 |
|
| 401 |
-
lines.extend(["
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
|
| 403 |
for index, task in enumerate(tasks, start=1):
|
| 404 |
lines.extend([
|
| 405 |
-
"",
|
| 406 |
f"### {index}. {task['title']}",
|
| 407 |
task.get("description") or "No task description provided.",
|
| 408 |
"",
|
| 409 |
-
_format_output_for_report(task.get("output_data"))
|
|
|
|
| 410 |
])
|
| 411 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
lines.extend([
|
|
|
|
|
|
|
| 413 |
"",
|
| 414 |
"## Completion Status",
|
| 415 |
f"All {len(tasks)} tasks are approved. Project status: completed."
|
|
@@ -477,38 +569,54 @@ class OrchestratorService:
|
|
| 477 |
system_prompt=planner_agent_data.get("system_prompt")
|
| 478 |
)
|
| 479 |
|
| 480 |
-
prompt = f"""Decompose the following project into 3-5 clear, actionable tasks.
|
| 481 |
Project Name: {project['name']}
|
| 482 |
Description: {project['description']}
|
| 483 |
Context: {project.get('context', 'None')}
|
| 484 |
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
"""
|
| 489 |
|
| 490 |
try:
|
| 491 |
result = await planner.run(prompt, [])
|
| 492 |
-
|
| 493 |
-
content = result["data"]
|
| 494 |
-
tasks_data = planner._parse_json_output(content) if isinstance(content, str) else content
|
| 495 |
|
| 496 |
-
#
|
| 497 |
if isinstance(tasks_data, dict):
|
| 498 |
-
# If agent wrapped it in {"tasks": [...]}, extract it
|
| 499 |
if "tasks" in tasks_data and isinstance(tasks_data["tasks"], list):
|
| 500 |
tasks_data = tasks_data["tasks"]
|
| 501 |
else:
|
| 502 |
-
# Single task as object, wrap in list
|
| 503 |
tasks_data = [tasks_data]
|
| 504 |
|
| 505 |
if not isinstance(tasks_data, list):
|
| 506 |
raise ValueError(f"Agent returned invalid format: {type(tasks_data)}. Expected list or dict.")
|
| 507 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
# Insert tasks
|
| 509 |
from .project_service import project_service
|
| 510 |
-
await project_service.add_tasks_to_project(project_id,
|
| 511 |
-
logger.info(f"Auto-decomposed project {project_id} into {len(
|
| 512 |
except Exception as e:
|
| 513 |
logger.error(f"Project decomposition failed: {e}")
|
| 514 |
|
|
|
|
| 164 |
try:
|
| 165 |
# 1. Fetch task and agents
|
| 166 |
task = supabase.table("tasks").select("*").eq("id", task_id).single().execute().data
|
| 167 |
+
agent_a_data = supabase.table("agents").select("*").eq("id", agent_a_id).single().execute().data
|
| 168 |
+
agent_b_data = supabase.table("agents").select("*").eq("id", agent_b_id).single().execute().data
|
| 169 |
+
|
| 170 |
+
if not task or not agent_a_data or not agent_b_data:
|
| 171 |
+
raise ValueError("Task or agents not found for debate.")
|
| 172 |
+
|
| 173 |
+
# Update status to in_progress
|
| 174 |
+
supabase.table("tasks").update({"status": "in_progress"}).eq("id", task_id).execute()
|
| 175 |
|
| 176 |
# 2. Agent A generates initial response
|
| 177 |
+
initial_res, _ = await AgentRunnerService.run_agent_task(
|
| 178 |
+
task,
|
| 179 |
+
agent_a_data,
|
| 180 |
+
start_action="debate_initial_start",
|
| 181 |
+
start_content=f"Debate Step 1: {agent_a_data['name']} generating initial proposal.",
|
| 182 |
+
complete_action="debate_initial_complete",
|
| 183 |
+
update_task=False
|
| 184 |
+
)
|
| 185 |
|
| 186 |
# 3. Agent B reviews and critiques
|
| 187 |
+
# We temporarily modify the task description for this run
|
| 188 |
+
task_critique = task.copy()
|
| 189 |
+
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'])}"
|
| 190 |
+
|
| 191 |
+
critique_res, _ = await AgentRunnerService.run_agent_task(
|
| 192 |
+
task_critique,
|
| 193 |
+
agent_b_data,
|
| 194 |
+
start_action="debate_critique_start",
|
| 195 |
+
start_content=f"Debate Step 2: {agent_b_data['name']} critiquing the proposal.",
|
| 196 |
+
complete_action="debate_critique_complete",
|
| 197 |
+
update_task=False
|
| 198 |
+
)
|
| 199 |
|
| 200 |
# 4. Agent A refines based on critique
|
| 201 |
+
task_refinement = task.copy()
|
| 202 |
+
task_refinement["description"] = f"Refine your initial output for the task: '{task['description']}' based on this critique: {json.dumps(critique_res['data'])}"
|
| 203 |
|
| 204 |
+
final_res, _ = await AgentRunnerService.run_agent_task(
|
| 205 |
+
task_refinement,
|
| 206 |
+
agent_a_data,
|
| 207 |
+
start_action="debate_refinement_start",
|
| 208 |
+
start_content=f"Debate Step 3: {agent_a_data['name']} refining proposal based on feedback.",
|
| 209 |
+
complete_action="debate_refinement_complete",
|
| 210 |
+
update_task=False
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
# 5. Save consolidated result and mark for approval
|
| 214 |
+
consolidated_output = {
|
| 215 |
+
"agent_name": agent_a_data["name"],
|
| 216 |
+
"provider": agent_a_data["api_provider"],
|
| 217 |
+
"model": agent_a_data["model"],
|
| 218 |
+
"is_debate": True,
|
| 219 |
+
"data": final_res["data"],
|
| 220 |
+
"debate_history": {
|
| 221 |
"initial": initial_res["data"],
|
| 222 |
"critique": critique_res["data"],
|
| 223 |
"final": final_res["data"]
|
| 224 |
}
|
| 225 |
+
}
|
| 226 |
+
|
| 227 |
+
supabase.table("tasks").update({
|
| 228 |
+
"status": "awaiting_approval",
|
| 229 |
+
"output_data": consolidated_output
|
| 230 |
}).eq("id", task_id).execute()
|
| 231 |
|
| 232 |
logger.info(f"Debate completed for task {task_id}")
|
| 233 |
|
| 234 |
except Exception as e:
|
| 235 |
logger.error(f"Debate failed: {str(e)}")
|
| 236 |
+
supabase.table("tasks").update({
|
| 237 |
+
"status": "failed",
|
| 238 |
+
"output_data": {"error": str(e)}
|
| 239 |
+
}).eq("id", task_id).execute()
|
| 240 |
+
|
| 241 |
+
# LOG ERROR TO AGENT CONSOLE
|
| 242 |
+
supabase.table("agent_logs").insert({
|
| 243 |
+
"task_id": task_id,
|
| 244 |
+
"action": "debate_failed",
|
| 245 |
+
"content": f"DEBATE ERROR: {str(e)}"
|
| 246 |
+
}).execute()
|
| 247 |
|
| 248 |
async def run_project(self, project_id: str):
|
| 249 |
"""
|
|
|
|
| 267 |
or []
|
| 268 |
)
|
| 269 |
|
| 270 |
+
# Check if ANY tasks exist for this project (regardless of status) to avoid re-decomposing
|
| 271 |
+
all_tasks_res = supabase.table("tasks").select("id", count="exact").eq("project_id", project_id).limit(1).execute()
|
| 272 |
+
has_any_tasks = all_tasks_res.count > 0 if all_tasks_res.count is not None else len(all_tasks_res.data) > 0
|
| 273 |
+
|
| 274 |
+
# Automatic Decomposition: Only if no tasks exist AT ALL
|
| 275 |
+
if not has_any_tasks:
|
| 276 |
logger.info(f"No tasks found for project {project_id}. Triggering auto-decomposition.")
|
| 277 |
await self.decompose_project(project_id)
|
| 278 |
# Re-fetch tasks after decomposition
|
|
|
|
| 440 |
if incomplete:
|
| 441 |
raise ValueError(f"Final report is available after all tasks are approved. Pending tasks: {len(incomplete)}")
|
| 442 |
|
| 443 |
+
# 0. Header and Description
|
| 444 |
report_title = REPORT_VARIANTS[variant]["title"]
|
| 445 |
lines = [
|
| 446 |
f"# {report_title}: {project['name']}",
|
| 447 |
"",
|
| 448 |
+
"## Project Overview",
|
| 449 |
+
project.get("description") or "No description provided.",
|
| 450 |
+
""
|
| 451 |
]
|
| 452 |
|
| 453 |
+
# Add Context if exists
|
| 454 |
if project.get("context"):
|
| 455 |
+
lines.extend(["## Context", project["context"], ""])
|
| 456 |
|
| 457 |
+
lines.extend(["## Execution Summary", ""])
|
| 458 |
+
|
| 459 |
+
# We will add the tabular summary later in the UI or via charts,
|
| 460 |
+
# but for the text report, we include the approved work summary.
|
| 461 |
+
lines.extend(["## Approved Work Summary", ""])
|
| 462 |
|
| 463 |
for index, task in enumerate(tasks, start=1):
|
| 464 |
lines.extend([
|
|
|
|
| 465 |
f"### {index}. {task['title']}",
|
| 466 |
task.get("description") or "No task description provided.",
|
| 467 |
"",
|
| 468 |
+
_format_output_for_report(task.get("output_data")),
|
| 469 |
+
""
|
| 470 |
])
|
| 471 |
|
| 472 |
+
# Final Conclusion Generation
|
| 473 |
+
conclusion = (
|
| 474 |
+
"Based on the approved task outputs, the project has successfully established a foundational framework. "
|
| 475 |
+
"The key findings suggest a viable path forward by focusing on the identified entry wedge and "
|
| 476 |
+
"mitigating primary risks through phased execution."
|
| 477 |
+
)
|
| 478 |
+
|
| 479 |
+
if variant == "full":
|
| 480 |
+
try:
|
| 481 |
+
# Use the 'Brief Writer' or any available agent to summarize a conclusion
|
| 482 |
+
agent_data = self._select_report_agent(project, "brief")
|
| 483 |
+
if agent_data:
|
| 484 |
+
agent = AgentFactory.get_agent(
|
| 485 |
+
provider=agent_data["api_provider"],
|
| 486 |
+
name=agent_data["name"],
|
| 487 |
+
role=agent_data["role"],
|
| 488 |
+
model=agent_data["model"],
|
| 489 |
+
system_prompt="You write a 2-3 sentence strategic conclusion and 3 actionable next steps for a project report."
|
| 490 |
+
)
|
| 491 |
+
report_so_far = "\n".join(lines)
|
| 492 |
+
res = await agent.run(f"Based on this project report, write a final strategic conclusion and 3 next steps:\n\n{report_so_far}", [])
|
| 493 |
+
if res.get("status") != "error":
|
| 494 |
+
data = res.get("data")
|
| 495 |
+
if isinstance(data, str):
|
| 496 |
+
conclusion = data
|
| 497 |
+
elif isinstance(data, dict):
|
| 498 |
+
conclusion = data.get("conclusion") or data.get("content") or str(data)
|
| 499 |
+
except Exception as exc:
|
| 500 |
+
logger.warning(f"Failed to generate dynamic conclusion: {exc}")
|
| 501 |
+
|
| 502 |
lines.extend([
|
| 503 |
+
"## Strategic Conclusion",
|
| 504 |
+
conclusion,
|
| 505 |
"",
|
| 506 |
"## Completion Status",
|
| 507 |
f"All {len(tasks)} tasks are approved. Project status: completed."
|
|
|
|
| 569 |
system_prompt=planner_agent_data.get("system_prompt")
|
| 570 |
)
|
| 571 |
|
| 572 |
+
prompt = f"""Decompose the following project into 3-5 clear, actionable implementation tasks.
|
| 573 |
Project Name: {project['name']}
|
| 574 |
Description: {project['description']}
|
| 575 |
Context: {project.get('context', 'None')}
|
| 576 |
|
| 577 |
+
### Output Requirements:
|
| 578 |
+
You MUST return a valid JSON array of objects. Each object represents a task.
|
| 579 |
+
Do not include any conversational text, markdown formatting outside of the JSON, or explanations.
|
| 580 |
+
|
| 581 |
+
### JSON Schema:
|
| 582 |
+
[
|
| 583 |
+
{{
|
| 584 |
+
"title": "string (The name of the task)",
|
| 585 |
+
"description": "string (Detailed instructions for the agent)",
|
| 586 |
+
"priority": "integer (1-5, where 5 is highest priority)"
|
| 587 |
+
}}
|
| 588 |
+
]
|
| 589 |
+
|
| 590 |
+
IMPORTANT: Return a flat array. Do not wrap it in a parent 'tasks' object.
|
| 591 |
"""
|
| 592 |
|
| 593 |
try:
|
| 594 |
result = await planner.run(prompt, [])
|
| 595 |
+
tasks_data = result.get("data")
|
|
|
|
|
|
|
| 596 |
|
| 597 |
+
# Handle common LLM wrapping patterns
|
| 598 |
if isinstance(tasks_data, dict):
|
|
|
|
| 599 |
if "tasks" in tasks_data and isinstance(tasks_data["tasks"], list):
|
| 600 |
tasks_data = tasks_data["tasks"]
|
| 601 |
else:
|
|
|
|
| 602 |
tasks_data = [tasks_data]
|
| 603 |
|
| 604 |
if not isinstance(tasks_data, list):
|
| 605 |
raise ValueError(f"Agent returned invalid format: {type(tasks_data)}. Expected list or dict.")
|
| 606 |
|
| 607 |
+
# Filter out invalid tasks
|
| 608 |
+
valid_tasks = [
|
| 609 |
+
t for t in tasks_data
|
| 610 |
+
if isinstance(t, dict) and t.get("title")
|
| 611 |
+
]
|
| 612 |
+
|
| 613 |
+
if not valid_tasks:
|
| 614 |
+
raise ValueError("No valid tasks extracted from agent output.")
|
| 615 |
+
|
| 616 |
# Insert tasks
|
| 617 |
from .project_service import project_service
|
| 618 |
+
await project_service.add_tasks_to_project(project_id, valid_tasks)
|
| 619 |
+
logger.info(f"Auto-decomposed project {project_id} into {len(valid_tasks)} tasks.")
|
| 620 |
except Exception as e:
|
| 621 |
logger.error(f"Project decomposition failed: {e}")
|
| 622 |
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
services:
|
| 2 |
+
backend:
|
| 3 |
+
build:
|
| 4 |
+
context: ./backend
|
| 5 |
+
dockerfile: Dockerfile
|
| 6 |
+
ports:
|
| 7 |
+
- "8000:8000"
|
| 8 |
+
env_file:
|
| 9 |
+
- ./backend/.env
|
| 10 |
+
environment:
|
| 11 |
+
- ALLOWED_ORIGINS=http://localhost:80,http://localhost:5173
|
| 12 |
+
volumes:
|
| 13 |
+
- ./backend/outputs:/app/outputs
|
| 14 |
+
restart: unless-stopped
|
| 15 |
+
|
| 16 |
+
frontend:
|
| 17 |
+
build:
|
| 18 |
+
context: ./frontend
|
| 19 |
+
dockerfile: Dockerfile
|
| 20 |
+
ports:
|
| 21 |
+
- "80:80"
|
| 22 |
+
environment:
|
| 23 |
+
- VITE_API_URL=http://localhost:8000
|
| 24 |
+
depends_on:
|
| 25 |
+
- backend
|
| 26 |
+
restart: unless-stopped
|
frontend/src/App.tsx
CHANGED
|
@@ -27,6 +27,9 @@ import SettingsView from './components/SettingsView';
|
|
| 27 |
import Dashboard from './components/Dashboard';
|
| 28 |
import ProjectDetail from './components/ProjectDetail';
|
| 29 |
import AgentsView from './components/AgentsView';
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
type AppTab = 'dashboard' | 'project-detail' | 'agents' | 'marketplace' | 'debate' | 'voice' | 'spatial' | 'monitoring' | 'new-project' | 'settings';
|
| 32 |
|
|
@@ -35,6 +38,12 @@ const App: React.FC = () => {
|
|
| 35 |
const [activeTab, setActiveTab] = useState<AppTab>('dashboard');
|
| 36 |
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
| 37 |
const [isSidebarOpen, setIsSidebarOpen] = useState(() => typeof window === 'undefined' || window.innerWidth >= 900);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
const navigateTo = (tab: AppTab) => {
|
| 40 |
setActiveTab(tab);
|
|
@@ -43,7 +52,7 @@ const App: React.FC = () => {
|
|
| 43 |
}
|
| 44 |
};
|
| 45 |
|
| 46 |
-
if (loading) return
|
| 47 |
if (!session) return <Login />;
|
| 48 |
|
| 49 |
return (
|
|
@@ -186,18 +195,8 @@ const App: React.FC = () => {
|
|
| 186 |
{activeTab === 'settings' && <SettingsView />}
|
| 187 |
</section>
|
| 188 |
|
| 189 |
-
{/* Real-time Console
|
| 190 |
-
<
|
| 191 |
-
<div style={{ padding: 'var(--space-sm) var(--space-md)', borderBottom: '1px solid var(--glass-border)', display: 'flex', alignItems: 'center', gap: 'var(--space-sm)' }}>
|
| 192 |
-
<Terminal size={16} color="var(--accent)" />
|
| 193 |
-
<span style={{ fontSize: '0.8rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.1em' }}>Agent Console</span>
|
| 194 |
-
</div>
|
| 195 |
-
<div style={{ padding: 'var(--space-md)', height: '150px', overflowY: 'auto', fontFamily: 'monospace', fontSize: '0.85rem', color: 'var(--accent)' }}>
|
| 196 |
-
<div>[System] Initializing orchestrator...</div>
|
| 197 |
-
<div>[Orchestrator] Scanning for pending tasks...</div>
|
| 198 |
-
<div>[Agent: Researcher] Starting web search for "Market trends 2026"...</div>
|
| 199 |
-
</div>
|
| 200 |
-
</section>
|
| 201 |
</main>
|
| 202 |
</div>
|
| 203 |
);
|
|
|
|
| 27 |
import Dashboard from './components/Dashboard';
|
| 28 |
import ProjectDetail from './components/ProjectDetail';
|
| 29 |
import AgentsView from './components/AgentsView';
|
| 30 |
+
import AgentConsole from './components/AgentConsole';
|
| 31 |
+
import SplashScreen from './components/SplashScreen';
|
| 32 |
+
import { useEffect } from 'react';
|
| 33 |
|
| 34 |
type AppTab = 'dashboard' | 'project-detail' | 'agents' | 'marketplace' | 'debate' | 'voice' | 'spatial' | 'monitoring' | 'new-project' | 'settings';
|
| 35 |
|
|
|
|
| 38 |
const [activeTab, setActiveTab] = useState<AppTab>('dashboard');
|
| 39 |
const [selectedProjectId, setSelectedProjectId] = useState<string | null>(null);
|
| 40 |
const [isSidebarOpen, setIsSidebarOpen] = useState(() => typeof window === 'undefined' || window.innerWidth >= 900);
|
| 41 |
+
const [showSplash, setShowSplash] = useState(true);
|
| 42 |
+
|
| 43 |
+
useEffect(() => {
|
| 44 |
+
const timer = setTimeout(() => setShowSplash(false), 2500);
|
| 45 |
+
return () => clearTimeout(timer);
|
| 46 |
+
}, []);
|
| 47 |
|
| 48 |
const navigateTo = (tab: AppTab) => {
|
| 49 |
setActiveTab(tab);
|
|
|
|
| 52 |
}
|
| 53 |
};
|
| 54 |
|
| 55 |
+
if (loading || showSplash) return <AnimatePresence><SplashScreen /></AnimatePresence>;
|
| 56 |
if (!session) return <Login />;
|
| 57 |
|
| 58 |
return (
|
|
|
|
| 195 |
{activeTab === 'settings' && <SettingsView />}
|
| 196 |
</section>
|
| 197 |
|
| 198 |
+
{/* Real-time Agent Console */}
|
| 199 |
+
<AgentConsole />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
</main>
|
| 201 |
</div>
|
| 202 |
);
|
frontend/src/components/AgentConsole.tsx
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useState, useRef } from 'react';
|
| 2 |
+
import { Terminal } from 'lucide-react';
|
| 3 |
+
import { supabase } from '../services/supabase';
|
| 4 |
+
|
| 5 |
+
interface LogEntry {
|
| 6 |
+
id: string;
|
| 7 |
+
created_at: string;
|
| 8 |
+
action: string;
|
| 9 |
+
content: string;
|
| 10 |
+
task_id: string | null;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const AgentConsole: React.FC = () => {
|
| 14 |
+
const [logs, setLogs] = useState<LogEntry[]>([]);
|
| 15 |
+
const [error, setError] = useState<string | null>(null);
|
| 16 |
+
const scrollRef = useRef<HTMLDivElement>(null);
|
| 17 |
+
|
| 18 |
+
useEffect(() => {
|
| 19 |
+
const fetchLogs = async () => {
|
| 20 |
+
const { data, error: supabaseError } = await supabase
|
| 21 |
+
.from('agent_logs')
|
| 22 |
+
.select('*')
|
| 23 |
+
.order('created_at', { ascending: false })
|
| 24 |
+
.limit(50);
|
| 25 |
+
|
| 26 |
+
if (supabaseError) {
|
| 27 |
+
console.error('Error fetching logs:', supabaseError);
|
| 28 |
+
setError(supabaseError.message);
|
| 29 |
+
return;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
setError(null);
|
| 33 |
+
if (data) {
|
| 34 |
+
setLogs(data.reverse());
|
| 35 |
+
}
|
| 36 |
+
};
|
| 37 |
+
|
| 38 |
+
fetchLogs();
|
| 39 |
+
|
| 40 |
+
// Fallback polling every 3 seconds
|
| 41 |
+
const pollInterval = setInterval(fetchLogs, 3000);
|
| 42 |
+
|
| 43 |
+
// Set up real-time subscription
|
| 44 |
+
const channel = supabase
|
| 45 |
+
.channel('agent_logs_changes')
|
| 46 |
+
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'agent_logs' }, (payload) => {
|
| 47 |
+
setLogs(prev => {
|
| 48 |
+
const newLog = payload.new as LogEntry;
|
| 49 |
+
if (prev.some(l => l.id === newLog.id)) return prev;
|
| 50 |
+
return [...prev, newLog].slice(-50);
|
| 51 |
+
});
|
| 52 |
+
})
|
| 53 |
+
.subscribe();
|
| 54 |
+
|
| 55 |
+
return () => {
|
| 56 |
+
clearInterval(pollInterval);
|
| 57 |
+
supabase.removeChannel(channel);
|
| 58 |
+
};
|
| 59 |
+
}, []);
|
| 60 |
+
|
| 61 |
+
useEffect(() => {
|
| 62 |
+
if (scrollRef.current) {
|
| 63 |
+
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
| 64 |
+
}
|
| 65 |
+
}, [logs]);
|
| 66 |
+
|
| 67 |
+
const formatTimestamp = (ts: string) => {
|
| 68 |
+
const date = new Date(ts);
|
| 69 |
+
return date.toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
return (
|
| 73 |
+
<section className="glass-panel app-console">
|
| 74 |
+
<div style={{ padding: 'var(--space-sm) var(--space-md)', borderBottom: '1px solid var(--glass-border)', display: 'flex', alignItems: 'center', gap: 'var(--space-sm)' }}>
|
| 75 |
+
<Terminal size={16} color="var(--accent)" />
|
| 76 |
+
<span style={{ fontSize: '0.8rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.1em' }}>Agent Console</span>
|
| 77 |
+
</div>
|
| 78 |
+
<div
|
| 79 |
+
ref={scrollRef}
|
| 80 |
+
style={{
|
| 81 |
+
padding: 'var(--space-md)',
|
| 82 |
+
height: '150px',
|
| 83 |
+
overflowY: 'auto',
|
| 84 |
+
fontFamily: 'monospace',
|
| 85 |
+
fontSize: '0.85rem',
|
| 86 |
+
color: 'var(--accent)',
|
| 87 |
+
display: 'flex',
|
| 88 |
+
flexDirection: 'column',
|
| 89 |
+
gap: '4px'
|
| 90 |
+
}}
|
| 91 |
+
>
|
| 92 |
+
{error && (
|
| 93 |
+
<div style={{ color: 'var(--danger)', padding: 'var(--space-sm)', border: '1px solid var(--danger)', borderRadius: 'var(--radius-sm)', marginBottom: '8px' }}>
|
| 94 |
+
[ERROR] {error}. This might be due to Supabase RLS policies.
|
| 95 |
+
</div>
|
| 96 |
+
)}
|
| 97 |
+
{logs.length === 0 && !error && <div style={{ color: 'var(--text-dim)' }}>[System] Waiting for logs...</div>}
|
| 98 |
+
{logs.map((log) => (
|
| 99 |
+
<div key={log.id}>
|
| 100 |
+
<span style={{ color: 'var(--text-dim)', marginRight: '8px' }}>[{formatTimestamp(log.created_at)}]</span>
|
| 101 |
+
<span style={{ color: 'var(--info)', marginRight: '8px' }}>[{log.action.toUpperCase()}]</span>
|
| 102 |
+
<span>{log.content}</span>
|
| 103 |
+
</div>
|
| 104 |
+
))}
|
| 105 |
+
</div>
|
| 106 |
+
</section>
|
| 107 |
+
);
|
| 108 |
+
};
|
| 109 |
+
|
| 110 |
+
export default AgentConsole;
|
frontend/src/components/Dashboard.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
| 2 |
-
import { FolderOpen, Play, RefreshCw } from 'lucide-react';
|
| 3 |
import { motion } from 'framer-motion';
|
| 4 |
import { supabase } from '../services/supabase';
|
| 5 |
import { useAuth } from '../context/useAuth';
|
|
@@ -72,6 +72,17 @@ const Dashboard: React.FC<DashboardProps> = ({ onNewProject, onOpenProject }) =>
|
|
| 72 |
loadDashboard();
|
| 73 |
}, [loadDashboard]);
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
const taskCounts = useMemo(() => {
|
| 76 |
return tasks.reduce<Record<string, { done: number; total: number }>>((acc, task) => {
|
| 77 |
if (!acc[task.project_id]) acc[task.project_id] = { done: 0, total: 0 };
|
|
@@ -125,6 +136,7 @@ const Dashboard: React.FC<DashboardProps> = ({ onNewProject, onOpenProject }) =>
|
|
| 125 |
tasksDone={counts.done}
|
| 126 |
tasksTotal={counts.total}
|
| 127 |
onOpen={() => onOpenProject(project.id)}
|
|
|
|
| 128 |
/>
|
| 129 |
);
|
| 130 |
})}
|
|
@@ -133,26 +145,35 @@ const Dashboard: React.FC<DashboardProps> = ({ onNewProject, onOpenProject }) =>
|
|
| 133 |
);
|
| 134 |
};
|
| 135 |
|
| 136 |
-
const ProjectCard: React.FC<{ name: string; description: string | null; status: string; tasksDone: number; tasksTotal: number; onOpen: () => void }> = ({
|
| 137 |
name,
|
| 138 |
description,
|
| 139 |
status,
|
| 140 |
tasksDone,
|
| 141 |
tasksTotal,
|
| 142 |
-
onOpen
|
|
|
|
| 143 |
}) => {
|
| 144 |
const progress = tasksTotal > 0 ? (tasksDone / tasksTotal) * 100 : 0;
|
| 145 |
|
| 146 |
return (
|
| 147 |
<motion.div whileHover={{ y: -5 }} className="glass-panel project-card" style={{ padding: 'var(--space-lg)', position: 'relative', overflow: 'hidden' }}>
|
| 148 |
-
<div className="project-card-header">
|
| 149 |
-
<h3 style={{ fontSize: '1.25rem' }}>{name}</h3>
|
| 150 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
</div>
|
| 152 |
|
| 153 |
-
|
| 154 |
-
{description || 'No description provided.'}
|
| 155 |
-
</p>
|
| 156 |
|
| 157 |
<div style={{ marginBottom: 'var(--space-lg)' }}>
|
| 158 |
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.85rem', marginBottom: 'var(--space-xs)' }}>
|
|
|
|
| 1 |
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
| 2 |
+
import { FolderOpen, Play, RefreshCw, Trash2 } from 'lucide-react';
|
| 3 |
import { motion } from 'framer-motion';
|
| 4 |
import { supabase } from '../services/supabase';
|
| 5 |
import { useAuth } from '../context/useAuth';
|
|
|
|
| 72 |
loadDashboard();
|
| 73 |
}, [loadDashboard]);
|
| 74 |
|
| 75 |
+
const handleDeleteProject = async (id: string, name: string) => {
|
| 76 |
+
if (!window.confirm(`Are you sure you want to delete "${name}"? This action cannot be undone.`)) return;
|
| 77 |
+
|
| 78 |
+
const { error: deleteError } = await supabase.from('projects').delete().eq('id', id);
|
| 79 |
+
if (deleteError) {
|
| 80 |
+
setError(`Error deleting project: ${deleteError.message}`);
|
| 81 |
+
} else {
|
| 82 |
+
loadDashboard();
|
| 83 |
+
}
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
const taskCounts = useMemo(() => {
|
| 87 |
return tasks.reduce<Record<string, { done: number; total: number }>>((acc, task) => {
|
| 88 |
if (!acc[task.project_id]) acc[task.project_id] = { done: 0, total: 0 };
|
|
|
|
| 136 |
tasksDone={counts.done}
|
| 137 |
tasksTotal={counts.total}
|
| 138 |
onOpen={() => onOpenProject(project.id)}
|
| 139 |
+
onDelete={() => handleDeleteProject(project.id, project.name)}
|
| 140 |
/>
|
| 141 |
);
|
| 142 |
})}
|
|
|
|
| 145 |
);
|
| 146 |
};
|
| 147 |
|
| 148 |
+
const ProjectCard: React.FC<{ name: string; description: string | null; status: string; tasksDone: number; tasksTotal: number; onOpen: () => void; onDelete: () => void }> = ({
|
| 149 |
name,
|
| 150 |
description,
|
| 151 |
status,
|
| 152 |
tasksDone,
|
| 153 |
tasksTotal,
|
| 154 |
+
onOpen,
|
| 155 |
+
onDelete
|
| 156 |
}) => {
|
| 157 |
const progress = tasksTotal > 0 ? (tasksDone / tasksTotal) * 100 : 0;
|
| 158 |
|
| 159 |
return (
|
| 160 |
<motion.div whileHover={{ y: -5 }} className="glass-panel project-card" style={{ padding: 'var(--space-lg)', position: 'relative', overflow: 'hidden' }}>
|
| 161 |
+
<div className="project-card-header" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 'var(--space-md)' }}>
|
| 162 |
+
<h3 style={{ fontSize: '1.25rem', margin: 0, flex: 1, lineHeight: 1.2 }}>{name}</h3>
|
| 163 |
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center', flexShrink: 0 }}>
|
| 164 |
+
<StatusBadge status={status} />
|
| 165 |
+
<button
|
| 166 |
+
className="btn btn-icon"
|
| 167 |
+
onClick={(e) => { e.stopPropagation(); onDelete(); }}
|
| 168 |
+
style={{ color: 'var(--danger)', opacity: 0.6, padding: '4px' }}
|
| 169 |
+
title="Delete Project"
|
| 170 |
+
>
|
| 171 |
+
<Trash2 size={16} />
|
| 172 |
+
</button>
|
| 173 |
+
</div>
|
| 174 |
</div>
|
| 175 |
|
| 176 |
+
{/* Description removed as requested for a cleaner layout */}
|
|
|
|
|
|
|
| 177 |
|
| 178 |
<div style={{ marginBottom: 'var(--space-lg)' }}>
|
| 179 |
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '0.85rem', marginBottom: 'var(--space-xs)' }}>
|
frontend/src/components/DebateView.tsx
CHANGED
|
@@ -13,8 +13,95 @@ interface DebateAgent {
|
|
| 13 |
interface DebateTask {
|
| 14 |
id: string;
|
| 15 |
title: string;
|
|
|
|
| 16 |
}
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
const DebateView: React.FC = () => {
|
| 19 |
const [agents, setAgents] = useState<DebateAgent[]>([]);
|
| 20 |
const [tasks, setTasks] = useState<DebateTask[]>([]);
|
|
@@ -23,17 +110,40 @@ const DebateView: React.FC = () => {
|
|
| 23 |
const [agentB, setAgentB] = useState('');
|
| 24 |
const [loading, setLoading] = useState(false);
|
| 25 |
const [status, setStatus] = useState<string | null>(null);
|
|
|
|
| 26 |
|
| 27 |
useEffect(() => {
|
| 28 |
const fetchData = async () => {
|
| 29 |
const { data: agentsData } = await supabase.from('agents').select('id,name,model');
|
| 30 |
-
const { data: tasksData } = await supabase.from('tasks')
|
|
|
|
|
|
|
| 31 |
if (agentsData) setAgents(agentsData);
|
| 32 |
if (tasksData) setTasks(tasksData);
|
| 33 |
};
|
| 34 |
fetchData();
|
| 35 |
}, []);
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
const handleStartDebate = async () => {
|
| 38 |
if (!selectedTask || !agentA || !agentB) {
|
| 39 |
alert('Please select a task and two different agents.');
|
|
@@ -44,6 +154,7 @@ const DebateView: React.FC = () => {
|
|
| 44 |
return;
|
| 45 |
}
|
| 46 |
|
|
|
|
| 47 |
setLoading(true);
|
| 48 |
setStatus('Initializing debate flow...');
|
| 49 |
|
|
@@ -60,20 +171,29 @@ const DebateView: React.FC = () => {
|
|
| 60 |
|
| 61 |
if (response.ok) {
|
| 62 |
setStatus('Debate started! Monitor the agent console for progress.');
|
|
|
|
| 63 |
} else {
|
| 64 |
setStatus('Failed to start debate.');
|
|
|
|
| 65 |
}
|
| 66 |
} catch {
|
| 67 |
setStatus('Error connecting to backend.');
|
|
|
|
| 68 |
}
|
| 69 |
-
setLoading(false);
|
| 70 |
};
|
| 71 |
|
| 72 |
return (
|
| 73 |
<motion.div
|
| 74 |
initial={{ opacity: 0, y: 20 }}
|
| 75 |
animate={{ opacity: 1, y: 0 }}
|
| 76 |
-
className="glass-panel
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
>
|
| 78 |
<div className="panel-heading">
|
| 79 |
<MessageSquare size={32} color="var(--accent)" />
|
|
@@ -92,7 +212,7 @@ const DebateView: React.FC = () => {
|
|
| 92 |
style={{ width: '100%', padding: '0.8rem', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--glass-border)', borderRadius: 'var(--radius-md)', color: 'white' }}
|
| 93 |
>
|
| 94 |
<option value="">-- Choose a pending task --</option>
|
| 95 |
-
{tasks.map(t => <option key={t.id} value={t.id}>{t.title}</option>)}
|
| 96 |
</select>
|
| 97 |
</div>
|
| 98 |
|
|
@@ -135,8 +255,46 @@ const DebateView: React.FC = () => {
|
|
| 135 |
style={{ width: '100%', padding: '1rem', marginTop: 'var(--space-md)' }}
|
| 136 |
>
|
| 137 |
<Play size={18} fill="white" />
|
| 138 |
-
{loading ? 'Processing...' : 'Execute Debate Flow'}
|
| 139 |
</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
</div>
|
| 141 |
</motion.div>
|
| 142 |
);
|
|
|
|
| 13 |
interface DebateTask {
|
| 14 |
id: string;
|
| 15 |
title: string;
|
| 16 |
+
status: string;
|
| 17 |
}
|
| 18 |
|
| 19 |
+
const renderContent = (content: any) => {
|
| 20 |
+
if (!content) return null;
|
| 21 |
+
if (typeof content === 'string') return content;
|
| 22 |
+
|
| 23 |
+
if (Array.isArray(content) && content.length > 0 && typeof content[0] === 'object' && !Array.isArray(content[0])) {
|
| 24 |
+
const keys = Object.keys(content[0]);
|
| 25 |
+
const isTableCandidate = content.every(item =>
|
| 26 |
+
item && typeof item === 'object' &&
|
| 27 |
+
Object.keys(item).length === keys.length &&
|
| 28 |
+
keys.every(k => Object.keys(item).includes(k))
|
| 29 |
+
);
|
| 30 |
+
|
| 31 |
+
if (isTableCandidate && keys.length <= 6) {
|
| 32 |
+
return (
|
| 33 |
+
<div style={{ overflowX: 'auto', marginBottom: '16px', borderRadius: 'var(--radius-md)', border: '1px solid rgba(255,255,255,0.05)' }}>
|
| 34 |
+
<table style={{ width: '100%', borderCollapse: 'collapse', fontSize: '0.8rem' }}>
|
| 35 |
+
<thead>
|
| 36 |
+
<tr style={{ background: 'rgba(110, 89, 255, 0.1)', borderBottom: '2px solid rgba(110, 89, 255, 0.2)' }}>
|
| 37 |
+
{keys.map(k => (
|
| 38 |
+
<th key={k} style={{ textAlign: 'left', padding: '12px 8px', color: 'var(--accent)', textTransform: 'uppercase', letterSpacing: '1px', fontWeight: 700 }}>
|
| 39 |
+
{k.replace(/_/g, ' ')}
|
| 40 |
+
</th>
|
| 41 |
+
))}
|
| 42 |
+
</tr>
|
| 43 |
+
</thead>
|
| 44 |
+
<tbody>
|
| 45 |
+
{content.map((item, i) => (
|
| 46 |
+
<tr key={i} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)', background: i % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.02)' }}>
|
| 47 |
+
{keys.map(k => (
|
| 48 |
+
<td key={k} style={{ padding: '10px 8px', color: 'rgba(255,255,255,0.9)' }}>
|
| 49 |
+
{typeof item[k] === 'object' ? JSON.stringify(item[k]) : String(item[k])}
|
| 50 |
+
</td>
|
| 51 |
+
))}
|
| 52 |
+
</tr>
|
| 53 |
+
))}
|
| 54 |
+
</tbody>
|
| 55 |
+
</table>
|
| 56 |
+
</div>
|
| 57 |
+
);
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
if (Array.isArray(content)) {
|
| 62 |
+
return (
|
| 63 |
+
<ul style={{ paddingLeft: '20px', margin: 0 }}>
|
| 64 |
+
{content.map((item, i) => (
|
| 65 |
+
<li key={i} style={{ marginBottom: '8px' }}>
|
| 66 |
+
{typeof item === 'object' ? renderContent(item) : String(item)}
|
| 67 |
+
</li>
|
| 68 |
+
))}
|
| 69 |
+
</ul>
|
| 70 |
+
);
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
if (typeof content === 'object') {
|
| 74 |
+
return (
|
| 75 |
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
| 76 |
+
{Object.entries(content).map(([key, value]) => (
|
| 77 |
+
<div key={key}>
|
| 78 |
+
<div style={{
|
| 79 |
+
fontWeight: 700,
|
| 80 |
+
color: 'var(--accent)',
|
| 81 |
+
fontSize: '0.7rem',
|
| 82 |
+
textTransform: 'uppercase',
|
| 83 |
+
letterSpacing: '1px',
|
| 84 |
+
marginBottom: '6px',
|
| 85 |
+
opacity: 0.8
|
| 86 |
+
}}>
|
| 87 |
+
{key.replace(/_/g, ' ').replace(/-/g, ' ')}
|
| 88 |
+
</div>
|
| 89 |
+
<div style={{
|
| 90 |
+
paddingLeft: '12px',
|
| 91 |
+
borderLeft: '2px solid rgba(110, 89, 255, 0.3)',
|
| 92 |
+
color: 'rgba(255,255,255,0.9)',
|
| 93 |
+
lineHeight: '1.5'
|
| 94 |
+
}}>
|
| 95 |
+
{typeof value === 'object' ? renderContent(value) : String(value)}
|
| 96 |
+
</div>
|
| 97 |
+
</div>
|
| 98 |
+
))}
|
| 99 |
+
</div>
|
| 100 |
+
);
|
| 101 |
+
}
|
| 102 |
+
return String(content);
|
| 103 |
+
};
|
| 104 |
+
|
| 105 |
const DebateView: React.FC = () => {
|
| 106 |
const [agents, setAgents] = useState<DebateAgent[]>([]);
|
| 107 |
const [tasks, setTasks] = useState<DebateTask[]>([]);
|
|
|
|
| 110 |
const [agentB, setAgentB] = useState('');
|
| 111 |
const [loading, setLoading] = useState(false);
|
| 112 |
const [status, setStatus] = useState<string | null>(null);
|
| 113 |
+
const [debateResult, setDebateResult] = useState<any>(null);
|
| 114 |
|
| 115 |
useEffect(() => {
|
| 116 |
const fetchData = async () => {
|
| 117 |
const { data: agentsData } = await supabase.from('agents').select('id,name,model');
|
| 118 |
+
const { data: tasksData } = await supabase.from('tasks')
|
| 119 |
+
.select('id,title,status')
|
| 120 |
+
.in('status', ['todo', 'awaiting_approval']);
|
| 121 |
if (agentsData) setAgents(agentsData);
|
| 122 |
if (tasksData) setTasks(tasksData);
|
| 123 |
};
|
| 124 |
fetchData();
|
| 125 |
}, []);
|
| 126 |
|
| 127 |
+
useEffect(() => {
|
| 128 |
+
let interval: number;
|
| 129 |
+
if (loading && selectedTask) {
|
| 130 |
+
interval = window.setInterval(async () => {
|
| 131 |
+
const { data } = await supabase.from('tasks').select('status, output_data').eq('id', selectedTask).single();
|
| 132 |
+
if (data && data.status !== 'in_progress') {
|
| 133 |
+
setLoading(false);
|
| 134 |
+
setStatus(data.status === 'awaiting_approval' ? 'Debate completed successfully!' : `Debate finished with status: ${data.status}`);
|
| 135 |
+
|
| 136 |
+
if (data.status === 'awaiting_approval' && data.output_data?.debate_history) {
|
| 137 |
+
setDebateResult(data.output_data.debate_history);
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
window.clearInterval(interval);
|
| 141 |
+
}
|
| 142 |
+
}, 3000);
|
| 143 |
+
}
|
| 144 |
+
return () => window.clearInterval(interval);
|
| 145 |
+
}, [loading, selectedTask]);
|
| 146 |
+
|
| 147 |
const handleStartDebate = async () => {
|
| 148 |
if (!selectedTask || !agentA || !agentB) {
|
| 149 |
alert('Please select a task and two different agents.');
|
|
|
|
| 154 |
return;
|
| 155 |
}
|
| 156 |
|
| 157 |
+
setDebateResult(null);
|
| 158 |
setLoading(true);
|
| 159 |
setStatus('Initializing debate flow...');
|
| 160 |
|
|
|
|
| 171 |
|
| 172 |
if (response.ok) {
|
| 173 |
setStatus('Debate started! Monitor the agent console for progress.');
|
| 174 |
+
// We keep loading=true, the useEffect will poll until completion
|
| 175 |
} else {
|
| 176 |
setStatus('Failed to start debate.');
|
| 177 |
+
setLoading(false);
|
| 178 |
}
|
| 179 |
} catch {
|
| 180 |
setStatus('Error connecting to backend.');
|
| 181 |
+
setLoading(false);
|
| 182 |
}
|
|
|
|
| 183 |
};
|
| 184 |
|
| 185 |
return (
|
| 186 |
<motion.div
|
| 187 |
initial={{ opacity: 0, y: 20 }}
|
| 188 |
animate={{ opacity: 1, y: 0 }}
|
| 189 |
+
className="glass-panel"
|
| 190 |
+
style={{
|
| 191 |
+
width: '100%',
|
| 192 |
+
maxWidth: debateResult ? '1000px' : '600px',
|
| 193 |
+
margin: '0 auto',
|
| 194 |
+
padding: 'var(--space-xl)',
|
| 195 |
+
transition: 'max-width 0.5s ease-in-out'
|
| 196 |
+
}}
|
| 197 |
>
|
| 198 |
<div className="panel-heading">
|
| 199 |
<MessageSquare size={32} color="var(--accent)" />
|
|
|
|
| 212 |
style={{ width: '100%', padding: '0.8rem', background: 'rgba(255,255,255,0.05)', border: '1px solid var(--glass-border)', borderRadius: 'var(--radius-md)', color: 'white' }}
|
| 213 |
>
|
| 214 |
<option value="">-- Choose a pending task --</option>
|
| 215 |
+
{tasks.map(t => <option key={t.id} value={t.id}>{t.title} ({t.status.replace('_', ' ')})</option>)}
|
| 216 |
</select>
|
| 217 |
</div>
|
| 218 |
|
|
|
|
| 255 |
style={{ width: '100%', padding: '1rem', marginTop: 'var(--space-md)' }}
|
| 256 |
>
|
| 257 |
<Play size={18} fill="white" />
|
| 258 |
+
{loading ? 'Processing Debate...' : 'Execute Debate Flow'}
|
| 259 |
</button>
|
| 260 |
+
|
| 261 |
+
{debateResult && (
|
| 262 |
+
<motion.div
|
| 263 |
+
initial={{ opacity: 0, height: 0 }}
|
| 264 |
+
animate={{ opacity: 1, height: 'auto' }}
|
| 265 |
+
style={{ marginTop: 'var(--space-xl)', borderTop: '1px solid var(--glass-border)', paddingTop: 'var(--space-lg)' }}
|
| 266 |
+
>
|
| 267 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: 'var(--space-md)' }}>
|
| 268 |
+
<CheckCircle2 size={20} color="var(--success)" />
|
| 269 |
+
<h3 style={{ fontSize: '1.1rem', margin: 0 }}>Debate Results: Before & After</h3>
|
| 270 |
+
</div>
|
| 271 |
+
|
| 272 |
+
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-lg)' }}>
|
| 273 |
+
<div className="glass-panel" style={{ padding: 'var(--space-md)', background: 'rgba(255,255,255,0.02)' }}>
|
| 274 |
+
<div style={{ fontSize: '0.7rem', color: 'var(--text-dim)', textTransform: 'uppercase', marginBottom: '12px', letterSpacing: '1px' }}>Initial Proposal</div>
|
| 275 |
+
<div style={{ fontSize: '0.9rem', color: 'rgba(255,255,255,0.8)', maxHeight: '500px', overflowY: 'auto' }}>
|
| 276 |
+
{renderContent(debateResult.initial)}
|
| 277 |
+
</div>
|
| 278 |
+
</div>
|
| 279 |
+
|
| 280 |
+
<div className="glass-panel" style={{ padding: 'var(--space-md)', background: 'rgba(110, 89, 255, 0.05)', border: '1px solid rgba(110, 89, 255, 0.2)' }}>
|
| 281 |
+
<div style={{ fontSize: '0.7rem', color: 'var(--accent)', textTransform: 'uppercase', marginBottom: '12px', letterSpacing: '1px' }}>Refined Final Result</div>
|
| 282 |
+
<div style={{ fontSize: '0.9rem', color: 'white', maxHeight: '500px', overflowY: 'auto' }}>
|
| 283 |
+
{renderContent(debateResult.final)}
|
| 284 |
+
</div>
|
| 285 |
+
</div>
|
| 286 |
+
</div>
|
| 287 |
+
|
| 288 |
+
{debateResult.critique && (
|
| 289 |
+
<div style={{ marginTop: 'var(--space-md)', padding: 'var(--space-md)', background: 'rgba(255, 107, 107, 0.05)', borderRadius: 'var(--radius-md)', border: '1px solid rgba(255, 107, 107, 0.1)' }}>
|
| 290 |
+
<div style={{ fontSize: '0.7rem', color: 'var(--danger)', textTransform: 'uppercase', marginBottom: '8px', letterSpacing: '1px' }}>Critique Context</div>
|
| 291 |
+
<div style={{ fontSize: '0.85rem', color: 'var(--text-dim)', fontStyle: 'italic' }}>
|
| 292 |
+
{renderContent(debateResult.critique)}
|
| 293 |
+
</div>
|
| 294 |
+
</div>
|
| 295 |
+
)}
|
| 296 |
+
</motion.div>
|
| 297 |
+
)}
|
| 298 |
</div>
|
| 299 |
</motion.div>
|
| 300 |
);
|
frontend/src/components/ProjectDetail.tsx
CHANGED
|
@@ -75,8 +75,10 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
|
|
| 75 |
const [agentId, setAgentId] = useState('');
|
| 76 |
const [saving, setSaving] = useState(false);
|
| 77 |
const [orchestrating, setOrchestrating] = useState(false);
|
|
|
|
| 78 |
const [error, setError] = useState<string | null>(null);
|
| 79 |
const [message, setMessage] = useState<string | null>(null);
|
|
|
|
| 80 |
const [taskActionError, setTaskActionError] = useState<string | null>(null);
|
| 81 |
const [taskActionPending, setTaskActionPending] = useState(false);
|
| 82 |
const [finalReport, setFinalReport] = useState<string | null>(null);
|
|
@@ -204,13 +206,32 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
|
|
| 204 |
};
|
| 205 |
|
| 206 |
const runOrchestrator = async () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 207 |
setError(null);
|
| 208 |
setMessage(null);
|
| 209 |
-
setOrchestrating(true);
|
| 210 |
|
| 211 |
try {
|
| 212 |
const apiUrl = getApiUrl();
|
| 213 |
-
|
| 214 |
const response = await fetch(`${apiUrl}/orchestrator/projects/${projectId}/run`, {
|
| 215 |
method: 'POST'
|
| 216 |
});
|
|
@@ -220,13 +241,35 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
|
|
| 220 |
`Backend returned ${response.status} for POST /orchestrator/projects/${projectId}/run. Stop the stale process on port 8000 and restart backend from D:\\sistemas\\Aubm\\backend.`
|
| 221 |
);
|
| 222 |
setMessage('Project orchestrator started.');
|
| 223 |
-
|
|
|
|
| 224 |
} catch (exc) {
|
| 225 |
setError(exc instanceof Error ? exc.message : 'Failed to start orchestrator.');
|
| 226 |
} finally {
|
| 227 |
-
|
|
|
|
| 228 |
}
|
| 229 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 230 |
|
| 231 |
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
| 232 |
const allTasksApproved = tasks.length > 0 && tasks.every((task) => task.status === 'done');
|
|
@@ -271,7 +314,15 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
|
|
| 271 |
|
| 272 |
if (typeof output === 'object') {
|
| 273 |
const outputRecord = output as Record<string, unknown>;
|
|
|
|
|
|
|
| 274 |
const primaryOutput = outputRecord.data ?? outputRecord.raw_output ?? outputRecord.final ?? output;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
return typeof primaryOutput === 'string' ? primaryOutput : formatHumanReadable(primaryOutput).join('\n');
|
| 276 |
}
|
| 277 |
|
|
@@ -438,6 +489,12 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
|
|
| 438 |
Pessimistic Analysis
|
| 439 |
</button>
|
| 440 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 441 |
<button className="btn btn-primary" onClick={runOrchestrator} disabled={orchestrating}>
|
| 442 |
<PlayCircle size={18} />
|
| 443 |
{orchestrating ? 'Starting...' : 'Run Orchestrator'}
|
|
@@ -504,10 +561,33 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
|
|
| 504 |
<ListTodo size={22} color="var(--accent)" />
|
| 505 |
<h3>Tasks</h3>
|
| 506 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
{tasks.length === 0 && <p style={{ color: 'var(--text-dim)' }}>No tasks yet.</p>}
|
| 508 |
<div className="task-list">
|
| 509 |
-
{tasks
|
| 510 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 511 |
<div style={{ flex: 1 }}>
|
| 512 |
<strong>{task.title}</strong>
|
| 513 |
<p>{task.description || 'No description provided.'}</p>
|
|
@@ -519,7 +599,8 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
|
|
| 519 |
{task.status === 'awaiting_approval' && (
|
| 520 |
<button
|
| 521 |
className="btn btn-glass btn-sm"
|
| 522 |
-
onClick={() => {
|
|
|
|
| 523 |
setTaskActionError(null);
|
| 524 |
setSelectedTask(task);
|
| 525 |
}}
|
|
@@ -543,12 +624,20 @@ const ProjectDetail: React.FC<ProjectDetailProps> = ({ projectId, onBack }) => {
|
|
| 543 |
</div>
|
| 544 |
{taskActionError && <div className="inline-status modal-error">{taskActionError}</div>}
|
| 545 |
<div className="button-row modal-actions">
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
<button className="btn btn-glass" onClick={() => setSelectedTask(null)} disabled={taskActionPending}>
|
| 553 |
Close
|
| 554 |
</button>
|
|
|
|
| 75 |
const [agentId, setAgentId] = useState('');
|
| 76 |
const [saving, setSaving] = useState(false);
|
| 77 |
const [orchestrating, setOrchestrating] = useState(false);
|
| 78 |
+
const [approvingAll, setApprovingAll] = useState(false);
|
| 79 |
const [error, setError] = useState<string | null>(null);
|
| 80 |
const [message, setMessage] = useState<string | null>(null);
|
| 81 |
+
const [filter, setFilter] = useState<string>('all');
|
| 82 |
const [taskActionError, setTaskActionError] = useState<string | null>(null);
|
| 83 |
const [taskActionPending, setTaskActionPending] = useState(false);
|
| 84 |
const [finalReport, setFinalReport] = useState<string | null>(null);
|
|
|
|
| 206 |
};
|
| 207 |
|
| 208 |
const runOrchestrator = async () => {
|
| 209 |
+
if (tasks.length > 0) {
|
| 210 |
+
const confirmReset = window.confirm(
|
| 211 |
+
"This project already has tasks. Re-orchestrating will delete all existing tasks and progress to generate a fresh plan. Do you want to continue?"
|
| 212 |
+
);
|
| 213 |
+
if (!confirmReset) return;
|
| 214 |
+
|
| 215 |
+
// Clear existing tasks for a fresh start
|
| 216 |
+
setOrchestrating(true);
|
| 217 |
+
setError(null);
|
| 218 |
+
setMessage(null);
|
| 219 |
+
try {
|
| 220 |
+
const { error: deleteError } = await supabase.from('tasks').delete().eq('project_id', projectId);
|
| 221 |
+
if (deleteError) throw deleteError;
|
| 222 |
+
} catch (err: any) {
|
| 223 |
+
setError(`Failed to clear existing tasks: ${err.message}`);
|
| 224 |
+
setOrchestrating(false);
|
| 225 |
+
return;
|
| 226 |
+
}
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
setOrchestrating(true);
|
| 230 |
setError(null);
|
| 231 |
setMessage(null);
|
|
|
|
| 232 |
|
| 233 |
try {
|
| 234 |
const apiUrl = getApiUrl();
|
|
|
|
| 235 |
const response = await fetch(`${apiUrl}/orchestrator/projects/${projectId}/run`, {
|
| 236 |
method: 'POST'
|
| 237 |
});
|
|
|
|
| 241 |
`Backend returned ${response.status} for POST /orchestrator/projects/${projectId}/run. Stop the stale process on port 8000 and restart backend from D:\\sistemas\\Aubm\\backend.`
|
| 242 |
);
|
| 243 |
setMessage('Project orchestrator started.');
|
| 244 |
+
// Refresh after a delay to show the new tasks
|
| 245 |
+
window.setTimeout(loadProject, 2000);
|
| 246 |
} catch (exc) {
|
| 247 |
setError(exc instanceof Error ? exc.message : 'Failed to start orchestrator.');
|
| 248 |
} finally {
|
| 249 |
+
// We keep orchestrating=true for a bit longer to allow the backend to finish decomposition
|
| 250 |
+
window.setTimeout(() => setOrchestrating(false), 2000);
|
| 251 |
}
|
| 252 |
};
|
| 253 |
+
const handleApproveAll = async () => {
|
| 254 |
+
if (!projectId) return;
|
| 255 |
+
setApprovingAll(true);
|
| 256 |
+
setError(null);
|
| 257 |
+
setMessage(null);
|
| 258 |
+
try {
|
| 259 |
+
const response = await fetch(`${getApiUrl()}/tasks/project/${projectId}/approve-all`, {
|
| 260 |
+
method: 'POST'
|
| 261 |
+
});
|
| 262 |
+
if (response.ok) {
|
| 263 |
+
setMessage('All pending tasks approved!');
|
| 264 |
+
loadProject();
|
| 265 |
+
} else {
|
| 266 |
+
setError('Failed to approve all tasks.');
|
| 267 |
+
}
|
| 268 |
+
} catch {
|
| 269 |
+
setError('Error connecting to backend.');
|
| 270 |
+
}
|
| 271 |
+
setApprovingAll(false);
|
| 272 |
+
};
|
| 273 |
|
| 274 |
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
| 275 |
const allTasksApproved = tasks.length > 0 && tasks.every((task) => task.status === 'done');
|
|
|
|
| 314 |
|
| 315 |
if (typeof output === 'object') {
|
| 316 |
const outputRecord = output as Record<string, unknown>;
|
| 317 |
+
|
| 318 |
+
// Handle unified debate structure or standard agent result
|
| 319 |
const primaryOutput = outputRecord.data ?? outputRecord.raw_output ?? outputRecord.final ?? output;
|
| 320 |
+
|
| 321 |
+
if (outputRecord.is_debate && outputRecord.debate_history) {
|
| 322 |
+
// We could also show a "Debate Consensus" prefix here
|
| 323 |
+
return typeof primaryOutput === 'string' ? primaryOutput : formatHumanReadable(primaryOutput).join('\n');
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
return typeof primaryOutput === 'string' ? primaryOutput : formatHumanReadable(primaryOutput).join('\n');
|
| 327 |
}
|
| 328 |
|
|
|
|
| 489 |
Pessimistic Analysis
|
| 490 |
</button>
|
| 491 |
)}
|
| 492 |
+
{tasks.some(t => t.status === 'awaiting_approval') && (
|
| 493 |
+
<button className="btn btn-glass" onClick={handleApproveAll} disabled={approvingAll} style={{ borderColor: 'var(--success)', color: 'var(--success)' }}>
|
| 494 |
+
<CheckCircle2 size={18} />
|
| 495 |
+
{approvingAll ? 'Approving...' : 'Approve All'}
|
| 496 |
+
</button>
|
| 497 |
+
)}
|
| 498 |
<button className="btn btn-primary" onClick={runOrchestrator} disabled={orchestrating}>
|
| 499 |
<PlayCircle size={18} />
|
| 500 |
{orchestrating ? 'Starting...' : 'Run Orchestrator'}
|
|
|
|
| 561 |
<ListTodo size={22} color="var(--accent)" />
|
| 562 |
<h3>Tasks</h3>
|
| 563 |
</div>
|
| 564 |
+
<div className="filter-bar" style={{ display: 'flex', gap: '8px', marginBottom: '16px', overflowX: 'auto', paddingBottom: '4px' }}>
|
| 565 |
+
{['all', 'todo', 'in_progress', 'awaiting_approval', 'done', 'failed'].map((f) => (
|
| 566 |
+
<button
|
| 567 |
+
key={f}
|
| 568 |
+
className={`btn ${filter === f ? 'btn-primary' : 'btn-glass'}`}
|
| 569 |
+
onClick={() => setFilter(f)}
|
| 570 |
+
style={{ fontSize: '0.75rem', padding: '4px 12px', textTransform: 'capitalize' }}
|
| 571 |
+
>
|
| 572 |
+
{f.replace('_', ' ')}
|
| 573 |
+
</button>
|
| 574 |
+
))}
|
| 575 |
+
</div>
|
| 576 |
{tasks.length === 0 && <p style={{ color: 'var(--text-dim)' }}>No tasks yet.</p>}
|
| 577 |
<div className="task-list">
|
| 578 |
+
{tasks
|
| 579 |
+
.filter((t) => filter === 'all' || t.status === filter)
|
| 580 |
+
.map((task) => (
|
| 581 |
+
<div
|
| 582 |
+
key={task.id}
|
| 583 |
+
className={`task-row ${task.output_data ? 'clickable' : ''}`}
|
| 584 |
+
onClick={() => {
|
| 585 |
+
if (task.output_data) {
|
| 586 |
+
setTaskActionError(null);
|
| 587 |
+
setSelectedTask(task);
|
| 588 |
+
}
|
| 589 |
+
}}
|
| 590 |
+
>
|
| 591 |
<div style={{ flex: 1 }}>
|
| 592 |
<strong>{task.title}</strong>
|
| 593 |
<p>{task.description || 'No description provided.'}</p>
|
|
|
|
| 599 |
{task.status === 'awaiting_approval' && (
|
| 600 |
<button
|
| 601 |
className="btn btn-glass btn-sm"
|
| 602 |
+
onClick={(e) => {
|
| 603 |
+
e.stopPropagation();
|
| 604 |
setTaskActionError(null);
|
| 605 |
setSelectedTask(task);
|
| 606 |
}}
|
|
|
|
| 624 |
</div>
|
| 625 |
{taskActionError && <div className="inline-status modal-error">{taskActionError}</div>}
|
| 626 |
<div className="button-row modal-actions">
|
| 627 |
+
{selectedTask.status === 'awaiting_approval' ? (
|
| 628 |
+
<>
|
| 629 |
+
<button className="btn btn-primary" onClick={() => approveTask(selectedTask.id)} disabled={taskActionPending}>
|
| 630 |
+
{taskActionPending ? 'Saving...' : 'Approve Task'}
|
| 631 |
+
</button>
|
| 632 |
+
<button className="btn btn-glass" onClick={() => rejectTask(selectedTask.id)} disabled={taskActionPending}>
|
| 633 |
+
Reject & Re-run
|
| 634 |
+
</button>
|
| 635 |
+
</>
|
| 636 |
+
) : (
|
| 637 |
+
<div style={{ flex: 1, textAlign: 'left', color: 'var(--text-dim)', fontSize: '0.9rem' }}>
|
| 638 |
+
This task is completed and approved.
|
| 639 |
+
</div>
|
| 640 |
+
)}
|
| 641 |
<button className="btn btn-glass" onClick={() => setSelectedTask(null)} disabled={taskActionPending}>
|
| 642 |
Close
|
| 643 |
</button>
|
frontend/src/components/SplashScreen.tsx
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import { motion } from 'framer-motion';
|
| 3 |
+
import { Bot } from 'lucide-react';
|
| 4 |
+
|
| 5 |
+
const SplashScreen: React.FC = () => {
|
| 6 |
+
return (
|
| 7 |
+
<motion.div
|
| 8 |
+
initial={{ opacity: 1 }}
|
| 9 |
+
exit={{ opacity: 0 }}
|
| 10 |
+
transition={{ duration: 0.8 }}
|
| 11 |
+
style={{
|
| 12 |
+
position: 'fixed',
|
| 13 |
+
top: 0,
|
| 14 |
+
left: 0,
|
| 15 |
+
width: '100%',
|
| 16 |
+
height: '100%',
|
| 17 |
+
background: 'var(--bg-dark)',
|
| 18 |
+
display: 'flex',
|
| 19 |
+
flexDirection: 'column',
|
| 20 |
+
alignItems: 'center',
|
| 21 |
+
justifyContent: 'center',
|
| 22 |
+
zIndex: 9999,
|
| 23 |
+
}}
|
| 24 |
+
>
|
| 25 |
+
<motion.div
|
| 26 |
+
initial={{ scale: 0.5, opacity: 0 }}
|
| 27 |
+
animate={{ scale: 1, opacity: 1 }}
|
| 28 |
+
transition={{
|
| 29 |
+
type: "spring",
|
| 30 |
+
stiffness: 260,
|
| 31 |
+
damping: 20,
|
| 32 |
+
delay: 0.2
|
| 33 |
+
}}
|
| 34 |
+
style={{
|
| 35 |
+
width: '120px',
|
| 36 |
+
height: '120px',
|
| 37 |
+
borderRadius: '30px',
|
| 38 |
+
background: 'linear-gradient(135deg, var(--accent) 0%, var(--primary) 100%)',
|
| 39 |
+
display: 'flex',
|
| 40 |
+
alignItems: 'center',
|
| 41 |
+
justifyContent: 'center',
|
| 42 |
+
marginBottom: 'var(--space-xl)',
|
| 43 |
+
boxShadow: '0 20px 40px rgba(110, 89, 255, 0.3)',
|
| 44 |
+
}}
|
| 45 |
+
>
|
| 46 |
+
<Bot size={60} color="white" />
|
| 47 |
+
</motion.div>
|
| 48 |
+
|
| 49 |
+
<motion.h1
|
| 50 |
+
initial={{ y: 20, opacity: 0 }}
|
| 51 |
+
animate={{ y: 0, opacity: 1 }}
|
| 52 |
+
transition={{ delay: 0.4 }}
|
| 53 |
+
style={{
|
| 54 |
+
fontSize: '3rem',
|
| 55 |
+
fontWeight: 'bold',
|
| 56 |
+
background: 'linear-gradient(to right, #fff, #6e59ff)',
|
| 57 |
+
WebkitBackgroundClip: 'text',
|
| 58 |
+
WebkitTextFillColor: 'transparent',
|
| 59 |
+
marginBottom: 'var(--space-md)'
|
| 60 |
+
}}
|
| 61 |
+
>
|
| 62 |
+
Aubm
|
| 63 |
+
</motion.h1>
|
| 64 |
+
|
| 65 |
+
<motion.div
|
| 66 |
+
initial={{ scaleX: 0 }}
|
| 67 |
+
animate={{ scaleX: 1 }}
|
| 68 |
+
transition={{ delay: 0.6, duration: 1.5, ease: "easeInOut" }}
|
| 69 |
+
style={{
|
| 70 |
+
width: '200px',
|
| 71 |
+
height: '4px',
|
| 72 |
+
background: 'rgba(255,255,255,0.1)',
|
| 73 |
+
borderRadius: '2px',
|
| 74 |
+
overflow: 'hidden',
|
| 75 |
+
position: 'relative'
|
| 76 |
+
}}
|
| 77 |
+
>
|
| 78 |
+
<motion.div
|
| 79 |
+
animate={{
|
| 80 |
+
x: ['-100%', '100%']
|
| 81 |
+
}}
|
| 82 |
+
transition={{
|
| 83 |
+
repeat: Infinity,
|
| 84 |
+
duration: 1.5,
|
| 85 |
+
ease: "linear"
|
| 86 |
+
}}
|
| 87 |
+
style={{
|
| 88 |
+
position: 'absolute',
|
| 89 |
+
top: 0,
|
| 90 |
+
left: 0,
|
| 91 |
+
width: '100%',
|
| 92 |
+
height: '100%',
|
| 93 |
+
background: 'var(--accent)',
|
| 94 |
+
boxShadow: '0 0 10px var(--accent)'
|
| 95 |
+
}}
|
| 96 |
+
/>
|
| 97 |
+
</motion.div>
|
| 98 |
+
|
| 99 |
+
<motion.p
|
| 100 |
+
initial={{ opacity: 0 }}
|
| 101 |
+
animate={{ opacity: 1 }}
|
| 102 |
+
transition={{ delay: 1.2 }}
|
| 103 |
+
style={{
|
| 104 |
+
marginTop: 'var(--space-lg)',
|
| 105 |
+
color: 'var(--text-dim)',
|
| 106 |
+
fontSize: '0.9rem',
|
| 107 |
+
letterSpacing: '2px',
|
| 108 |
+
textTransform: 'uppercase'
|
| 109 |
+
}}
|
| 110 |
+
>
|
| 111 |
+
Orchestrating Intelligence
|
| 112 |
+
</motion.p>
|
| 113 |
+
</motion.div>
|
| 114 |
+
);
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
export default SplashScreen;
|
frontend/src/index.css
CHANGED
|
@@ -699,9 +699,11 @@
|
|
| 699 |
appearance: auto;
|
| 700 |
}
|
| 701 |
|
| 702 |
-
.project-form select option
|
| 703 |
-
|
| 704 |
-
|
|
|
|
|
|
|
| 705 |
}
|
| 706 |
|
| 707 |
.project-form input:focus,
|
|
@@ -725,14 +727,23 @@
|
|
| 725 |
}
|
| 726 |
|
| 727 |
.inline-status {
|
| 728 |
-
display: flex;
|
| 729 |
align-items: center;
|
| 730 |
gap: var(--space-sm);
|
| 731 |
-
padding:
|
| 732 |
-
|
|
|
|
| 733 |
border-radius: var(--radius-md);
|
| 734 |
-
|
| 735 |
-
color: var(--text-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 736 |
}
|
| 737 |
|
| 738 |
.settings-page {
|
|
@@ -776,10 +787,6 @@
|
|
| 776 |
color-scheme: dark;
|
| 777 |
}
|
| 778 |
|
| 779 |
-
.settings-section select option {
|
| 780 |
-
color: #111827;
|
| 781 |
-
background: #ffffff;
|
| 782 |
-
}
|
| 783 |
|
| 784 |
.settings-section-title {
|
| 785 |
display: flex;
|
|
@@ -852,6 +859,17 @@
|
|
| 852 |
background: rgba(255,255,255,0.04);
|
| 853 |
}
|
| 854 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 855 |
.task-row p {
|
| 856 |
color: var(--text-dim);
|
| 857 |
margin-top: var(--space-xs);
|
|
|
|
| 699 |
appearance: auto;
|
| 700 |
}
|
| 701 |
|
| 702 |
+
.project-form select option,
|
| 703 |
+
.settings-section select option,
|
| 704 |
+
select option {
|
| 705 |
+
color: white;
|
| 706 |
+
background: #111827;
|
| 707 |
}
|
| 708 |
|
| 709 |
.project-form input:focus,
|
|
|
|
| 727 |
}
|
| 728 |
|
| 729 |
.inline-status {
|
| 730 |
+
display: inline-flex;
|
| 731 |
align-items: center;
|
| 732 |
gap: var(--space-sm);
|
| 733 |
+
padding: 0.6rem 1rem;
|
| 734 |
+
background: rgba(110, 89, 255, 0.08);
|
| 735 |
+
border: 1px solid rgba(110, 89, 255, 0.2);
|
| 736 |
border-radius: var(--radius-md);
|
| 737 |
+
margin-bottom: var(--space-md);
|
| 738 |
+
color: var(--text-h);
|
| 739 |
+
font-size: 0.85rem;
|
| 740 |
+
font-weight: 500;
|
| 741 |
+
animation: slideDown 0.3s ease;
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
@keyframes slideDown {
|
| 745 |
+
from { opacity: 0; transform: translateY(-10px); }
|
| 746 |
+
to { opacity: 1; transform: translateY(0); }
|
| 747 |
}
|
| 748 |
|
| 749 |
.settings-page {
|
|
|
|
| 787 |
color-scheme: dark;
|
| 788 |
}
|
| 789 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 790 |
|
| 791 |
.settings-section-title {
|
| 792 |
display: flex;
|
|
|
|
| 859 |
background: rgba(255,255,255,0.04);
|
| 860 |
}
|
| 861 |
|
| 862 |
+
.task-row.clickable {
|
| 863 |
+
cursor: pointer;
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
.task-row.clickable:hover {
|
| 867 |
+
background: rgba(255, 255, 255, 0.07);
|
| 868 |
+
border-color: var(--accent);
|
| 869 |
+
transform: translateX(4px);
|
| 870 |
+
transition: all 0.2s ease;
|
| 871 |
+
}
|
| 872 |
+
|
| 873 |
.task-row p {
|
| 874 |
color: var(--text-dim);
|
| 875 |
margin-top: var(--space-xs);
|
frontend/src/services/llmConfig.ts
CHANGED
|
@@ -8,7 +8,23 @@ export const providerOptions: Array<{
|
|
| 8 |
{
|
| 9 |
id: 'groq',
|
| 10 |
label: 'Groq',
|
| 11 |
-
models: [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
},
|
| 13 |
{
|
| 14 |
id: 'openai',
|
|
|
|
| 8 |
{
|
| 9 |
id: 'groq',
|
| 10 |
label: 'Groq',
|
| 11 |
+
models: [
|
| 12 |
+
'llama-3.3-70b-versatile',
|
| 13 |
+
'llama-3.1-8b-instant',
|
| 14 |
+
'openai/gpt-oss-120b',
|
| 15 |
+
'openai/gpt-oss-20b',
|
| 16 |
+
'openai/gpt-oss-safeguard-20b',
|
| 17 |
+
'meta-llama/llama-4-scout-17b-16e-instruct',
|
| 18 |
+
'qwen/qwen3-32b',
|
| 19 |
+
'groq/compound',
|
| 20 |
+
'groq/compound-mini',
|
| 21 |
+
'allam-2-7b',
|
| 22 |
+
'meta-llama/llama-prompt-guard-2-22m',
|
| 23 |
+
'meta-llama/llama-prompt-guard-2-86m',
|
| 24 |
+
'canopylabs/orpheus-arabic-saudi',
|
| 25 |
+
'canopylabs/orpheus-v1-english',
|
| 26 |
+
'mixtral-8x7b-32768'
|
| 27 |
+
]
|
| 28 |
},
|
| 29 |
{
|
| 30 |
id: 'openai',
|
vercel.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"version": 2,
|
| 3 |
+
"builds": [
|
| 4 |
+
{
|
| 5 |
+
"src": "frontend/package.json",
|
| 6 |
+
"use": "@vercel/static-build",
|
| 7 |
+
"config": { "distDir": "dist" }
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"src": "backend/api/index.py",
|
| 11 |
+
"use": "@vercel/python"
|
| 12 |
+
}
|
| 13 |
+
],
|
| 14 |
+
"rewrites": [
|
| 15 |
+
{
|
| 16 |
+
"source": "/api/(.*)",
|
| 17 |
+
"destination": "backend/api/index.py"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"source": "/(.*)",
|
| 21 |
+
"destination": "frontend/$1"
|
| 22 |
+
}
|
| 23 |
+
],
|
| 24 |
+
"env": {
|
| 25 |
+
"PYTHON_VERSION": "3.11"
|
| 26 |
+
}
|
| 27 |
+
}
|