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.
- letta/__init__.py +1 -1
- letta/agent.py +10 -14
- letta/agents/base_agent.py +18 -0
- letta/agents/helpers.py +32 -7
- letta/agents/letta_agent.py +953 -762
- letta/agents/voice_agent.py +1 -1
- letta/client/streaming.py +0 -1
- letta/constants.py +11 -8
- letta/errors.py +9 -0
- letta/functions/function_sets/base.py +77 -69
- letta/functions/function_sets/builtin.py +41 -22
- letta/functions/function_sets/multi_agent.py +1 -2
- letta/functions/schema_generator.py +0 -1
- letta/helpers/converters.py +8 -3
- letta/helpers/datetime_helpers.py +5 -4
- letta/helpers/message_helper.py +1 -2
- letta/helpers/pinecone_utils.py +0 -1
- letta/helpers/tool_rule_solver.py +10 -0
- letta/helpers/tpuf_client.py +848 -0
- letta/interface.py +8 -8
- letta/interfaces/anthropic_streaming_interface.py +7 -0
- letta/interfaces/openai_streaming_interface.py +29 -6
- letta/llm_api/anthropic_client.py +188 -18
- letta/llm_api/azure_client.py +0 -1
- letta/llm_api/bedrock_client.py +1 -2
- letta/llm_api/deepseek_client.py +319 -5
- letta/llm_api/google_vertex_client.py +75 -17
- letta/llm_api/groq_client.py +0 -1
- letta/llm_api/helpers.py +2 -2
- letta/llm_api/llm_api_tools.py +1 -50
- letta/llm_api/llm_client.py +6 -8
- letta/llm_api/mistral.py +1 -1
- letta/llm_api/openai.py +16 -13
- letta/llm_api/openai_client.py +31 -16
- letta/llm_api/together_client.py +0 -1
- letta/llm_api/xai_client.py +0 -1
- letta/local_llm/chat_completion_proxy.py +7 -6
- letta/local_llm/settings/settings.py +1 -1
- letta/orm/__init__.py +1 -0
- letta/orm/agent.py +8 -6
- letta/orm/archive.py +9 -1
- letta/orm/block.py +3 -4
- letta/orm/block_history.py +3 -1
- letta/orm/group.py +2 -3
- letta/orm/identity.py +1 -2
- letta/orm/job.py +1 -2
- letta/orm/llm_batch_items.py +1 -2
- letta/orm/message.py +8 -4
- letta/orm/mixins.py +18 -0
- letta/orm/organization.py +2 -0
- letta/orm/passage.py +8 -1
- letta/orm/passage_tag.py +55 -0
- letta/orm/sandbox_config.py +1 -3
- letta/orm/step.py +1 -2
- letta/orm/tool.py +1 -0
- letta/otel/resource.py +2 -2
- letta/plugins/plugins.py +1 -1
- letta/prompts/prompt_generator.py +10 -2
- letta/schemas/agent.py +11 -0
- letta/schemas/archive.py +4 -0
- letta/schemas/block.py +13 -0
- letta/schemas/embedding_config.py +0 -1
- letta/schemas/enums.py +24 -7
- letta/schemas/group.py +12 -0
- letta/schemas/letta_message.py +55 -1
- letta/schemas/letta_message_content.py +28 -0
- letta/schemas/letta_request.py +21 -4
- letta/schemas/letta_stop_reason.py +9 -1
- letta/schemas/llm_config.py +24 -8
- letta/schemas/mcp.py +0 -3
- letta/schemas/memory.py +14 -0
- letta/schemas/message.py +245 -141
- letta/schemas/openai/chat_completion_request.py +2 -1
- letta/schemas/passage.py +1 -0
- letta/schemas/providers/bedrock.py +1 -1
- letta/schemas/providers/openai.py +2 -2
- letta/schemas/tool.py +11 -5
- letta/schemas/tool_execution_result.py +0 -1
- letta/schemas/tool_rule.py +71 -0
- letta/serialize_schemas/marshmallow_agent.py +1 -2
- letta/server/rest_api/app.py +3 -3
- letta/server/rest_api/auth/index.py +0 -1
- letta/server/rest_api/interface.py +3 -11
- letta/server/rest_api/redis_stream_manager.py +3 -4
- letta/server/rest_api/routers/v1/agents.py +143 -84
- letta/server/rest_api/routers/v1/blocks.py +1 -1
- letta/server/rest_api/routers/v1/folders.py +1 -1
- letta/server/rest_api/routers/v1/groups.py +23 -22
- letta/server/rest_api/routers/v1/internal_templates.py +68 -0
- letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
- letta/server/rest_api/routers/v1/sources.py +1 -1
- letta/server/rest_api/routers/v1/tools.py +167 -15
- letta/server/rest_api/streaming_response.py +4 -3
- letta/server/rest_api/utils.py +75 -18
- letta/server/server.py +24 -35
- letta/services/agent_manager.py +359 -45
- letta/services/agent_serialization_manager.py +23 -3
- letta/services/archive_manager.py +72 -3
- letta/services/block_manager.py +1 -2
- letta/services/context_window_calculator/token_counter.py +11 -6
- letta/services/file_manager.py +1 -3
- letta/services/files_agents_manager.py +2 -4
- letta/services/group_manager.py +73 -12
- letta/services/helpers/agent_manager_helper.py +5 -5
- letta/services/identity_manager.py +8 -3
- letta/services/job_manager.py +2 -14
- letta/services/llm_batch_manager.py +1 -3
- letta/services/mcp/base_client.py +1 -2
- letta/services/mcp_manager.py +5 -6
- letta/services/message_manager.py +536 -15
- letta/services/organization_manager.py +1 -2
- letta/services/passage_manager.py +287 -12
- letta/services/provider_manager.py +1 -3
- letta/services/sandbox_config_manager.py +12 -7
- letta/services/source_manager.py +1 -2
- letta/services/step_manager.py +0 -1
- letta/services/summarizer/summarizer.py +4 -2
- letta/services/telemetry_manager.py +1 -3
- letta/services/tool_executor/builtin_tool_executor.py +136 -316
- letta/services/tool_executor/core_tool_executor.py +231 -74
- letta/services/tool_executor/files_tool_executor.py +2 -2
- letta/services/tool_executor/mcp_tool_executor.py +0 -1
- letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
- letta/services/tool_executor/sandbox_tool_executor.py +0 -1
- letta/services/tool_executor/tool_execution_sandbox.py +2 -3
- letta/services/tool_manager.py +181 -64
- letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
- letta/services/user_manager.py +1 -2
- letta/settings.py +5 -3
- letta/streaming_interface.py +3 -3
- letta/system.py +1 -1
- letta/utils.py +0 -1
- {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/METADATA +11 -7
- {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/RECORD +137 -135
- letta/llm_api/deepseek.py +0 -303
- {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.6.dev20250902104140.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/entry_points.txt +0 -0
- {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.
|
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.
|
33
|
-
return server.group_manager.
|
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
|
-
|
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.
|
85
|
-
return server.group_manager.
|
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.
|
120
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
120
121
|
try:
|
121
|
-
server.group_manager.
|
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.
|
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.
|
233
|
-
group = server.group_manager.
|
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.
|
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.
|
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.
|
271
|
-
server.group_manager.
|
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
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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.
|
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
|
-
|
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
|
-
|
112
|
-
|
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
|
-
|
115
|
-
|
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(
|
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
|
-
|
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(
|
752
|
-
yield oauth_stream_event(OauthStreamEvent.ERROR, message=
|
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(
|
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":
|
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:
|