feat: run tool by for a given agent [LET-6320] (#6386)

This commit is contained in:
Sarah Wooders
2025-11-25 21:55:27 -08:00
committed by Caren Thomas
parent 3a0bbe5495
commit 0fc86c4968
5 changed files with 155 additions and 19 deletions

View File

@@ -1915,7 +1915,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/letta__server__rest_api__routers__v1__tools__MCPToolExecuteRequest"
"$ref": "#/components/schemas/letta__server__rest_api__routers__v1__tools__ToolExecuteRequest"
}
}
}
@@ -5254,6 +5254,74 @@
}
}
},
"/v1/agents/{agent_id}/tools/{tool_name}/run": {
"post": {
"tags": ["agents"],
"summary": "Run Tool For Agent",
"description": "Trigger a tool by name on a specific agent, providing the necessary arguments.\n\nThis endpoint executes a tool that is attached to the agent, using the agent's\nstate and environment variables for execution context.",
"operationId": "run_tool_for_agent",
"parameters": [
{
"name": "agent_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"minLength": 42,
"maxLength": 42,
"pattern": "^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
"description": "The ID of the agent in the format 'agent-<uuid4>'",
"examples": ["agent-123e4567-e89b-42d3-8456-426614174000"],
"title": "Agent Id"
},
"description": "The ID of the agent in the format 'agent-<uuid4>'"
},
{
"name": "tool_name",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Tool Name"
}
}
],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/letta__schemas__mcp_server__ToolExecuteRequest",
"default": {
"args": {}
}
}
}
}
},
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ToolExecutionResult"
}
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
}
}
}
}
},
"/v1/agents/{agent_id}/sources/attach/{source_id}": {
"patch": {
"tags": ["agents"],
@@ -11791,7 +11859,7 @@
"post": {
"tags": ["mcp-servers"],
"summary": "Run Mcp Tool",
"description": "Execute a specific MCP tool\n\nThe request body should contain the tool arguments in the MCPToolExecuteRequest format.",
"description": "Execute a specific MCP tool\n\nThe request body should contain the tool arguments in the ToolExecuteRequest format.",
"operationId": "mcp_run_tool",
"parameters": [
{
@@ -11817,7 +11885,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/letta__schemas__mcp_server__MCPToolExecuteRequest",
"$ref": "#/components/schemas/letta__schemas__mcp_server__ToolExecuteRequest",
"default": {
"args": {}
}
@@ -39480,19 +39548,19 @@
"title": "UpdateStreamableHTTPMCPServer",
"description": "Update a Streamable HTTP MCP server"
},
"letta__schemas__mcp_server__MCPToolExecuteRequest": {
"letta__schemas__mcp_server__ToolExecuteRequest": {
"properties": {
"args": {
"additionalProperties": true,
"type": "object",
"title": "Args",
"description": "Arguments to pass to the MCP tool"
"description": "Arguments to pass to the tool"
}
},
"additionalProperties": false,
"type": "object",
"title": "MCPToolExecuteRequest",
"description": "Request to execute an MCP tool by IDs."
"title": "ToolExecuteRequest",
"description": "Request to execute a tool."
},
"letta__schemas__mcp_server__UpdateSSEMCPServer": {
"properties": {
@@ -40082,17 +40150,17 @@
],
"title": "ToolSchema"
},
"letta__server__rest_api__routers__v1__tools__MCPToolExecuteRequest": {
"letta__server__rest_api__routers__v1__tools__ToolExecuteRequest": {
"properties": {
"args": {
"additionalProperties": true,
"type": "object",
"title": "Args",
"description": "Arguments to pass to the MCP tool"
"description": "Arguments to pass to the tool"
}
},
"type": "object",
"title": "MCPToolExecuteRequest"
"title": "ToolExecuteRequest"
},
"openai__types__chat__chat_completion_message_function_tool_call__Function": {
"properties": {

View File

@@ -259,10 +259,10 @@ class MCPServerResyncResult(LettaBase):
added: List[str] = Field(default_factory=list, description="List of added tool names")
class MCPToolExecuteRequest(LettaBase):
"""Request to execute an MCP tool by IDs."""
class ToolExecuteRequest(LettaBase):
"""Request to execute a tool."""
args: Dict[str, Any] = Field(default_factory=dict, description="Arguments to pass to the MCP tool")
args: Dict[str, Any] = Field(default_factory=dict, description="Arguments to pass to the tool")
# Wrapper models for API requests with discriminated unions

View File

@@ -43,6 +43,7 @@ from letta.schemas.letta_message_content import TextContent
from letta.schemas.letta_request import LettaAsyncRequest, LettaRequest, LettaStreamingRequest
from letta.schemas.letta_response import LettaResponse, LettaStreamingResponse
from letta.schemas.letta_stop_reason import StopReasonType
from letta.schemas.mcp_server import ToolExecuteRequest
from letta.schemas.memory import (
ArchivalMemorySearchResponse,
ArchivalMemorySearchResult,
@@ -55,6 +56,7 @@ from letta.schemas.passage import Passage
from letta.schemas.run import Run as PydanticRun, RunUpdate
from letta.schemas.source import BaseSource, Source
from letta.schemas.tool import BaseTool, Tool
from letta.schemas.tool_execution_result import ToolExecutionResult
from letta.schemas.user import User
from letta.serialize_schemas.pydantic_agent_schema import AgentSchema
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
@@ -598,6 +600,72 @@ async def modify_approval_for_tool(
return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor)
@router.post("/{agent_id}/tools/{tool_name}/run", response_model=ToolExecutionResult, operation_id="run_tool_for_agent")
async def run_tool_for_agent(
agent_id: AgentId,
tool_name: str,
request: ToolExecuteRequest = Body(default=ToolExecuteRequest()),
server: "SyncServer" = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):
"""
Trigger a tool by name on a specific agent, providing the necessary arguments.
This endpoint executes a tool that is attached to the agent, using the agent's
state and environment variables for execution context.
"""
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
# Get agent with tools and environment variables
agent = await server.agent_manager.get_agent_by_id_async(
agent_id=agent_id,
actor=actor,
include_relationships=["tools", "tool_exec_environment_variables"],
)
# Find the tool by name among attached tools
tool = None
if agent.tools:
for t in agent.tools:
if t.name == tool_name:
tool = t
break
if tool is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Tool '{tool_name}' not found or not attached to agent '{agent_id}'",
)
# Build environment variables dict from agent secrets
sandbox_env_vars = {}
if agent.tool_exec_environment_variables:
for env_var in agent.tool_exec_environment_variables:
sandbox_env_vars[env_var.key] = env_var.value
# Create tool execution manager and execute the tool
from letta.services.tool_executor.tool_execution_manager import ToolExecutionManager
tool_execution_manager = ToolExecutionManager(
agent_state=agent,
message_manager=server.message_manager,
agent_manager=server.agent_manager,
block_manager=server.block_manager,
run_manager=server.run_manager,
passage_manager=server.passage_manager,
actor=actor,
sandbox_env_vars=sandbox_env_vars,
)
tool_execution_result = await tool_execution_manager.execute_tool_async(
function_name=tool_name,
function_args=request.args,
tool=tool,
)
return tool_execution_result
@router.patch("/{agent_id}/sources/attach/{source_id}", response_model=AgentState, operation_id="attach_source_to_agent", deprecated=True)
async def attach_source(
source_id: SourceId,

View File

@@ -10,7 +10,7 @@ from letta.schemas.letta_message import ToolReturnMessage
from letta.schemas.mcp_server import (
CreateMCPServerRequest,
MCPServerUnion,
MCPToolExecuteRequest,
ToolExecuteRequest,
UpdateMCPServerRequest,
convert_generic_to_union,
convert_update_to_internal,
@@ -164,12 +164,12 @@ async def run_mcp_tool(
tool_id: str,
server: SyncServer = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
request: MCPToolExecuteRequest = Body(default=MCPToolExecuteRequest()),
request: ToolExecuteRequest = Body(default=ToolExecuteRequest()),
):
"""
Execute a specific MCP tool
The request body should contain the tool arguments in the MCPToolExecuteRequest format.
The request body should contain the tool arguments in the ToolExecuteRequest format.
"""
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)

View File

@@ -757,15 +757,15 @@ async def generate_json_schema(
# TODO: @jnjpng move this and other models above to appropriate file for schemas
class MCPToolExecuteRequest(BaseModel):
args: Dict[str, Any] = Field(default_factory=dict, description="Arguments to pass to the MCP tool")
class ToolExecuteRequest(BaseModel):
args: Dict[str, Any] = Field(default_factory=dict, description="Arguments to pass to the tool")
@router.post("/mcp/servers/{mcp_server_name}/tools/{tool_name}/execute", operation_id="execute_mcp_tool")
async def execute_mcp_tool(
mcp_server_name: str,
tool_name: str,
request: MCPToolExecuteRequest = Body(...),
request: ToolExecuteRequest = Body(...),
server: SyncServer = Depends(get_letta_server),
headers: HeaderParams = Depends(get_headers),
):