feat: complete Phase 1 MVP - add agent executor and WebSocket real-time updates
Agent Executor Service: - BaseAgent class with LLM integration via LiteLLM proxy - AgentExecutor service that processes pending executions every 5 seconds - Action execution: create_subtask, update_status, send_notification, add_comment - Approval workflow: CxO level agents require user approval before execution - Telegram notifications for approvals and completions - Error handling and execution status tracking WebSocket Real-Time Updates: - WebSocket endpoint at /ws/opc for real-time task updates - ConnectionManager for broadcasting to all connected clients - Frontend WebSocket client with auto-reconnect - Live updates for task create/update/move/delete actions - Agent execution status broadcasts Integration: - Agent executor starts on dashboard startup - Task router broadcasts WebSocket updates on all mutations - Frontend OPC page subscribes to WebSocket and updates UI in real-time - Agents (PM, CTO, COO) can now execute tasks autonomously Phase 1 MVP Complete: ✅ PostgreSQL database with full schema ✅ Task CRUD API with automatic time tracking ✅ Kanban board with drag-and-drop ✅ Agent executor service with LLM integration ✅ WebSocket real-time updates ✅ 3 core agents ready to execute Next: Agent panel UI, email notifications, PDF invoicing
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
"""
|
||||
Base Agent Class - Foundation for all OPC agents
|
||||
"""
|
||||
from typing import Dict, Any, List, Optional
|
||||
import json
|
||||
import httpx
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class BaseAgent:
|
||||
"""Base class for all OPC agents"""
|
||||
|
||||
def __init__(self, agent_id: str, name: str, role: str, system_prompt: str, config: Dict[str, Any]):
|
||||
self.agent_id = agent_id
|
||||
self.name = name
|
||||
self.role = role
|
||||
self.system_prompt = system_prompt
|
||||
self.config = config
|
||||
self.litellm_url = os.getenv("LITELLM_URL", "http://litellm:4005")
|
||||
|
||||
async def execute(self, task: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Execute agent on a task
|
||||
Returns: {
|
||||
"actions_proposed": [...],
|
||||
"reasoning": "...",
|
||||
"requires_approval": bool
|
||||
}
|
||||
"""
|
||||
# Build prompt with task context
|
||||
user_prompt = self._build_prompt(task, context)
|
||||
|
||||
# Call LLM
|
||||
response = await self._call_llm(user_prompt)
|
||||
|
||||
# Parse response and extract actions
|
||||
result = self._parse_response(response)
|
||||
|
||||
return result
|
||||
|
||||
def _build_prompt(self, task: Dict[str, Any], context: Dict[str, Any]) -> str:
|
||||
"""Build the prompt for the LLM"""
|
||||
prompt = f"""You are {self.name}, a {self.role} agent.
|
||||
|
||||
Task Details:
|
||||
- Title: {task.get('title')}
|
||||
- Description: {task.get('description', 'No description')}
|
||||
- Status: {task.get('status')}
|
||||
- Priority: {task.get('priority')}
|
||||
- Tags: {', '.join(task.get('tags', []))}
|
||||
|
||||
Context:
|
||||
- Project: {context.get('project', 'None')}
|
||||
- Related tasks: {len(context.get('related_tasks', []))}
|
||||
- Company goals: {context.get('goals', 'None')}
|
||||
|
||||
Your role: {self.system_prompt}
|
||||
|
||||
Based on this task, propose concrete actions you would take. Format your response as JSON:
|
||||
{{
|
||||
"reasoning": "Your analysis of the task",
|
||||
"actions": [
|
||||
{{"type": "create_subtask", "title": "...", "description": "..."}},
|
||||
{{"type": "update_task_status", "status": "..."}},
|
||||
{{"type": "send_notification", "message": "..."}},
|
||||
{{"type": "request_approval", "question": "..."}}
|
||||
],
|
||||
"requires_approval": false
|
||||
}}
|
||||
|
||||
Only propose actions you can actually execute. Be specific and actionable."""
|
||||
|
||||
return prompt
|
||||
|
||||
async def _call_llm(self, prompt: str) -> str:
|
||||
"""Call LiteLLM proxy"""
|
||||
model = self.config.get("model", "claude-sonnet-4-6")
|
||||
temperature = self.config.get("temperature", 0.7)
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||
response = await client.post(
|
||||
f"{self.litellm_url}/chat/completions",
|
||||
json={
|
||||
"model": model,
|
||||
"messages": [
|
||||
{"role": "system", "content": self.system_prompt},
|
||||
{"role": "user", "content": prompt}
|
||||
],
|
||||
"temperature": temperature,
|
||||
"max_tokens": 2000
|
||||
}
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data["choices"][0]["message"]["content"]
|
||||
except Exception as e:
|
||||
raise Exception(f"LLM call failed: {str(e)}")
|
||||
|
||||
def _parse_response(self, response: str) -> Dict[str, Any]:
|
||||
"""Parse LLM response and extract actions"""
|
||||
try:
|
||||
# Try to extract JSON from response
|
||||
if "```json" in response:
|
||||
json_str = response.split("```json")[1].split("```")[0].strip()
|
||||
elif "```" in response:
|
||||
json_str = response.split("```")[1].split("```")[0].strip()
|
||||
else:
|
||||
json_str = response.strip()
|
||||
|
||||
result = json.loads(json_str)
|
||||
|
||||
# Validate structure
|
||||
if "actions" not in result:
|
||||
result["actions"] = []
|
||||
if "reasoning" not in result:
|
||||
result["reasoning"] = "No reasoning provided"
|
||||
if "requires_approval" not in result:
|
||||
# Check if any action requires approval
|
||||
result["requires_approval"] = any(
|
||||
action.get("type") == "request_approval" for action in result["actions"]
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
# Fallback: return error action
|
||||
return {
|
||||
"reasoning": f"Failed to parse response: {str(e)}",
|
||||
"actions": [
|
||||
{
|
||||
"type": "error",
|
||||
"message": f"Agent response parsing failed: {str(e)}"
|
||||
}
|
||||
],
|
||||
"requires_approval": False
|
||||
}
|
||||
|
||||
def get_capabilities(self) -> List[str]:
|
||||
"""Get agent capabilities"""
|
||||
return self.config.get("capabilities", [])
|
||||
|
||||
def requires_approval(self) -> bool:
|
||||
"""Check if agent requires approval for actions"""
|
||||
return self.config.get("approval_level") == "cxo"
|
||||
Reference in New Issue
Block a user