File size: 28,103 Bytes
a10e62e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 |
import asyncio
import datetime
import logging
from typing import Any, Dict, List, Optional
import uuid
from advanced_workflow_orchestrator import AdvancedWorkflowOrchestrator
from fastapi import BackgroundTasks, Depends
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from core.agent_governance_service import AgentGovernanceService
from core.agent_world_model import AgentExperience, WorldModelService
from core.base_routes import BaseAPIRouter
from core.database import SessionLocal, get_db, get_db_session
from core.enterprise_security import AuditEvent, EventType, SecurityLevel, enterprise_security
from core.models import (
AgentFeedback,
AgentJob,
AgentRegistry,
AgentStatus,
HITLAction,
HITLActionStatus,
User,
)
from core.notification_manager import notification_manager
from core.rbac_service import Permission
from core.security_dependencies import require_permission
from core.websockets import manager as ws_manager
logger = logging.getLogger(__name__)
router = BaseAPIRouter(prefix="/api/agents", tags=["Agents"])
# --- Data Models ---
class AgentRunRequest(BaseModel):
agent_id: str
parameters: Dict[str, Any] = Field(default_factory=dict)
class AgentUpdateRequest(BaseModel):
agent_id: str
name: Optional[str] = None
description: Optional[str] = None
class AgentInfo(BaseModel):
id: str
name: str
description: str
status: str # idle, running, failed, success
last_run: Optional[str] = None
category: str
# --- Registry (Mock for MVP, real app would scan or register classes) ---
class AgentFeedbackRequest(BaseModel):
user_correction: str
input_context: Optional[str] = None
original_output: str
class HITLApprovalRequest(BaseModel):
decision: str # approved | rejected
feedback: Optional[str] = None
# --- Endpoints ---
@router.get("/", response_model=List[AgentInfo])
async def list_agents(
category: Optional[str] = None,
user: User = Depends(require_permission(Permission.AGENT_VIEW)),
db: Session = Depends(get_db)
):
"""List all available Computer Use Agents from Registry"""
governance_service = AgentGovernanceService(db)
agents_db = governance_service.list_agents(category)
# Get last run times
from sqlalchemy import func
latest_jobs = db.query(AgentJob.agent_id, func.max(AgentJob.start_time).label('last_run'))\
.group_by(AgentJob.agent_id)\
.all()
last_run_map = {job.agent_id: job.last_run.isoformat() for job in latest_jobs if job.last_run}
return [
AgentInfo(
id=a.id,
name=a.name,
description=a.description,
status=a.status,
last_run=last_run_map.get(a.id),
category=a.category
) for a in agents_db
]
# --- Endpoints ---
@router.get("/{agent_id}")
async def get_agent(
agent_id: str,
user: User = Depends(require_permission(Permission.AGENT_VIEW)),
db: Session = Depends(get_db)
):
"""Get a specific agent by ID"""
agent = db.query(AgentRegistry).filter(AgentRegistry.id == agent_id).first()
if not agent:
raise router.not_found_error("Agent", agent_id)
# Get last run time
from sqlalchemy import func
latest_job = db.query(func.max(AgentJob.start_time))\
.filter(AgentJob.agent_id == agent_id)\
.scalar()
return router.success_response(
data={
"id": agent.id,
"name": agent.name,
"description": agent.description,
"category": agent.category,
"status": agent.status,
"confidence_score": agent.confidence_score,
"module_path": agent.module_path,
"class_name": agent.class_name,
"configuration": agent.configuration,
"schedule_config": agent.schedule_config,
"version": agent.version,
"last_run": latest_job.isoformat() if latest_job else None
},
message="Agent retrieved successfully"
)
@router.get("/{agent_id}/status")
async def get_agent_status(
agent_id: str,
user: User = Depends(require_permission(Permission.AGENT_VIEW)),
db: Session = Depends(get_db)
):
"""Get the current status of an agent"""
agent = db.query(AgentRegistry).filter(AgentRegistry.id == agent_id).first()
if not agent:
raise router.not_found_error("Agent", agent_id)
# Check for running tasks
from core.agent_task_registry import agent_task_registry
try:
running_tasks = await agent_task_registry.get_active_tasks(agent_id)
except Exception:
running_tasks = []
return router.success_response(
data={
"agent_id": agent.id,
"name": agent.name,
"status": agent.status,
"confidence_score": agent.confidence_score,
"is_running": len(running_tasks) > 0,
"active_tasks": len(running_tasks)
},
message="Agent status retrieved successfully"
)
@router.delete("/{agent_id}")
async def delete_agent(
agent_id: str,
user: User = Depends(require_permission(Permission.AGENT_MANAGE)),
db: Session = Depends(get_db)
):
"""Delete an agent"""
agent = db.query(AgentRegistry).filter(AgentRegistry.id == agent_id).first()
if not agent:
raise router.not_found_error("Agent", agent_id)
# Check if agent has running tasks
from core.agent_task_registry import agent_task_registry
try:
running_tasks = await agent_task_registry.get_active_tasks(agent_id)
except Exception:
running_tasks = []
if running_tasks:
raise router.error_response(
error_code="AGENT_HAS_RUNNING_TASKS",
message=f"Cannot delete agent with {len(running_tasks)} running task(s)",
status_code=400
)
agent_name = agent.name
db.delete(agent)
db.commit()
return router.success_response(
data={"agent_id": agent_id},
message=f"Agent {agent_name} deleted successfully"
)
@router.post("/{agent_id}/run")
async def run_agent(
agent_id: str,
run_req: AgentRunRequest,
background_tasks: BackgroundTasks,
user: User = Depends(require_permission(Permission.AGENT_RUN)),
db: Session = Depends(get_db)
):
"""Trigger an agent execution in the background"""
agent = db.query(AgentRegistry).filter(AgentRegistry.id == agent_id).first()
if not agent:
raise router.not_found_error("Agent", agent_id)
# Check if agent is deprecated or paused
if agent.status in [AgentStatus.DEPRECATED.value, AgentStatus.PAUSED.value]:
raise router.error_response(
error_code="AGENT_INVALID_STATE",
message=f"Agent is {agent.status}",
status_code=400
)
if agent.status == "running":
raise router.conflict_error(
message="Agent is already running",
details={"agent_id": agent_id, "current_status": agent.status}
)
# Check if we should run synchronously (for testing)
is_sync = run_req.parameters.get("sync", False)
if is_sync:
# Run immediately and return result
# Note: calling execute_agent_task directly might have session issues if it creates its own session
# but execute_agent_task creates a SessionLocal(), so it is fine.
# We need to capture the return value from execute_agent_task (which currently returns nothing/void, just logs/notifies).
# We need to refactor execute_agent_task to return result if needed.
# Let's import it or call the logic directly.
# Actually, let's just instantiate GenericAgent here if it's a generic agent to get the Result object?
# Or better, refactor execute_agent_task to return the result.
# Refactoring execute_agent_task is best.
result = await execute_agent_task(agent_id, run_req.parameters)
return router.success_response(
data={"agent_id": agent_id, "result": result},
message="Agent execution completed"
)
# Run in background
# We pass agent_id only, task will re-fetch to ensure fresh state/object access
background_tasks.add_task(execute_agent_task, agent_id, run_req.parameters)
return router.success_response(
data={"agent_id": agent_id},
message="Agent execution started"
)
@router.patch("/{agent_id}")
async def update_agent(
agent_id: str,
update_data: AgentUpdateRequest,
user: User = Depends(require_permission(Permission.AGENT_MANAGE)),
db: Session = Depends(get_db)
):
"""Update agent details"""
agent = db.query(AgentRegistry).filter(AgentRegistry.id == agent_id).first()
if not agent:
raise router.not_found_error("Agent", agent_id)
if update_data.name:
agent.name = update_data.name
if update_data.description is not None:
agent.description = update_data.description
db.commit()
db.refresh(agent)
return router.success_response(
data={
"id": agent.id,
"name": agent.name,
"description": agent.description
},
message="Agent updated successfully"
)
@router.post("/{agent_id}/feedback")
async def submit_agent_feedback(
agent_id: str,
feedback: AgentFeedbackRequest,
user: User = Depends(require_permission(Permission.AGENT_RUN)), # Members can submit feedback
db: Session = Depends(get_db)
):
"""Submit feedback/corrections for an agent"""
service = AgentGovernanceService(db)
result = await service.submit_feedback(
agent_id=agent_id,
user_id=user.id,
original_output=feedback.original_output,
user_correction=feedback.user_correction,
input_context=feedback.input_context
)
return router.success_response(
data={
"feedback_id": result.id,
"adjudication": result.status,
"reasoning": result.ai_reasoning
},
message="Feedback submitted successfully"
)
@router.post("/{agent_id}/promote")
async def promote_agent(
agent_id: str,
user: User = Depends(require_permission(Permission.AGENT_MANAGE)),
db: Session = Depends(get_db)
):
"""Promote agent to Autonomous mode"""
service = AgentGovernanceService(db)
agent = service.promote_to_autonomous(agent_id, user)
return router.success_response(
data={"agent_status": agent.status},
message=f"Agent {agent_id} promoted to autonomous successfully"
)
@router.get("/approvals/pending", response_model=List[Dict[str, Any]])
async def list_pending_approvals(
user: User = Depends(require_permission(Permission.AGENT_MANAGE)),
db: Session = Depends(get_db)
):
"""List all actions waiting for human approval"""
actions = db.query(HITLAction).filter(HITLAction.status == HITLActionStatus.PENDING.value).all()
return [{
"id": a.id,
"agent_id": a.agent_id,
"action_type": a.action_type,
"params": a.params,
"reason": a.reason,
"created_at": a.created_at.isoformat() if a.created_at else None
} for a in actions]
@router.post("/approvals/{action_id}")
async def decide_hitl_action(
action_id: str,
req: HITLApprovalRequest,
user: User = Depends(require_permission(Permission.AGENT_MANAGE)),
db: Session = Depends(get_db)
):
"""Approve or Reject a paused agent action"""
action = db.query(HITLAction).filter(HITLAction.id == action_id).first()
if not action:
raise router.not_found_error("HITLAction", action_id)
if req.decision.lower() == "approved":
action.status = HITLActionStatus.APPROVED.value
else:
action.status = HITLActionStatus.REJECTED.value
action.user_feedback = req.feedback
action.reviewed_at = datetime.datetime.now()
action.reviewed_by = user.id
db.commit()
# Broadcast update to UI via WebSocket
await ws_manager.broadcast("workspace:default", {
"type": "hitl_decision",
"action_id": action_id,
"decision": action.status
})
return router.success_response(
data={"decision": action.status, "action_id": action_id},
message=f"Action {action_id} {action.status} successfully"
)
async def execute_agent_task(agent_id: str, params: Dict[str, Any]):
"""Background task to run the agent logic"""
# Use context manager for background task
with get_db_session() as db:
result = None
try:
agent = db.query(AgentRegistry).filter(AgentRegistry.id == agent_id).first()
if not agent:
logger.error(f"Agent {agent_id} not found in background task")
return
logger.info(f"Starting agent {agent.name} (ID: {agent_id})...")
# 1. World Model Retrieval
wm_service = WorldModelService()
# Build a context string from params to query memory
task_context = f"Execute {agent.name} with params: {str(params)}"
relevant_memories = await wm_service.recall_experiences(agent, task_context)
if isinstance(relevant_memories, dict):
# Extract the actual experiences list from the dictionary response
experiences = relevant_memories.get("experiences", [])
if experiences:
logger.info(f"Agents {agent.name} found {len(experiences)} relevant past experiences.")
for mem in experiences:
# Defensive check if mem is object or dict (mock vs real)
if hasattr(mem, "input_summary"):
logger.info(f" [Memory] {mem.input_summary} -> {mem.learnings} ({mem.outcome})")
else:
logger.info(f" [Memory] {str(mem)}")
elif isinstance(relevant_memories, list):
# Legacy/Fallback support if it returns a list directly
logger.info(f"Agents {agent.name} found {len(relevant_memories)} relevant past experiences.")
for mem in relevant_memories:
if hasattr(mem, "input_summary"):
logger.info(f" [Memory] {mem.input_summary} -> {mem.learnings} ({mem.outcome})")
else:
logger.info(f" [Memory] {str(mem)}")
# Dynamic Import
# Unified Execution Logic using GenericAgent ReAct Loop
from core.generic_agent import GenericAgent
result = None
try:
# 1. Determine Tools based on Agent ID/Type (Migration compatibility)
# If the agent is legacy and doesn't have tools configured, we inject them here.
override_config = {}
if agent.id == "competitive_intel":
override_config["tools"] = ["track_competitor_pricing"]
override_config["system_prompt"] = "You are a Competitive Intelligence Agent. Use the 'track_competitor_pricing' tool to gather market data."
elif agent.id == "inventory_reconcile":
override_config["tools"] = ["reconcile_inventory"]
override_config["system_prompt"] = "You are an Inventory Manager. Use 'reconcile_inventory' to check for variance."
elif agent.id == "payroll_guardian":
override_config["tools"] = ["reconcile_payroll"]
override_config["system_prompt"] = "You are a Payroll Guardian. Use 'reconcile_payroll' to verify accuracy."
# 2. Instantiate Runtime
if override_config:
if not agent.configuration:
agent.configuration = {}
# Merge defaults if not present
for k, v in override_config.items():
if k not in agent.configuration:
agent.configuration[k] = v
runner = GenericAgent(agent)
# 3. Determine Input
# ReAct loop needs a natural language instruction.
task_input = params.get("task_input") or params.get("request")
# If input is missing but we have params, we construct a prompt
if not task_input:
if agent.id == "competitive_intel":
task_input = f"Track pricing for {params.get('product', 'configured products')} against {params.get('competitors', 'competitors')}."
elif agent.id == "inventory_reconcile":
task_input = f"Reconcile inventory for {params.get('skus', 'all SKUs')}."
elif agent.id == "payroll_guardian":
task_input = f"Reconcile payroll for period {params.get('period', 'current')}."
else:
task_input = f"Execute task with params: {params}"
# 4. Execute ReAct Loop with step streaming
logger.info(f"Executing Agent {agent.name} with ReAct Loop. Input: {task_input}")
async def streaming_callback(step_record):
await ws_manager.broadcast("workspace:default", {
"type": "agent_step_update",
"agent_id": agent_id,
"step": step_record
})
result_obj = await runner.execute(task_input, context=params, step_callback=streaming_callback)
# 5. Process Result
result = result_obj
# Success Notification
await ws_manager.broadcast("workspace:default", {
"type": "agent_status_change",
"agent_id": agent_id,
"status": "success",
"result": result
})
# --- [NEW] External Bridge Response Routing ---
source_platform = params.get("source_platform")
recipient_id = params.get("recipient_id") or params.get("channel_id")
if source_platform and recipient_id:
try:
from core.agent_integration_gateway import (
ActionType,
agent_integration_gateway,
)
final_output = result.get("final_output") if isinstance(result, dict) else str(result)
if final_output:
logger.info(f"Routing async agent result back to {source_platform}")
routing_params = {
"recipient_id": recipient_id,
"channel": params.get("channel_id") or recipient_id,
"content": f"✅ *{agent.name}* finished task:\n{final_output}",
"thread_ts": params.get("thread_ts")
}
# Phase 105: Include original sender for Agent-to-Agent loopback
if source_platform == "agent":
routing_params["sender_agent_id"] = params.get("agent_id") or params.get("sender_id")
await agent_integration_gateway.execute_action(
ActionType.SEND_MESSAGE,
source_platform,
routing_params
)
except Exception as route_err:
logger.error(f"Failed to route async agent result back to {source_platform}: {route_err}")
# 6. Record Experience happens inside GenericAgent.execute() now.
except Exception as e:
logger.error(f"Agent {agent_id} logic failed: {e}")
# Record Failure
await wm_service.record_experience(AgentExperience(
id=str(uuid.uuid4()),
agent_id=agent.id,
task_type=agent.class_name,
input_summary=str(params),
outcome="Failure",
learnings=f"Failed with error: {str(e)}",
agent_role=agent.category,
specialty=None,
timestamp=datetime.datetime.utcnow()
))
raise e
except Exception as e:
import sys
import traceback
error_msg = f"Agent execution FAILED: {str(e)}\n{traceback.format_exc()}"
logger.critical(f"!!! CRITICAL AGENT ERROR !!!\n{error_msg}")
logger.error(f"Agent {agent_id} execution wrapper failed: {e}")
# Urgent Notification (Phase 34 requirement)
await notification_manager.send_urgent_notification(
message=f"Agent execution FAILED: {str(e)}",
workspace_id="default_workspace",
channel="slack"
)
# Notify UI Status
await ws_manager.broadcast("workspace:default", {
"type": "agent_status_change",
"agent_id": agent_id,
"status": "failed",
"error": str(e),
"traceback": traceback.format_exc()
})
return result
# ==================== ATOM META-AGENT ENDPOINTS ====================
class AtomExecuteRequest(BaseModel):
request: str
context: Optional[Dict[str, Any]] = None
class AtomSpawnRequest(BaseModel):
template: str # e.g., "finance_analyst", "sales_assistant", "custom"
custom_params: Optional[Dict[str, Any]] = None
persist: bool = False
class AtomTriggerRequest(BaseModel):
event_type: str
data: Dict[str, Any]
@router.post("/atom/execute")
async def execute_atom(
req: AtomExecuteRequest,
user: User = Depends(require_permission(Permission.AGENT_RUN)),
):
"""
Execute the Atom Meta-Agent with a natural language request.
Atom will analyze the request and spawn specialty agents as needed.
"""
from core.atom_meta_agent import handle_manual_trigger
# Determine workspace from user context
workspace_id = "default"
result = await handle_manual_trigger(
request=req.request,
user=user,
workspace_id=workspace_id
)
return router.success_response(
data=result,
message="Atom meta-agent executed successfully"
)
@router.post("/spawn")
async def spawn_agent(
req: AtomSpawnRequest,
user: User = Depends(require_permission(Permission.AGENT_MANAGE)),
):
"""
Spawn a specialty agent on-demand from a template.
"""
from core.atom_meta_agent import get_atom_agent
atom = get_atom_agent()
agent = await atom.spawn_agent(
template_name=req.template,
custom_params=req.custom_params,
persist=req.persist
)
return router.success_response(
data={
"agent_id": agent.id,
"agent_name": agent.name,
"category": agent.category,
"persisted": req.persist
},
message=f"Agent {agent.name} spawned successfully"
)
@router.post("/atom/trigger")
async def trigger_atom_with_data(
req: AtomTriggerRequest,
# This endpoint may not require user auth if called by webhooks/internal systems
# For now, require basic auth
user: User = Depends(require_permission(Permission.AGENT_RUN)),
):
"""
Trigger Atom with new data (event-driven execution).
Used for webhooks, ingestion events, integration callbacks.
"""
from core.atom_meta_agent import handle_data_event_trigger
result = await handle_data_event_trigger(
event_type=req.event_type,
data=req.data,
workspace_id="default"
)
return router.success_response(
data=result,
message="Atom triggered with data event successfully"
)
class CustomAgentRequest(BaseModel):
name: str
description: Optional[str] = "Custom Agent"
category: str = "custom"
configuration: Dict[str, Any]
schedule_config: Optional[Dict[str, Any]] = None
@router.post("/custom")
async def create_custom_agent(
req: CustomAgentRequest,
user: User = Depends(require_permission(Permission.AGENT_MANAGE)),
db: Session = Depends(get_db)
):
"""Create a fully custom agent with configuration and schedule"""
# 1. Create Agent
registry_entry = AgentRegistry(
name=req.name,
description=req.description,
category=req.category,
configuration=req.configuration,
schedule_config=req.schedule_config,
module_path="core.generic_agent",
class_name="GenericAgent",
status=AgentStatus.STUDENT.value
)
db.add(registry_entry)
db.commit()
db.refresh(registry_entry)
# 2. Schedule if needed
if req.schedule_config and req.schedule_config.get("active"):
from core.scheduler import AgentScheduler
scheduler = AgentScheduler.get_instance()
scheduler.schedule_agent(registry_entry.id, req.schedule_config)
return router.success_response(
data={"agent_id": registry_entry.id},
message=f"Custom agent {req.name} created successfully"
)
@router.put("/{agent_id}")
async def update_agent(
agent_id: str,
req: CustomAgentRequest,
user: User = Depends(require_permission(Permission.AGENT_MANAGE)),
db: Session = Depends(get_db)
):
"""Update an agent's config or schedule"""
agent = db.query(AgentRegistry).filter(AgentRegistry.id == agent_id).first()
if not agent:
raise router.not_found_error("Agent", agent_id)
# Update fields
agent.name = req.name
agent.description = req.description
agent.category = req.category
agent.configuration = req.configuration
agent.schedule_config = req.schedule_config
db.commit()
# Update Scheduler
from core.scheduler import AgentScheduler
scheduler = AgentScheduler.get_instance()
# Ideally remove old job but for MVP we overwrite with new ID or let scheduler handle
# A robust implementation would cancel the old job_id if we stored it
if req.schedule_config and req.schedule_config.get("active"):
scheduler.schedule_agent(agent.id, req.schedule_config)
return router.success_response(
data={"agent_id": agent.id},
message=f"Agent {agent.name} updated successfully"
)
@router.post("/{agent_id}/stop")
async def stop_agent(
agent_id: str,
user: User = Depends(require_permission(Permission.AGENT_RUN)),
db: Session = Depends(get_db)
):
"""
Stop a running agent by cancelling its active tasks.
Uses the AgentTaskRegistry to cancel all running tasks for the agent.
"""
from core.agent_task_registry import agent_task_registry
logger.info(f"Stop request received for agent {agent_id} by user {user.id}")
# Try to cancel tasks via registry
cancelled_count = await agent_task_registry.cancel_agent_tasks(agent_id)
if cancelled_count > 0:
# Successfully cancelled tasks
return router.success_response(
data={
"agent_id": agent_id,
"cancelled_tasks": cancelled_count
},
message=f"Successfully stopped {cancelled_count} running task(s)"
)
else:
# No tasks in registry - agent might not be running or already stopped
# Check if agent exists
agent = db.query(AgentRegistry).filter(AgentRegistry.id == agent_id).first()
if not agent:
raise router.not_found_error("Agent", agent_id)
return router.success_response(
data={"agent_id": agent_id, "cancelled_tasks": 0},
message="No running tasks found for this agent"
)
|