fund-flow-backend / src /copilot /agents /action_executor.py
Aniket2006's picture
feat: calibrate account risk scores and threat tiers, resolve clumping in alert queue
37f9507
Raw
History Blame Contribute Delete
6.41 kB
"""
ActionExecutorAgent - Prepares actionable buttons for user
Determines what actions are available based on analysis context
"""
from typing import Dict
from .base import Agent, AgentConfig, AgentResult
import time
class ActionExecutorAgent(Agent):
"""
Prepares action buttons based on context.
Does NOT execute — just plans available actions.
Actual execution happens in /api/copilot/action endpoint.
"""
AVAILABLE_ACTIONS = {
"CREATE_CASE": {
"label": "Create Investigation Case",
"icon": "📁",
"category": "DIRECT_ACTION",
"requires_confirmation": False,
"priority": 1,
},
"GENERATE_STR": {
"label": "Generate STR Report (PDF)",
"icon": "📄",
"category": "DATA_ACTION",
"requires_confirmation": False,
"priority": 2,
},
"OPEN_STR_BUILDER": {
"label": "Open STR Builder",
"icon": "🔨",
"category": "NAVIGATION",
"requires_confirmation": False,
"priority": 3,
},
"VIEW_GRAPH": {
"label": "Open Network Graph",
"icon": "🕸",
"category": "NAVIGATION",
"requires_confirmation": False,
"priority": 4,
},
"ADD_NOTES": {
"label": "Add Investigation Notes",
"icon": "📝",
"category": "DIRECT_ACTION",
"requires_confirmation": False,
"priority": 7,
},
}
def __init__(self, api_pool):
config = AgentConfig(
name="ActionExecutorAgent",
model="llama-3.1-8b-instant",
temperature=0.0,
max_tokens=500,
timeout_ms=5000,
)
super().__init__(config, api_pool)
def _build_prompt(self, **inputs) -> str:
return ""
def _parse_response(self, response_text: str) -> Dict:
return {}
async def invoke(
self,
account_id: str = None,
intent: str = "GENERAL",
risk_assessment: Dict = None,
alert_data: Dict = None,
**kwargs,
) -> AgentResult:
"""Prepare list of actions to show user based on context."""
start_time = time.time()
try:
actions = []
risk_score = 0
risk_tier = "LOW"
if risk_assessment:
recommendation = risk_assessment.get("recommendation", {})
risk_tier = recommendation.get("priority", "MEDIUM")
if alert_data:
risk_score = alert_data.get("risk_score", 0)
risk_tier = alert_data.get("risk_tier", risk_tier)
is_high_risk = risk_tier == "CRITICAL"
is_medium_risk = risk_tier in ["HIGH", "MEDIUM"]
# Create case for medium/high risk accounts
if account_id and (is_high_risk or is_medium_risk):
actions.append(self._build_action("CREATE_CASE", {
"account_id": account_id,
"alert_id": alert_data.get("alert_id") if alert_data else None,
"description": f"Investigation for {account_id}",
"priority": "CRITICAL" if is_high_risk else "HIGH",
}))
# STR/SAR generation for NARRATIVE intent or high-risk
if account_id and (intent == "NARRATIVE" or is_high_risk):
actions.append(self._build_action("GENERATE_STR", {
"account_id": account_id,
"alert_id": alert_data.get("alert_id") if alert_data else None,
}))
actions.append(self._build_action("OPEN_STR_BUILDER", {
"account_id": account_id,
"url": f"#str-builder?account={account_id}",
}))
# Navigation — always available when account_id is known
if account_id:
actions.append(self._build_action("VIEW_GRAPH", {
"account_id": account_id,
"url": f"#investigation?account={account_id}",
"hops": 2,
"max_nodes": 50,
}))
# Add notes — only if a case exists (medium/high risk context)
if account_id and (is_high_risk or is_medium_risk):
actions.append(self._build_action("ADD_NOTES", {
"account_id": account_id,
"url": f"#cases?account={account_id}&action=new_note",
}))
actions.sort(key=lambda x: x.get("priority", 99))
result_data = {
"actions": actions,
"actions_count": len(actions),
"context_analyzed": {
"account_id": account_id,
"intent": intent,
"risk_score": risk_score,
"risk_tier": risk_tier,
},
}
self.logger.info(f"[OK] {self.config.name}: Prepared {len(actions)} actions")
return await self._create_result(
success=True,
data=result_data,
tokens_input=0,
tokens_output=0,
start_time=start_time,
)
except Exception as e:
self.logger.error(f"[FAIL] {self.config.name}: {e}")
return await self._create_result(
success=False,
data={"actions": []},
error=str(e),
start_time=start_time,
)
def _build_action(self, action_type: str, payload: Dict) -> Dict:
"""Build an action object with full metadata."""
if action_type not in self.AVAILABLE_ACTIONS:
return {}
cfg = self.AVAILABLE_ACTIONS[action_type]
return {
"id": f"action_{action_type.lower()}_{int(time.time() * 1000)}",
"type": action_type,
"label": cfg["label"],
"icon": cfg.get("icon", ""),
"category": cfg["category"],
"priority": cfg["priority"],
"requires_confirmation": cfg.get("requires_confirmation", False),
"confirmation_message": cfg.get("confirmation_message", ""),
"enabled": True,
"payload": payload,
}