letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.12.0.dev20251009104148__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- letta/__init__.py +1 -1
- letta/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/letta_llm_request_adapter.py +0 -1
- letta/adapters/letta_llm_stream_adapter.py +7 -2
- letta/adapters/simple_llm_request_adapter.py +88 -0
- letta/adapters/simple_llm_stream_adapter.py +192 -0
- letta/agents/agent_loop.py +6 -0
- letta/agents/ephemeral_summary_agent.py +2 -1
- letta/agents/helpers.py +142 -6
- letta/agents/letta_agent.py +13 -33
- letta/agents/letta_agent_batch.py +2 -4
- letta/agents/letta_agent_v2.py +87 -77
- letta/agents/letta_agent_v3.py +927 -0
- letta/agents/voice_agent.py +2 -6
- letta/constants.py +8 -4
- letta/database_utils.py +161 -0
- letta/errors.py +40 -0
- letta/functions/function_sets/base.py +84 -4
- letta/functions/function_sets/multi_agent.py +0 -3
- letta/functions/schema_generator.py +113 -71
- letta/groups/dynamic_multi_agent.py +3 -2
- letta/groups/helpers.py +1 -2
- letta/groups/round_robin_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/groups/sleeptime_multi_agent_v3.py +17 -17
- letta/groups/supervisor_multi_agent.py +84 -80
- letta/helpers/converters.py +3 -0
- letta/helpers/message_helper.py +4 -0
- letta/helpers/tool_rule_solver.py +92 -5
- letta/interfaces/anthropic_streaming_interface.py +409 -0
- letta/interfaces/gemini_streaming_interface.py +296 -0
- letta/interfaces/openai_streaming_interface.py +752 -1
- letta/llm_api/anthropic_client.py +127 -16
- letta/llm_api/bedrock_client.py +4 -2
- letta/llm_api/deepseek_client.py +4 -1
- letta/llm_api/google_vertex_client.py +124 -42
- letta/llm_api/groq_client.py +4 -1
- letta/llm_api/llm_api_tools.py +11 -4
- letta/llm_api/llm_client_base.py +6 -2
- letta/llm_api/openai.py +32 -2
- letta/llm_api/openai_client.py +423 -18
- letta/llm_api/xai_client.py +4 -1
- letta/main.py +9 -5
- letta/memory.py +1 -0
- letta/orm/__init__.py +2 -1
- letta/orm/agent.py +10 -0
- letta/orm/block.py +7 -16
- letta/orm/blocks_agents.py +8 -2
- letta/orm/files_agents.py +2 -0
- letta/orm/job.py +7 -5
- letta/orm/mcp_oauth.py +1 -0
- letta/orm/message.py +21 -6
- letta/orm/organization.py +2 -0
- letta/orm/provider.py +6 -2
- letta/orm/run.py +71 -0
- letta/orm/run_metrics.py +82 -0
- letta/orm/sandbox_config.py +7 -1
- letta/orm/sqlalchemy_base.py +0 -306
- letta/orm/step.py +6 -5
- letta/orm/step_metrics.py +5 -5
- letta/otel/tracing.py +28 -3
- letta/plugins/defaults.py +4 -4
- letta/prompts/system_prompts/__init__.py +2 -0
- letta/prompts/system_prompts/letta_v1.py +25 -0
- letta/schemas/agent.py +3 -2
- letta/schemas/agent_file.py +9 -3
- letta/schemas/block.py +23 -10
- letta/schemas/enums.py +21 -2
- letta/schemas/job.py +17 -4
- letta/schemas/letta_message_content.py +71 -2
- letta/schemas/letta_stop_reason.py +5 -5
- letta/schemas/llm_config.py +53 -3
- letta/schemas/memory.py +1 -1
- letta/schemas/message.py +564 -117
- letta/schemas/openai/responses_request.py +64 -0
- letta/schemas/providers/__init__.py +2 -0
- letta/schemas/providers/anthropic.py +16 -0
- letta/schemas/providers/ollama.py +115 -33
- letta/schemas/providers/openrouter.py +52 -0
- letta/schemas/providers/vllm.py +2 -1
- letta/schemas/run.py +48 -42
- letta/schemas/run_metrics.py +21 -0
- letta/schemas/step.py +2 -2
- letta/schemas/step_metrics.py +1 -1
- letta/schemas/tool.py +15 -107
- letta/schemas/tool_rule.py +88 -5
- letta/serialize_schemas/marshmallow_agent.py +1 -0
- letta/server/db.py +79 -408
- letta/server/rest_api/app.py +61 -10
- letta/server/rest_api/dependencies.py +14 -0
- letta/server/rest_api/redis_stream_manager.py +19 -8
- letta/server/rest_api/routers/v1/agents.py +364 -292
- letta/server/rest_api/routers/v1/blocks.py +14 -20
- letta/server/rest_api/routers/v1/identities.py +45 -110
- letta/server/rest_api/routers/v1/internal_templates.py +21 -0
- letta/server/rest_api/routers/v1/jobs.py +23 -6
- letta/server/rest_api/routers/v1/messages.py +1 -1
- letta/server/rest_api/routers/v1/runs.py +149 -99
- letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
- letta/server/rest_api/routers/v1/tools.py +281 -594
- letta/server/rest_api/routers/v1/voice.py +1 -1
- letta/server/rest_api/streaming_response.py +29 -29
- letta/server/rest_api/utils.py +122 -64
- letta/server/server.py +160 -887
- letta/services/agent_manager.py +236 -919
- letta/services/agent_serialization_manager.py +16 -0
- letta/services/archive_manager.py +0 -100
- letta/services/block_manager.py +211 -168
- letta/services/context_window_calculator/token_counter.py +1 -1
- letta/services/file_manager.py +1 -1
- letta/services/files_agents_manager.py +24 -33
- letta/services/group_manager.py +0 -142
- letta/services/helpers/agent_manager_helper.py +7 -2
- letta/services/helpers/run_manager_helper.py +69 -0
- letta/services/job_manager.py +96 -411
- letta/services/lettuce/__init__.py +6 -0
- letta/services/lettuce/lettuce_client_base.py +86 -0
- letta/services/mcp_manager.py +38 -6
- letta/services/message_manager.py +165 -362
- letta/services/organization_manager.py +0 -36
- letta/services/passage_manager.py +0 -345
- letta/services/provider_manager.py +0 -80
- letta/services/run_manager.py +364 -0
- letta/services/sandbox_config_manager.py +0 -234
- letta/services/step_manager.py +62 -39
- letta/services/summarizer/summarizer.py +9 -7
- letta/services/telemetry_manager.py +0 -16
- letta/services/tool_executor/builtin_tool_executor.py +35 -0
- letta/services/tool_executor/core_tool_executor.py +397 -2
- letta/services/tool_executor/files_tool_executor.py +3 -3
- letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
- letta/services/tool_executor/tool_execution_manager.py +6 -8
- letta/services/tool_executor/tool_executor_base.py +3 -3
- letta/services/tool_manager.py +85 -339
- letta/services/tool_sandbox/base.py +24 -13
- letta/services/tool_sandbox/e2b_sandbox.py +16 -1
- letta/services/tool_schema_generator.py +123 -0
- letta/services/user_manager.py +0 -99
- letta/settings.py +20 -4
- letta/system.py +5 -1
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +3 -5
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +146 -135
- letta/agents/temporal/activities/__init__.py +0 -4
- letta/agents/temporal/activities/example_activity.py +0 -7
- letta/agents/temporal/activities/prepare_messages.py +0 -10
- letta/agents/temporal/temporal_agent_workflow.py +0 -56
- letta/agents/temporal/types.py +0 -25
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/licenses/LICENSE +0 -0
@@ -2,32 +2,29 @@ import json
|
|
2
2
|
from collections.abc import AsyncGenerator
|
3
3
|
from typing import Any, Dict, List, Literal, Optional, Union
|
4
4
|
|
5
|
-
from composio.client import ComposioClientError, HTTPError, NoItemsFound
|
6
|
-
from composio.client.collections import ActionModel, AppModel
|
7
|
-
from composio.exceptions import (
|
8
|
-
ApiKeyNotProvidedError,
|
9
|
-
ComposioSDKError,
|
10
|
-
ConnectedAccountNotFoundError,
|
11
|
-
EnumMetadataNotFound,
|
12
|
-
EnumStringNotFound,
|
13
|
-
)
|
14
5
|
from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request
|
15
6
|
from httpx import ConnectError, HTTPStatusError
|
16
7
|
from pydantic import BaseModel, Field
|
17
8
|
from starlette.responses import StreamingResponse
|
18
9
|
|
19
|
-
from letta.errors import
|
10
|
+
from letta.errors import (
|
11
|
+
LettaInvalidArgumentError,
|
12
|
+
LettaInvalidMCPSchemaError,
|
13
|
+
LettaMCPConnectionError,
|
14
|
+
LettaMCPTimeoutError,
|
15
|
+
LettaToolCreateError,
|
16
|
+
LettaToolNameConflictError,
|
17
|
+
)
|
20
18
|
from letta.functions.functions import derive_openai_json_schema
|
21
19
|
from letta.functions.mcp_client.exceptions import MCPTimeoutError
|
22
20
|
from letta.functions.mcp_client.types import MCPTool, SSEServerConfig, StdioServerConfig, StreamableHTTPServerConfig
|
23
|
-
from letta.helpers.composio_helpers import get_composio_api_key
|
24
21
|
from letta.helpers.decorators import deprecated
|
25
22
|
from letta.llm_api.llm_client import LLMClient
|
26
23
|
from letta.log import get_logger
|
27
24
|
from letta.orm.errors import UniqueConstraintViolationError
|
28
25
|
from letta.orm.mcp_oauth import OAuthSessionStatus
|
29
26
|
from letta.prompts.gpt_system import get_system_text
|
30
|
-
from letta.schemas.enums import MessageRole, ToolType
|
27
|
+
from letta.schemas.enums import AgentType, MessageRole, ToolType
|
31
28
|
from letta.schemas.letta_message import ToolReturnMessage
|
32
29
|
from letta.schemas.letta_message_content import TextContent
|
33
30
|
from letta.schemas.mcp import UpdateSSEMCPServer, UpdateStdioMCPServer, UpdateStreamableHTTPMCPServer
|
@@ -80,78 +77,74 @@ async def count_tools(
|
|
80
77
|
"""
|
81
78
|
Get a count of all tools available to agents belonging to the org of the user.
|
82
79
|
"""
|
83
|
-
try:
|
84
|
-
# Helper function to parse tool types - supports both repeated params and comma-separated values
|
85
|
-
def parse_tool_types(tool_types_input: Optional[List[str]]) -> Optional[List[str]]:
|
86
|
-
if tool_types_input is None:
|
87
|
-
return None
|
88
|
-
|
89
|
-
# Flatten any comma-separated values and validate against ToolType enum
|
90
|
-
flattened_types = []
|
91
|
-
for item in tool_types_input:
|
92
|
-
# Split by comma in case user provided comma-separated values
|
93
|
-
types_in_item = [t.strip() for t in item.split(",") if t.strip()]
|
94
|
-
flattened_types.extend(types_in_item)
|
95
|
-
|
96
|
-
# Validate each type against the ToolType enum
|
97
|
-
valid_types = []
|
98
|
-
valid_values = [tt.value for tt in ToolType]
|
99
|
-
|
100
|
-
for tool_type in flattened_types:
|
101
|
-
if tool_type not in valid_values:
|
102
|
-
raise HTTPException(
|
103
|
-
status_code=400, detail=f"Invalid tool_type '{tool_type}'. Must be one of: {', '.join(valid_values)}"
|
104
|
-
)
|
105
|
-
valid_types.append(tool_type)
|
106
|
-
|
107
|
-
return valid_types if valid_types else None
|
108
|
-
|
109
|
-
# Parse and validate tool types (same logic as list_tools)
|
110
|
-
tool_types_str = parse_tool_types(tool_types)
|
111
|
-
exclude_tool_types_str = parse_tool_types(exclude_tool_types)
|
112
80
|
|
113
|
-
|
81
|
+
# Helper function to parse tool types - supports both repeated params and comma-separated values
|
82
|
+
def parse_tool_types(tool_types_input: Optional[List[str]]) -> Optional[List[str]]:
|
83
|
+
if tool_types_input is None:
|
84
|
+
return None
|
114
85
|
|
115
|
-
#
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
#
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
if
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
86
|
+
# Flatten any comma-separated values and validate against ToolType enum
|
87
|
+
flattened_types = []
|
88
|
+
for item in tool_types_input:
|
89
|
+
# Split by comma in case user provided comma-separated values
|
90
|
+
types_in_item = [t.strip() for t in item.split(",") if t.strip()]
|
91
|
+
flattened_types.extend(types_in_item)
|
92
|
+
|
93
|
+
# Validate each type against the ToolType enum
|
94
|
+
valid_types = []
|
95
|
+
valid_values = [tt.value for tt in ToolType]
|
96
|
+
|
97
|
+
for tool_type in flattened_types:
|
98
|
+
if tool_type not in valid_values:
|
99
|
+
raise HTTPException(status_code=400, detail=f"Invalid tool_type '{tool_type}'. Must be one of: {', '.join(valid_values)}")
|
100
|
+
valid_types.append(tool_type)
|
101
|
+
|
102
|
+
return valid_types if valid_types else None
|
103
|
+
|
104
|
+
# Parse and validate tool types (same logic as list_tools)
|
105
|
+
tool_types_str = parse_tool_types(tool_types)
|
106
|
+
exclude_tool_types_str = parse_tool_types(exclude_tool_types)
|
107
|
+
|
108
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
109
|
+
|
110
|
+
# Combine single name with names list for unified processing (same logic as list_tools)
|
111
|
+
combined_names = []
|
112
|
+
if name is not None:
|
113
|
+
combined_names.append(name)
|
114
|
+
if names is not None:
|
115
|
+
combined_names.extend(names)
|
116
|
+
|
117
|
+
# Use None if no names specified, otherwise use the combined list
|
118
|
+
final_names = combined_names if combined_names else None
|
119
|
+
|
120
|
+
# Helper function to parse tool IDs - supports both repeated params and comma-separated values
|
121
|
+
def parse_tool_ids(tool_ids_input: Optional[List[str]]) -> Optional[List[str]]:
|
122
|
+
if tool_ids_input is None:
|
123
|
+
return None
|
124
|
+
|
125
|
+
# Flatten any comma-separated values
|
126
|
+
flattened_ids = []
|
127
|
+
for item in tool_ids_input:
|
128
|
+
# Split by comma in case user provided comma-separated values
|
129
|
+
ids_in_item = [id.strip() for id in item.split(",") if id.strip()]
|
130
|
+
flattened_ids.extend(ids_in_item)
|
131
|
+
|
132
|
+
return flattened_ids if flattened_ids else None
|
133
|
+
|
134
|
+
# Parse tool IDs (same logic as list_tools)
|
135
|
+
final_tool_ids = parse_tool_ids(tool_ids)
|
136
|
+
|
137
|
+
# Get the count of tools using unified query
|
138
|
+
return await server.tool_manager.count_tools_async(
|
139
|
+
actor=actor,
|
140
|
+
tool_types=tool_types_str,
|
141
|
+
exclude_tool_types=exclude_tool_types_str,
|
142
|
+
names=final_names,
|
143
|
+
tool_ids=final_tool_ids,
|
144
|
+
search=search,
|
145
|
+
return_only_letta_tools=return_only_letta_tools,
|
146
|
+
exclude_letta_tools=exclude_letta_tools,
|
147
|
+
)
|
155
148
|
|
156
149
|
|
157
150
|
@router.get("/{tool_id}", response_model=Tool, operation_id="retrieve_tool")
|
@@ -201,81 +194,77 @@ async def list_tools(
|
|
201
194
|
"""
|
202
195
|
Get a list of all tools available to agents.
|
203
196
|
"""
|
204
|
-
try:
|
205
|
-
# Helper function to parse tool types - supports both repeated params and comma-separated values
|
206
|
-
def parse_tool_types(tool_types_input: Optional[List[str]]) -> Optional[List[str]]:
|
207
|
-
if tool_types_input is None:
|
208
|
-
return None
|
209
|
-
|
210
|
-
# Flatten any comma-separated values and validate against ToolType enum
|
211
|
-
flattened_types = []
|
212
|
-
for item in tool_types_input:
|
213
|
-
# Split by comma in case user provided comma-separated values
|
214
|
-
types_in_item = [t.strip() for t in item.split(",") if t.strip()]
|
215
|
-
flattened_types.extend(types_in_item)
|
216
|
-
|
217
|
-
# Validate each type against the ToolType enum
|
218
|
-
valid_types = []
|
219
|
-
valid_values = [tt.value for tt in ToolType]
|
220
|
-
|
221
|
-
for tool_type in flattened_types:
|
222
|
-
if tool_type not in valid_values:
|
223
|
-
raise HTTPException(
|
224
|
-
status_code=400, detail=f"Invalid tool_type '{tool_type}'. Must be one of: {', '.join(valid_values)}"
|
225
|
-
)
|
226
|
-
valid_types.append(tool_type)
|
227
|
-
|
228
|
-
return valid_types if valid_types else None
|
229
|
-
|
230
|
-
# Parse and validate tool types
|
231
|
-
tool_types_str = parse_tool_types(tool_types)
|
232
|
-
exclude_tool_types_str = parse_tool_types(exclude_tool_types)
|
233
197
|
|
234
|
-
|
198
|
+
# Helper function to parse tool types - supports both repeated params and comma-separated values
|
199
|
+
def parse_tool_types(tool_types_input: Optional[List[str]]) -> Optional[List[str]]:
|
200
|
+
if tool_types_input is None:
|
201
|
+
return None
|
235
202
|
|
236
|
-
#
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
#
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
if
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
203
|
+
# Flatten any comma-separated values and validate against ToolType enum
|
204
|
+
flattened_types = []
|
205
|
+
for item in tool_types_input:
|
206
|
+
# Split by comma in case user provided comma-separated values
|
207
|
+
types_in_item = [t.strip() for t in item.split(",") if t.strip()]
|
208
|
+
flattened_types.extend(types_in_item)
|
209
|
+
|
210
|
+
# Validate each type against the ToolType enum
|
211
|
+
valid_types = []
|
212
|
+
valid_values = [tt.value for tt in ToolType]
|
213
|
+
|
214
|
+
for tool_type in flattened_types:
|
215
|
+
if tool_type not in valid_values:
|
216
|
+
raise HTTPException(status_code=400, detail=f"Invalid tool_type '{tool_type}'. Must be one of: {', '.join(valid_values)}")
|
217
|
+
valid_types.append(tool_type)
|
218
|
+
|
219
|
+
return valid_types if valid_types else None
|
220
|
+
|
221
|
+
# Parse and validate tool types
|
222
|
+
tool_types_str = parse_tool_types(tool_types)
|
223
|
+
exclude_tool_types_str = parse_tool_types(exclude_tool_types)
|
224
|
+
|
225
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
226
|
+
|
227
|
+
# Combine single name with names list for unified processing
|
228
|
+
combined_names = []
|
229
|
+
if name is not None:
|
230
|
+
combined_names.append(name)
|
231
|
+
if names is not None:
|
232
|
+
combined_names.extend(names)
|
233
|
+
|
234
|
+
# Use None if no names specified, otherwise use the combined list
|
235
|
+
final_names = combined_names if combined_names else None
|
236
|
+
|
237
|
+
# Helper function to parse tool IDs - supports both repeated params and comma-separated values
|
238
|
+
def parse_tool_ids(tool_ids_input: Optional[List[str]]) -> Optional[List[str]]:
|
239
|
+
if tool_ids_input is None:
|
240
|
+
return None
|
241
|
+
|
242
|
+
# Flatten any comma-separated values
|
243
|
+
flattened_ids = []
|
244
|
+
for item in tool_ids_input:
|
245
|
+
# Split by comma in case user provided comma-separated values
|
246
|
+
ids_in_item = [id.strip() for id in item.split(",") if id.strip()]
|
247
|
+
flattened_ids.extend(ids_in_item)
|
248
|
+
|
249
|
+
return flattened_ids if flattened_ids else None
|
250
|
+
|
251
|
+
# Parse tool IDs
|
252
|
+
final_tool_ids = parse_tool_ids(tool_ids)
|
253
|
+
|
254
|
+
# Get the list of tools using unified query
|
255
|
+
return await server.tool_manager.list_tools_async(
|
256
|
+
actor=actor,
|
257
|
+
before=before,
|
258
|
+
after=after,
|
259
|
+
limit=limit,
|
260
|
+
ascending=(order == "asc"),
|
261
|
+
tool_types=tool_types_str,
|
262
|
+
exclude_tool_types=exclude_tool_types_str,
|
263
|
+
names=final_names,
|
264
|
+
tool_ids=final_tool_ids,
|
265
|
+
search=search,
|
266
|
+
return_only_letta_tools=return_only_letta_tools,
|
267
|
+
)
|
279
268
|
|
280
269
|
|
281
270
|
@router.post("/", response_model=Tool, operation_id="create_tool")
|
@@ -287,19 +276,9 @@ async def create_tool(
|
|
287
276
|
"""
|
288
277
|
Create a new tool
|
289
278
|
"""
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
return await server.tool_manager.create_tool_async(pydantic_tool=tool, actor=actor)
|
294
|
-
except UniqueConstraintViolationError as e:
|
295
|
-
clean_error_message = "Tool with this name already exists."
|
296
|
-
raise HTTPException(status_code=409, detail=clean_error_message)
|
297
|
-
except LettaToolCreateError as e:
|
298
|
-
# HTTP 400 == Bad Request
|
299
|
-
raise HTTPException(status_code=400, detail=str(e))
|
300
|
-
except Exception as e:
|
301
|
-
# Catch other unexpected errors and raise an internal server error
|
302
|
-
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
|
279
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
280
|
+
tool = Tool(**request.model_dump(exclude_unset=True))
|
281
|
+
return await server.tool_manager.create_or_update_tool_async(pydantic_tool=tool, actor=actor)
|
303
282
|
|
304
283
|
|
305
284
|
@router.put("/", response_model=Tool, operation_id="upsert_tool")
|
@@ -311,21 +290,9 @@ async def upsert_tool(
|
|
311
290
|
"""
|
312
291
|
Create or update a tool
|
313
292
|
"""
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
pydantic_tool=Tool(**request.model_dump(exclude_unset=True)), actor=actor
|
318
|
-
)
|
319
|
-
return tool
|
320
|
-
except UniqueConstraintViolationError as e:
|
321
|
-
# Log the error and raise a conflict exception
|
322
|
-
raise HTTPException(status_code=409, detail=str(e))
|
323
|
-
except LettaToolCreateError as e:
|
324
|
-
# HTTP 400 == Bad Request
|
325
|
-
raise HTTPException(status_code=400, detail=str(e))
|
326
|
-
except Exception as e:
|
327
|
-
# Catch other unexpected errors and raise an internal server error
|
328
|
-
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
|
293
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
294
|
+
tool = await server.tool_manager.create_or_update_tool_async(pydantic_tool=Tool(**request.model_dump(exclude_unset=True)), actor=actor)
|
295
|
+
return tool
|
329
296
|
|
330
297
|
|
331
298
|
@router.patch("/{tool_id}", response_model=Tool, operation_id="modify_tool")
|
@@ -338,19 +305,9 @@ async def modify_tool(
|
|
338
305
|
"""
|
339
306
|
Update an existing tool
|
340
307
|
"""
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
return tool
|
345
|
-
except LettaToolNameConflictError as e:
|
346
|
-
# HTTP 409 == Conflict
|
347
|
-
raise HTTPException(status_code=409, detail=str(e))
|
348
|
-
except LettaToolCreateError as e:
|
349
|
-
# HTTP 400 == Bad Request
|
350
|
-
raise HTTPException(status_code=400, detail=str(e))
|
351
|
-
except Exception as e:
|
352
|
-
# Catch other unexpected errors and raise an internal server error
|
353
|
-
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
|
308
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
309
|
+
tool = await server.tool_manager.update_tool_by_id_async(tool_id=tool_id, tool_update=request, actor=actor)
|
310
|
+
return tool
|
354
311
|
|
355
312
|
|
356
313
|
@router.post("/add-base-tools", response_model=List[Tool], operation_id="add_base_tools")
|
@@ -376,151 +333,17 @@ async def run_tool_from_source(
|
|
376
333
|
"""
|
377
334
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
378
335
|
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
)
|
391
|
-
except LettaToolCreateError as e:
|
392
|
-
# HTTP 400 == Bad Request
|
393
|
-
raise HTTPException(status_code=400, detail=str(e))
|
394
|
-
|
395
|
-
except Exception as e:
|
396
|
-
# Catch other unexpected errors and raise an internal server error
|
397
|
-
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
|
398
|
-
|
399
|
-
|
400
|
-
# Specific routes for Composio
|
401
|
-
@router.get("/composio/apps", response_model=List[AppModel], operation_id="list_composio_apps")
|
402
|
-
def list_composio_apps(
|
403
|
-
server: SyncServer = Depends(get_letta_server),
|
404
|
-
headers: HeaderParams = Depends(get_headers),
|
405
|
-
):
|
406
|
-
"""
|
407
|
-
Get a list of all Composio apps
|
408
|
-
"""
|
409
|
-
actor = server.user_manager.get_user_or_default(user_id=headers.actor_id)
|
410
|
-
composio_api_key = get_composio_api_key(actor=actor, logger=logger)
|
411
|
-
if not composio_api_key:
|
412
|
-
raise HTTPException(
|
413
|
-
status_code=400, # Bad Request
|
414
|
-
detail="No API keys found for Composio. Please add your Composio API Key as an environment variable for your sandbox configuration, or set it as environment variable COMPOSIO_API_KEY.",
|
415
|
-
)
|
416
|
-
return server.get_composio_apps(api_key=composio_api_key)
|
417
|
-
|
418
|
-
|
419
|
-
@router.get("/composio/apps/{composio_app_name}/actions", response_model=List[ActionModel], operation_id="list_composio_actions_by_app")
|
420
|
-
def list_composio_actions_by_app(
|
421
|
-
composio_app_name: str,
|
422
|
-
server: SyncServer = Depends(get_letta_server),
|
423
|
-
headers: HeaderParams = Depends(get_headers),
|
424
|
-
):
|
425
|
-
"""
|
426
|
-
Get a list of all Composio actions for a specific app
|
427
|
-
"""
|
428
|
-
actor = server.user_manager.get_user_or_default(user_id=headers.actor_id)
|
429
|
-
composio_api_key = get_composio_api_key(actor=actor, logger=logger)
|
430
|
-
if not composio_api_key:
|
431
|
-
raise HTTPException(
|
432
|
-
status_code=400, # Bad Request
|
433
|
-
detail="No API keys found for Composio. Please add your Composio API Key as an environment variable for your sandbox configuration, or set it as environment variable COMPOSIO_API_KEY.",
|
434
|
-
)
|
435
|
-
return server.get_composio_actions_from_app_name(composio_app_name=composio_app_name, api_key=composio_api_key)
|
436
|
-
|
437
|
-
|
438
|
-
@router.post("/composio/{composio_action_name}", response_model=Tool, operation_id="add_composio_tool")
|
439
|
-
async def add_composio_tool(
|
440
|
-
composio_action_name: str,
|
441
|
-
server: SyncServer = Depends(get_letta_server),
|
442
|
-
headers: HeaderParams = Depends(get_headers),
|
443
|
-
):
|
444
|
-
"""
|
445
|
-
Add a new Composio tool by action name (Composio refers to each tool as an `Action`)
|
446
|
-
"""
|
447
|
-
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
448
|
-
|
449
|
-
try:
|
450
|
-
tool_create = ToolCreate.from_composio(action_name=composio_action_name)
|
451
|
-
return await server.tool_manager.create_or_update_composio_tool_async(tool_create=tool_create, actor=actor)
|
452
|
-
except ConnectedAccountNotFoundError as e:
|
453
|
-
raise HTTPException(
|
454
|
-
status_code=400, # Bad Request
|
455
|
-
detail={
|
456
|
-
"code": "ConnectedAccountNotFoundError",
|
457
|
-
"message": str(e),
|
458
|
-
"composio_action_name": composio_action_name,
|
459
|
-
},
|
460
|
-
)
|
461
|
-
except EnumStringNotFound as e:
|
462
|
-
raise HTTPException(
|
463
|
-
status_code=400, # Bad Request
|
464
|
-
detail={
|
465
|
-
"code": "EnumStringNotFound",
|
466
|
-
"message": str(e),
|
467
|
-
"composio_action_name": composio_action_name,
|
468
|
-
},
|
469
|
-
)
|
470
|
-
except EnumMetadataNotFound as e:
|
471
|
-
raise HTTPException(
|
472
|
-
status_code=400, # Bad Request
|
473
|
-
detail={
|
474
|
-
"code": "EnumMetadataNotFound",
|
475
|
-
"message": str(e),
|
476
|
-
"composio_action_name": composio_action_name,
|
477
|
-
},
|
478
|
-
)
|
479
|
-
except HTTPError as e:
|
480
|
-
raise HTTPException(
|
481
|
-
status_code=400, # Bad Request
|
482
|
-
detail={
|
483
|
-
"code": "HTTPError",
|
484
|
-
"message": str(e),
|
485
|
-
"composio_action_name": composio_action_name,
|
486
|
-
},
|
487
|
-
)
|
488
|
-
except NoItemsFound as e:
|
489
|
-
raise HTTPException(
|
490
|
-
status_code=400, # Bad Request
|
491
|
-
detail={
|
492
|
-
"code": "NoItemsFound",
|
493
|
-
"message": str(e),
|
494
|
-
"composio_action_name": composio_action_name,
|
495
|
-
},
|
496
|
-
)
|
497
|
-
except ApiKeyNotProvidedError as e:
|
498
|
-
raise HTTPException(
|
499
|
-
status_code=400, # Bad Request
|
500
|
-
detail={
|
501
|
-
"code": "ApiKeyNotProvidedError",
|
502
|
-
"message": str(e),
|
503
|
-
"composio_action_name": composio_action_name,
|
504
|
-
},
|
505
|
-
)
|
506
|
-
except ComposioClientError as e:
|
507
|
-
raise HTTPException(
|
508
|
-
status_code=400, # Bad Request
|
509
|
-
detail={
|
510
|
-
"code": "ComposioClientError",
|
511
|
-
"message": str(e),
|
512
|
-
"composio_action_name": composio_action_name,
|
513
|
-
},
|
514
|
-
)
|
515
|
-
except ComposioSDKError as e:
|
516
|
-
raise HTTPException(
|
517
|
-
status_code=400, # Bad Request
|
518
|
-
detail={
|
519
|
-
"code": "ComposioSDKError",
|
520
|
-
"message": str(e),
|
521
|
-
"composio_action_name": composio_action_name,
|
522
|
-
},
|
523
|
-
)
|
336
|
+
return await server.run_tool_from_source(
|
337
|
+
tool_source=request.source_code,
|
338
|
+
tool_source_type=request.source_type,
|
339
|
+
tool_args=request.args,
|
340
|
+
tool_env_vars=request.env_vars,
|
341
|
+
tool_name=request.name,
|
342
|
+
tool_args_json_schema=request.args_json_schema,
|
343
|
+
tool_json_schema=request.json_schema,
|
344
|
+
pip_requirements=request.pip_requirements,
|
345
|
+
actor=actor,
|
346
|
+
)
|
524
347
|
|
525
348
|
|
526
349
|
# Specific routes for MCP
|
@@ -555,38 +378,18 @@ async def list_mcp_tools_by_server(
|
|
555
378
|
"""
|
556
379
|
Get a list of all tools for a specific MCP server
|
557
380
|
"""
|
381
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
558
382
|
try:
|
559
|
-
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
560
383
|
mcp_tools = await server.mcp_manager.list_mcp_server_tools(mcp_server_name=mcp_server_name, actor=actor)
|
561
384
|
return mcp_tools
|
562
|
-
except
|
563
|
-
|
564
|
-
|
565
|
-
|
566
|
-
|
567
|
-
|
568
|
-
"message": str(e),
|
569
|
-
"mcp_server_name": mcp_server_name,
|
570
|
-
},
|
571
|
-
)
|
572
|
-
if isinstance(e, HTTPStatusError):
|
573
|
-
raise HTTPException(
|
574
|
-
status_code=401,
|
575
|
-
detail={
|
576
|
-
"code": "MCPListToolsError",
|
577
|
-
"message": str(e),
|
578
|
-
"mcp_server_name": mcp_server_name,
|
579
|
-
},
|
580
|
-
)
|
385
|
+
except (ConnectError, ConnectionError) as e:
|
386
|
+
raise LettaMCPConnectionError(str(e), server_name=mcp_server_name)
|
387
|
+
except HTTPStatusError as e:
|
388
|
+
# HTTPStatusError from the MCP server likely means auth issue
|
389
|
+
if e.response.status_code == 401:
|
390
|
+
raise LettaMCPConnectionError(f"Authentication failed: {e}", server_name=mcp_server_name)
|
581
391
|
else:
|
582
|
-
raise
|
583
|
-
status_code=500,
|
584
|
-
detail={
|
585
|
-
"code": "MCPListToolsError",
|
586
|
-
"message": str(e),
|
587
|
-
"mcp_server_name": mcp_server_name,
|
588
|
-
},
|
589
|
-
)
|
392
|
+
raise LettaMCPConnectionError(f"HTTP error from MCP server: {e}", server_name=mcp_server_name)
|
590
393
|
|
591
394
|
|
592
395
|
@router.post("/mcp/servers/{mcp_server_name}/resync", operation_id="resync_mcp_server_tools")
|
@@ -606,29 +409,8 @@ async def resync_mcp_server_tools(
|
|
606
409
|
Returns a summary of changes made.
|
607
410
|
"""
|
608
411
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
609
|
-
|
610
|
-
|
611
|
-
result = await server.mcp_manager.resync_mcp_server_tools(mcp_server_name=mcp_server_name, actor=actor, agent_id=agent_id)
|
612
|
-
return result
|
613
|
-
except ValueError as e:
|
614
|
-
raise HTTPException(
|
615
|
-
status_code=404,
|
616
|
-
detail={
|
617
|
-
"code": "MCPServerNotFoundError",
|
618
|
-
"message": str(e),
|
619
|
-
"mcp_server_name": mcp_server_name,
|
620
|
-
},
|
621
|
-
)
|
622
|
-
except Exception as e:
|
623
|
-
logger.error(f"Unexpected error refreshing MCP server tools: {e}")
|
624
|
-
raise HTTPException(
|
625
|
-
status_code=404,
|
626
|
-
detail={
|
627
|
-
"code": "MCPRefreshError",
|
628
|
-
"message": f"Failed to refresh MCP server tools: {str(e)}",
|
629
|
-
"mcp_server_name": mcp_server_name,
|
630
|
-
},
|
631
|
-
)
|
412
|
+
result = await server.mcp_manager.resync_mcp_server_tools(mcp_server_name=mcp_server_name, actor=actor, agent_id=agent_id)
|
413
|
+
return result
|
632
414
|
|
633
415
|
|
634
416
|
@router.post("/mcp/servers/{mcp_server_name}/{mcp_tool_name}", response_model=Tool, operation_id="add_mcp_tool")
|
@@ -641,30 +423,13 @@ async def add_mcp_tool(
|
|
641
423
|
"""
|
642
424
|
Register a new MCP tool as a Letta server by MCP server + tool name
|
643
425
|
"""
|
644
|
-
actor = server.user_manager.
|
426
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
645
427
|
|
646
428
|
if tool_settings.mcp_read_from_config:
|
647
429
|
try:
|
648
430
|
available_tools = await server.get_tools_from_mcp_server(mcp_server_name=mcp_server_name)
|
649
|
-
except ValueError as e:
|
650
|
-
# ValueError means that the MCP server name doesn't exist
|
651
|
-
raise HTTPException(
|
652
|
-
status_code=400, # Bad Request
|
653
|
-
detail={
|
654
|
-
"code": "MCPServerNotFoundError",
|
655
|
-
"message": str(e),
|
656
|
-
"mcp_server_name": mcp_server_name,
|
657
|
-
},
|
658
|
-
)
|
659
431
|
except MCPTimeoutError as e:
|
660
|
-
raise
|
661
|
-
status_code=408, # Timeout
|
662
|
-
detail={
|
663
|
-
"code": "MCPTimeoutError",
|
664
|
-
"message": str(e),
|
665
|
-
"mcp_server_name": mcp_server_name,
|
666
|
-
},
|
667
|
-
)
|
432
|
+
raise LettaMCPTimeoutError(str(e), server_name=mcp_server_name)
|
668
433
|
|
669
434
|
# See if the tool is in the available list
|
670
435
|
mcp_tool = None
|
@@ -673,27 +438,18 @@ async def add_mcp_tool(
|
|
673
438
|
mcp_tool = tool
|
674
439
|
break
|
675
440
|
if not mcp_tool:
|
676
|
-
raise
|
677
|
-
|
678
|
-
|
679
|
-
"code": "MCPToolNotFoundError",
|
680
|
-
"message": f"Tool {mcp_tool_name} not found in MCP server {mcp_server_name} - available tools: {', '.join([tool.name for tool in available_tools])}",
|
681
|
-
"mcp_tool_name": mcp_tool_name,
|
682
|
-
},
|
441
|
+
raise LettaInvalidArgumentError(
|
442
|
+
f"Tool {mcp_tool_name} not found in MCP server {mcp_server_name} - available tools: {', '.join([tool.name for tool in available_tools])}",
|
443
|
+
argument_name="mcp_tool_name",
|
683
444
|
)
|
684
445
|
|
685
446
|
# Check tool health - reject only INVALID tools
|
686
447
|
if mcp_tool.health:
|
687
448
|
if mcp_tool.health.status == "INVALID":
|
688
|
-
raise
|
689
|
-
|
690
|
-
|
691
|
-
|
692
|
-
"message": f"Tool {mcp_tool_name} has an invalid schema and cannot be attached",
|
693
|
-
"mcp_tool_name": mcp_tool_name,
|
694
|
-
"health_status": mcp_tool.health.status,
|
695
|
-
"reasons": mcp_tool.health.reasons,
|
696
|
-
},
|
449
|
+
raise LettaInvalidMCPSchemaError(
|
450
|
+
server_name=mcp_server_name,
|
451
|
+
mcp_tool_name=mcp_tool_name,
|
452
|
+
reasons=mcp_tool.health.reasons,
|
697
453
|
)
|
698
454
|
|
699
455
|
tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=mcp_tool)
|
@@ -720,40 +476,27 @@ async def add_mcp_server_to_config(
|
|
720
476
|
"""
|
721
477
|
Add a new MCP server to the Letta MCP server config
|
722
478
|
"""
|
723
|
-
|
724
|
-
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
479
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
725
480
|
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
|
481
|
+
if tool_settings.mcp_read_from_config:
|
482
|
+
# write to config file
|
483
|
+
return await server.add_mcp_server_to_config(server_config=request, allow_upsert=True)
|
484
|
+
else:
|
485
|
+
# log to DB
|
486
|
+
# Check if stdio servers are disabled
|
487
|
+
if isinstance(request, StdioServerConfig) and tool_settings.mcp_disable_stdio:
|
488
|
+
raise HTTPException(
|
489
|
+
status_code=400,
|
490
|
+
detail="stdio is not supported in the current environment, please use a self-hosted Letta server in order to add a stdio MCP server",
|
491
|
+
)
|
737
492
|
|
738
|
-
|
739
|
-
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
except UniqueConstraintViolationError:
|
746
|
-
# If server name already exists, throw 409 conflict error
|
747
|
-
raise HTTPException(
|
748
|
-
status_code=409,
|
749
|
-
detail={
|
750
|
-
"code": "MCPServerNameAlreadyExistsError",
|
751
|
-
"message": f"MCP server with name '{request.server_name}' already exists",
|
752
|
-
"server_name": request.server_name,
|
753
|
-
},
|
754
|
-
)
|
755
|
-
except Exception as e:
|
756
|
-
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
|
493
|
+
# Create MCP server and optimistically sync tools
|
494
|
+
# The mcp_manager will handle encryption of sensitive fields
|
495
|
+
await server.mcp_manager.create_mcp_server_from_config_with_tools(request, actor=actor)
|
496
|
+
|
497
|
+
# TODO: don't do this in the future (just return MCPServer)
|
498
|
+
all_servers = await server.mcp_manager.list_mcp_servers(actor=actor)
|
499
|
+
return [server.to_config() for server in all_servers]
|
757
500
|
|
758
501
|
|
759
502
|
@router.patch(
|
@@ -770,21 +513,15 @@ async def update_mcp_server(
|
|
770
513
|
"""
|
771
514
|
Update an existing MCP server configuration
|
772
515
|
"""
|
773
|
-
|
774
|
-
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
516
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
775
517
|
|
776
|
-
|
777
|
-
|
778
|
-
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
except HTTPException:
|
784
|
-
# Re-raise HTTP exceptions (like 404)
|
785
|
-
raise
|
786
|
-
except Exception as e:
|
787
|
-
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
|
518
|
+
if tool_settings.mcp_read_from_config:
|
519
|
+
raise HTTPException(status_code=501, detail="Update not implemented for config file mode, config files to be deprecated.")
|
520
|
+
else:
|
521
|
+
updated_server = await server.mcp_manager.update_mcp_server_by_name(
|
522
|
+
mcp_server_name=mcp_server_name, mcp_server_update=request, actor=actor
|
523
|
+
)
|
524
|
+
return updated_server.to_config()
|
788
525
|
|
789
526
|
|
790
527
|
@router.delete(
|
@@ -836,32 +573,9 @@ async def test_mcp_server(
|
|
836
573
|
|
837
574
|
return {"status": "success", "tools": tools}
|
838
575
|
except ConnectionError as e:
|
839
|
-
raise
|
840
|
-
status_code=400,
|
841
|
-
detail={
|
842
|
-
"code": "MCPServerConnectionError",
|
843
|
-
"message": str(e),
|
844
|
-
"server_name": request.server_name,
|
845
|
-
},
|
846
|
-
)
|
576
|
+
raise LettaMCPConnectionError(str(e), server_name=request.server_name)
|
847
577
|
except MCPTimeoutError as e:
|
848
|
-
raise
|
849
|
-
status_code=408,
|
850
|
-
detail={
|
851
|
-
"code": "MCPTimeoutError",
|
852
|
-
"message": f"MCP server connection timed out: {str(e)}",
|
853
|
-
"server_name": request.server_name,
|
854
|
-
},
|
855
|
-
)
|
856
|
-
except Exception as e:
|
857
|
-
raise HTTPException(
|
858
|
-
status_code=500,
|
859
|
-
detail={
|
860
|
-
"code": "MCPServerTestError",
|
861
|
-
"message": f"Failed to test MCP server: {str(e)}",
|
862
|
-
"server_name": request.server_name,
|
863
|
-
},
|
864
|
-
)
|
578
|
+
raise LettaMCPTimeoutError(f"MCP server connection timed out: {str(e)}", server_name=request.server_name)
|
865
579
|
finally:
|
866
580
|
if client:
|
867
581
|
try:
|
@@ -978,18 +692,14 @@ async def generate_json_schema(
|
|
978
692
|
Generate a JSON schema from the given source code defining a function or class.
|
979
693
|
Supports both Python and TypeScript source code.
|
980
694
|
"""
|
981
|
-
|
982
|
-
|
983
|
-
from letta.functions.typescript_parser import derive_typescript_json_schema
|
984
|
-
|
985
|
-
schema = derive_typescript_json_schema(source_code=request.code)
|
986
|
-
else:
|
987
|
-
# Default to Python for backwards compatibility
|
988
|
-
schema = derive_openai_json_schema(source_code=request.code)
|
989
|
-
return schema
|
695
|
+
if request.source_type == "typescript":
|
696
|
+
from letta.functions.typescript_parser import derive_typescript_json_schema
|
990
697
|
|
991
|
-
|
992
|
-
|
698
|
+
schema = derive_typescript_json_schema(source_code=request.code)
|
699
|
+
else:
|
700
|
+
# Default to Python for backwards compatibility
|
701
|
+
schema = derive_openai_json_schema(source_code=request.code)
|
702
|
+
return schema
|
993
703
|
|
994
704
|
|
995
705
|
# TODO: @jnjpng move this and other models above to appropriate file for schemas
|
@@ -1016,14 +726,9 @@ async def execute_mcp_tool(
|
|
1016
726
|
# Get the MCP server by name
|
1017
727
|
mcp_server = await server.mcp_manager.get_mcp_server(mcp_server_name, actor)
|
1018
728
|
if not mcp_server:
|
1019
|
-
|
1020
|
-
|
1021
|
-
|
1022
|
-
"code": "MCPServerNotFound",
|
1023
|
-
"message": f"MCP server '{mcp_server_name}' not found",
|
1024
|
-
"server_name": mcp_server_name,
|
1025
|
-
},
|
1026
|
-
)
|
729
|
+
from letta.orm.errors import NoResultFound
|
730
|
+
|
731
|
+
raise NoResultFound(f"MCP server '{mcp_server_name}' not found")
|
1027
732
|
|
1028
733
|
# Create client and connect
|
1029
734
|
server_config = mcp_server.to_config()
|
@@ -1038,19 +743,6 @@ async def execute_mcp_tool(
|
|
1038
743
|
"result": result,
|
1039
744
|
"success": success,
|
1040
745
|
}
|
1041
|
-
except HTTPException:
|
1042
|
-
raise
|
1043
|
-
except Exception as e:
|
1044
|
-
logger.warning(f"Error executing MCP tool: {str(e)}")
|
1045
|
-
raise HTTPException(
|
1046
|
-
status_code=500,
|
1047
|
-
detail={
|
1048
|
-
"code": "MCPToolExecutionError",
|
1049
|
-
"message": f"Failed to execute MCP tool: {str(e)}",
|
1050
|
-
"server_name": mcp_server_name,
|
1051
|
-
"tool_name": tool_name,
|
1052
|
-
},
|
1053
|
-
)
|
1054
746
|
finally:
|
1055
747
|
if client:
|
1056
748
|
try:
|
@@ -1120,69 +812,64 @@ async def generate_tool_from_prompt(
|
|
1120
812
|
"""
|
1121
813
|
Generate a tool from the given user prompt.
|
1122
814
|
"""
|
1123
|
-
|
1124
|
-
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
1128
|
-
|
1129
|
-
|
1130
|
-
|
1131
|
-
|
1132
|
-
|
1133
|
-
|
1134
|
-
|
1135
|
-
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1139
|
-
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1145
|
-
|
1146
|
-
|
1147
|
-
|
1148
|
-
|
1149
|
-
"
|
1150
|
-
"
|
1151
|
-
|
1152
|
-
"
|
1153
|
-
|
1154
|
-
"
|
1155
|
-
|
1156
|
-
|
1157
|
-
|
1158
|
-
|
1159
|
-
"pip_requirements_json": {
|
1160
|
-
"type": "string",
|
1161
|
-
"description": "Optional JSON dict that contains pip packages to be installed if needed by the source code. Key is the name of the pip package and value is the version number.",
|
1162
|
-
},
|
815
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
816
|
+
llm_config = await server.get_cached_llm_config_async(actor=actor, handle=request.handle or "anthropic/claude-3-5-sonnet-20240620")
|
817
|
+
formatted_prompt = (
|
818
|
+
f"Generate a python function named {request.tool_name} using the instructions below "
|
819
|
+
+ (f"based on this starter code: \n\n```\n{request.starter_code}\n```\n\n" if request.starter_code else "\n")
|
820
|
+
+ (f"Note the following validation errors: \n{' '.join(request.validation_errors)}\n\n" if request.validation_errors else "\n")
|
821
|
+
+ f"Instructions: {request.prompt}"
|
822
|
+
)
|
823
|
+
llm_client = LLMClient.create(
|
824
|
+
provider_type=llm_config.model_endpoint_type,
|
825
|
+
actor=actor,
|
826
|
+
)
|
827
|
+
assert llm_client is not None
|
828
|
+
|
829
|
+
assistant_message_ack = "Understood, I will respond with generated python source code and sample arguments that can be used to test the functionality once I receive the user prompt. I'm ready."
|
830
|
+
|
831
|
+
input_messages = [
|
832
|
+
Message(role=MessageRole.system, content=[TextContent(text=get_system_text("memgpt_generate_tool"))]),
|
833
|
+
Message(role=MessageRole.assistant, content=[TextContent(text=assistant_message_ack)]),
|
834
|
+
Message(role=MessageRole.user, content=[TextContent(text=formatted_prompt)]),
|
835
|
+
]
|
836
|
+
|
837
|
+
tool = {
|
838
|
+
"name": "generate_tool",
|
839
|
+
"description": "This method generates the raw source code for a custom tool that can be attached to and agent for llm invocation.",
|
840
|
+
"parameters": {
|
841
|
+
"type": "object",
|
842
|
+
"properties": {
|
843
|
+
"raw_source_code": {"type": "string", "description": "The raw python source code of the custom tool."},
|
844
|
+
"sample_args_json": {
|
845
|
+
"type": "string",
|
846
|
+
"description": "The JSON dict that contains sample args for a test run of the python function. Key is the name of the function parameter and value is an example argument that is passed in.",
|
847
|
+
},
|
848
|
+
"pip_requirements_json": {
|
849
|
+
"type": "string",
|
850
|
+
"description": "Optional JSON dict that contains pip packages to be installed if needed by the source code. Key is the name of the pip package and value is the version number.",
|
1163
851
|
},
|
1164
|
-
"required": ["raw_source_code", "sample_args_json", "pip_requirements_json"],
|
1165
852
|
},
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
|
1178
|
-
|
1179
|
-
|
1180
|
-
|
1181
|
-
|
1182
|
-
|
1183
|
-
|
1184
|
-
|
1185
|
-
)
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
853
|
+
"required": ["raw_source_code", "sample_args_json", "pip_requirements_json"],
|
854
|
+
},
|
855
|
+
}
|
856
|
+
request_data = llm_client.build_request_data(
|
857
|
+
AgentType.letta_v1_agent,
|
858
|
+
input_messages,
|
859
|
+
llm_config,
|
860
|
+
tools=[tool],
|
861
|
+
)
|
862
|
+
response_data = await llm_client.request_async(request_data, llm_config)
|
863
|
+
response = llm_client.convert_response_to_chat_completion(response_data, input_messages, llm_config)
|
864
|
+
output = json.loads(response.choices[0].message.tool_calls[0].function.arguments)
|
865
|
+
pip_requirements = [PipRequirement(name=k, version=v or None) for k, v in json.loads(output["pip_requirements_json"]).items()]
|
866
|
+
return GenerateToolOutput(
|
867
|
+
tool=Tool(
|
868
|
+
name=request.tool_name,
|
869
|
+
source_type="python",
|
870
|
+
source_code=output["raw_source_code"],
|
871
|
+
pip_requirements=pip_requirements,
|
872
|
+
),
|
873
|
+
sample_args=json.loads(output["sample_args_json"]),
|
874
|
+
response=response.choices[0].message.content,
|
875
|
+
)
|