fix: complete OPC system implementation
Deploy Dashboard (Dev) / deploy-dev (push) Failing after 10m4s
Run Tests / Frontend Tests (push) Failing after 14m28s
Run Tests / Backend Tests (push) Failing after 14m50s
Run Tests / Test Summary (push) Has been cancelled

- 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:
Gan, Jimmy
2026-04-03 22:30:51 +08:00
parent a42e3d3050
commit 5da2ae01a0
5 changed files with 333 additions and 56 deletions
+221 -10
View File
@@ -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