Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
**/*dont_commit_me*
web/packages/agenta-api-client/dist/
web/tsconfig.tsbuildinfo
# Agent Pi extension bundle, built by `pnpm run build:extension` and in the Docker image.
services/agent/dist/

__pycache__/
**/__pycache__/
Expand Down
18 changes: 18 additions & 0 deletions api/oss/src/apis/fastapi/tools/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
ToolConnectionCreate,
# Tool Calls
ToolResult,
# Agent tools
AgentToolReference,
ResolvedAgentTool,
)


Expand Down Expand Up @@ -87,3 +90,18 @@ class ToolConnectionsResponse(BaseModel):

class ToolCallResponse(BaseModel):
call: ToolResult


# ---------------------------------------------------------------------------
# Agent tool resolution
# ---------------------------------------------------------------------------


class ToolResolveRequest(BaseModel):
tools: List[AgentToolReference] = []


class ToolResolveResponse(BaseModel):
count: int = 0
builtins: List[str] = []
custom: List[ResolvedAgentTool] = []
89 changes: 60 additions & 29 deletions api/oss/src/apis/fastapi/tools/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@
ToolConnectionsResponse,
#
ToolCallResponse,
#
ToolResolveRequest,
ToolResolveResponse,
)

from oss.src.core.shared.dtos import Status
Expand All @@ -42,10 +45,12 @@
ToolResultData,
)
from oss.src.core.tools.exceptions import (
ActionNotFoundError,
AdapterError,
ConnectionInactiveError,
ConnectionInvalidError,
ConnectionNotFoundError,
ToolSlugInvalidError,
)
from oss.src.core.tools.service import (
ToolsService,
Expand Down Expand Up @@ -208,6 +213,14 @@ def __init__(
)

# --- Tool operations ---
self.router.add_api_route(
"/resolve",
self.resolve_tools,
methods=["POST"],
operation_id="resolve_agent_tools",
response_model=ToolResolveResponse,
response_model_exclude_none=True,
)
self.router.add_api_route(
"/call",
self.call_tool,
Expand Down Expand Up @@ -886,6 +899,51 @@ async def callback_connection(
# Tool Calls
# -----------------------------------------------------------------------

@intercept_exceptions()
@handle_adapter_exceptions()
async def resolve_tools(
self,
request: Request,
*,
body: ToolResolveRequest,
) -> ToolResolveResponse:
"""Resolve an agent's tool references into model-ready specs.

Validates Composio connections up front and enriches each action from the
catalog, so a running agent (e.g. Pi) gets ``customTools`` whose ``execute``
routes back through ``POST /tools/call`` — provider keys stay server-side.
"""
if is_ee():
has_permission = await check_action_access(
user_uid=request.state.user_id,
project_id=request.state.project_id,
permission=Permission.VIEW_TOOLS,
)
if not has_permission:
raise FORBIDDEN_EXCEPTION

try:
resolution = await self.tools_service.resolve_agent_tools(
project_id=UUID(request.state.project_id),
tools=body.tools,
)
except ConnectionNotFoundError as e:
raise HTTPException(status_code=404, detail=e.message) from e
except ConnectionInactiveError as e:
raise HTTPException(status_code=400, detail=e.message) from e
except ConnectionInvalidError as e:
raise HTTPException(status_code=400, detail=e.message) from e
except ToolSlugInvalidError as e:
raise HTTPException(status_code=400, detail=e.message) from e
except ActionNotFoundError as e:
raise HTTPException(status_code=404, detail=e.message) from e

return ToolResolveResponse(
count=len(resolution.builtins) + len(resolution.custom),
builtins=resolution.builtins,
custom=resolution.custom,
)

@intercept_exceptions()
@handle_adapter_exceptions()
async def call_tool(
Expand Down Expand Up @@ -931,39 +989,12 @@ async def call_tool(
connection_slug = slug_parts[4]

try:
connections = await self.tools_service.query_connections(
connection = await self.tools_service.resolve_connection_by_slug(
project_id=UUID(request.state.project_id),
provider_key=provider_key,
integration_key=integration_key,
connection_slug=connection_slug,
)

connection = next(
(c for c in connections if c.slug == connection_slug), None
)

if not connection:
raise ConnectionNotFoundError(
connection_slug=connection_slug,
provider_key=provider_key,
integration_key=integration_key,
)

if not connection.is_active:
raise ConnectionInactiveError(connection_id=connection_slug)

if not connection.is_valid:
raise ConnectionInvalidError(
connection_slug=connection_slug,
detail="Please refresh the connection.",
)

if not connection.provider_connection_id:
raise ConnectionNotFoundError(
connection_slug=connection_slug,
provider_key=provider_key,
integration_key=integration_key,
)

except ConnectionNotFoundError as e:
raise HTTPException(status_code=404, detail=e.message) from e
except ConnectionInactiveError as e:
Expand Down
62 changes: 60 additions & 2 deletions api/oss/src/core/tools/dtos.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from enum import Enum
from typing import Any, Dict, List, Optional
from typing import Annotated, Any, Dict, List, Literal, Optional, Union

from agenta.sdk.models.workflows import JsonSchemas
from pydantic import BaseModel
from pydantic import BaseModel, Field

from oss.src.core.shared.dtos import (
Header,
Expand Down Expand Up @@ -238,3 +238,61 @@ class ToolExecutionResponse(BaseModel):
data: Optional[Json] = None
error: Optional[str] = None
successful: bool = False


# ---------------------------------------------------------------------------
# Agent tools (config references + resolution)
# ---------------------------------------------------------------------------

# A provider-agnostic list of tool references lives under an agent revision's
# ``parameters["tools"]``. Each entry is a discriminated union on ``type``: config
# holds references and display metadata only, never secrets. The backend resolves
# them into model-ready specs at invoke time (see ToolsService.resolve_agent_tools).


class AgentBuiltinTool(BaseModel):
"""A Pi built-in tool, referenced by name (e.g. ``read``, ``bash``)."""

type: Literal["builtin"] = "builtin"
name: str


class AgentComposioTool(BaseModel):
"""A Composio action, carrying the slug segments ``/tools/call`` parses."""

type: Literal["composio"] = "composio"
integration: str
action: str
connection: str
# Function name shown to the model. Defaults to ``{integration}__{action}``.
name: Optional[str] = None


AgentToolReference = Annotated[
Union[AgentBuiltinTool, AgentComposioTool],
Field(discriminator="type"),
]


class ResolvedAgentTool(BaseModel):
"""A runnable reference resolved into a model-ready tool spec.

``call_ref`` is the ``tools.{provider}.{integration}.{action}.{connection}`` slug
the execution bridge sends back to ``POST /tools/call``.
"""

name: str
description: Optional[str] = None
input_schema: Optional[Dict[str, Any]] = None
call_ref: str


class AgentToolsResolution(BaseModel):
"""Outcome of resolving an agent's ``tools`` list.

``builtins`` pass straight into Pi's ``tools: string[]``; ``custom`` become Pi
``customTools`` whose ``execute`` routes through ``/tools/call``.
"""

builtins: List[str] = []
custom: List[ResolvedAgentTool] = []
18 changes: 18 additions & 0 deletions api/oss/src/core/tools/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,24 @@ def __init__(
super().__init__(msg)


class ActionNotFoundError(ToolsError):
"""Raised when a catalog action cannot be found for an integration."""

def __init__(
self,
*,
provider_key: str,
integration_key: str,
action_key: str,
):
self.provider_key = provider_key
self.integration_key = integration_key
self.action_key = action_key
super().__init__(
f"Action not found: {provider_key}/{integration_key}/{action_key}"
)


class ConnectionSlugConflictError(ToolsError):
"""Raised when a connection slug already exists for the integration."""

Expand Down
Loading
Loading