Files
nas-tools/dashboard/backend/services/agents/base_agent.py
T
Gan, Jimmy b981c06d59
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 2m26s
Run Tests / Backend Tests (push) Failing after 2m36s
Run Tests / Frontend Tests (push) Failing after 2m23s
Run Tests / Test Summary (push) Failing after 13s
feat: comprehensive test infrastructure improvements
- Fix unit test imports: add env setup in conftest.py before module imports
- Add 24 new auth router tests (RBAC, preferences, password validation)
- Add 16 new tests for litellm and chat_summary routers
- Apply black formatting and ruff linting across codebase
- Add pre-commit hooks configuration (black, ruff, file checks)
- Increase CI coverage threshold from 40% to 50%

Test Results:
- 206 tests passing (91 unit + 115 integration)
- Coverage: 58.79% on core modules
- auth.py: 57% → 85%, litellm.py: 23% → 87%, chat_summary.py: 41% → 100%
- auth_service: 96.51%, config: 100%, rbac: 93.48%
2026-04-08 00:21:32 +08:00

164 lines
5.8 KiB
Python

"""
Base Agent Class - Foundation for all OPC agents
"""
import json
import os
from typing import Any
import httpx
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")
self.litellm_api_key = os.getenv("LITELLM_API_KEY", "")
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"""
import logging
logger = logging.getLogger(__name__)
model = self.config.get("model", "claude-sonnet-4-6")
temperature = self.config.get("temperature", 0.7)
try:
headers = {"Content-Type": "application/json"}
# Only add auth header if API key is set and not empty
if self.litellm_api_key and self.litellm_api_key.strip():
headers["Authorization"] = f"Bearer {self.litellm_api_key}"
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
f"{self.litellm_url}/chat/completions",
headers=headers,
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 httpx.ConnectError as e:
logger.error(f"LiteLLM connection failed: {e}")
raise Exception(f"LiteLLM service unavailable: {str(e)}")
except httpx.TimeoutException as e:
logger.error(f"LiteLLM request timeout: {e}")
raise Exception(f"LiteLLM request timeout: {str(e)}")
except httpx.HTTPStatusError as e:
logger.error(f"LiteLLM HTTP error: {e.response.status_code} - {e.response.text}")
raise Exception(f"LiteLLM HTTP error {e.response.status_code}: {e.response.text}")
except Exception as e:
logger.error(f"LiteLLM call failed: {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"