letta-nightly 0.11.6.dev20250902104140__py3-none-any.whl → 0.11.7.dev20250904045700__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 (138) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +10 -14
  3. letta/agents/base_agent.py +18 -0
  4. letta/agents/helpers.py +32 -7
  5. letta/agents/letta_agent.py +953 -762
  6. letta/agents/voice_agent.py +1 -1
  7. letta/client/streaming.py +0 -1
  8. letta/constants.py +11 -8
  9. letta/errors.py +9 -0
  10. letta/functions/function_sets/base.py +77 -69
  11. letta/functions/function_sets/builtin.py +41 -22
  12. letta/functions/function_sets/multi_agent.py +1 -2
  13. letta/functions/schema_generator.py +0 -1
  14. letta/helpers/converters.py +8 -3
  15. letta/helpers/datetime_helpers.py +5 -4
  16. letta/helpers/message_helper.py +1 -2
  17. letta/helpers/pinecone_utils.py +0 -1
  18. letta/helpers/tool_rule_solver.py +10 -0
  19. letta/helpers/tpuf_client.py +848 -0
  20. letta/interface.py +8 -8
  21. letta/interfaces/anthropic_streaming_interface.py +7 -0
  22. letta/interfaces/openai_streaming_interface.py +29 -6
  23. letta/llm_api/anthropic_client.py +188 -18
  24. letta/llm_api/azure_client.py +0 -1
  25. letta/llm_api/bedrock_client.py +1 -2
  26. letta/llm_api/deepseek_client.py +319 -5
  27. letta/llm_api/google_vertex_client.py +75 -17
  28. letta/llm_api/groq_client.py +0 -1
  29. letta/llm_api/helpers.py +2 -2
  30. letta/llm_api/llm_api_tools.py +1 -50
  31. letta/llm_api/llm_client.py +6 -8
  32. letta/llm_api/mistral.py +1 -1
  33. letta/llm_api/openai.py +16 -13
  34. letta/llm_api/openai_client.py +31 -16
  35. letta/llm_api/together_client.py +0 -1
  36. letta/llm_api/xai_client.py +0 -1
  37. letta/local_llm/chat_completion_proxy.py +7 -6
  38. letta/local_llm/settings/settings.py +1 -1
  39. letta/orm/__init__.py +1 -0
  40. letta/orm/agent.py +8 -6
  41. letta/orm/archive.py +9 -1
  42. letta/orm/block.py +3 -4
  43. letta/orm/block_history.py +3 -1
  44. letta/orm/group.py +2 -3
  45. letta/orm/identity.py +1 -2
  46. letta/orm/job.py +1 -2
  47. letta/orm/llm_batch_items.py +1 -2
  48. letta/orm/message.py +8 -4
  49. letta/orm/mixins.py +18 -0
  50. letta/orm/organization.py +2 -0
  51. letta/orm/passage.py +8 -1
  52. letta/orm/passage_tag.py +55 -0
  53. letta/orm/sandbox_config.py +1 -3
  54. letta/orm/step.py +1 -2
  55. letta/orm/tool.py +1 -0
  56. letta/otel/resource.py +2 -2
  57. letta/plugins/plugins.py +1 -1
  58. letta/prompts/prompt_generator.py +10 -2
  59. letta/schemas/agent.py +11 -0
  60. letta/schemas/archive.py +4 -0
  61. letta/schemas/block.py +13 -0
  62. letta/schemas/embedding_config.py +0 -1
  63. letta/schemas/enums.py +24 -7
  64. letta/schemas/group.py +12 -0
  65. letta/schemas/letta_message.py +55 -1
  66. letta/schemas/letta_message_content.py +28 -0
  67. letta/schemas/letta_request.py +21 -4
  68. letta/schemas/letta_stop_reason.py +9 -1
  69. letta/schemas/llm_config.py +24 -8
  70. letta/schemas/mcp.py +0 -3
  71. letta/schemas/memory.py +14 -0
  72. letta/schemas/message.py +245 -141
  73. letta/schemas/openai/chat_completion_request.py +2 -1
  74. letta/schemas/passage.py +1 -0
  75. letta/schemas/providers/bedrock.py +1 -1
  76. letta/schemas/providers/openai.py +2 -2
  77. letta/schemas/tool.py +11 -5
  78. letta/schemas/tool_execution_result.py +0 -1
  79. letta/schemas/tool_rule.py +71 -0
  80. letta/serialize_schemas/marshmallow_agent.py +1 -2
  81. letta/server/rest_api/app.py +3 -3
  82. letta/server/rest_api/auth/index.py +0 -1
  83. letta/server/rest_api/interface.py +3 -11
  84. letta/server/rest_api/redis_stream_manager.py +3 -4
  85. letta/server/rest_api/routers/v1/agents.py +143 -84
  86. letta/server/rest_api/routers/v1/blocks.py +1 -1
  87. letta/server/rest_api/routers/v1/folders.py +1 -1
  88. letta/server/rest_api/routers/v1/groups.py +23 -22
  89. letta/server/rest_api/routers/v1/internal_templates.py +68 -0
  90. letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
  91. letta/server/rest_api/routers/v1/sources.py +1 -1
  92. letta/server/rest_api/routers/v1/tools.py +167 -15
  93. letta/server/rest_api/streaming_response.py +4 -3
  94. letta/server/rest_api/utils.py +75 -18
  95. letta/server/server.py +24 -35
  96. letta/services/agent_manager.py +359 -45
  97. letta/services/agent_serialization_manager.py +23 -3
  98. letta/services/archive_manager.py +72 -3
  99. letta/services/block_manager.py +1 -2
  100. letta/services/context_window_calculator/token_counter.py +11 -6
  101. letta/services/file_manager.py +1 -3
  102. letta/services/files_agents_manager.py +2 -4
  103. letta/services/group_manager.py +73 -12
  104. letta/services/helpers/agent_manager_helper.py +5 -5
  105. letta/services/identity_manager.py +8 -3
  106. letta/services/job_manager.py +2 -14
  107. letta/services/llm_batch_manager.py +1 -3
  108. letta/services/mcp/base_client.py +1 -2
  109. letta/services/mcp_manager.py +5 -6
  110. letta/services/message_manager.py +536 -15
  111. letta/services/organization_manager.py +1 -2
  112. letta/services/passage_manager.py +287 -12
  113. letta/services/provider_manager.py +1 -3
  114. letta/services/sandbox_config_manager.py +12 -7
  115. letta/services/source_manager.py +1 -2
  116. letta/services/step_manager.py +0 -1
  117. letta/services/summarizer/summarizer.py +4 -2
  118. letta/services/telemetry_manager.py +1 -3
  119. letta/services/tool_executor/builtin_tool_executor.py +136 -316
  120. letta/services/tool_executor/core_tool_executor.py +231 -74
  121. letta/services/tool_executor/files_tool_executor.py +2 -2
  122. letta/services/tool_executor/mcp_tool_executor.py +0 -1
  123. letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
  124. letta/services/tool_executor/sandbox_tool_executor.py +0 -1
  125. letta/services/tool_executor/tool_execution_sandbox.py +2 -3
  126. letta/services/tool_manager.py +181 -64
  127. letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
  128. letta/services/user_manager.py +1 -2
  129. letta/settings.py +5 -3
  130. letta/streaming_interface.py +3 -3
  131. letta/system.py +1 -1
  132. letta/utils.py +0 -1
  133. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/METADATA +11 -7
  134. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/RECORD +137 -135
  135. letta/llm_api/deepseek.py +0 -303
  136. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/WHEEL +0 -0
  137. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/entry_points.txt +0 -0
  138. {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/licenses/LICENSE +0 -0
@@ -351,7 +351,7 @@ async def list_folder_passages(
351
351
  List all passages associated with a data folder.
352
352
  """
353
353
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
354
- return await server.agent_manager.list_passages_async(
354
+ return await server.agent_manager.query_source_passages_async(
355
355
  actor=actor,
356
356
  source_id=folder_id,
357
357
  after=after,
@@ -17,7 +17,7 @@ router = APIRouter(prefix="/groups", tags=["groups"])
17
17
 
18
18
 
19
19
  @router.get("/", response_model=List[Group], operation_id="list_groups")
20
- def list_groups(
20
+ async def list_groups(
21
21
  server: "SyncServer" = Depends(get_letta_server),
22
22
  actor_id: Optional[str] = Header(None, alias="user_id"),
23
23
  manager_type: Optional[ManagerType] = Query(None, description="Search groups by manager type"),
@@ -29,8 +29,8 @@ def list_groups(
29
29
  """
30
30
  Fetch all multi-agent groups matching query.
31
31
  """
32
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
33
- return server.group_manager.list_groups(
32
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
33
+ return await server.group_manager.list_groups_async(
34
34
  actor=actor,
35
35
  project_id=project_id,
36
36
  manager_type=manager_type,
@@ -41,14 +41,15 @@ def list_groups(
41
41
 
42
42
 
43
43
  @router.get("/count", response_model=int, operation_id="count_groups")
44
- def count_groups(
44
+ async def count_groups(
45
45
  server: SyncServer = Depends(get_letta_server),
46
46
  actor_id: Optional[str] = Header(None, alias="user_id"),
47
47
  ):
48
48
  """
49
49
  Get the count of all groups associated with a given user.
50
50
  """
51
- return server.group_manager.size(actor=server.user_manager.get_user_or_default(user_id=actor_id))
51
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
52
+ return await server.group_manager.size(actor=actor)
52
53
 
53
54
 
54
55
  @router.get("/{group_id}", response_model=Group, operation_id="retrieve_group")
@@ -69,7 +70,7 @@ async def retrieve_group(
69
70
 
70
71
 
71
72
  @router.post("/", response_model=Group, operation_id="create_group")
72
- def create_group(
73
+ async def create_group(
73
74
  group: GroupCreate = Body(...),
74
75
  server: "SyncServer" = Depends(get_letta_server),
75
76
  actor_id: Optional[str] = Header(None, alias="user_id"),
@@ -81,8 +82,8 @@ def create_group(
81
82
  Create a new multi-agent group with the specified configuration.
82
83
  """
83
84
  try:
84
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
85
- return server.group_manager.create_group(group, actor=actor)
85
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
86
+ return await server.group_manager.create_group_async(group, actor=actor)
86
87
  except Exception as e:
87
88
  raise HTTPException(status_code=500, detail=str(e))
88
89
 
@@ -108,7 +109,7 @@ async def modify_group(
108
109
 
109
110
 
110
111
  @router.delete("/{group_id}", response_model=None, operation_id="delete_group")
111
- def delete_group(
112
+ async def delete_group(
112
113
  group_id: str,
113
114
  server: "SyncServer" = Depends(get_letta_server),
114
115
  actor_id: Optional[str] = Header(None, alias="user_id"),
@@ -116,9 +117,9 @@ def delete_group(
116
117
  """
117
118
  Delete a multi-agent group.
118
119
  """
119
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
120
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
120
121
  try:
121
- server.group_manager.delete_group(group_id=group_id, actor=actor)
122
+ await server.group_manager.delete_group_async(group_id=group_id, actor=actor)
122
123
  return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Group id={group_id} successfully deleted"})
123
124
  except NoResultFound:
124
125
  raise HTTPException(status_code=404, detail=f"Group id={group_id} not found for user_id={actor.id}.")
@@ -199,7 +200,7 @@ GroupMessagesResponse = Annotated[
199
200
 
200
201
 
201
202
  @router.patch("/{group_id}/messages/{message_id}", response_model=LettaMessageUnion, operation_id="modify_group_message")
202
- def modify_group_message(
203
+ async def modify_group_message(
203
204
  group_id: str,
204
205
  message_id: str,
205
206
  request: LettaMessageUpdateUnion = Body(...),
@@ -210,12 +211,12 @@ def modify_group_message(
210
211
  Update the details of a message associated with an agent.
211
212
  """
212
213
  # TODO: support modifying tool calls/returns
213
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
214
- return server.message_manager.update_message_by_letta_message(message_id=message_id, letta_message_update=request, actor=actor)
214
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
215
+ return await server.message_manager.update_message_by_letta_message(message_id=message_id, letta_message_update=request, actor=actor)
215
216
 
216
217
 
217
218
  @router.get("/{group_id}/messages", response_model=GroupMessagesResponse, operation_id="list_group_messages")
218
- def list_group_messages(
219
+ async def list_group_messages(
219
220
  group_id: str,
220
221
  server: "SyncServer" = Depends(get_letta_server),
221
222
  after: Optional[str] = Query(None, description="Message after which to retrieve the returned messages."),
@@ -229,10 +230,10 @@ def list_group_messages(
229
230
  """
230
231
  Retrieve message history for an agent.
231
232
  """
232
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
233
- group = server.group_manager.retrieve_group(group_id=group_id, actor=actor)
233
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
234
+ group = await server.group_manager.retrieve_group_async(group_id=group_id, actor=actor)
234
235
  if group.manager_agent_id:
235
- return server.get_agent_recall(
236
+ return await server.get_agent_recall_async(
236
237
  user_id=actor.id,
237
238
  agent_id=group.manager_agent_id,
238
239
  after=after,
@@ -246,7 +247,7 @@ def list_group_messages(
246
247
  assistant_message_tool_kwarg=assistant_message_tool_kwarg,
247
248
  )
248
249
  else:
249
- return server.group_manager.list_group_messages(
250
+ return await server.group_manager.list_group_messages_async(
250
251
  group_id=group_id,
251
252
  after=after,
252
253
  before=before,
@@ -259,7 +260,7 @@ def list_group_messages(
259
260
 
260
261
 
261
262
  @router.patch("/{group_id}/reset-messages", response_model=None, operation_id="reset_group_messages")
262
- def reset_group_messages(
263
+ async def reset_group_messages(
263
264
  group_id: str,
264
265
  server: "SyncServer" = Depends(get_letta_server),
265
266
  actor_id: Optional[str] = Header(None, alias="user_id"),
@@ -267,5 +268,5 @@ def reset_group_messages(
267
268
  """
268
269
  Delete the group messages for all agents that are part of the multi-agent group.
269
270
  """
270
- actor = server.user_manager.get_user_or_default(user_id=actor_id)
271
- server.group_manager.reset_messages(group_id=group_id, actor=actor)
271
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
272
+ await server.group_manager.reset_messages_async(group_id=group_id, actor=actor)
@@ -0,0 +1,68 @@
1
+ from typing import Optional
2
+
3
+ from fastapi import APIRouter, Body, Depends, Header, HTTPException
4
+
5
+ from letta.schemas.agent import AgentState, InternalTemplateAgentCreate
6
+ from letta.schemas.block import Block, InternalTemplateBlockCreate
7
+ from letta.schemas.group import Group, InternalTemplateGroupCreate
8
+ from letta.server.rest_api.utils import get_letta_server
9
+ from letta.server.server import SyncServer
10
+
11
+ router = APIRouter(prefix="/_internal_templates", tags=["_internal_templates"])
12
+
13
+
14
+ @router.post("/groups", response_model=Group, operation_id="create_internal_template_group")
15
+ async def create_group(
16
+ group: InternalTemplateGroupCreate = Body(...),
17
+ server: "SyncServer" = Depends(get_letta_server),
18
+ actor_id: Optional[str] = Header(None, alias="user_id"),
19
+ x_project: Optional[str] = Header(
20
+ None, alias="X-Project", description="The project slug to associate with the group (cloud only)."
21
+ ), # Only handled by next js middleware
22
+ ):
23
+ """
24
+ Create a new multi-agent group with the specified configuration.
25
+ """
26
+ try:
27
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
28
+ return await server.group_manager.create_group_async(group, actor=actor)
29
+ except Exception as e:
30
+ raise HTTPException(status_code=500, detail=str(e))
31
+
32
+
33
+ @router.post("/agents", response_model=AgentState, operation_id="create_internal_template_agent")
34
+ async def create_agent(
35
+ agent: InternalTemplateAgentCreate = Body(...),
36
+ server: "SyncServer" = Depends(get_letta_server),
37
+ actor_id: Optional[str] = Header(None, alias="user_id"),
38
+ x_project: Optional[str] = Header(
39
+ None, alias="X-Project", description="The project slug to associate with the agent (cloud only)."
40
+ ), # Only handled by next js middleware
41
+ ):
42
+ """
43
+ Create a new agent with template-related fields.
44
+ """
45
+ try:
46
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
47
+ return await server.agent_manager.create_agent_async(agent, actor=actor)
48
+ except Exception as e:
49
+ raise HTTPException(status_code=500, detail=str(e))
50
+
51
+
52
+ @router.post("/blocks", response_model=Block, operation_id="create_internal_template_block")
53
+ async def create_block(
54
+ block: InternalTemplateBlockCreate = Body(...),
55
+ server: "SyncServer" = Depends(get_letta_server),
56
+ actor_id: Optional[str] = Header(None, alias="user_id"),
57
+ x_project: Optional[str] = Header(
58
+ None, alias="X-Project", description="The project slug to associate with the block (cloud only)."
59
+ ), # Only handled by next js middleware
60
+ ):
61
+ """
62
+ Create a new block with template-related fields.
63
+ """
64
+ try:
65
+ actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
66
+ return await server.block_manager.create_or_update_block_async(block, actor=actor)
67
+ except Exception as e:
68
+ raise HTTPException(status_code=500, detail=str(e))
@@ -6,11 +6,17 @@ from fastapi import APIRouter, Depends, HTTPException, Query
6
6
 
7
7
  from letta.log import get_logger
8
8
  from letta.schemas.enums import SandboxType
9
- from letta.schemas.environment_variables import SandboxEnvironmentVariable as PydanticEnvVar
10
- from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate, SandboxEnvironmentVariableUpdate
11
- from letta.schemas.sandbox_config import LocalSandboxConfig
12
- from letta.schemas.sandbox_config import SandboxConfig as PydanticSandboxConfig
13
- from letta.schemas.sandbox_config import SandboxConfigCreate, SandboxConfigUpdate
9
+ from letta.schemas.environment_variables import (
10
+ SandboxEnvironmentVariable as PydanticEnvVar,
11
+ SandboxEnvironmentVariableCreate,
12
+ SandboxEnvironmentVariableUpdate,
13
+ )
14
+ from letta.schemas.sandbox_config import (
15
+ LocalSandboxConfig,
16
+ SandboxConfig as PydanticSandboxConfig,
17
+ SandboxConfigCreate,
18
+ SandboxConfigUpdate,
19
+ )
14
20
  from letta.server.rest_api.utils import get_letta_server, get_user_id
15
21
  from letta.server.server import SyncServer
16
22
  from letta.services.helpers.tool_execution_helper import create_venv_for_local_sandbox, install_pip_requirements_for_sandbox
@@ -349,7 +349,7 @@ async def list_source_passages(
349
349
  List all passages associated with a data source.
350
350
  """
351
351
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
352
- return await server.agent_manager.list_passages_async(
352
+ return await server.agent_manager.query_source_passages_async(
353
353
  actor=actor,
354
354
  source_id=source_id,
355
355
  after=after,
@@ -27,7 +27,7 @@ from letta.log import get_logger
27
27
  from letta.orm.errors import UniqueConstraintViolationError
28
28
  from letta.orm.mcp_oauth import OAuthSessionStatus
29
29
  from letta.prompts.gpt_system import get_system_text
30
- from letta.schemas.enums import MessageRole
30
+ from letta.schemas.enums import MessageRole, ToolType
31
31
  from letta.schemas.letta_message import ToolReturnMessage
32
32
  from letta.schemas.letta_message_content import TextContent
33
33
  from letta.schemas.mcp import UpdateSSEMCPServer, UpdateStdioMCPServer, UpdateStreamableHTTPMCPServer
@@ -62,16 +62,94 @@ async def delete_tool(
62
62
 
63
63
  @router.get("/count", response_model=int, operation_id="count_tools")
64
64
  async def count_tools(
65
+ name: Optional[str] = None,
66
+ names: Optional[List[str]] = Query(None, description="Filter by specific tool names"),
67
+ tool_ids: Optional[List[str]] = Query(
68
+ None, description="Filter by specific tool IDs - accepts repeated params or comma-separated values"
69
+ ),
70
+ search: Optional[str] = Query(None, description="Search tool names (case-insensitive partial match)"),
71
+ tool_types: Optional[List[str]] = Query(None, description="Filter by tool type(s) - accepts repeated params or comma-separated values"),
72
+ exclude_tool_types: Optional[List[str]] = Query(
73
+ None, description="Tool type(s) to exclude - accepts repeated params or comma-separated values"
74
+ ),
75
+ return_only_letta_tools: Optional[bool] = Query(False, description="Count only tools with tool_type starting with 'letta_'"),
76
+ exclude_letta_tools: Optional[bool] = Query(False, description="Exclude built-in Letta tools from the count"),
65
77
  server: SyncServer = Depends(get_letta_server),
66
78
  actor_id: Optional[str] = Header(None, alias="user_id"),
67
- include_base_tools: Optional[bool] = Query(False, description="Include built-in Letta tools in the count"),
68
79
  ):
69
80
  """
70
81
  Get a count of all tools available to agents belonging to the org of the user.
71
82
  """
72
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
+
73
113
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
74
- return await server.tool_manager.size_async(actor=actor, include_base_tools=include_base_tools)
114
+
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
+ )
75
153
  except Exception as e:
76
154
  print(f"Error occurred: {e}")
77
155
  raise HTTPException(status_code=500, detail=str(e))
@@ -99,6 +177,16 @@ async def list_tools(
99
177
  after: Optional[str] = None,
100
178
  limit: Optional[int] = 50,
101
179
  name: Optional[str] = None,
180
+ names: Optional[List[str]] = Query(None, description="Filter by specific tool names"),
181
+ tool_ids: Optional[List[str]] = Query(
182
+ None, description="Filter by specific tool IDs - accepts repeated params or comma-separated values"
183
+ ),
184
+ search: Optional[str] = Query(None, description="Search tool names (case-insensitive partial match)"),
185
+ tool_types: Optional[List[str]] = Query(None, description="Filter by tool type(s) - accepts repeated params or comma-separated values"),
186
+ exclude_tool_types: Optional[List[str]] = Query(
187
+ None, description="Tool type(s) to exclude - accepts repeated params or comma-separated values"
188
+ ),
189
+ return_only_letta_tools: Optional[bool] = Query(False, description="Return only tools with tool_type starting with 'letta_'"),
102
190
  server: SyncServer = Depends(get_letta_server),
103
191
  actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
104
192
  ):
@@ -106,13 +194,76 @@ async def list_tools(
106
194
  Get a list of all tools available to agents belonging to the org of the user
107
195
  """
108
196
  try:
197
+ # Helper function to parse tool types - supports both repeated params and comma-separated values
198
+ def parse_tool_types(tool_types_input: Optional[List[str]]) -> Optional[List[str]]:
199
+ if tool_types_input is None:
200
+ return None
201
+
202
+ # Flatten any comma-separated values and validate against ToolType enum
203
+ flattened_types = []
204
+ for item in tool_types_input:
205
+ # Split by comma in case user provided comma-separated values
206
+ types_in_item = [t.strip() for t in item.split(",") if t.strip()]
207
+ flattened_types.extend(types_in_item)
208
+
209
+ # Validate each type against the ToolType enum
210
+ valid_types = []
211
+ valid_values = [tt.value for tt in ToolType]
212
+
213
+ for tool_type in flattened_types:
214
+ if tool_type not in valid_values:
215
+ raise HTTPException(
216
+ status_code=400, detail=f"Invalid tool_type '{tool_type}'. Must be one of: {', '.join(valid_values)}"
217
+ )
218
+ valid_types.append(tool_type)
219
+
220
+ return valid_types if valid_types else None
221
+
222
+ # Parse and validate tool types
223
+ tool_types_str = parse_tool_types(tool_types)
224
+ exclude_tool_types_str = parse_tool_types(exclude_tool_types)
225
+
109
226
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
227
+
228
+ # Combine single name with names list for unified processing
229
+ combined_names = []
110
230
  if name is not None:
111
- tool = await server.tool_manager.get_tool_by_name_async(tool_name=name, actor=actor)
112
- return [tool] if tool else []
231
+ combined_names.append(name)
232
+ if names is not None:
233
+ combined_names.extend(names)
234
+
235
+ # Use None if no names specified, otherwise use the combined list
236
+ final_names = combined_names if combined_names else None
237
+
238
+ # Helper function to parse tool IDs - supports both repeated params and comma-separated values
239
+ def parse_tool_ids(tool_ids_input: Optional[List[str]]) -> Optional[List[str]]:
240
+ if tool_ids_input is None:
241
+ return None
113
242
 
114
- # Get the list of tools
115
- return await server.tool_manager.list_tools_async(actor=actor, after=after, limit=limit)
243
+ # Flatten any comma-separated values
244
+ flattened_ids = []
245
+ for item in tool_ids_input:
246
+ # Split by comma in case user provided comma-separated values
247
+ ids_in_item = [id.strip() for id in item.split(",") if id.strip()]
248
+ flattened_ids.extend(ids_in_item)
249
+
250
+ return flattened_ids if flattened_ids else None
251
+
252
+ # Parse tool IDs
253
+ final_tool_ids = parse_tool_ids(tool_ids)
254
+
255
+ # Get the list of tools using unified query
256
+ return await server.tool_manager.list_tools_async(
257
+ actor=actor,
258
+ after=after,
259
+ limit=limit,
260
+ tool_types=tool_types_str,
261
+ exclude_tool_types=exclude_tool_types_str,
262
+ names=final_names,
263
+ tool_ids=final_tool_ids,
264
+ search=search,
265
+ return_only_letta_tools=return_only_letta_tools,
266
+ )
116
267
  except Exception as e:
117
268
  # Log or print the full exception here for debugging
118
269
  print(f"Error occurred: {e}")
@@ -130,7 +281,7 @@ async def create_tool(
130
281
  """
131
282
  try:
132
283
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
133
- tool = Tool(**request.model_dump())
284
+ tool = Tool(**request.model_dump(exclude_unset=True))
134
285
  return await server.tool_manager.create_tool_async(pydantic_tool=tool, actor=actor)
135
286
  except UniqueConstraintViolationError as e:
136
287
  # Log or print the full exception here for debugging
@@ -162,7 +313,9 @@ async def upsert_tool(
162
313
  """
163
314
  try:
164
315
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
165
- tool = await server.tool_manager.create_or_update_tool_async(pydantic_tool=Tool(**request.model_dump()), actor=actor)
316
+ tool = await server.tool_manager.create_or_update_tool_async(
317
+ pydantic_tool=Tool(**request.model_dump(exclude_unset=True)), actor=actor
318
+ )
166
319
  return tool
167
320
  except UniqueConstraintViolationError as e:
168
321
  # Log the error and raise a conflict exception
@@ -190,18 +343,17 @@ async def modify_tool(
190
343
  """
191
344
  try:
192
345
  actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
193
- return await server.tool_manager.update_tool_by_id_async(tool_id=tool_id, tool_update=request, actor=actor)
346
+ tool = await server.tool_manager.update_tool_by_id_async(tool_id=tool_id, tool_update=request, actor=actor)
347
+ print("FINAL TOOL", tool)
348
+ return tool
194
349
  except LettaToolNameConflictError as e:
195
350
  # HTTP 409 == Conflict
196
- print(f"Tool name conflict during update: {e}")
197
351
  raise HTTPException(status_code=409, detail=str(e))
198
352
  except LettaToolCreateError as e:
199
353
  # HTTP 400 == Bad Request
200
- print(f"Error occurred during tool update: {e}")
201
354
  raise HTTPException(status_code=400, detail=str(e))
202
355
  except Exception as e:
203
356
  # Catch other unexpected errors and raise an internal server error
204
- print(f"Unexpected error occurred: {e}")
205
357
  raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
206
358
 
207
359
 
@@ -748,8 +900,8 @@ async def connect_mcp_server(
748
900
  except ConnectionError:
749
901
  # TODO: jnjpng make this connection error check more specific to the 401 unauthorized error
750
902
  if isinstance(client, AsyncStdioMCPClient):
751
- logger.warning(f"OAuth not supported for stdio")
752
- yield oauth_stream_event(OauthStreamEvent.ERROR, message=f"OAuth not supported for stdio")
903
+ logger.warning("OAuth not supported for stdio")
904
+ yield oauth_stream_event(OauthStreamEvent.ERROR, message="OAuth not supported for stdio")
753
905
  return
754
906
  # Continue to OAuth flow
755
907
  logger.info(f"Attempting OAuth flow for {request}...")
@@ -185,7 +185,7 @@ class StreamingResponseWithStatusCode(StreamingResponse):
185
185
  try:
186
186
  await asyncio.shield(self._protected_stream_response(send))
187
187
  except asyncio.CancelledError:
188
- logger.info(f"Stream response was cancelled, but shielded task should continue")
188
+ logger.info("Stream response was cancelled, but shielded task should continue")
189
189
  except anyio.ClosedResourceError:
190
190
  logger.info("Client disconnected, but shielded task should continue")
191
191
  self._client_connected = False
@@ -296,9 +296,10 @@ class StreamingResponseWithStatusCode(StreamingResponse):
296
296
  raise LettaUnexpectedStreamCancellationError("Stream was terminated due to unexpected cancellation from server")
297
297
 
298
298
  except Exception as exc:
299
- logger.exception("Unhandled Streaming Error")
299
+ logger.exception(f"Unhandled Streaming Error: {str(exc)}")
300
300
  more_body = False
301
- error_resp = {"error": {"message": "Internal Server Error"}}
301
+ # error_resp = {"error": {"message": str(exc)}}
302
+ error_resp = {"error": str(exc), "code": "INTERNAL_SERVER_ERROR"}
302
303
  error_event = f"event: error\ndata: {json.dumps(error_resp)}\n\n".encode(self.charset)
303
304
  logger.debug("response_started:", self.response_started)
304
305
  if not self.response_started: