letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.11.7.dev20251008104128__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/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/letta_llm_request_adapter.py +0 -1
- letta/adapters/letta_llm_stream_adapter.py +7 -2
- letta/adapters/simple_llm_request_adapter.py +88 -0
- letta/adapters/simple_llm_stream_adapter.py +192 -0
- letta/agents/agent_loop.py +6 -0
- letta/agents/ephemeral_summary_agent.py +2 -1
- letta/agents/helpers.py +142 -6
- letta/agents/letta_agent.py +13 -33
- letta/agents/letta_agent_batch.py +2 -4
- letta/agents/letta_agent_v2.py +87 -77
- letta/agents/letta_agent_v3.py +899 -0
- letta/agents/voice_agent.py +2 -6
- letta/constants.py +8 -4
- letta/errors.py +40 -0
- letta/functions/function_sets/base.py +84 -4
- letta/functions/function_sets/multi_agent.py +0 -3
- letta/functions/schema_generator.py +113 -71
- letta/groups/dynamic_multi_agent.py +3 -2
- letta/groups/helpers.py +1 -2
- letta/groups/round_robin_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/groups/sleeptime_multi_agent_v3.py +17 -17
- letta/groups/supervisor_multi_agent.py +84 -80
- letta/helpers/converters.py +3 -0
- letta/helpers/message_helper.py +4 -0
- letta/helpers/tool_rule_solver.py +92 -5
- letta/interfaces/anthropic_streaming_interface.py +409 -0
- letta/interfaces/gemini_streaming_interface.py +296 -0
- letta/interfaces/openai_streaming_interface.py +752 -1
- letta/llm_api/anthropic_client.py +126 -16
- letta/llm_api/bedrock_client.py +4 -2
- letta/llm_api/deepseek_client.py +4 -1
- letta/llm_api/google_vertex_client.py +123 -42
- letta/llm_api/groq_client.py +4 -1
- letta/llm_api/llm_api_tools.py +11 -4
- letta/llm_api/llm_client_base.py +6 -2
- letta/llm_api/openai.py +32 -2
- letta/llm_api/openai_client.py +423 -18
- letta/llm_api/xai_client.py +4 -1
- letta/main.py +9 -5
- letta/memory.py +1 -0
- letta/orm/__init__.py +1 -1
- letta/orm/agent.py +10 -0
- letta/orm/block.py +7 -16
- letta/orm/blocks_agents.py +8 -2
- letta/orm/files_agents.py +2 -0
- letta/orm/job.py +7 -5
- letta/orm/mcp_oauth.py +1 -0
- letta/orm/message.py +21 -6
- letta/orm/organization.py +2 -0
- letta/orm/provider.py +6 -2
- letta/orm/run.py +71 -0
- letta/orm/sandbox_config.py +7 -1
- letta/orm/sqlalchemy_base.py +0 -306
- letta/orm/step.py +6 -5
- letta/orm/step_metrics.py +5 -5
- letta/otel/tracing.py +28 -3
- letta/plugins/defaults.py +4 -4
- letta/prompts/system_prompts/__init__.py +2 -0
- letta/prompts/system_prompts/letta_v1.py +25 -0
- letta/schemas/agent.py +3 -2
- letta/schemas/agent_file.py +9 -3
- letta/schemas/block.py +23 -10
- letta/schemas/enums.py +21 -2
- letta/schemas/job.py +17 -4
- letta/schemas/letta_message_content.py +71 -2
- letta/schemas/letta_stop_reason.py +5 -5
- letta/schemas/llm_config.py +53 -3
- letta/schemas/memory.py +1 -1
- letta/schemas/message.py +504 -117
- letta/schemas/openai/responses_request.py +64 -0
- letta/schemas/providers/__init__.py +2 -0
- letta/schemas/providers/anthropic.py +16 -0
- letta/schemas/providers/ollama.py +115 -33
- letta/schemas/providers/openrouter.py +52 -0
- letta/schemas/providers/vllm.py +2 -1
- letta/schemas/run.py +48 -42
- letta/schemas/step.py +2 -2
- letta/schemas/step_metrics.py +1 -1
- letta/schemas/tool.py +15 -107
- letta/schemas/tool_rule.py +88 -5
- letta/serialize_schemas/marshmallow_agent.py +1 -0
- letta/server/db.py +86 -408
- letta/server/rest_api/app.py +61 -10
- letta/server/rest_api/dependencies.py +14 -0
- letta/server/rest_api/redis_stream_manager.py +19 -8
- letta/server/rest_api/routers/v1/agents.py +364 -292
- letta/server/rest_api/routers/v1/blocks.py +14 -20
- letta/server/rest_api/routers/v1/identities.py +45 -110
- letta/server/rest_api/routers/v1/internal_templates.py +21 -0
- letta/server/rest_api/routers/v1/jobs.py +23 -6
- letta/server/rest_api/routers/v1/messages.py +1 -1
- letta/server/rest_api/routers/v1/runs.py +126 -85
- letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
- letta/server/rest_api/routers/v1/tools.py +281 -594
- letta/server/rest_api/routers/v1/voice.py +1 -1
- letta/server/rest_api/streaming_response.py +29 -29
- letta/server/rest_api/utils.py +122 -64
- letta/server/server.py +160 -887
- letta/services/agent_manager.py +236 -919
- letta/services/agent_serialization_manager.py +16 -0
- letta/services/archive_manager.py +0 -100
- letta/services/block_manager.py +211 -168
- letta/services/file_manager.py +1 -1
- letta/services/files_agents_manager.py +24 -33
- letta/services/group_manager.py +0 -142
- letta/services/helpers/agent_manager_helper.py +7 -2
- letta/services/helpers/run_manager_helper.py +85 -0
- letta/services/job_manager.py +96 -411
- letta/services/lettuce/__init__.py +6 -0
- letta/services/lettuce/lettuce_client_base.py +86 -0
- letta/services/mcp_manager.py +38 -6
- letta/services/message_manager.py +165 -362
- letta/services/organization_manager.py +0 -36
- letta/services/passage_manager.py +0 -345
- letta/services/provider_manager.py +0 -80
- letta/services/run_manager.py +301 -0
- letta/services/sandbox_config_manager.py +0 -234
- letta/services/step_manager.py +62 -39
- letta/services/summarizer/summarizer.py +9 -7
- letta/services/telemetry_manager.py +0 -16
- letta/services/tool_executor/builtin_tool_executor.py +35 -0
- letta/services/tool_executor/core_tool_executor.py +397 -2
- letta/services/tool_executor/files_tool_executor.py +3 -3
- letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
- letta/services/tool_executor/tool_execution_manager.py +6 -8
- letta/services/tool_executor/tool_executor_base.py +3 -3
- letta/services/tool_manager.py +85 -339
- letta/services/tool_sandbox/base.py +24 -13
- letta/services/tool_sandbox/e2b_sandbox.py +16 -1
- letta/services/tool_schema_generator.py +123 -0
- letta/services/user_manager.py +0 -99
- letta/settings.py +20 -4
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/METADATA +3 -5
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/RECORD +140 -132
- letta/agents/temporal/activities/__init__.py +0 -4
- letta/agents/temporal/activities/example_activity.py +0 -7
- letta/agents/temporal/activities/prepare_messages.py +0 -10
- letta/agents/temporal/temporal_agent_workflow.py +0 -56
- letta/agents/temporal/types.py +0 -25
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/licenses/LICENSE +0 -0
letta/agents/voice_agent.py
CHANGED
@@ -214,7 +214,6 @@ class VoiceAgent(BaseAgent):
|
|
214
214
|
response_text=content,
|
215
215
|
agent_id=agent_state.id,
|
216
216
|
model=agent_state.llm_config.model,
|
217
|
-
actor=self.actor,
|
218
217
|
timezone=agent_state.timezone,
|
219
218
|
)
|
220
219
|
letta_message_db_queue.extend(assistant_msgs)
|
@@ -273,11 +272,9 @@ class VoiceAgent(BaseAgent):
|
|
273
272
|
function_name=tool_call_name,
|
274
273
|
function_arguments=tool_args,
|
275
274
|
tool_call_id=tool_call_id,
|
276
|
-
function_call_success=success_flag,
|
277
275
|
function_response=tool_result,
|
278
276
|
tool_execution_result=tool_execution_result,
|
279
277
|
timezone=agent_state.timezone,
|
280
|
-
actor=self.actor,
|
281
278
|
continue_stepping=True,
|
282
279
|
)
|
283
280
|
letta_message_db_queue.extend(tool_call_messages)
|
@@ -343,8 +340,7 @@ class VoiceAgent(BaseAgent):
|
|
343
340
|
tools = [
|
344
341
|
t
|
345
342
|
for t in agent_state.tools
|
346
|
-
if t.tool_type
|
347
|
-
in {ToolType.EXTERNAL_COMPOSIO, ToolType.CUSTOM, ToolType.LETTA_FILES_CORE, ToolType.LETTA_BUILTIN, ToolType.EXTERNAL_MCP}
|
343
|
+
if t.tool_type in {ToolType.CUSTOM, ToolType.LETTA_FILES_CORE, ToolType.LETTA_BUILTIN, ToolType.EXTERNAL_MCP}
|
348
344
|
]
|
349
345
|
else:
|
350
346
|
tools = agent_state.tools
|
@@ -504,7 +500,7 @@ class VoiceAgent(BaseAgent):
|
|
504
500
|
keyword_results = {}
|
505
501
|
if convo_keyword_queries:
|
506
502
|
for keyword in convo_keyword_queries:
|
507
|
-
messages = await self.message_manager.
|
503
|
+
messages = await self.message_manager.list_messages(
|
508
504
|
agent_id=self.agent_id,
|
509
505
|
actor=self.actor,
|
510
506
|
query_text=keyword,
|
letta/constants.py
CHANGED
@@ -13,9 +13,6 @@ API_PREFIX = "/v1"
|
|
13
13
|
OLLAMA_API_PREFIX = "/v1"
|
14
14
|
OPENAI_API_PREFIX = "/openai"
|
15
15
|
|
16
|
-
COMPOSIO_ENTITY_ENV_VAR_KEY = "COMPOSIO_ENTITY"
|
17
|
-
COMPOSIO_TOOL_TAG_NAME = "composio"
|
18
|
-
|
19
16
|
MCP_CONFIG_NAME = "mcp_config.json"
|
20
17
|
MCP_TOOL_TAG_NAME_PREFIX = "mcp" # full format, mcp:server_name
|
21
18
|
|
@@ -89,7 +86,7 @@ SEND_MESSAGE_TOOL_NAME = "send_message"
|
|
89
86
|
BASE_TOOLS = [SEND_MESSAGE_TOOL_NAME, "conversation_search", "archival_memory_insert", "archival_memory_search"]
|
90
87
|
DEPRECATED_LETTA_TOOLS = ["archival_memory_insert", "archival_memory_search"]
|
91
88
|
# Base memory tools CAN be edited, and are added by default by the server
|
92
|
-
BASE_MEMORY_TOOLS = ["core_memory_append", "core_memory_replace"]
|
89
|
+
BASE_MEMORY_TOOLS = ["core_memory_append", "core_memory_replace", "memory"]
|
93
90
|
# New v2 collection of the base memory tools (effecitvely same as sleeptime set), to pair with memgpt_v2 prompt
|
94
91
|
BASE_MEMORY_TOOLS_V2 = [
|
95
92
|
"memory_replace",
|
@@ -98,6 +95,11 @@ BASE_MEMORY_TOOLS_V2 = [
|
|
98
95
|
# "memory_rethink",
|
99
96
|
# "memory_finish_edits",
|
100
97
|
]
|
98
|
+
|
99
|
+
# v3 collection, currently just a omni memory tool for anthropic
|
100
|
+
BASE_MEMORY_TOOLS_V3 = [
|
101
|
+
"memory",
|
102
|
+
]
|
101
103
|
# Base tools if the memgpt agent has enable_sleeptime on
|
102
104
|
BASE_SLEEPTIME_CHAT_TOOLS = [SEND_MESSAGE_TOOL_NAME, "conversation_search", "archival_memory_search"]
|
103
105
|
# Base memory tools for sleeptime agent
|
@@ -118,6 +120,7 @@ BASE_VOICE_SLEEPTIME_TOOLS = [
|
|
118
120
|
"rethink_user_memory",
|
119
121
|
"finish_rethinking_memory",
|
120
122
|
]
|
123
|
+
|
121
124
|
# Multi agent tools
|
122
125
|
MULTI_AGENT_TOOLS = ["send_message_to_agent_and_wait_for_reply", "send_message_to_agents_matching_tags", "send_message_to_agent_async"]
|
123
126
|
LOCAL_ONLY_MULTI_AGENT_TOOLS = ["send_message_to_agent_async"]
|
@@ -219,6 +222,7 @@ LLM_MAX_TOKENS = {
|
|
219
222
|
"gpt-5-mini-2025-08-07": 272000,
|
220
223
|
"gpt-5-nano": 272000,
|
221
224
|
"gpt-5-nano-2025-08-07": 272000,
|
225
|
+
"gpt-5-codex": 272000,
|
222
226
|
# reasoners
|
223
227
|
"o1": 200000,
|
224
228
|
# "o1-pro": 200000, # responses API only
|
letta/errors.py
CHANGED
@@ -97,6 +97,46 @@ class LettaUserNotFoundError(LettaError):
|
|
97
97
|
"""Error raised when a user is not found."""
|
98
98
|
|
99
99
|
|
100
|
+
class LettaInvalidArgumentError(LettaError):
|
101
|
+
"""Error raised when an invalid argument is provided."""
|
102
|
+
|
103
|
+
def __init__(self, message: str, argument_name: Optional[str] = None):
|
104
|
+
details = {"argument_name": argument_name} if argument_name else {}
|
105
|
+
super().__init__(message=message, code=ErrorCode.INVALID_ARGUMENT, details=details)
|
106
|
+
|
107
|
+
|
108
|
+
class LettaMCPError(LettaError):
|
109
|
+
"""Base error for MCP-related issues."""
|
110
|
+
|
111
|
+
|
112
|
+
class LettaInvalidMCPSchemaError(LettaMCPError):
|
113
|
+
"""Error raised when an invalid MCP schema is provided."""
|
114
|
+
|
115
|
+
def __init__(self, server_name: str, mcp_tool_name: str, reasons: List[str]):
|
116
|
+
details = {"server_name": server_name, "mcp_tool_name": mcp_tool_name, "reasons": reasons}
|
117
|
+
super().__init__(
|
118
|
+
message=f"MCP tool {mcp_tool_name} has an invalid schema and cannot be attached - reasons: {reasons}",
|
119
|
+
code=ErrorCode.INVALID_ARGUMENT,
|
120
|
+
details=details,
|
121
|
+
)
|
122
|
+
|
123
|
+
|
124
|
+
class LettaMCPConnectionError(LettaMCPError):
|
125
|
+
"""Error raised when unable to connect to MCP server."""
|
126
|
+
|
127
|
+
def __init__(self, message: str, server_name: Optional[str] = None):
|
128
|
+
details = {"server_name": server_name} if server_name else {}
|
129
|
+
super().__init__(message=message, code=ErrorCode.INTERNAL_SERVER_ERROR, details=details)
|
130
|
+
|
131
|
+
|
132
|
+
class LettaMCPTimeoutError(LettaMCPError):
|
133
|
+
"""Error raised when MCP server operation times out."""
|
134
|
+
|
135
|
+
def __init__(self, message: str, server_name: Optional[str] = None):
|
136
|
+
details = {"server_name": server_name} if server_name else {}
|
137
|
+
super().__init__(message=message, code=ErrorCode.TIMEOUT, details=details)
|
138
|
+
|
139
|
+
|
100
140
|
class LettaUnexpectedStreamCancellationError(LettaError):
|
101
141
|
"""Error raised when a streaming request is terminated unexpectedly."""
|
102
142
|
|
@@ -1,8 +1,82 @@
|
|
1
|
-
from typing import List, Literal, Optional
|
1
|
+
from typing import TYPE_CHECKING, Any, List, Literal, Optional
|
2
2
|
|
3
|
-
from letta.agent import Agent
|
4
3
|
from letta.constants import CORE_MEMORY_LINE_NUMBER_WARNING
|
5
4
|
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from letta.schemas.agent import AgentState
|
7
|
+
|
8
|
+
|
9
|
+
def memory(
|
10
|
+
agent_state: "AgentState",
|
11
|
+
command: str,
|
12
|
+
path: Optional[str] = None,
|
13
|
+
file_text: Optional[str] = None,
|
14
|
+
description: Optional[str] = None,
|
15
|
+
old_str: Optional[str] = None,
|
16
|
+
new_str: Optional[str] = None,
|
17
|
+
insert_line: Optional[int] = None,
|
18
|
+
insert_text: Optional[str] = None,
|
19
|
+
old_path: Optional[str] = None,
|
20
|
+
new_path: Optional[str] = None,
|
21
|
+
) -> Optional[str]:
|
22
|
+
"""
|
23
|
+
Memory management tool with various sub-commands for memory block operations.
|
24
|
+
|
25
|
+
Args:
|
26
|
+
command (str): The sub-command to execute. Supported commands:
|
27
|
+
- "view": List memory blocks or view specific block content
|
28
|
+
- "create": Create a new memory block
|
29
|
+
- "str_replace": Replace text in a memory block
|
30
|
+
- "insert": Insert text at a specific line in a memory block
|
31
|
+
- "delete": Delete a memory block
|
32
|
+
- "rename": Rename a memory block
|
33
|
+
path (Optional[str]): Path to the memory block (for str_replace, insert, delete)
|
34
|
+
file_text (Optional[str]): The value to set in the memory block (for create)
|
35
|
+
description (Optional[str]): The description to set in the memory block (for create, rename)
|
36
|
+
old_str (Optional[str]): Old text to replace (for str_replace)
|
37
|
+
new_str (Optional[str]): New text to replace with (for str_replace)
|
38
|
+
insert_line (Optional[int]): Line number to insert at (for insert)
|
39
|
+
insert_text (Optional[str]): Text to insert (for insert)
|
40
|
+
old_path (Optional[str]): Old path for rename operation
|
41
|
+
new_path (Optional[str]): New path for rename operation
|
42
|
+
view_range (Optional[int]): Range of lines to view (for view)
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
Optional[str]: Success message or error description
|
46
|
+
|
47
|
+
Examples:
|
48
|
+
# List all memory blocks
|
49
|
+
memory(agent_state, "view", path="/memories")
|
50
|
+
|
51
|
+
# View specific memory block content
|
52
|
+
memory(agent_state, "view", path="/memories/user_preferences")
|
53
|
+
|
54
|
+
# View first 10 lines of a memory block
|
55
|
+
memory(agent_state, "view", path="/memories/user_preferences", view_range=10)
|
56
|
+
|
57
|
+
# Replace text in a memory block
|
58
|
+
memory(agent_state, "str_replace", path="/memories/user_preferences", old_str="theme: dark", new_str="theme: light")
|
59
|
+
|
60
|
+
# Insert text at line 5
|
61
|
+
memory(agent_state, "insert", path="/memories/notes", insert_line=5, insert_text="New note here")
|
62
|
+
|
63
|
+
# Delete a memory block
|
64
|
+
memory(agent_state, "delete", path="/memories/old_notes")
|
65
|
+
|
66
|
+
# Rename a memory block
|
67
|
+
memory(agent_state, "rename", old_path="/memories/temp", new_path="/memories/permanent")
|
68
|
+
|
69
|
+
# Update the description of a memory block
|
70
|
+
memory(agent_state, "rename", path="/memories/temp", description="The user's temporary notes.")
|
71
|
+
|
72
|
+
# Create a memory block with starting text
|
73
|
+
memory(agent_state, "create", path="/memories/coding_preferences", "description": "The user's coding preferences.", "file_text": "The user seems to add type hints to all of their Python code.")
|
74
|
+
|
75
|
+
# Create an empty memory block
|
76
|
+
memory(agent_state, "create", path="/memories/coding_preferences", "description": "The user's coding preferences.")
|
77
|
+
"""
|
78
|
+
raise NotImplementedError("This should never be invoked directly. Contact Letta if you see this error message.")
|
79
|
+
|
6
80
|
|
7
81
|
def send_message(self: "Agent", message: str) -> Optional[str]:
|
8
82
|
"""
|
@@ -202,7 +276,10 @@ def rethink_memory(agent_state: "AgentState", new_memory: str, target_block_labe
|
|
202
276
|
"""
|
203
277
|
|
204
278
|
if agent_state.memory.get_block(target_block_label) is None:
|
205
|
-
|
279
|
+
from letta.schemas.block import Block
|
280
|
+
|
281
|
+
new_block = Block(label=target_block_label, value=new_memory)
|
282
|
+
agent_state.memory.set_block(new_block)
|
206
283
|
|
207
284
|
agent_state.memory.update_block_value(label=target_block_label, value=new_memory)
|
208
285
|
return None
|
@@ -395,7 +472,10 @@ def memory_rethink(agent_state: "AgentState", label: str, new_memory: str) -> No
|
|
395
472
|
)
|
396
473
|
|
397
474
|
if agent_state.memory.get_block(label) is None:
|
398
|
-
|
475
|
+
from letta.schemas.block import Block
|
476
|
+
|
477
|
+
new_block = Block(label=label, value=new_memory)
|
478
|
+
agent_state.memory.set_block(new_block)
|
399
479
|
|
400
480
|
agent_state.memory.update_block_value(label=label, value=new_memory)
|
401
481
|
|
@@ -14,9 +14,6 @@ from letta.schemas.message import MessageCreate
|
|
14
14
|
from letta.server.rest_api.dependencies import get_letta_server
|
15
15
|
from letta.settings import settings
|
16
16
|
|
17
|
-
if TYPE_CHECKING:
|
18
|
-
from letta.agent import Agent
|
19
|
-
|
20
17
|
|
21
18
|
def send_message_to_agent_and_wait_for_reply(self: "Agent", message: str, other_agent_id: str) -> str:
|
22
19
|
"""
|
@@ -2,7 +2,6 @@ import inspect
|
|
2
2
|
import warnings
|
3
3
|
from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_args, get_origin
|
4
4
|
|
5
|
-
from composio.client.collections import ActionParametersModel
|
6
5
|
from docstring_parser import parse
|
7
6
|
from pydantic import BaseModel
|
8
7
|
from typing_extensions import Literal
|
@@ -588,11 +587,111 @@ def generate_schema_from_args_schema_v2(
|
|
588
587
|
return function_call_json
|
589
588
|
|
590
589
|
|
590
|
+
def normalize_mcp_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
|
591
|
+
"""
|
592
|
+
Normalize an MCP JSON schema to fix common issues:
|
593
|
+
1. Add explicit 'additionalProperties': false to all object types
|
594
|
+
2. Add explicit 'type' field to properties using $ref
|
595
|
+
3. Process $defs recursively
|
596
|
+
|
597
|
+
Args:
|
598
|
+
schema: The JSON schema to normalize (will be modified in-place)
|
599
|
+
|
600
|
+
Returns:
|
601
|
+
The normalized schema (same object, modified in-place)
|
602
|
+
"""
|
603
|
+
import copy
|
604
|
+
|
605
|
+
# Work on a deep copy to avoid modifying the original
|
606
|
+
schema = copy.deepcopy(schema)
|
607
|
+
|
608
|
+
def normalize_object_schema(obj_schema: Dict[str, Any], defs: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
609
|
+
"""Recursively normalize an object schema."""
|
610
|
+
|
611
|
+
# If this is an object type, add additionalProperties if missing
|
612
|
+
if obj_schema.get("type") == "object":
|
613
|
+
if "additionalProperties" not in obj_schema:
|
614
|
+
obj_schema["additionalProperties"] = False
|
615
|
+
|
616
|
+
# Handle properties
|
617
|
+
if "properties" in obj_schema:
|
618
|
+
for prop_name, prop_schema in obj_schema["properties"].items():
|
619
|
+
# Handle $ref references
|
620
|
+
if "$ref" in prop_schema:
|
621
|
+
# Add explicit type based on the reference
|
622
|
+
if "type" not in prop_schema:
|
623
|
+
# Try to resolve the type from $defs if available
|
624
|
+
if defs and prop_schema["$ref"].startswith("#/$defs/"):
|
625
|
+
def_name = prop_schema["$ref"].split("/")[-1]
|
626
|
+
if def_name in defs:
|
627
|
+
ref_schema = defs[def_name]
|
628
|
+
if "type" in ref_schema:
|
629
|
+
prop_schema["type"] = ref_schema["type"]
|
630
|
+
|
631
|
+
# If still no type, assume object (common case for model references)
|
632
|
+
if "type" not in prop_schema:
|
633
|
+
prop_schema["type"] = "object"
|
634
|
+
|
635
|
+
# Don't add additionalProperties to properties with $ref
|
636
|
+
# The $ref schema itself will have additionalProperties
|
637
|
+
# Adding it here makes the validator think it allows empty objects
|
638
|
+
continue
|
639
|
+
|
640
|
+
# Recursively normalize nested objects
|
641
|
+
if isinstance(prop_schema, dict):
|
642
|
+
if prop_schema.get("type") == "object":
|
643
|
+
normalize_object_schema(prop_schema, defs)
|
644
|
+
|
645
|
+
# Handle arrays with object items
|
646
|
+
if prop_schema.get("type") == "array" and "items" in prop_schema:
|
647
|
+
items = prop_schema["items"]
|
648
|
+
if isinstance(items, dict):
|
649
|
+
# Handle $ref in items
|
650
|
+
if "$ref" in items and "type" not in items:
|
651
|
+
if defs and items["$ref"].startswith("#/$defs/"):
|
652
|
+
def_name = items["$ref"].split("/")[-1]
|
653
|
+
if def_name in defs and "type" in defs[def_name]:
|
654
|
+
items["type"] = defs[def_name]["type"]
|
655
|
+
if "type" not in items:
|
656
|
+
items["type"] = "object"
|
657
|
+
|
658
|
+
# Recursively normalize items
|
659
|
+
if items.get("type") == "object":
|
660
|
+
normalize_object_schema(items, defs)
|
661
|
+
|
662
|
+
# Handle anyOf (complex union types)
|
663
|
+
if "anyOf" in prop_schema:
|
664
|
+
for option in prop_schema["anyOf"]:
|
665
|
+
if isinstance(option, dict) and option.get("type") == "object":
|
666
|
+
normalize_object_schema(option, defs)
|
667
|
+
|
668
|
+
# Handle array items at the top level
|
669
|
+
if "items" in obj_schema and isinstance(obj_schema["items"], dict):
|
670
|
+
if obj_schema["items"].get("type") == "object":
|
671
|
+
normalize_object_schema(obj_schema["items"], defs)
|
672
|
+
|
673
|
+
return obj_schema
|
674
|
+
|
675
|
+
# Process $defs first if they exist
|
676
|
+
defs = schema.get("$defs", {})
|
677
|
+
if defs:
|
678
|
+
for def_name, def_schema in defs.items():
|
679
|
+
if isinstance(def_schema, dict):
|
680
|
+
normalize_object_schema(def_schema, defs)
|
681
|
+
|
682
|
+
# Process the main schema
|
683
|
+
normalize_object_schema(schema, defs)
|
684
|
+
|
685
|
+
return schema
|
686
|
+
|
687
|
+
|
591
688
|
def generate_tool_schema_for_mcp(
|
592
689
|
mcp_tool: MCPTool,
|
593
690
|
append_heartbeat: bool = True,
|
594
691
|
strict: bool = False,
|
595
692
|
) -> Dict[str, Any]:
|
693
|
+
from letta.functions.schema_validator import validate_complete_json_schema
|
694
|
+
|
596
695
|
# MCP tool.inputSchema is a JSON schema
|
597
696
|
# https://github.com/modelcontextprotocol/python-sdk/blob/775f87981300660ee957b63c2a14b448ab9c3675/src/mcp/types.py#L678
|
598
697
|
parameters_schema = mcp_tool.inputSchema
|
@@ -603,11 +702,16 @@ def generate_tool_schema_for_mcp(
|
|
603
702
|
assert "properties" in parameters_schema, parameters_schema
|
604
703
|
# assert "required" in parameters_schema, parameters_schema
|
605
704
|
|
705
|
+
# Normalize the schema to fix common issues with MCP schemas
|
706
|
+
# This adds additionalProperties: false and explicit types for $ref properties
|
707
|
+
parameters_schema = normalize_mcp_schema(parameters_schema)
|
708
|
+
|
606
709
|
# Zero-arg tools often omit "required" because nothing is required.
|
607
710
|
# Normalise so downstream code can treat it consistently.
|
608
711
|
parameters_schema.setdefault("required", [])
|
609
712
|
|
610
713
|
# Process properties to handle anyOf types and make optional fields strict-compatible
|
714
|
+
# TODO: de-duplicate with handling in normalize_mcp_schema
|
611
715
|
if "properties" in parameters_schema:
|
612
716
|
for field_name, field_props in parameters_schema["properties"].items():
|
613
717
|
# Handle anyOf types by flattening to type array
|
@@ -660,6 +764,14 @@ def generate_tool_schema_for_mcp(
|
|
660
764
|
if REQUEST_HEARTBEAT_PARAM not in parameters_schema["required"]:
|
661
765
|
parameters_schema["required"].append(REQUEST_HEARTBEAT_PARAM)
|
662
766
|
|
767
|
+
# Re-validate the schema after normalization and update the health status
|
768
|
+
# This allows previously INVALID schemas to pass if normalization fixed them
|
769
|
+
if mcp_tool.health:
|
770
|
+
health_status, health_reasons = validate_complete_json_schema(parameters_schema)
|
771
|
+
mcp_tool.health.status = health_status.value
|
772
|
+
mcp_tool.health.reasons = health_reasons
|
773
|
+
logger.debug(f"MCP tool {name} schema health after normalization: {health_status.value}, reasons: {health_reasons}")
|
774
|
+
|
663
775
|
# Return the final schema
|
664
776
|
if strict:
|
665
777
|
# https://platform.openai.com/docs/guides/function-calling#strict-mode
|
@@ -679,73 +791,3 @@ def generate_tool_schema_for_mcp(
|
|
679
791
|
"description": description,
|
680
792
|
"parameters": parameters_schema,
|
681
793
|
}
|
682
|
-
|
683
|
-
|
684
|
-
def generate_tool_schema_for_composio(
|
685
|
-
parameters_model: ActionParametersModel,
|
686
|
-
name: str,
|
687
|
-
description: str,
|
688
|
-
append_heartbeat: bool = True,
|
689
|
-
strict: bool = False,
|
690
|
-
) -> Dict[str, Any]:
|
691
|
-
properties_json = {}
|
692
|
-
required_fields = parameters_model.required or []
|
693
|
-
|
694
|
-
# Extract properties from the ActionParametersModel
|
695
|
-
for field_name, field_props in parameters_model.properties.items():
|
696
|
-
# Initialize the property structure
|
697
|
-
property_schema = {
|
698
|
-
"type": field_props["type"],
|
699
|
-
"description": field_props.get("description", ""),
|
700
|
-
}
|
701
|
-
|
702
|
-
# Handle optional default values
|
703
|
-
if "default" in field_props:
|
704
|
-
property_schema["default"] = field_props["default"]
|
705
|
-
|
706
|
-
# Handle enumerations
|
707
|
-
if "enum" in field_props:
|
708
|
-
property_schema["enum"] = field_props["enum"]
|
709
|
-
|
710
|
-
# Handle array item types
|
711
|
-
if field_props["type"] == "array":
|
712
|
-
if "items" in field_props:
|
713
|
-
property_schema["items"] = field_props["items"]
|
714
|
-
elif "anyOf" in field_props:
|
715
|
-
property_schema["items"] = [t for t in field_props["anyOf"] if "items" in t][0]["items"]
|
716
|
-
|
717
|
-
# Add the property to the schema
|
718
|
-
properties_json[field_name] = property_schema
|
719
|
-
|
720
|
-
# Add the optional heartbeat parameter
|
721
|
-
if append_heartbeat:
|
722
|
-
properties_json[REQUEST_HEARTBEAT_PARAM] = {
|
723
|
-
"type": "boolean",
|
724
|
-
"description": REQUEST_HEARTBEAT_DESCRIPTION,
|
725
|
-
}
|
726
|
-
required_fields.append(REQUEST_HEARTBEAT_PARAM)
|
727
|
-
|
728
|
-
# Return the final schema
|
729
|
-
if strict:
|
730
|
-
# https://platform.openai.com/docs/guides/function-calling#strict-mode
|
731
|
-
return {
|
732
|
-
"name": name,
|
733
|
-
"description": description,
|
734
|
-
"strict": True, # NOTE
|
735
|
-
"parameters": {
|
736
|
-
"type": "object",
|
737
|
-
"properties": properties_json,
|
738
|
-
"additionalProperties": False, # NOTE
|
739
|
-
"required": required_fields,
|
740
|
-
},
|
741
|
-
}
|
742
|
-
else:
|
743
|
-
return {
|
744
|
-
"name": name,
|
745
|
-
"description": description,
|
746
|
-
"parameters": {
|
747
|
-
"type": "object",
|
748
|
-
"properties": properties_json,
|
749
|
-
"required": required_fields,
|
750
|
-
},
|
751
|
-
}
|
@@ -1,8 +1,9 @@
|
|
1
1
|
from typing import List, Optional
|
2
2
|
|
3
|
-
from letta.
|
3
|
+
from letta.agents.base_agent import BaseAgent
|
4
4
|
from letta.interface import AgentInterface
|
5
5
|
from letta.orm import User
|
6
|
+
from letta.schemas.agent import AgentState
|
6
7
|
from letta.schemas.block import Block
|
7
8
|
from letta.schemas.letta_message_content import TextContent
|
8
9
|
from letta.schemas.message import Message, MessageCreate
|
@@ -11,7 +12,7 @@ from letta.schemas.usage import LettaUsageStatistics
|
|
11
12
|
from letta.services.tool_manager import ToolManager
|
12
13
|
|
13
14
|
|
14
|
-
class DynamicMultiAgent(
|
15
|
+
class DynamicMultiAgent(BaseAgent):
|
15
16
|
def __init__(
|
16
17
|
self,
|
17
18
|
interface: AgentInterface,
|
letta/groups/helpers.py
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
import json
|
2
2
|
from typing import Dict, Optional, Union
|
3
3
|
|
4
|
-
from letta.agent import Agent
|
5
4
|
from letta.interface import AgentInterface
|
6
5
|
from letta.orm.group import Group
|
7
6
|
from letta.orm.user import User
|
@@ -18,7 +17,7 @@ def load_multi_agent(
|
|
18
17
|
actor: User,
|
19
18
|
interface: Union[AgentInterface, None] = None,
|
20
19
|
mcp_clients: Optional[Dict[str, AsyncBaseMCPClient]] = None,
|
21
|
-
) -> Agent:
|
20
|
+
) -> "Agent":
|
22
21
|
if len(group.agent_ids) == 0:
|
23
22
|
raise ValueError("Empty group: group must have at least one agent")
|
24
23
|
|
@@ -1,15 +1,16 @@
|
|
1
1
|
from typing import List, Optional
|
2
2
|
|
3
|
-
from letta.
|
3
|
+
from letta.agents.base_agent import BaseAgent
|
4
4
|
from letta.interface import AgentInterface
|
5
5
|
from letta.orm import User
|
6
|
+
from letta.schemas.agent import AgentState
|
6
7
|
from letta.schemas.letta_message_content import TextContent
|
7
8
|
from letta.schemas.message import Message, MessageCreate
|
8
9
|
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
9
10
|
from letta.schemas.usage import LettaUsageStatistics
|
10
11
|
|
11
12
|
|
12
|
-
class RoundRobinMultiAgent(
|
13
|
+
class RoundRobinMultiAgent(BaseAgent):
|
13
14
|
def __init__(
|
14
15
|
self,
|
15
16
|
interface: AgentInterface,
|
@@ -3,10 +3,11 @@ import threading
|
|
3
3
|
from datetime import datetime, timezone
|
4
4
|
from typing import List, Optional
|
5
5
|
|
6
|
-
from letta.
|
6
|
+
from letta.agents.base_agent import BaseAgent
|
7
7
|
from letta.groups.helpers import stringify_message
|
8
8
|
from letta.interface import AgentInterface
|
9
9
|
from letta.orm import User
|
10
|
+
from letta.schemas.agent import AgentState
|
10
11
|
from letta.schemas.enums import JobStatus
|
11
12
|
from letta.schemas.job import JobUpdate
|
12
13
|
from letta.schemas.letta_message_content import TextContent
|
@@ -19,7 +20,7 @@ from letta.services.job_manager import JobManager
|
|
19
20
|
from letta.services.message_manager import MessageManager
|
20
21
|
|
21
22
|
|
22
|
-
class SleeptimeMultiAgent(
|
23
|
+
class SleeptimeMultiAgent(BaseAgent):
|
23
24
|
def __init__(
|
24
25
|
self,
|
25
26
|
interface: AgentInterface,
|
@@ -268,7 +268,7 @@ class SleeptimeMultiAgentV2(BaseAgent):
|
|
268
268
|
prior_messages = []
|
269
269
|
if self.group.sleeptime_agent_frequency:
|
270
270
|
try:
|
271
|
-
prior_messages = await self.message_manager.
|
271
|
+
prior_messages = await self.message_manager.list_messages(
|
272
272
|
agent_id=foreground_agent_id,
|
273
273
|
actor=self.actor,
|
274
274
|
after=last_processed_message_id,
|
@@ -7,14 +7,14 @@ from letta.constants import DEFAULT_MAX_STEPS
|
|
7
7
|
from letta.groups.helpers import stringify_message
|
8
8
|
from letta.otel.tracing import trace_method
|
9
9
|
from letta.schemas.agent import AgentState
|
10
|
-
from letta.schemas.enums import JobStatus
|
10
|
+
from letta.schemas.enums import JobStatus, RunStatus
|
11
11
|
from letta.schemas.group import Group, ManagerType
|
12
12
|
from letta.schemas.job import JobUpdate
|
13
13
|
from letta.schemas.letta_message import MessageType
|
14
14
|
from letta.schemas.letta_message_content import TextContent
|
15
15
|
from letta.schemas.letta_response import LettaResponse
|
16
16
|
from letta.schemas.message import Message, MessageCreate
|
17
|
-
from letta.schemas.run import Run
|
17
|
+
from letta.schemas.run import Run, RunUpdate
|
18
18
|
from letta.schemas.user import User
|
19
19
|
from letta.services.group_manager import GroupManager
|
20
20
|
from letta.utils import safe_create_task
|
@@ -134,14 +134,14 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
|
|
134
134
|
use_assistant_message: bool = True,
|
135
135
|
) -> str:
|
136
136
|
run = Run(
|
137
|
-
|
138
|
-
status=
|
137
|
+
agent_id=sleeptime_agent_id,
|
138
|
+
status=RunStatus.created,
|
139
139
|
metadata={
|
140
|
-
"
|
140
|
+
"run_type": "sleeptime_agent_send_message_async", # is this right?
|
141
141
|
"agent_id": sleeptime_agent_id,
|
142
142
|
},
|
143
143
|
)
|
144
|
-
run = await self.
|
144
|
+
run = await self.run_manager.create_run(pydantic_run=run, actor=self.actor)
|
145
145
|
|
146
146
|
safe_create_task(
|
147
147
|
self._participant_agent_step(
|
@@ -167,15 +167,15 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
|
|
167
167
|
use_assistant_message: bool = True,
|
168
168
|
) -> LettaResponse:
|
169
169
|
try:
|
170
|
-
# Update
|
171
|
-
|
172
|
-
await self.
|
170
|
+
# Update run status
|
171
|
+
run_update = RunUpdate(status=RunStatus.running)
|
172
|
+
await self.run_manager.update_run_by_id_async(run_id=run_id, update=run_update, actor=self.actor)
|
173
173
|
|
174
174
|
# Create conversation transcript
|
175
175
|
prior_messages = []
|
176
176
|
if self.group.sleeptime_agent_frequency:
|
177
177
|
try:
|
178
|
-
prior_messages = await self.message_manager.
|
178
|
+
prior_messages = await self.message_manager.list_messages(
|
179
179
|
agent_id=foreground_agent_id,
|
180
180
|
actor=self.actor,
|
181
181
|
after=last_processed_message_id,
|
@@ -212,22 +212,22 @@ class SleeptimeMultiAgentV3(LettaAgentV2):
|
|
212
212
|
use_assistant_message=use_assistant_message,
|
213
213
|
)
|
214
214
|
|
215
|
-
# Update
|
216
|
-
|
217
|
-
status=
|
215
|
+
# Update run status
|
216
|
+
run_update = RunUpdate(
|
217
|
+
status=RunStatus.completed,
|
218
218
|
completed_at=datetime.now(timezone.utc).replace(tzinfo=None),
|
219
219
|
metadata={
|
220
220
|
"result": result.model_dump(mode="json"),
|
221
221
|
"agent_id": sleeptime_agent_state.id,
|
222
222
|
},
|
223
223
|
)
|
224
|
-
await self.
|
224
|
+
await self.run_manager.update_run_by_id_async(run_id=run_id, update=run_update, actor=self.actor)
|
225
225
|
return result
|
226
226
|
except Exception as e:
|
227
|
-
|
228
|
-
status=
|
227
|
+
run_update = RunUpdate(
|
228
|
+
status=RunStatus.failed,
|
229
229
|
completed_at=datetime.now(timezone.utc).replace(tzinfo=None),
|
230
230
|
metadata={"error": str(e)},
|
231
231
|
)
|
232
|
-
await self.
|
232
|
+
await self.run_manager.update_run_by_id_async(run_id=run_id, update=run_update, actor=self.actor)
|
233
233
|
raise
|