fix: complete OPC system implementation
- Add database connection retry logic (3 attempts, 5s delay) - Add missing database functions: get_task_by_id, get_agent_execution, approve_agent_execution, update_agent_config, update_execution_status - Complete agent execution approval workflow - Fix agent executor to handle approved executions - Fix inefficient task queries (use direct ID lookup) - Add error action handling in agent executor - Improve LiteLLM error handling with specific exception types - Reduce LiteLLM timeout from 60s to 30s - Fix execution status update to only set started_at once - Consolidate WebSocket datetime serialization with helper function - Implement update agent endpoint
This commit is contained in:
+221
-10
@@ -19,18 +19,38 @@ _pool: Optional[asyncpg.Pool] = None
|
||||
|
||||
|
||||
async def get_pool() -> asyncpg.Pool:
|
||||
"""Get or create database connection pool"""
|
||||
"""Get or create database connection pool with retry logic"""
|
||||
global _pool
|
||||
if _pool is None:
|
||||
_pool = await asyncpg.create_pool(
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
database=DB_NAME,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD,
|
||||
min_size=2,
|
||||
max_size=10,
|
||||
)
|
||||
import asyncio
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
max_retries = 3
|
||||
retry_delay = 5
|
||||
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
logger.info(f"Attempting to connect to OPC database (attempt {attempt + 1}/{max_retries})")
|
||||
_pool = await asyncpg.create_pool(
|
||||
host=DB_HOST,
|
||||
port=DB_PORT,
|
||||
database=DB_NAME,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD,
|
||||
min_size=2,
|
||||
max_size=10,
|
||||
)
|
||||
logger.info("Successfully connected to OPC database")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect to OPC database (attempt {attempt + 1}/{max_retries}): {e}")
|
||||
if attempt < max_retries - 1:
|
||||
logger.info(f"Retrying in {retry_delay} seconds...")
|
||||
await asyncio.sleep(retry_delay)
|
||||
else:
|
||||
logger.error("All connection attempts failed")
|
||||
raise Exception(f"Failed to connect to OPC database after {max_retries} attempts: {e}")
|
||||
return _pool
|
||||
|
||||
|
||||
@@ -606,3 +626,194 @@ async def get_agent_executions(
|
||||
execution[json_field] = json.loads(execution[json_field])
|
||||
executions.append(execution)
|
||||
return executions
|
||||
|
||||
|
||||
async def get_task_by_id(task_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get a single task by ID"""
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow("SELECT * FROM tasks WHERE id = $1", task_id)
|
||||
if not row:
|
||||
return None
|
||||
|
||||
task = dict(row)
|
||||
task["tags"] = json.loads(task["tags"]) if task["tags"] else []
|
||||
return task
|
||||
|
||||
|
||||
async def get_agent_execution(execution_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get a single agent execution by ID"""
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
row = await conn.fetchrow("SELECT * FROM agent_executions WHERE id = $1", execution_id)
|
||||
if not row:
|
||||
return None
|
||||
|
||||
execution = dict(row)
|
||||
for json_field in ["input_context", "output_result", "actions_proposed", "actions_executed"]:
|
||||
if execution[json_field]:
|
||||
execution[json_field] = json.loads(execution[json_field])
|
||||
return execution
|
||||
|
||||
|
||||
async def approve_agent_execution(
|
||||
execution_id: int,
|
||||
approved: bool,
|
||||
approved_by: str,
|
||||
feedback: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Approve or reject an agent execution"""
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
# Update execution with approval status
|
||||
row = await conn.fetchrow("""
|
||||
UPDATE agent_executions
|
||||
SET approved_by = $1,
|
||||
approved_at = CURRENT_TIMESTAMP,
|
||||
status = CASE WHEN $2 THEN 'approved' ELSE 'rejected' END
|
||||
WHERE id = $3
|
||||
RETURNING *
|
||||
""", approved_by, approved, execution_id)
|
||||
|
||||
if not row:
|
||||
raise ValueError(f"Execution {execution_id} not found")
|
||||
|
||||
execution = dict(row)
|
||||
for json_field in ["input_context", "output_result", "actions_proposed", "actions_executed"]:
|
||||
if execution[json_field]:
|
||||
execution[json_field] = json.loads(execution[json_field])
|
||||
|
||||
# If feedback provided, add to output_result
|
||||
if feedback:
|
||||
if not execution["output_result"]:
|
||||
execution["output_result"] = {}
|
||||
execution["output_result"]["approval_feedback"] = feedback
|
||||
|
||||
await conn.execute("""
|
||||
UPDATE agent_executions
|
||||
SET output_result = $1
|
||||
WHERE id = $2
|
||||
""", json.dumps(execution["output_result"]), execution_id)
|
||||
|
||||
return execution
|
||||
|
||||
|
||||
async def update_agent_config(
|
||||
agent_id: str,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
config: Optional[Dict[str, Any]] = None,
|
||||
enabled: Optional[bool] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Update agent configuration"""
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
# Build update query dynamically
|
||||
updates = []
|
||||
params = []
|
||||
param_count = 0
|
||||
|
||||
if name is not None:
|
||||
param_count += 1
|
||||
updates.append(f"name = ${param_count}")
|
||||
params.append(name)
|
||||
|
||||
if description is not None:
|
||||
param_count += 1
|
||||
updates.append(f"description = ${param_count}")
|
||||
params.append(description)
|
||||
|
||||
if system_prompt is not None:
|
||||
param_count += 1
|
||||
updates.append(f"system_prompt = ${param_count}")
|
||||
params.append(system_prompt)
|
||||
|
||||
if config is not None:
|
||||
param_count += 1
|
||||
updates.append(f"config = ${param_count}")
|
||||
params.append(json.dumps(config))
|
||||
|
||||
if enabled is not None:
|
||||
param_count += 1
|
||||
updates.append(f"enabled = ${param_count}")
|
||||
params.append(enabled)
|
||||
|
||||
if not updates:
|
||||
# No updates provided, just return current agent
|
||||
return await get_agent(agent_id)
|
||||
|
||||
param_count += 1
|
||||
params.append(agent_id)
|
||||
|
||||
query = f"UPDATE agents SET {', '.join(updates)} WHERE id = ${param_count} RETURNING *"
|
||||
row = await conn.fetchrow(query, *params)
|
||||
|
||||
if not row:
|
||||
raise ValueError(f"Agent {agent_id} not found")
|
||||
|
||||
agent = dict(row)
|
||||
agent["capabilities"] = json.loads(agent["capabilities"]) if agent["capabilities"] else []
|
||||
agent["config"] = json.loads(agent["config"]) if agent["config"] else {}
|
||||
return agent
|
||||
|
||||
|
||||
async def update_execution_status(
|
||||
execution_id: int,
|
||||
status: str,
|
||||
output_result: Optional[Dict[str, Any]] = None,
|
||||
actions_proposed: Optional[List[Dict[str, Any]]] = None,
|
||||
actions_executed: Optional[List[Dict[str, Any]]] = None,
|
||||
error_message: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""Update agent execution status and results"""
|
||||
pool = await get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
# Check if started_at is already set
|
||||
current = await conn.fetchrow("SELECT started_at FROM agent_executions WHERE id = $1", execution_id)
|
||||
|
||||
updates = ["status = $1"]
|
||||
params = [status]
|
||||
param_count = 1
|
||||
|
||||
# Only set started_at if transitioning to running and not already set
|
||||
if status == "running" and (not current or not current["started_at"]):
|
||||
updates.append("started_at = CURRENT_TIMESTAMP")
|
||||
|
||||
if status in ["completed", "failed", "rejected"]:
|
||||
updates.append("completed_at = CURRENT_TIMESTAMP")
|
||||
|
||||
if output_result is not None:
|
||||
param_count += 1
|
||||
updates.append(f"output_result = ${param_count}")
|
||||
params.append(json.dumps(output_result))
|
||||
|
||||
if actions_proposed is not None:
|
||||
param_count += 1
|
||||
updates.append(f"actions_proposed = ${param_count}")
|
||||
params.append(json.dumps(actions_proposed))
|
||||
|
||||
if actions_executed is not None:
|
||||
param_count += 1
|
||||
updates.append(f"actions_executed = ${param_count}")
|
||||
params.append(json.dumps(actions_executed))
|
||||
|
||||
if error_message is not None:
|
||||
param_count += 1
|
||||
updates.append(f"error_message = ${param_count}")
|
||||
params.append(error_message)
|
||||
|
||||
param_count += 1
|
||||
params.append(execution_id)
|
||||
|
||||
query = f"UPDATE agent_executions SET {', '.join(updates)} WHERE id = ${param_count} RETURNING *"
|
||||
row = await conn.fetchrow(query, *params)
|
||||
|
||||
if not row:
|
||||
raise ValueError(f"Execution {execution_id} not found")
|
||||
|
||||
execution = dict(row)
|
||||
for json_field in ["input_context", "output_result", "actions_proposed", "actions_executed"]:
|
||||
if execution[json_field]:
|
||||
execution[json_field] = json.loads(execution[json_field])
|
||||
return execution
|
||||
|
||||
Reference in New Issue
Block a user