mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-30 03:00:41 -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>
130 lines
3.8 KiB
Python
130 lines
3.8 KiB
Python
"""Base classes and shared utilities for chat tools."""
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from openai.types.chat import ChatCompletionToolParam
|
|
|
|
from backend.copilot.model import ChatSession
|
|
from backend.copilot.response_model import StreamToolOutputAvailable
|
|
|
|
from .models import ErrorResponse, NeedLoginResponse, ToolResponseBase
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class BaseTool:
|
|
"""Base class for all chat tools."""
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Tool name for OpenAI function calling."""
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def description(self) -> str:
|
|
"""Tool description for OpenAI."""
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def parameters(self) -> dict[str, Any]:
|
|
"""Tool parameters schema for OpenAI."""
|
|
raise NotImplementedError
|
|
|
|
@property
|
|
def requires_auth(self) -> bool:
|
|
"""Whether this tool requires authentication."""
|
|
return False
|
|
|
|
@property
|
|
def is_long_running(self) -> bool:
|
|
"""Whether this tool is long-running and should execute in background.
|
|
|
|
Long-running tools (like agent generation) are executed via background
|
|
tasks to survive SSE disconnections. The result is persisted to chat
|
|
history and visible when the user refreshes.
|
|
"""
|
|
return False
|
|
|
|
def as_openai_tool(self) -> ChatCompletionToolParam:
|
|
"""Convert to OpenAI tool format."""
|
|
return ChatCompletionToolParam(
|
|
type="function",
|
|
function={
|
|
"name": self.name,
|
|
"description": self.description,
|
|
"parameters": self.parameters,
|
|
},
|
|
)
|
|
|
|
async def execute(
|
|
self,
|
|
user_id: str | None,
|
|
session: ChatSession,
|
|
tool_call_id: str,
|
|
**kwargs,
|
|
) -> StreamToolOutputAvailable:
|
|
"""Execute the tool with authentication check.
|
|
|
|
Args:
|
|
user_id: User ID (may be anonymous like "anon_123")
|
|
session_id: Chat session ID
|
|
**kwargs: Tool-specific parameters
|
|
|
|
Returns:
|
|
Pydantic response object
|
|
|
|
"""
|
|
if self.requires_auth and not user_id:
|
|
logger.error(
|
|
f"Attempted tool call for {self.name} but user not authenticated"
|
|
)
|
|
return StreamToolOutputAvailable(
|
|
toolCallId=tool_call_id,
|
|
toolName=self.name,
|
|
output=NeedLoginResponse(
|
|
message=f"Please sign in to use {self.name}",
|
|
session_id=session.session_id,
|
|
).model_dump_json(),
|
|
success=False,
|
|
)
|
|
|
|
try:
|
|
result = await self._execute(user_id, session, **kwargs)
|
|
return StreamToolOutputAvailable(
|
|
toolCallId=tool_call_id,
|
|
toolName=self.name,
|
|
output=result.model_dump_json(),
|
|
)
|
|
except Exception as e:
|
|
logger.error(f"Error in {self.name}: {e}", exc_info=True)
|
|
return StreamToolOutputAvailable(
|
|
toolCallId=tool_call_id,
|
|
toolName=self.name,
|
|
output=ErrorResponse(
|
|
message=f"An error occurred while executing {self.name}",
|
|
error=str(e),
|
|
session_id=session.session_id,
|
|
).model_dump_json(),
|
|
success=False,
|
|
)
|
|
|
|
async def _execute(
|
|
self,
|
|
user_id: str | None,
|
|
session: ChatSession,
|
|
**kwargs,
|
|
) -> ToolResponseBase:
|
|
"""Internal execution logic to be implemented by subclasses.
|
|
|
|
Args:
|
|
user_id: User ID (authenticated or anonymous)
|
|
session_id: Chat session ID
|
|
**kwargs: Tool-specific parameters
|
|
|
|
Returns:
|
|
Pydantic response object
|
|
|
|
"""
|
|
raise NotImplementedError
|