letta-nightly 0.7.30.dev20250603104343__py3-none-any.whl → 0.8.0.dev20250604104349__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 +7 -1
- letta/agent.py +14 -7
- letta/agents/base_agent.py +1 -0
- letta/agents/ephemeral_summary_agent.py +104 -0
- letta/agents/helpers.py +35 -3
- letta/agents/letta_agent.py +492 -176
- letta/agents/letta_agent_batch.py +22 -16
- letta/agents/prompts/summary_system_prompt.txt +62 -0
- letta/agents/voice_agent.py +22 -7
- letta/agents/voice_sleeptime_agent.py +13 -8
- letta/constants.py +33 -1
- letta/data_sources/connectors.py +52 -36
- letta/errors.py +4 -0
- letta/functions/ast_parsers.py +13 -30
- letta/functions/function_sets/base.py +3 -1
- letta/functions/functions.py +2 -0
- letta/functions/mcp_client/base_client.py +151 -97
- letta/functions/mcp_client/sse_client.py +49 -31
- letta/functions/mcp_client/stdio_client.py +107 -106
- letta/functions/schema_generator.py +22 -22
- letta/groups/helpers.py +3 -4
- letta/groups/sleeptime_multi_agent.py +4 -4
- letta/groups/sleeptime_multi_agent_v2.py +22 -0
- letta/helpers/composio_helpers.py +16 -0
- letta/helpers/converters.py +20 -0
- letta/helpers/datetime_helpers.py +1 -6
- letta/helpers/tool_rule_solver.py +2 -1
- letta/interfaces/anthropic_streaming_interface.py +17 -2
- letta/interfaces/openai_chat_completions_streaming_interface.py +1 -0
- letta/interfaces/openai_streaming_interface.py +18 -2
- letta/llm_api/anthropic_client.py +24 -3
- letta/llm_api/google_ai_client.py +0 -15
- letta/llm_api/google_vertex_client.py +6 -5
- letta/llm_api/llm_client_base.py +15 -0
- letta/llm_api/openai.py +2 -2
- letta/llm_api/openai_client.py +60 -8
- letta/orm/__init__.py +2 -0
- letta/orm/agent.py +45 -43
- letta/orm/base.py +0 -2
- letta/orm/block.py +1 -0
- letta/orm/custom_columns.py +13 -0
- letta/orm/enums.py +5 -0
- letta/orm/file.py +3 -1
- letta/orm/files_agents.py +68 -0
- letta/orm/mcp_server.py +48 -0
- letta/orm/message.py +1 -0
- letta/orm/organization.py +11 -2
- letta/orm/passage.py +25 -10
- letta/orm/sandbox_config.py +5 -2
- letta/orm/sqlalchemy_base.py +171 -110
- letta/prompts/system/memgpt_base.txt +6 -1
- letta/prompts/system/memgpt_v2_chat.txt +57 -0
- letta/prompts/system/sleeptime.txt +2 -0
- letta/prompts/system/sleeptime_v2.txt +28 -0
- letta/schemas/agent.py +87 -20
- letta/schemas/block.py +7 -1
- letta/schemas/file.py +57 -0
- letta/schemas/mcp.py +74 -0
- letta/schemas/memory.py +5 -2
- letta/schemas/message.py +9 -0
- letta/schemas/openai/openai.py +0 -6
- letta/schemas/providers.py +33 -4
- letta/schemas/tool.py +26 -21
- letta/schemas/tool_execution_result.py +5 -0
- letta/server/db.py +23 -8
- letta/server/rest_api/app.py +73 -56
- letta/server/rest_api/interface.py +4 -4
- letta/server/rest_api/routers/v1/agents.py +132 -47
- letta/server/rest_api/routers/v1/blocks.py +3 -2
- letta/server/rest_api/routers/v1/embeddings.py +3 -3
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/jobs.py +14 -17
- letta/server/rest_api/routers/v1/organizations.py +10 -10
- letta/server/rest_api/routers/v1/providers.py +12 -10
- letta/server/rest_api/routers/v1/runs.py +3 -3
- letta/server/rest_api/routers/v1/sandbox_configs.py +12 -12
- letta/server/rest_api/routers/v1/sources.py +108 -43
- letta/server/rest_api/routers/v1/steps.py +8 -6
- letta/server/rest_api/routers/v1/tools.py +134 -95
- letta/server/rest_api/utils.py +12 -1
- letta/server/server.py +272 -73
- letta/services/agent_manager.py +246 -313
- letta/services/block_manager.py +30 -9
- letta/services/context_window_calculator/__init__.py +0 -0
- letta/services/context_window_calculator/context_window_calculator.py +150 -0
- letta/services/context_window_calculator/token_counter.py +82 -0
- letta/services/file_processor/__init__.py +0 -0
- letta/services/file_processor/chunker/__init__.py +0 -0
- letta/services/file_processor/chunker/llama_index_chunker.py +29 -0
- letta/services/file_processor/embedder/__init__.py +0 -0
- letta/services/file_processor/embedder/openai_embedder.py +84 -0
- letta/services/file_processor/file_processor.py +123 -0
- letta/services/file_processor/parser/__init__.py +0 -0
- letta/services/file_processor/parser/base_parser.py +9 -0
- letta/services/file_processor/parser/mistral_parser.py +54 -0
- letta/services/file_processor/types.py +0 -0
- letta/services/files_agents_manager.py +184 -0
- letta/services/group_manager.py +118 -0
- letta/services/helpers/agent_manager_helper.py +76 -21
- letta/services/helpers/tool_execution_helper.py +3 -0
- letta/services/helpers/tool_parser_helper.py +100 -0
- letta/services/identity_manager.py +44 -42
- letta/services/job_manager.py +21 -10
- letta/services/mcp/base_client.py +5 -2
- letta/services/mcp/sse_client.py +3 -5
- letta/services/mcp/stdio_client.py +3 -5
- letta/services/mcp_manager.py +281 -0
- letta/services/message_manager.py +40 -26
- letta/services/organization_manager.py +55 -19
- letta/services/passage_manager.py +211 -13
- letta/services/provider_manager.py +48 -2
- letta/services/sandbox_config_manager.py +105 -0
- letta/services/source_manager.py +4 -5
- letta/services/step_manager.py +9 -6
- letta/services/summarizer/summarizer.py +50 -23
- letta/services/telemetry_manager.py +7 -0
- letta/services/tool_executor/tool_execution_manager.py +11 -52
- letta/services/tool_executor/tool_execution_sandbox.py +4 -34
- letta/services/tool_executor/tool_executor.py +107 -105
- letta/services/tool_manager.py +56 -17
- letta/services/tool_sandbox/base.py +39 -92
- letta/services/tool_sandbox/e2b_sandbox.py +16 -11
- letta/services/tool_sandbox/local_sandbox.py +51 -23
- letta/services/user_manager.py +36 -3
- letta/settings.py +10 -3
- letta/templates/__init__.py +0 -0
- letta/templates/sandbox_code_file.py.j2 +47 -0
- letta/templates/template_helper.py +16 -0
- letta/tracing.py +30 -1
- letta/types/__init__.py +7 -0
- letta/utils.py +25 -1
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/METADATA +7 -2
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/RECORD +136 -110
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604104349.dist-info}/entry_points.txt +0 -0
@@ -12,7 +12,7 @@ router = APIRouter(prefix="/steps", tags=["steps"])
|
|
12
12
|
|
13
13
|
|
14
14
|
@router.get("/", response_model=List[Step], operation_id="list_steps")
|
15
|
-
def list_steps(
|
15
|
+
async def list_steps(
|
16
16
|
before: Optional[str] = Query(None, description="Return steps before this step ID"),
|
17
17
|
after: Optional[str] = Query(None, description="Return steps after this step ID"),
|
18
18
|
limit: Optional[int] = Query(50, description="Maximum number of steps to return"),
|
@@ -21,6 +21,7 @@ def list_steps(
|
|
21
21
|
end_date: Optional[str] = Query(None, description='Return steps before this ISO datetime (e.g. "2025-01-29T15:01:19-08:00")'),
|
22
22
|
model: Optional[str] = Query(None, description="Filter by the name of the model used for the step"),
|
23
23
|
agent_id: Optional[str] = Query(None, description="Filter by the ID of the agent that performed the step"),
|
24
|
+
trace_ids: Optional[list[str]] = Query(None, description="Filter by trace ids returned by the server"),
|
24
25
|
server: SyncServer = Depends(get_letta_server),
|
25
26
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
26
27
|
):
|
@@ -28,13 +29,13 @@ def list_steps(
|
|
28
29
|
List steps with optional pagination and date filters.
|
29
30
|
Dates should be provided in ISO 8601 format (e.g. 2025-01-29T15:01:19-08:00)
|
30
31
|
"""
|
31
|
-
actor = server.user_manager.
|
32
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
32
33
|
|
33
34
|
# Convert ISO strings to datetime objects if provided
|
34
35
|
start_dt = datetime.fromisoformat(start_date) if start_date else None
|
35
36
|
end_dt = datetime.fromisoformat(end_date) if end_date else None
|
36
37
|
|
37
|
-
return server.step_manager.
|
38
|
+
return await server.step_manager.list_steps_async(
|
38
39
|
actor=actor,
|
39
40
|
before=before,
|
40
41
|
after=after,
|
@@ -44,11 +45,12 @@ def list_steps(
|
|
44
45
|
order=order,
|
45
46
|
model=model,
|
46
47
|
agent_id=agent_id,
|
48
|
+
trace_ids=trace_ids,
|
47
49
|
)
|
48
50
|
|
49
51
|
|
50
52
|
@router.get("/{step_id}", response_model=Step, operation_id="retrieve_step")
|
51
|
-
def retrieve_step(
|
53
|
+
async def retrieve_step(
|
52
54
|
step_id: str,
|
53
55
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
54
56
|
server: SyncServer = Depends(get_letta_server),
|
@@ -57,8 +59,8 @@ def retrieve_step(
|
|
57
59
|
Get a step by ID.
|
58
60
|
"""
|
59
61
|
try:
|
60
|
-
actor = server.user_manager.
|
61
|
-
return server.step_manager.
|
62
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
63
|
+
return await server.step_manager.get_step_async(step_id=step_id, actor=actor)
|
62
64
|
except NoResultFound:
|
63
65
|
raise HTTPException(status_code=404, detail="Step not found")
|
64
66
|
|
@@ -21,6 +21,7 @@ from letta.schemas.letta_message import ToolReturnMessage
|
|
21
21
|
from letta.schemas.tool import Tool, ToolCreate, ToolRunFromSource, ToolUpdate
|
22
22
|
from letta.server.rest_api.utils import get_letta_server
|
23
23
|
from letta.server.server import SyncServer
|
24
|
+
from letta.settings import tool_settings
|
24
25
|
|
25
26
|
router = APIRouter(prefix="/tools", tags=["tools"])
|
26
27
|
|
@@ -28,7 +29,7 @@ logger = get_logger(__name__)
|
|
28
29
|
|
29
30
|
|
30
31
|
@router.delete("/{tool_id}", operation_id="delete_tool")
|
31
|
-
def delete_tool(
|
32
|
+
async def delete_tool(
|
32
33
|
tool_id: str,
|
33
34
|
server: SyncServer = Depends(get_letta_server),
|
34
35
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
@@ -36,12 +37,12 @@ def delete_tool(
|
|
36
37
|
"""
|
37
38
|
Delete a tool by name
|
38
39
|
"""
|
39
|
-
actor = server.user_manager.
|
40
|
-
server.tool_manager.
|
40
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
41
|
+
await server.tool_manager.delete_tool_by_id_async(tool_id=tool_id, actor=actor)
|
41
42
|
|
42
43
|
|
43
44
|
@router.get("/count", response_model=int, operation_id="count_tools")
|
44
|
-
def count_tools(
|
45
|
+
async def count_tools(
|
45
46
|
server: SyncServer = Depends(get_letta_server),
|
46
47
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
47
48
|
include_base_tools: Optional[bool] = Query(False, description="Include built-in Letta tools in the count"),
|
@@ -50,9 +51,8 @@ def count_tools(
|
|
50
51
|
Get a count of all tools available to agents belonging to the org of the user.
|
51
52
|
"""
|
52
53
|
try:
|
53
|
-
|
54
|
-
|
55
|
-
)
|
54
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
55
|
+
return await server.tool_manager.size_async(actor=actor, include_base_tools=include_base_tools)
|
56
56
|
except Exception as e:
|
57
57
|
print(f"Error occurred: {e}")
|
58
58
|
raise HTTPException(status_code=500, detail=str(e))
|
@@ -114,7 +114,7 @@ def count_tools(
|
|
114
114
|
|
115
115
|
|
116
116
|
@router.post("/", response_model=Tool, operation_id="create_tool")
|
117
|
-
def create_tool(
|
117
|
+
async def create_tool(
|
118
118
|
request: ToolCreate = Body(...),
|
119
119
|
server: SyncServer = Depends(get_letta_server),
|
120
120
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
@@ -123,9 +123,9 @@ def create_tool(
|
|
123
123
|
Create a new tool
|
124
124
|
"""
|
125
125
|
try:
|
126
|
-
actor = server.user_manager.
|
126
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
127
127
|
tool = Tool(**request.model_dump())
|
128
|
-
return server.tool_manager.
|
128
|
+
return await server.tool_manager.create_tool_async(pydantic_tool=tool, actor=actor)
|
129
129
|
except UniqueConstraintViolationError as e:
|
130
130
|
# Log or print the full exception here for debugging
|
131
131
|
print(f"Error occurred: {e}")
|
@@ -146,7 +146,7 @@ def create_tool(
|
|
146
146
|
|
147
147
|
|
148
148
|
@router.put("/", response_model=Tool, operation_id="upsert_tool")
|
149
|
-
def upsert_tool(
|
149
|
+
async def upsert_tool(
|
150
150
|
request: ToolCreate = Body(...),
|
151
151
|
server: SyncServer = Depends(get_letta_server),
|
152
152
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
@@ -155,8 +155,8 @@ def upsert_tool(
|
|
155
155
|
Create or update a tool
|
156
156
|
"""
|
157
157
|
try:
|
158
|
-
actor = server.user_manager.
|
159
|
-
tool = server.tool_manager.
|
158
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
159
|
+
tool = await server.tool_manager.create_or_update_tool_async(pydantic_tool=Tool(**request.model_dump()), actor=actor)
|
160
160
|
return tool
|
161
161
|
except UniqueConstraintViolationError as e:
|
162
162
|
# Log the error and raise a conflict exception
|
@@ -173,7 +173,7 @@ def upsert_tool(
|
|
173
173
|
|
174
174
|
|
175
175
|
@router.patch("/{tool_id}", response_model=Tool, operation_id="modify_tool")
|
176
|
-
def modify_tool(
|
176
|
+
async def modify_tool(
|
177
177
|
tool_id: str,
|
178
178
|
request: ToolUpdate = Body(...),
|
179
179
|
server: SyncServer = Depends(get_letta_server),
|
@@ -183,8 +183,8 @@ def modify_tool(
|
|
183
183
|
Update an existing tool
|
184
184
|
"""
|
185
185
|
try:
|
186
|
-
actor = server.user_manager.
|
187
|
-
return server.tool_manager.
|
186
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
187
|
+
return await server.tool_manager.update_tool_by_id_async(tool_id=tool_id, tool_update=request, actor=actor)
|
188
188
|
except LettaToolCreateError as e:
|
189
189
|
# HTTP 400 == Bad Request
|
190
190
|
print(f"Error occurred during tool update: {e}")
|
@@ -208,7 +208,7 @@ async def upsert_base_tools(
|
|
208
208
|
|
209
209
|
|
210
210
|
@router.post("/run", response_model=ToolReturnMessage, operation_id="run_tool_from_source")
|
211
|
-
def run_tool_from_source(
|
211
|
+
async def run_tool_from_source(
|
212
212
|
server: SyncServer = Depends(get_letta_server),
|
213
213
|
request: ToolRunFromSource = Body(...),
|
214
214
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
@@ -216,10 +216,10 @@ def run_tool_from_source(
|
|
216
216
|
"""
|
217
217
|
Attempt to build a tool from source, then run it on the provided arguments
|
218
218
|
"""
|
219
|
-
actor = server.user_manager.
|
219
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
220
220
|
|
221
221
|
try:
|
222
|
-
return server.run_tool_from_source(
|
222
|
+
return await server.run_tool_from_source(
|
223
223
|
tool_source=request.source_code,
|
224
224
|
tool_source_type=request.source_type,
|
225
225
|
tool_args=request.args,
|
@@ -280,7 +280,7 @@ def list_composio_actions_by_app(
|
|
280
280
|
|
281
281
|
|
282
282
|
@router.post("/composio/{composio_action_name}", response_model=Tool, operation_id="add_composio_tool")
|
283
|
-
def add_composio_tool(
|
283
|
+
async def add_composio_tool(
|
284
284
|
composio_action_name: str,
|
285
285
|
server: SyncServer = Depends(get_letta_server),
|
286
286
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
@@ -288,11 +288,11 @@ def add_composio_tool(
|
|
288
288
|
"""
|
289
289
|
Add a new Composio tool by action name (Composio refers to each tool as an `Action`)
|
290
290
|
"""
|
291
|
-
actor = server.user_manager.
|
291
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
292
292
|
|
293
293
|
try:
|
294
294
|
tool_create = ToolCreate.from_composio(action_name=composio_action_name)
|
295
|
-
return server.tool_manager.
|
295
|
+
return await server.tool_manager.create_or_update_composio_tool_async(tool_create=tool_create, actor=actor)
|
296
296
|
except ConnectedAccountNotFoundError as e:
|
297
297
|
raise HTTPException(
|
298
298
|
status_code=400, # Bad Request
|
@@ -369,18 +369,22 @@ def add_composio_tool(
|
|
369
369
|
|
370
370
|
# Specific routes for MCP
|
371
371
|
@router.get("/mcp/servers", response_model=dict[str, Union[SSEServerConfig, StdioServerConfig]], operation_id="list_mcp_servers")
|
372
|
-
def list_mcp_servers(server: SyncServer = Depends(get_letta_server), user_id: Optional[str] = Header(None, alias="user_id")):
|
372
|
+
async def list_mcp_servers(server: SyncServer = Depends(get_letta_server), user_id: Optional[str] = Header(None, alias="user_id")):
|
373
373
|
"""
|
374
374
|
Get a list of all configured MCP servers
|
375
375
|
"""
|
376
|
-
|
377
|
-
|
376
|
+
if tool_settings.mcp_read_from_config:
|
377
|
+
return server.get_mcp_servers()
|
378
|
+
else:
|
379
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=user_id)
|
380
|
+
mcp_servers = await server.mcp_manager.list_mcp_servers(actor=actor)
|
381
|
+
return {server.server_name: server.to_config() for server in mcp_servers}
|
378
382
|
|
379
383
|
|
380
384
|
# NOTE: async because the MCP client/session calls are async
|
381
385
|
# TODO: should we make the return type MCPTool, not Tool (since we don't have ID)?
|
382
386
|
@router.get("/mcp/servers/{mcp_server_name}/tools", response_model=List[MCPTool], operation_id="list_mcp_tools_by_server")
|
383
|
-
def list_mcp_tools_by_server(
|
387
|
+
async def list_mcp_tools_by_server(
|
384
388
|
mcp_server_name: str,
|
385
389
|
server: SyncServer = Depends(get_letta_server),
|
386
390
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
@@ -388,32 +392,36 @@ def list_mcp_tools_by_server(
|
|
388
392
|
"""
|
389
393
|
Get a list of all tools for a specific MCP server
|
390
394
|
"""
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
395
|
+
if tool_settings.mcp_read_from_config:
|
396
|
+
try:
|
397
|
+
return await server.get_tools_from_mcp_server(mcp_server_name=mcp_server_name)
|
398
|
+
except ValueError as e:
|
399
|
+
# ValueError means that the MCP server name doesn't exist
|
400
|
+
raise HTTPException(
|
401
|
+
status_code=400, # Bad Request
|
402
|
+
detail={
|
403
|
+
"code": "MCPServerNotFoundError",
|
404
|
+
"message": str(e),
|
405
|
+
"mcp_server_name": mcp_server_name,
|
406
|
+
},
|
407
|
+
)
|
408
|
+
except MCPTimeoutError as e:
|
409
|
+
raise HTTPException(
|
410
|
+
status_code=408, # Timeout
|
411
|
+
detail={
|
412
|
+
"code": "MCPTimeoutError",
|
413
|
+
"message": str(e),
|
414
|
+
"mcp_server_name": mcp_server_name,
|
415
|
+
},
|
416
|
+
)
|
417
|
+
else:
|
418
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
419
|
+
mcp_tools = await server.mcp_manager.list_mcp_server_tools(mcp_server_name=mcp_server_name, actor=actor)
|
420
|
+
return mcp_tools
|
413
421
|
|
414
422
|
|
415
423
|
@router.post("/mcp/servers/{mcp_server_name}/{mcp_tool_name}", response_model=Tool, operation_id="add_mcp_tool")
|
416
|
-
def add_mcp_tool(
|
424
|
+
async def add_mcp_tool(
|
417
425
|
mcp_server_name: str,
|
418
426
|
mcp_tool_name: str,
|
419
427
|
server: SyncServer = Depends(get_letta_server),
|
@@ -424,50 +432,55 @@ def add_mcp_tool(
|
|
424
432
|
"""
|
425
433
|
actor = server.user_manager.get_user_or_default(user_id=actor_id)
|
426
434
|
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
435
|
+
if tool_settings.mcp_read_from_config:
|
436
|
+
|
437
|
+
try:
|
438
|
+
available_tools = await server.get_tools_from_mcp_server(mcp_server_name=mcp_server_name)
|
439
|
+
except ValueError as e:
|
440
|
+
# ValueError means that the MCP server name doesn't exist
|
441
|
+
raise HTTPException(
|
442
|
+
status_code=400, # Bad Request
|
443
|
+
detail={
|
444
|
+
"code": "MCPServerNotFoundError",
|
445
|
+
"message": str(e),
|
446
|
+
"mcp_server_name": mcp_server_name,
|
447
|
+
},
|
448
|
+
)
|
449
|
+
except MCPTimeoutError as e:
|
450
|
+
raise HTTPException(
|
451
|
+
status_code=408, # Timeout
|
452
|
+
detail={
|
453
|
+
"code": "MCPTimeoutError",
|
454
|
+
"message": str(e),
|
455
|
+
"mcp_server_name": mcp_server_name,
|
456
|
+
},
|
457
|
+
)
|
458
|
+
|
459
|
+
# See if the tool is in the available list
|
460
|
+
mcp_tool = None
|
461
|
+
for tool in available_tools:
|
462
|
+
if tool.name == mcp_tool_name:
|
463
|
+
mcp_tool = tool
|
464
|
+
break
|
465
|
+
if not mcp_tool:
|
466
|
+
raise HTTPException(
|
467
|
+
status_code=400, # Bad Request
|
468
|
+
detail={
|
469
|
+
"code": "MCPToolNotFoundError",
|
470
|
+
"message": f"Tool {mcp_tool_name} not found in MCP server {mcp_server_name} - available tools: {', '.join([tool.name for tool in available_tools])}",
|
471
|
+
"mcp_tool_name": mcp_tool_name,
|
472
|
+
},
|
473
|
+
)
|
474
|
+
|
475
|
+
tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=mcp_tool)
|
476
|
+
return await server.tool_manager.create_mcp_tool_async(tool_create=tool_create, mcp_server_name=mcp_server_name, actor=actor)
|
477
|
+
|
478
|
+
else:
|
479
|
+
return await server.mcp_manager.add_tool_from_mcp_server(mcp_server_name=mcp_server_name, mcp_tool_name=mcp_tool_name, actor=actor)
|
467
480
|
|
468
481
|
|
469
482
|
@router.put("/mcp/servers", response_model=List[Union[StdioServerConfig, SSEServerConfig]], operation_id="add_mcp_server")
|
470
|
-
def add_mcp_server_to_config(
|
483
|
+
async def add_mcp_server_to_config(
|
471
484
|
request: Union[StdioServerConfig, SSEServerConfig] = Body(...),
|
472
485
|
server: SyncServer = Depends(get_letta_server),
|
473
486
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
@@ -475,14 +488,34 @@ def add_mcp_server_to_config(
|
|
475
488
|
"""
|
476
489
|
Add a new MCP server to the Letta MCP server config
|
477
490
|
"""
|
478
|
-
|
479
|
-
|
491
|
+
|
492
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
493
|
+
|
494
|
+
if tool_settings.mcp_read_from_config:
|
495
|
+
# write to config file
|
496
|
+
return await server.add_mcp_server_to_config(server_config=request, allow_upsert=True)
|
497
|
+
else:
|
498
|
+
# log to DB
|
499
|
+
from letta.schemas.mcp import MCPServer
|
500
|
+
|
501
|
+
if isinstance(request, StdioServerConfig):
|
502
|
+
mapped_request = MCPServer(server_name=request.server_name, server_type=request.type, stdio_config=request)
|
503
|
+
# don't allow stdio servers
|
504
|
+
raise HTTPException(status_code=400, detail="StdioServerConfig is not supported")
|
505
|
+
elif isinstance(request, SSEServerConfig):
|
506
|
+
mapped_request = MCPServer(server_name=request.server_name, server_type=request.type, server_url=request.server_url)
|
507
|
+
# TODO: add HTTP streaming
|
508
|
+
mcp_server = await server.mcp_manager.create_or_update_mcp_server(mapped_request, actor=actor)
|
509
|
+
|
510
|
+
# TODO: don't do this in the future (just return MCPServer)
|
511
|
+
all_servers = await server.mcp_manager.list_mcp_servers(actor=actor)
|
512
|
+
return [server.to_config() for server in all_servers]
|
480
513
|
|
481
514
|
|
482
515
|
@router.delete(
|
483
516
|
"/mcp/servers/{mcp_server_name}", response_model=List[Union[StdioServerConfig, SSEServerConfig]], operation_id="delete_mcp_server"
|
484
517
|
)
|
485
|
-
def delete_mcp_server_from_config(
|
518
|
+
async def delete_mcp_server_from_config(
|
486
519
|
mcp_server_name: str,
|
487
520
|
server: SyncServer = Depends(get_letta_server),
|
488
521
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
@@ -490,5 +523,11 @@ def delete_mcp_server_from_config(
|
|
490
523
|
"""
|
491
524
|
Add a new MCP server to the Letta MCP server config
|
492
525
|
"""
|
493
|
-
|
494
|
-
|
526
|
+
if tool_settings.mcp_read_from_config:
|
527
|
+
# write to config file
|
528
|
+
return server.delete_mcp_server_from_config(server_name=mcp_server_name)
|
529
|
+
else:
|
530
|
+
# log to DB
|
531
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
532
|
+
mcp_server_id = await server.mcp_manager.get_mcp_server_id_by_name(mcp_server_name, actor)
|
533
|
+
return server.mcp_manager.delete_mcp_server_by_id(mcp_server_id, actor=actor)
|
letta/server/rest_api/utils.py
CHANGED
@@ -21,7 +21,8 @@ from letta.log import get_logger
|
|
21
21
|
from letta.schemas.enums import MessageRole
|
22
22
|
from letta.schemas.letta_message_content import OmittedReasoningContent, ReasoningContent, RedactedReasoningContent, TextContent
|
23
23
|
from letta.schemas.llm_config import LLMConfig
|
24
|
-
from letta.schemas.message import Message, MessageCreate
|
24
|
+
from letta.schemas.message import Message, MessageCreate, ToolReturn
|
25
|
+
from letta.schemas.tool_execution_result import ToolExecutionResult
|
25
26
|
from letta.schemas.usage import LettaUsageStatistics
|
26
27
|
from letta.schemas.user import User
|
27
28
|
from letta.server.rest_api.interface import StreamingServerInterface
|
@@ -181,6 +182,7 @@ def create_letta_messages_from_llm_response(
|
|
181
182
|
model: str,
|
182
183
|
function_name: str,
|
183
184
|
function_arguments: Dict,
|
185
|
+
tool_execution_result: ToolExecutionResult,
|
184
186
|
tool_call_id: str,
|
185
187
|
function_call_success: bool,
|
186
188
|
function_response: Optional[str],
|
@@ -234,6 +236,14 @@ def create_letta_messages_from_llm_response(
|
|
234
236
|
created_at=get_utc_time(),
|
235
237
|
name=function_name,
|
236
238
|
batch_item_id=llm_batch_item_id,
|
239
|
+
tool_returns=[
|
240
|
+
ToolReturn(
|
241
|
+
status=tool_execution_result.status,
|
242
|
+
stderr=tool_execution_result.stderr,
|
243
|
+
stdout=tool_execution_result.stdout,
|
244
|
+
# func_return=tool_execution_result.func_return,
|
245
|
+
)
|
246
|
+
],
|
237
247
|
)
|
238
248
|
if pre_computed_tool_message_id:
|
239
249
|
tool_message.id = pre_computed_tool_message_id
|
@@ -286,6 +296,7 @@ def create_assistant_messages_from_openai_response(
|
|
286
296
|
model=model,
|
287
297
|
function_name=DEFAULT_MESSAGE_TOOL,
|
288
298
|
function_arguments={DEFAULT_MESSAGE_TOOL_KWARG: response_text}, # Avoid raw string manipulation
|
299
|
+
tool_execution_result=ToolExecutionResult(status="success"),
|
289
300
|
tool_call_id=tool_call_id,
|
290
301
|
function_call_success=True,
|
291
302
|
function_response=None,
|