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.
Files changed (151) hide show
  1. letta/__init__.py +1 -1
  2. letta/adapters/letta_llm_adapter.py +1 -0
  3. letta/adapters/letta_llm_request_adapter.py +0 -1
  4. letta/adapters/letta_llm_stream_adapter.py +7 -2
  5. letta/adapters/simple_llm_request_adapter.py +88 -0
  6. letta/adapters/simple_llm_stream_adapter.py +192 -0
  7. letta/agents/agent_loop.py +6 -0
  8. letta/agents/ephemeral_summary_agent.py +2 -1
  9. letta/agents/helpers.py +142 -6
  10. letta/agents/letta_agent.py +13 -33
  11. letta/agents/letta_agent_batch.py +2 -4
  12. letta/agents/letta_agent_v2.py +87 -77
  13. letta/agents/letta_agent_v3.py +927 -0
  14. letta/agents/voice_agent.py +2 -6
  15. letta/constants.py +8 -4
  16. letta/database_utils.py +161 -0
  17. letta/errors.py +40 -0
  18. letta/functions/function_sets/base.py +84 -4
  19. letta/functions/function_sets/multi_agent.py +0 -3
  20. letta/functions/schema_generator.py +113 -71
  21. letta/groups/dynamic_multi_agent.py +3 -2
  22. letta/groups/helpers.py +1 -2
  23. letta/groups/round_robin_multi_agent.py +3 -2
  24. letta/groups/sleeptime_multi_agent.py +3 -2
  25. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  26. letta/groups/sleeptime_multi_agent_v3.py +17 -17
  27. letta/groups/supervisor_multi_agent.py +84 -80
  28. letta/helpers/converters.py +3 -0
  29. letta/helpers/message_helper.py +4 -0
  30. letta/helpers/tool_rule_solver.py +92 -5
  31. letta/interfaces/anthropic_streaming_interface.py +409 -0
  32. letta/interfaces/gemini_streaming_interface.py +296 -0
  33. letta/interfaces/openai_streaming_interface.py +752 -1
  34. letta/llm_api/anthropic_client.py +127 -16
  35. letta/llm_api/bedrock_client.py +4 -2
  36. letta/llm_api/deepseek_client.py +4 -1
  37. letta/llm_api/google_vertex_client.py +124 -42
  38. letta/llm_api/groq_client.py +4 -1
  39. letta/llm_api/llm_api_tools.py +11 -4
  40. letta/llm_api/llm_client_base.py +6 -2
  41. letta/llm_api/openai.py +32 -2
  42. letta/llm_api/openai_client.py +423 -18
  43. letta/llm_api/xai_client.py +4 -1
  44. letta/main.py +9 -5
  45. letta/memory.py +1 -0
  46. letta/orm/__init__.py +2 -1
  47. letta/orm/agent.py +10 -0
  48. letta/orm/block.py +7 -16
  49. letta/orm/blocks_agents.py +8 -2
  50. letta/orm/files_agents.py +2 -0
  51. letta/orm/job.py +7 -5
  52. letta/orm/mcp_oauth.py +1 -0
  53. letta/orm/message.py +21 -6
  54. letta/orm/organization.py +2 -0
  55. letta/orm/provider.py +6 -2
  56. letta/orm/run.py +71 -0
  57. letta/orm/run_metrics.py +82 -0
  58. letta/orm/sandbox_config.py +7 -1
  59. letta/orm/sqlalchemy_base.py +0 -306
  60. letta/orm/step.py +6 -5
  61. letta/orm/step_metrics.py +5 -5
  62. letta/otel/tracing.py +28 -3
  63. letta/plugins/defaults.py +4 -4
  64. letta/prompts/system_prompts/__init__.py +2 -0
  65. letta/prompts/system_prompts/letta_v1.py +25 -0
  66. letta/schemas/agent.py +3 -2
  67. letta/schemas/agent_file.py +9 -3
  68. letta/schemas/block.py +23 -10
  69. letta/schemas/enums.py +21 -2
  70. letta/schemas/job.py +17 -4
  71. letta/schemas/letta_message_content.py +71 -2
  72. letta/schemas/letta_stop_reason.py +5 -5
  73. letta/schemas/llm_config.py +53 -3
  74. letta/schemas/memory.py +1 -1
  75. letta/schemas/message.py +564 -117
  76. letta/schemas/openai/responses_request.py +64 -0
  77. letta/schemas/providers/__init__.py +2 -0
  78. letta/schemas/providers/anthropic.py +16 -0
  79. letta/schemas/providers/ollama.py +115 -33
  80. letta/schemas/providers/openrouter.py +52 -0
  81. letta/schemas/providers/vllm.py +2 -1
  82. letta/schemas/run.py +48 -42
  83. letta/schemas/run_metrics.py +21 -0
  84. letta/schemas/step.py +2 -2
  85. letta/schemas/step_metrics.py +1 -1
  86. letta/schemas/tool.py +15 -107
  87. letta/schemas/tool_rule.py +88 -5
  88. letta/serialize_schemas/marshmallow_agent.py +1 -0
  89. letta/server/db.py +79 -408
  90. letta/server/rest_api/app.py +61 -10
  91. letta/server/rest_api/dependencies.py +14 -0
  92. letta/server/rest_api/redis_stream_manager.py +19 -8
  93. letta/server/rest_api/routers/v1/agents.py +364 -292
  94. letta/server/rest_api/routers/v1/blocks.py +14 -20
  95. letta/server/rest_api/routers/v1/identities.py +45 -110
  96. letta/server/rest_api/routers/v1/internal_templates.py +21 -0
  97. letta/server/rest_api/routers/v1/jobs.py +23 -6
  98. letta/server/rest_api/routers/v1/messages.py +1 -1
  99. letta/server/rest_api/routers/v1/runs.py +149 -99
  100. letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
  101. letta/server/rest_api/routers/v1/tools.py +281 -594
  102. letta/server/rest_api/routers/v1/voice.py +1 -1
  103. letta/server/rest_api/streaming_response.py +29 -29
  104. letta/server/rest_api/utils.py +122 -64
  105. letta/server/server.py +160 -887
  106. letta/services/agent_manager.py +236 -919
  107. letta/services/agent_serialization_manager.py +16 -0
  108. letta/services/archive_manager.py +0 -100
  109. letta/services/block_manager.py +211 -168
  110. letta/services/context_window_calculator/token_counter.py +1 -1
  111. letta/services/file_manager.py +1 -1
  112. letta/services/files_agents_manager.py +24 -33
  113. letta/services/group_manager.py +0 -142
  114. letta/services/helpers/agent_manager_helper.py +7 -2
  115. letta/services/helpers/run_manager_helper.py +69 -0
  116. letta/services/job_manager.py +96 -411
  117. letta/services/lettuce/__init__.py +6 -0
  118. letta/services/lettuce/lettuce_client_base.py +86 -0
  119. letta/services/mcp_manager.py +38 -6
  120. letta/services/message_manager.py +165 -362
  121. letta/services/organization_manager.py +0 -36
  122. letta/services/passage_manager.py +0 -345
  123. letta/services/provider_manager.py +0 -80
  124. letta/services/run_manager.py +364 -0
  125. letta/services/sandbox_config_manager.py +0 -234
  126. letta/services/step_manager.py +62 -39
  127. letta/services/summarizer/summarizer.py +9 -7
  128. letta/services/telemetry_manager.py +0 -16
  129. letta/services/tool_executor/builtin_tool_executor.py +35 -0
  130. letta/services/tool_executor/core_tool_executor.py +397 -2
  131. letta/services/tool_executor/files_tool_executor.py +3 -3
  132. letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
  133. letta/services/tool_executor/tool_execution_manager.py +6 -8
  134. letta/services/tool_executor/tool_executor_base.py +3 -3
  135. letta/services/tool_manager.py +85 -339
  136. letta/services/tool_sandbox/base.py +24 -13
  137. letta/services/tool_sandbox/e2b_sandbox.py +16 -1
  138. letta/services/tool_schema_generator.py +123 -0
  139. letta/services/user_manager.py +0 -99
  140. letta/settings.py +20 -4
  141. letta/system.py +5 -1
  142. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +3 -5
  143. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +146 -135
  144. letta/agents/temporal/activities/__init__.py +0 -4
  145. letta/agents/temporal/activities/example_activity.py +0 -7
  146. letta/agents/temporal/activities/prepare_messages.py +0 -10
  147. letta/agents/temporal/temporal_agent_workflow.py +0 -56
  148. letta/agents/temporal/types.py +0 -25
  149. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
  150. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
  151. {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 LettaToolCreateError, LettaToolNameConflictError
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
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
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
- # Combine single name with names list for unified processing (same logic as list_tools)
116
- combined_names = []
117
- if name is not None:
118
- combined_names.append(name)
119
- if names is not None:
120
- combined_names.extend(names)
121
-
122
- # Use None if no names specified, otherwise use the combined list
123
- final_names = combined_names if combined_names else None
124
-
125
- # Helper function to parse tool IDs - supports both repeated params and comma-separated values
126
- def parse_tool_ids(tool_ids_input: Optional[List[str]]) -> Optional[List[str]]:
127
- if tool_ids_input is None:
128
- return None
129
-
130
- # Flatten any comma-separated values
131
- flattened_ids = []
132
- for item in tool_ids_input:
133
- # Split by comma in case user provided comma-separated values
134
- ids_in_item = [id.strip() for id in item.split(",") if id.strip()]
135
- flattened_ids.extend(ids_in_item)
136
-
137
- return flattened_ids if flattened_ids else None
138
-
139
- # Parse tool IDs (same logic as list_tools)
140
- final_tool_ids = parse_tool_ids(tool_ids)
141
-
142
- # Get the count of tools using unified query
143
- return await server.tool_manager.count_tools_async(
144
- actor=actor,
145
- tool_types=tool_types_str,
146
- exclude_tool_types=exclude_tool_types_str,
147
- names=final_names,
148
- tool_ids=final_tool_ids,
149
- search=search,
150
- return_only_letta_tools=return_only_letta_tools,
151
- exclude_letta_tools=exclude_letta_tools,
152
- )
153
- except Exception as e:
154
- raise HTTPException(status_code=500, detail=str(e))
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
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
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
- # Combine single name with names list for unified processing
237
- combined_names = []
238
- if name is not None:
239
- combined_names.append(name)
240
- if names is not None:
241
- combined_names.extend(names)
242
-
243
- # Use None if no names specified, otherwise use the combined list
244
- final_names = combined_names if combined_names else None
245
-
246
- # Helper function to parse tool IDs - supports both repeated params and comma-separated values
247
- def parse_tool_ids(tool_ids_input: Optional[List[str]]) -> Optional[List[str]]:
248
- if tool_ids_input is None:
249
- return None
250
-
251
- # Flatten any comma-separated values
252
- flattened_ids = []
253
- for item in tool_ids_input:
254
- # Split by comma in case user provided comma-separated values
255
- ids_in_item = [id.strip() for id in item.split(",") if id.strip()]
256
- flattened_ids.extend(ids_in_item)
257
-
258
- return flattened_ids if flattened_ids else None
259
-
260
- # Parse tool IDs
261
- final_tool_ids = parse_tool_ids(tool_ids)
262
-
263
- # Get the list of tools using unified query
264
- return await server.tool_manager.list_tools_async(
265
- actor=actor,
266
- before=before,
267
- after=after,
268
- limit=limit,
269
- ascending=(order == "asc"),
270
- tool_types=tool_types_str,
271
- exclude_tool_types=exclude_tool_types_str,
272
- names=final_names,
273
- tool_ids=final_tool_ids,
274
- search=search,
275
- return_only_letta_tools=return_only_letta_tools,
276
- )
277
- except Exception as e:
278
- raise HTTPException(status_code=500, detail=str(e))
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
- try:
291
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
292
- tool = Tool(**request.model_dump(exclude_unset=True))
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
- try:
315
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
316
- tool = await server.tool_manager.create_or_update_tool_async(
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
- try:
342
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
343
- tool = await server.tool_manager.update_tool_by_id_async(tool_id=tool_id, tool_update=request, actor=actor)
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
- try:
380
- return await server.run_tool_from_source(
381
- tool_source=request.source_code,
382
- tool_source_type=request.source_type,
383
- tool_args=request.args,
384
- tool_env_vars=request.env_vars,
385
- tool_name=request.name,
386
- tool_args_json_schema=request.args_json_schema,
387
- tool_json_schema=request.json_schema,
388
- pip_requirements=request.pip_requirements,
389
- actor=actor,
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 Exception as e:
563
- if isinstance(e, ConnectError) or isinstance(e, ConnectionError):
564
- raise HTTPException(
565
- status_code=404,
566
- detail={
567
- "code": "MCPListToolsError",
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 HTTPException(
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
- try:
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.get_user_or_default(user_id=headers.actor_id)
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 HTTPException(
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 HTTPException(
677
- status_code=400, # Bad Request
678
- detail={
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 HTTPException(
689
- status_code=400,
690
- detail={
691
- "code": "MCPToolSchemaInvalid",
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
- try:
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
- if tool_settings.mcp_read_from_config:
727
- # write to config file
728
- return await server.add_mcp_server_to_config(server_config=request, allow_upsert=True)
729
- else:
730
- # log to DB
731
- # Check if stdio servers are disabled
732
- if isinstance(request, StdioServerConfig) and tool_settings.mcp_disable_stdio:
733
- raise HTTPException(
734
- status_code=400,
735
- detail="stdio is not supported in the current environment, please use a self-hosted Letta server in order to add a stdio MCP server",
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
- # Create MCP server and optimistically sync tools
739
- # The mcp_manager will handle encryption of sensitive fields
740
- await server.mcp_manager.create_mcp_server_from_config_with_tools(request, actor=actor)
741
-
742
- # TODO: don't do this in the future (just return MCPServer)
743
- all_servers = await server.mcp_manager.list_mcp_servers(actor=actor)
744
- return [server.to_config() for server in all_servers]
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
- try:
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
- if tool_settings.mcp_read_from_config:
777
- raise HTTPException(status_code=501, detail="Update not implemented for config file mode, config files to be deprecated.")
778
- else:
779
- updated_server = await server.mcp_manager.update_mcp_server_by_name(
780
- mcp_server_name=mcp_server_name, mcp_server_update=request, actor=actor
781
- )
782
- return updated_server.to_config()
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 HTTPException(
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 HTTPException(
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
- try:
982
- if request.source_type == "typescript":
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
- except Exception as e:
992
- raise HTTPException(status_code=400, detail=f"Failed to generate schema: {str(e)}")
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
- raise HTTPException(
1020
- status_code=404,
1021
- detail={
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
- response_data = None
1124
-
1125
- try:
1126
- actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
1127
- llm_config = await server.get_cached_llm_config_async(actor=actor, handle=request.handle or "anthropic/claude-3-5-sonnet-20240620")
1128
- formatted_prompt = (
1129
- f"Generate a python function named {request.tool_name} using the instructions below "
1130
- + (f"based on this starter code: \n\n```\n{request.starter_code}\n```\n\n" if request.starter_code else "\n")
1131
- + (f"Note the following validation errors: \n{' '.join(request.validation_errors)}\n\n" if request.validation_errors else "\n")
1132
- + f"Instructions: {request.prompt}"
1133
- )
1134
- llm_client = LLMClient.create(
1135
- provider_type=llm_config.model_endpoint_type,
1136
- actor=actor,
1137
- )
1138
- assert llm_client is not None
1139
-
1140
- 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."
1141
-
1142
- input_messages = [
1143
- Message(role=MessageRole.system, content=[TextContent(text=get_system_text("memgpt_generate_tool"))]),
1144
- Message(role=MessageRole.assistant, content=[TextContent(text=assistant_message_ack)]),
1145
- Message(role=MessageRole.user, content=[TextContent(text=formatted_prompt)]),
1146
- ]
1147
-
1148
- tool = {
1149
- "name": "generate_tool",
1150
- "description": "This method generates the raw source code for a custom tool that can be attached to and agent for llm invocation.",
1151
- "parameters": {
1152
- "type": "object",
1153
- "properties": {
1154
- "raw_source_code": {"type": "string", "description": "The raw python source code of the custom tool."},
1155
- "sample_args_json": {
1156
- "type": "string",
1157
- "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.",
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
- request_data = llm_client.build_request_data(
1168
- input_messages,
1169
- llm_config,
1170
- tools=[tool],
1171
- )
1172
- response_data = await llm_client.request_async(request_data, llm_config)
1173
- response = llm_client.convert_response_to_chat_completion(response_data, input_messages, llm_config)
1174
- output = json.loads(response.choices[0].message.tool_calls[0].function.arguments)
1175
- pip_requirements = [PipRequirement(name=k, version=v or None) for k, v in json.loads(output["pip_requirements_json"]).items()]
1176
- return GenerateToolOutput(
1177
- tool=Tool(
1178
- name=request.tool_name,
1179
- source_type="python",
1180
- source_code=output["raw_source_code"],
1181
- pip_requirements=pip_requirements,
1182
- ),
1183
- sample_args=json.loads(output["sample_args_json"]),
1184
- response=response.choices[0].message.content,
1185
- )
1186
- except Exception as e:
1187
- logger.error(f"Failed to generate tool: {str(e)}. Raw response: {response_data}")
1188
- raise HTTPException(status_code=500, detail=f"Failed to generate tool: {str(e)}")
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
+ )