mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
Uncouple Copilot task execution from the REST API server. This should help performance and scalability, and allows task execution to continue regardless of the state of the user's connection. - Resolves #12023 ### Changes 🏗️ - Add `backend.copilot.executor`->`CoPilotExecutor` (setup similar to `backend.executor`->`ExecutionManager`). This executor service uses RabbitMQ-based task distribution, and sticks with the existing Redis Streams setup for task output. It uses a cluster lock mechanism to ensure a task is only executed by one pod, and the `DatabaseManager` for pooled DB access. - Add `backend.data.db_accessors` for automatic choice of direct/proxied DB access Chat requests now flow: API → RabbitMQ → CoPilot Executor → Redis Streams → SSE Client. This enables horizontal scaling of chat processing and isolates long-running LLM operations from the API service. - Move non-API Copilot stuff into `backend.copilot` (from `backend.api.features.chat`) - Updated import paths for all usages - Move `backend.executor.database` to `backend.data.db_manager` and add methods for copilot executor - Updated import paths for all usages - Make `backend.copilot.db` RPC-compatible (-> DB ops return ~~Prisma~~ Pydantic models) - Make `backend.data.workspace` RPC-compatible - Make `backend.data.graphs.get_store_listed_graphs` RPC-compatible DX: - Add `copilot_executor` service to Docker setup Config: - Add `Config.num_copilot_workers` (default 5) and `Config.copilot_executor_port` (default 8008) - Remove unused `Config.agent_server_port` > [!WARNING] > **This change adds a new microservice to the system, with entrypoint `backend.copilot.executor`.** > The `docker compose` setup has been updated, but if you run the Platform on something else, you'll have to update your deployment config to include this new service. > > When running locally, the `CoPilotExecutor` uses port 8008 by default. ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Copilot works - [x] Processes messages when triggered - [x] Can use its tools #### For configuration changes: - [x] `.env.default` is updated or already compatible with my changes - [x] `docker-compose.yml` is updated or already compatible with my changes - [x] I have included a list of my configuration changes in the PR description (under **Changes**) --------- Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co>
153 lines
5.0 KiB
Python
153 lines
5.0 KiB
Python
"""External API routes for chat tools - stateless HTTP endpoints.
|
|
|
|
Note: These endpoints use ephemeral sessions that are not persisted to Redis.
|
|
As a result, session-based rate limiting (max_agent_runs, max_agent_schedules)
|
|
is not enforced for external API calls. Each request creates a fresh session
|
|
with zeroed counters. Rate limiting for external API consumers should be
|
|
handled separately (e.g., via API key quotas).
|
|
"""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from fastapi import APIRouter, Security
|
|
from prisma.enums import APIKeyPermission
|
|
from pydantic import BaseModel, Field
|
|
|
|
from backend.api.external.middleware import require_permission
|
|
from backend.copilot.model import ChatSession
|
|
from backend.copilot.tools import find_agent_tool, run_agent_tool
|
|
from backend.copilot.tools.models import ToolResponseBase
|
|
from backend.data.auth.base import APIAuthorizationInfo
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
tools_router = APIRouter(prefix="/tools", tags=["tools"])
|
|
|
|
# Note: We use Security() as a function parameter dependency (auth: APIAuthorizationInfo = Security(...))
|
|
# rather than in the decorator's dependencies= list. This avoids duplicate permission checks
|
|
# while still enforcing auth AND giving us access to auth for extracting user_id.
|
|
|
|
|
|
# Request models
|
|
class FindAgentRequest(BaseModel):
|
|
query: str = Field(..., description="Search query for finding agents")
|
|
|
|
|
|
class RunAgentRequest(BaseModel):
|
|
"""Request to run or schedule an agent.
|
|
|
|
The tool automatically handles the setup flow:
|
|
- First call returns available inputs so user can decide what values to use
|
|
- Returns missing credentials if user needs to configure them
|
|
- Executes when inputs are provided OR use_defaults=true
|
|
- Schedules execution if schedule_name and cron are provided
|
|
"""
|
|
|
|
username_agent_slug: str = Field(
|
|
...,
|
|
description="The marketplace agent slug (e.g., 'username/agent-name')",
|
|
)
|
|
inputs: dict[str, Any] = Field(
|
|
default_factory=dict,
|
|
description="Dictionary of input values for the agent",
|
|
)
|
|
use_defaults: bool = Field(
|
|
default=False,
|
|
description="Set to true to run with default values (user must confirm)",
|
|
)
|
|
schedule_name: str | None = Field(
|
|
None,
|
|
description="Name for scheduled execution (triggers scheduling mode)",
|
|
)
|
|
cron: str | None = Field(
|
|
None,
|
|
description="Cron expression (5 fields: minute hour day month weekday)",
|
|
)
|
|
timezone: str = Field(
|
|
default="UTC",
|
|
description="IANA timezone (e.g., 'America/New_York', 'UTC')",
|
|
)
|
|
|
|
|
|
def _create_ephemeral_session(user_id: str) -> ChatSession:
|
|
"""Create an ephemeral session for stateless API requests."""
|
|
return ChatSession.new(user_id)
|
|
|
|
|
|
@tools_router.post(
|
|
path="/find-agent",
|
|
)
|
|
async def find_agent(
|
|
request: FindAgentRequest,
|
|
auth: APIAuthorizationInfo = Security(
|
|
require_permission(APIKeyPermission.USE_TOOLS)
|
|
),
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Search for agents in the marketplace based on capabilities and user needs.
|
|
|
|
Args:
|
|
request: Search query for finding agents
|
|
|
|
Returns:
|
|
List of matching agents or no results response
|
|
"""
|
|
session = _create_ephemeral_session(auth.user_id)
|
|
result = await find_agent_tool._execute(
|
|
user_id=auth.user_id,
|
|
session=session,
|
|
query=request.query,
|
|
)
|
|
return _response_to_dict(result)
|
|
|
|
|
|
@tools_router.post(
|
|
path="/run-agent",
|
|
)
|
|
async def run_agent(
|
|
request: RunAgentRequest,
|
|
auth: APIAuthorizationInfo = Security(
|
|
require_permission(APIKeyPermission.USE_TOOLS)
|
|
),
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Run or schedule an agent from the marketplace.
|
|
|
|
The endpoint automatically handles the setup flow:
|
|
- Returns missing inputs if required fields are not provided
|
|
- Returns missing credentials if user needs to configure them
|
|
- Executes immediately if all requirements are met
|
|
- Schedules execution if schedule_name and cron are provided
|
|
|
|
For scheduled execution:
|
|
- Cron format: "minute hour day month weekday"
|
|
- Examples: "0 9 * * 1-5" (9am weekdays), "0 0 * * *" (daily at midnight)
|
|
- Timezone: Use IANA timezone names like "America/New_York"
|
|
|
|
Args:
|
|
request: Agent slug, inputs, and optional schedule config
|
|
|
|
Returns:
|
|
- setup_requirements: If inputs or credentials are missing
|
|
- execution_started: If agent was run or scheduled successfully
|
|
- error: If something went wrong
|
|
"""
|
|
session = _create_ephemeral_session(auth.user_id)
|
|
result = await run_agent_tool._execute(
|
|
user_id=auth.user_id,
|
|
session=session,
|
|
username_agent_slug=request.username_agent_slug,
|
|
inputs=request.inputs,
|
|
use_defaults=request.use_defaults,
|
|
schedule_name=request.schedule_name or "",
|
|
cron=request.cron or "",
|
|
timezone=request.timezone,
|
|
)
|
|
return _response_to_dict(result)
|
|
|
|
|
|
def _response_to_dict(result: ToolResponseBase) -> dict[str, Any]:
|
|
"""Convert a tool response to a dictionary for JSON serialization."""
|
|
return result.model_dump()
|