Files
fast-next-template/backend/app/api/routes/agents.py
Felipe Cardoso acd18ff694 chore(backend): standardize multiline formatting across modules
Reformatted multiline function calls, object definitions, and queries for improved code readability and consistency. Adjusted imports and constraints where necessary.
2026-01-03 01:35:18 +01:00

985 lines
32 KiB
Python

# app/api/routes/agents.py
"""
Agent Instance management endpoints for Syndarix projects.
These endpoints allow project owners and superusers to manage AI agent instances
within their projects, including spawning, pausing, resuming, and terminating agents.
"""
import logging
import os
from typing import Any
from uuid import UUID
from fastapi import APIRouter, Depends, Query, Request, status
from slowapi import Limiter
from slowapi.util import get_remote_address
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.dependencies.auth import get_current_user
from app.core.database import get_db
from app.core.exceptions import (
AuthorizationError,
NotFoundError,
ValidationException,
)
from app.crud.syndarix.agent_instance import agent_instance as agent_instance_crud
from app.crud.syndarix.agent_type import agent_type as agent_type_crud
from app.crud.syndarix.project import project as project_crud
from app.models.syndarix import AgentInstance, Project
from app.models.syndarix.enums import AgentStatus
from app.models.user import User
from app.schemas.common import (
MessageResponse,
PaginatedResponse,
PaginationParams,
create_pagination_meta,
)
from app.schemas.errors import ErrorCode
from app.schemas.syndarix.agent_instance import (
AgentInstanceCreate,
AgentInstanceMetrics,
AgentInstanceResponse,
AgentInstanceUpdate,
)
router = APIRouter()
logger = logging.getLogger(__name__)
# Initialize limiter for this router
limiter = Limiter(key_func=get_remote_address)
# Use higher rate limits in test environment
IS_TEST = os.getenv("IS_TEST", "False") == "True"
RATE_MULTIPLIER = 100 if IS_TEST else 1
# Valid status transitions for agent lifecycle management
VALID_STATUS_TRANSITIONS: dict[AgentStatus, set[AgentStatus]] = {
AgentStatus.IDLE: {AgentStatus.WORKING, AgentStatus.PAUSED, AgentStatus.TERMINATED},
AgentStatus.WORKING: {
AgentStatus.IDLE,
AgentStatus.WAITING,
AgentStatus.PAUSED,
AgentStatus.TERMINATED,
},
AgentStatus.WAITING: {
AgentStatus.IDLE,
AgentStatus.WORKING,
AgentStatus.PAUSED,
AgentStatus.TERMINATED,
},
AgentStatus.PAUSED: {AgentStatus.IDLE, AgentStatus.TERMINATED},
AgentStatus.TERMINATED: set(), # Terminal state, no transitions allowed
}
async def verify_project_access(
db: AsyncSession,
project_id: UUID,
user: User,
) -> Project:
"""
Verify user has access to a project.
Args:
db: Database session
project_id: UUID of the project to verify
user: Current authenticated user
Returns:
Project: The project if access is granted
Raises:
NotFoundError: If the project does not exist
AuthorizationError: If the user does not have access to the project
"""
project = await project_crud.get(db, id=project_id)
if not project:
raise NotFoundError(
message=f"Project {project_id} not found",
error_code=ErrorCode.NOT_FOUND,
)
if not user.is_superuser and project.owner_id != user.id:
raise AuthorizationError(
message="You do not have access to this project",
error_code=ErrorCode.INSUFFICIENT_PERMISSIONS,
)
return project
def validate_status_transition(
current_status: AgentStatus,
target_status: AgentStatus,
) -> None:
"""
Validate that a status transition is allowed.
Args:
current_status: The agent's current status
target_status: The desired target status
Raises:
ValidationException: If the transition is not allowed
"""
valid_targets = VALID_STATUS_TRANSITIONS.get(current_status, set())
if target_status not in valid_targets:
raise ValidationException(
message=f"Cannot transition from {current_status.value} to {target_status.value}",
error_code=ErrorCode.VALIDATION_ERROR,
field="status",
)
def build_agent_response(
agent: AgentInstance,
agent_type_name: str | None = None,
agent_type_slug: str | None = None,
project_name: str | None = None,
project_slug: str | None = None,
assigned_issues_count: int = 0,
) -> AgentInstanceResponse:
"""
Build an AgentInstanceResponse from an AgentInstance model.
Args:
agent: The agent instance model
agent_type_name: Name of the agent type
agent_type_slug: Slug of the agent type
project_name: Name of the project
project_slug: Slug of the project
assigned_issues_count: Number of issues assigned to this agent
Returns:
AgentInstanceResponse: The response schema
"""
return AgentInstanceResponse(
id=agent.id,
agent_type_id=agent.agent_type_id,
project_id=agent.project_id,
name=agent.name,
status=agent.status,
current_task=agent.current_task,
short_term_memory=agent.short_term_memory or {},
long_term_memory_ref=agent.long_term_memory_ref,
session_id=agent.session_id,
last_activity_at=agent.last_activity_at,
terminated_at=agent.terminated_at,
tasks_completed=agent.tasks_completed,
tokens_used=agent.tokens_used,
cost_incurred=agent.cost_incurred,
created_at=agent.created_at,
updated_at=agent.updated_at,
agent_type_name=agent_type_name,
agent_type_slug=agent_type_slug,
project_name=project_name,
project_slug=project_slug,
assigned_issues_count=assigned_issues_count,
)
# ===== Agent Instance Management Endpoints =====
@router.post(
"/projects/{project_id}/agents",
response_model=AgentInstanceResponse,
status_code=status.HTTP_201_CREATED,
summary="Spawn Agent Instance",
description="Spawn a new agent instance in a project. Requires project ownership or superuser.",
operation_id="spawn_agent",
)
@limiter.limit(f"{20 * RATE_MULTIPLIER}/minute")
async def spawn_agent(
request: Request,
project_id: UUID,
agent_in: AgentInstanceCreate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
"""
Spawn a new agent instance in a project.
Creates a new agent instance from an agent type template and assigns it
to the specified project. The agent starts in IDLE status by default.
Args:
request: FastAPI request object (for rate limiting)
project_id: UUID of the project to spawn the agent in
agent_in: Agent instance creation data
current_user: Current authenticated user
db: Database session
Returns:
AgentInstanceResponse: The newly created agent instance
Raises:
NotFoundError: If the project is not found
AuthorizationError: If the user lacks access to the project
ValidationException: If the agent creation data is invalid
"""
try:
# Verify project access
project = await verify_project_access(db, project_id, current_user)
# Ensure the agent is being created for the correct project
if agent_in.project_id != project_id:
raise ValidationException(
message="Agent project_id must match the URL project_id",
error_code=ErrorCode.VALIDATION_ERROR,
field="project_id",
)
# Validate that the agent type exists and is active
agent_type = await agent_type_crud.get(db, id=agent_in.agent_type_id)
if not agent_type:
raise NotFoundError(
message=f"Agent type {agent_in.agent_type_id} not found",
error_code=ErrorCode.NOT_FOUND,
)
if not agent_type.is_active:
raise ValidationException(
message=f"Agent type '{agent_type.name}' is inactive and cannot be used",
error_code=ErrorCode.VALIDATION_ERROR,
field="agent_type_id",
)
# Create the agent instance
agent = await agent_instance_crud.create(db, obj_in=agent_in)
logger.info(
f"User {current_user.email} spawned agent '{agent.name}' "
f"(id={agent.id}) in project {project.slug}"
)
# Get agent details for response
details = await agent_instance_crud.get_with_details(db, instance_id=agent.id)
if details:
return build_agent_response(
agent=details["instance"],
agent_type_name=details.get("agent_type_name"),
agent_type_slug=details.get("agent_type_slug"),
project_name=details.get("project_name"),
project_slug=details.get("project_slug"),
assigned_issues_count=details.get("assigned_issues_count", 0),
)
return build_agent_response(agent)
except (NotFoundError, AuthorizationError, ValidationException):
raise
except ValueError as e:
logger.warning(f"Failed to spawn agent: {e!s}")
raise ValidationException(
message=str(e),
error_code=ErrorCode.VALIDATION_ERROR,
)
except Exception as e:
logger.error(f"Error spawning agent: {e!s}", exc_info=True)
raise
@router.get(
"/projects/{project_id}/agents",
response_model=PaginatedResponse[AgentInstanceResponse],
summary="List Project Agents",
description="List all agent instances in a project with optional filtering.",
operation_id="list_project_agents",
)
@limiter.limit(f"{60 * RATE_MULTIPLIER}/minute")
async def list_project_agents(
request: Request,
project_id: UUID,
pagination: PaginationParams = Depends(),
status_filter: AgentStatus | None = Query(
None, alias="status", description="Filter by agent status"
),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
"""
List all agent instances in a project.
Returns a paginated list of agents with optional status filtering.
Results are ordered by creation date (newest first).
Args:
request: FastAPI request object (for rate limiting)
project_id: UUID of the project
pagination: Pagination parameters
status_filter: Optional filter by agent status
current_user: Current authenticated user
db: Database session
Returns:
PaginatedResponse[AgentInstanceResponse]: Paginated list of agents
Raises:
NotFoundError: If the project is not found
AuthorizationError: If the user lacks access to the project
"""
try:
# Verify project access
project = await verify_project_access(db, project_id, current_user)
# Get agents for the project
agents, total = await agent_instance_crud.get_by_project(
db,
project_id=project_id,
status=status_filter,
skip=pagination.offset,
limit=pagination.limit,
)
# Build response objects
agent_responses = []
for agent in agents:
# Get details for each agent (could be optimized with bulk query)
details = await agent_instance_crud.get_with_details(
db, instance_id=agent.id
)
if details:
agent_responses.append(
build_agent_response(
agent=details["instance"],
agent_type_name=details.get("agent_type_name"),
agent_type_slug=details.get("agent_type_slug"),
project_name=details.get("project_name"),
project_slug=details.get("project_slug"),
assigned_issues_count=details.get("assigned_issues_count", 0),
)
)
else:
agent_responses.append(build_agent_response(agent))
pagination_meta = create_pagination_meta(
total=total,
page=pagination.page,
limit=pagination.limit,
items_count=len(agent_responses),
)
logger.debug(
f"User {current_user.email} listed {len(agent_responses)} agents "
f"in project {project.slug}"
)
return PaginatedResponse(data=agent_responses, pagination=pagination_meta)
except (NotFoundError, AuthorizationError):
raise
except Exception as e:
logger.error(f"Error listing project agents: {e!s}", exc_info=True)
raise
# ===== Project Agent Metrics Endpoint =====
# NOTE: This endpoint MUST be defined before /{agent_id} routes
# to prevent FastAPI from trying to parse "metrics" as a UUID
@router.get(
"/projects/{project_id}/agents/metrics",
response_model=AgentInstanceMetrics,
summary="Get Project Agent Metrics",
description="Get aggregated usage metrics for all agents in a project.",
operation_id="get_project_agent_metrics",
)
@limiter.limit(f"{60 * RATE_MULTIPLIER}/minute")
async def get_project_agent_metrics(
request: Request,
project_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
"""
Get aggregated usage metrics for all agents in a project.
Returns aggregated metrics across all agents including total
tasks completed, tokens used, and cost incurred.
Args:
request: FastAPI request object (for rate limiting)
project_id: UUID of the project
current_user: Current authenticated user
db: Database session
Returns:
AgentInstanceMetrics: Aggregated project agent metrics
Raises:
NotFoundError: If the project is not found
AuthorizationError: If the user lacks access to the project
"""
try:
# Verify project access
project = await verify_project_access(db, project_id, current_user)
# Get aggregated metrics for the project
metrics = await agent_instance_crud.get_project_metrics(
db, project_id=project_id
)
logger.debug(
f"User {current_user.email} retrieved project metrics for {project.slug}"
)
return AgentInstanceMetrics(
total_instances=metrics["total_instances"],
active_instances=metrics["active_instances"],
idle_instances=metrics["idle_instances"],
total_tasks_completed=metrics["total_tasks_completed"],
total_tokens_used=metrics["total_tokens_used"],
total_cost_incurred=metrics["total_cost_incurred"],
)
except (NotFoundError, AuthorizationError):
raise
except Exception as e:
logger.error(f"Error getting project agent metrics: {e!s}", exc_info=True)
raise
@router.get(
"/projects/{project_id}/agents/{agent_id}",
response_model=AgentInstanceResponse,
summary="Get Agent Details",
description="Get detailed information about a specific agent instance.",
operation_id="get_agent",
)
@limiter.limit(f"{60 * RATE_MULTIPLIER}/minute")
async def get_agent(
request: Request,
project_id: UUID,
agent_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
"""
Get detailed information about a specific agent instance.
Returns full agent details including related entity information
(agent type name, project name) and assigned issues count.
Args:
request: FastAPI request object (for rate limiting)
project_id: UUID of the project
agent_id: UUID of the agent instance
current_user: Current authenticated user
db: Database session
Returns:
AgentInstanceResponse: The agent instance details
Raises:
NotFoundError: If the project or agent is not found
AuthorizationError: If the user lacks access to the project
"""
try:
# Verify project access
await verify_project_access(db, project_id, current_user)
# Get agent with full details
details = await agent_instance_crud.get_with_details(db, instance_id=agent_id)
if not details:
raise NotFoundError(
message=f"Agent {agent_id} not found",
error_code=ErrorCode.NOT_FOUND,
)
agent = details["instance"]
# Verify agent belongs to the specified project
if agent.project_id != project_id:
raise NotFoundError(
message=f"Agent {agent_id} not found in project {project_id}",
error_code=ErrorCode.NOT_FOUND,
)
logger.debug(
f"User {current_user.email} retrieved agent {agent.name} (id={agent_id})"
)
return build_agent_response(
agent=agent,
agent_type_name=details.get("agent_type_name"),
agent_type_slug=details.get("agent_type_slug"),
project_name=details.get("project_name"),
project_slug=details.get("project_slug"),
assigned_issues_count=details.get("assigned_issues_count", 0),
)
except (NotFoundError, AuthorizationError):
raise
except Exception as e:
logger.error(f"Error getting agent details: {e!s}", exc_info=True)
raise
@router.patch(
"/projects/{project_id}/agents/{agent_id}",
response_model=AgentInstanceResponse,
summary="Update Agent",
description="Update an agent instance's configuration and state.",
operation_id="update_agent",
)
@limiter.limit(f"{30 * RATE_MULTIPLIER}/minute")
async def update_agent(
request: Request,
project_id: UUID,
agent_id: UUID,
agent_in: AgentInstanceUpdate,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
"""
Update an agent instance's configuration and state.
Allows updating agent status, current task, memory, and other
configurable fields. Status transitions are validated according
to the agent lifecycle state machine.
Args:
request: FastAPI request object (for rate limiting)
project_id: UUID of the project
agent_id: UUID of the agent instance
agent_in: Agent update data
current_user: Current authenticated user
db: Database session
Returns:
AgentInstanceResponse: The updated agent instance
Raises:
NotFoundError: If the project or agent is not found
AuthorizationError: If the user lacks access to the project
ValidationException: If the status transition is invalid
"""
try:
# Verify project access
await verify_project_access(db, project_id, current_user)
# Get current agent
agent = await agent_instance_crud.get(db, id=agent_id)
if not agent:
raise NotFoundError(
message=f"Agent {agent_id} not found",
error_code=ErrorCode.NOT_FOUND,
)
# Verify agent belongs to the specified project
if agent.project_id != project_id:
raise NotFoundError(
message=f"Agent {agent_id} not found in project {project_id}",
error_code=ErrorCode.NOT_FOUND,
)
# Validate status transition if status is being changed
if agent_in.status is not None and agent_in.status != agent.status:
validate_status_transition(agent.status, agent_in.status)
# Update the agent
updated_agent = await agent_instance_crud.update(
db, db_obj=agent, obj_in=agent_in
)
logger.info(
f"User {current_user.email} updated agent {updated_agent.name} "
f"(id={agent_id})"
)
# Get updated details
details = await agent_instance_crud.get_with_details(
db, instance_id=updated_agent.id
)
if details:
return build_agent_response(
agent=details["instance"],
agent_type_name=details.get("agent_type_name"),
agent_type_slug=details.get("agent_type_slug"),
project_name=details.get("project_name"),
project_slug=details.get("project_slug"),
assigned_issues_count=details.get("assigned_issues_count", 0),
)
return build_agent_response(updated_agent)
except (NotFoundError, AuthorizationError, ValidationException):
raise
except Exception as e:
logger.error(f"Error updating agent: {e!s}", exc_info=True)
raise
@router.post(
"/projects/{project_id}/agents/{agent_id}/pause",
response_model=AgentInstanceResponse,
summary="Pause Agent",
description="Pause an agent instance, temporarily stopping its work.",
operation_id="pause_agent",
)
@limiter.limit(f"{20 * RATE_MULTIPLIER}/minute")
async def pause_agent(
request: Request,
project_id: UUID,
agent_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
"""
Pause an agent instance.
Transitions the agent to PAUSED status, temporarily stopping
its work. The agent can be resumed later with the resume endpoint.
Args:
request: FastAPI request object (for rate limiting)
project_id: UUID of the project
agent_id: UUID of the agent instance
current_user: Current authenticated user
db: Database session
Returns:
AgentInstanceResponse: The paused agent instance
Raises:
NotFoundError: If the project or agent is not found
AuthorizationError: If the user lacks access to the project
ValidationException: If the agent cannot be paused from its current state
"""
try:
# Verify project access
await verify_project_access(db, project_id, current_user)
# Get current agent
agent = await agent_instance_crud.get(db, id=agent_id)
if not agent:
raise NotFoundError(
message=f"Agent {agent_id} not found",
error_code=ErrorCode.NOT_FOUND,
)
# Verify agent belongs to the specified project
if agent.project_id != project_id:
raise NotFoundError(
message=f"Agent {agent_id} not found in project {project_id}",
error_code=ErrorCode.NOT_FOUND,
)
# Validate the transition to PAUSED
validate_status_transition(agent.status, AgentStatus.PAUSED)
# Update status to PAUSED
paused_agent = await agent_instance_crud.update_status(
db,
instance_id=agent_id,
status=AgentStatus.PAUSED,
)
if not paused_agent:
raise NotFoundError(
message=f"Agent {agent_id} not found",
error_code=ErrorCode.NOT_FOUND,
)
logger.info(
f"User {current_user.email} paused agent {paused_agent.name} "
f"(id={agent_id})"
)
# Get updated details
details = await agent_instance_crud.get_with_details(
db, instance_id=paused_agent.id
)
if details:
return build_agent_response(
agent=details["instance"],
agent_type_name=details.get("agent_type_name"),
agent_type_slug=details.get("agent_type_slug"),
project_name=details.get("project_name"),
project_slug=details.get("project_slug"),
assigned_issues_count=details.get("assigned_issues_count", 0),
)
return build_agent_response(paused_agent)
except (NotFoundError, AuthorizationError, ValidationException):
raise
except Exception as e:
logger.error(f"Error pausing agent: {e!s}", exc_info=True)
raise
@router.post(
"/projects/{project_id}/agents/{agent_id}/resume",
response_model=AgentInstanceResponse,
summary="Resume Agent",
description="Resume a paused agent instance.",
operation_id="resume_agent",
)
@limiter.limit(f"{20 * RATE_MULTIPLIER}/minute")
async def resume_agent(
request: Request,
project_id: UUID,
agent_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
"""
Resume a paused agent instance.
Transitions the agent from PAUSED back to IDLE status,
allowing it to accept new work.
Args:
request: FastAPI request object (for rate limiting)
project_id: UUID of the project
agent_id: UUID of the agent instance
current_user: Current authenticated user
db: Database session
Returns:
AgentInstanceResponse: The resumed agent instance
Raises:
NotFoundError: If the project or agent is not found
AuthorizationError: If the user lacks access to the project
ValidationException: If the agent cannot be resumed from its current state
"""
try:
# Verify project access
await verify_project_access(db, project_id, current_user)
# Get current agent
agent = await agent_instance_crud.get(db, id=agent_id)
if not agent:
raise NotFoundError(
message=f"Agent {agent_id} not found",
error_code=ErrorCode.NOT_FOUND,
)
# Verify agent belongs to the specified project
if agent.project_id != project_id:
raise NotFoundError(
message=f"Agent {agent_id} not found in project {project_id}",
error_code=ErrorCode.NOT_FOUND,
)
# Validate the transition to IDLE (resume)
validate_status_transition(agent.status, AgentStatus.IDLE)
# Update status to IDLE
resumed_agent = await agent_instance_crud.update_status(
db,
instance_id=agent_id,
status=AgentStatus.IDLE,
)
if not resumed_agent:
raise NotFoundError(
message=f"Agent {agent_id} not found",
error_code=ErrorCode.NOT_FOUND,
)
logger.info(
f"User {current_user.email} resumed agent {resumed_agent.name} "
f"(id={agent_id})"
)
# Get updated details
details = await agent_instance_crud.get_with_details(
db, instance_id=resumed_agent.id
)
if details:
return build_agent_response(
agent=details["instance"],
agent_type_name=details.get("agent_type_name"),
agent_type_slug=details.get("agent_type_slug"),
project_name=details.get("project_name"),
project_slug=details.get("project_slug"),
assigned_issues_count=details.get("assigned_issues_count", 0),
)
return build_agent_response(resumed_agent)
except (NotFoundError, AuthorizationError, ValidationException):
raise
except Exception as e:
logger.error(f"Error resuming agent: {e!s}", exc_info=True)
raise
@router.delete(
"/projects/{project_id}/agents/{agent_id}",
response_model=MessageResponse,
summary="Terminate Agent",
description="Terminate an agent instance, permanently stopping it.",
operation_id="terminate_agent",
)
@limiter.limit(f"{10 * RATE_MULTIPLIER}/minute")
async def terminate_agent(
request: Request,
project_id: UUID,
agent_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
"""
Terminate an agent instance.
Permanently terminates the agent, setting its status to TERMINATED.
This action cannot be undone - a new agent must be spawned if needed.
The agent's session and current task are cleared.
Args:
request: FastAPI request object (for rate limiting)
project_id: UUID of the project
agent_id: UUID of the agent instance
current_user: Current authenticated user
db: Database session
Returns:
MessageResponse: Confirmation message
Raises:
NotFoundError: If the project or agent is not found
AuthorizationError: If the user lacks access to the project
ValidationException: If the agent is already terminated
"""
try:
# Verify project access
await verify_project_access(db, project_id, current_user)
# Get current agent
agent = await agent_instance_crud.get(db, id=agent_id)
if not agent:
raise NotFoundError(
message=f"Agent {agent_id} not found",
error_code=ErrorCode.NOT_FOUND,
)
# Verify agent belongs to the specified project
if agent.project_id != project_id:
raise NotFoundError(
message=f"Agent {agent_id} not found in project {project_id}",
error_code=ErrorCode.NOT_FOUND,
)
# Check if already terminated
if agent.status == AgentStatus.TERMINATED:
raise ValidationException(
message="Agent is already terminated",
error_code=ErrorCode.VALIDATION_ERROR,
field="status",
)
# Validate the transition to TERMINATED
validate_status_transition(agent.status, AgentStatus.TERMINATED)
agent_name = agent.name
# Terminate the agent
terminated_agent = await agent_instance_crud.terminate(db, instance_id=agent_id)
if not terminated_agent:
raise NotFoundError(
message=f"Agent {agent_id} not found",
error_code=ErrorCode.NOT_FOUND,
)
logger.info(
f"User {current_user.email} terminated agent {agent_name} (id={agent_id})"
)
return MessageResponse(
success=True,
message=f"Agent '{agent_name}' has been terminated",
)
except (NotFoundError, AuthorizationError, ValidationException):
raise
except Exception as e:
logger.error(f"Error terminating agent: {e!s}", exc_info=True)
raise
@router.get(
"/projects/{project_id}/agents/{agent_id}/metrics",
response_model=AgentInstanceMetrics,
summary="Get Agent Metrics",
description="Get usage metrics for a specific agent instance.",
operation_id="get_agent_metrics",
)
@limiter.limit(f"{60 * RATE_MULTIPLIER}/minute")
async def get_agent_metrics(
request: Request,
project_id: UUID,
agent_id: UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
) -> Any:
"""
Get usage metrics for a specific agent instance.
Returns metrics including tasks completed, tokens used,
and cost incurred for the specified agent.
Args:
request: FastAPI request object (for rate limiting)
project_id: UUID of the project
agent_id: UUID of the agent instance
current_user: Current authenticated user
db: Database session
Returns:
AgentInstanceMetrics: Agent usage metrics
Raises:
NotFoundError: If the project or agent is not found
AuthorizationError: If the user lacks access to the project
"""
try:
# Verify project access
await verify_project_access(db, project_id, current_user)
# Get agent
agent = await agent_instance_crud.get(db, id=agent_id)
if not agent:
raise NotFoundError(
message=f"Agent {agent_id} not found",
error_code=ErrorCode.NOT_FOUND,
)
# Verify agent belongs to the specified project
if agent.project_id != project_id:
raise NotFoundError(
message=f"Agent {agent_id} not found in project {project_id}",
error_code=ErrorCode.NOT_FOUND,
)
# Calculate metrics for this single agent
# For a single agent, we report its individual metrics
is_active = agent.status == AgentStatus.WORKING
is_idle = agent.status == AgentStatus.IDLE
logger.debug(
f"User {current_user.email} retrieved metrics for agent {agent.name} "
f"(id={agent_id})"
)
return AgentInstanceMetrics(
total_instances=1,
active_instances=1 if is_active else 0,
idle_instances=1 if is_idle else 0,
total_tasks_completed=agent.tasks_completed,
total_tokens_used=agent.tokens_used,
total_cost_incurred=agent.cost_incurred,
)
except (NotFoundError, AuthorizationError):
raise
except Exception as e:
logger.error(f"Error getting agent metrics: {e!s}", exc_info=True)
raise