letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.12.0.dev20251009104148__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- letta/__init__.py +1 -1
- 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 +927 -0
- letta/agents/voice_agent.py +2 -6
- letta/constants.py +8 -4
- letta/database_utils.py +161 -0
- 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 +127 -16
- letta/llm_api/bedrock_client.py +4 -2
- letta/llm_api/deepseek_client.py +4 -1
- letta/llm_api/google_vertex_client.py +124 -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 +2 -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/run_metrics.py +82 -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 +564 -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/run_metrics.py +21 -0
- 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 +79 -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 +149 -99
- 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/context_window_calculator/token_counter.py +1 -1
- 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 +69 -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 +364 -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/system.py +5 -1
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +3 -5
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +146 -135
- 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.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.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/database_utils.py
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
"""
|
2
|
+
Database URI utilities for consistent database connection handling across the application.
|
3
|
+
|
4
|
+
This module provides utilities for parsing and converting database URIs to ensure
|
5
|
+
consistent behavior between the main application, alembic migrations, and other
|
6
|
+
database-related components.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from typing import Optional
|
10
|
+
from urllib.parse import urlparse, urlunparse
|
11
|
+
|
12
|
+
|
13
|
+
def parse_database_uri(uri: str) -> dict[str, Optional[str]]:
|
14
|
+
"""
|
15
|
+
Parse a database URI into its components.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
uri: Database URI (e.g., postgresql://user:pass@host:port/db)
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
Dictionary with parsed components: scheme, driver, user, password, host, port, database
|
22
|
+
"""
|
23
|
+
parsed = urlparse(uri)
|
24
|
+
|
25
|
+
# Extract driver from scheme (e.g., postgresql+asyncpg -> asyncpg)
|
26
|
+
scheme_parts = parsed.scheme.split("+")
|
27
|
+
base_scheme = scheme_parts[0] if scheme_parts else ""
|
28
|
+
driver = scheme_parts[1] if len(scheme_parts) > 1 else None
|
29
|
+
|
30
|
+
return {
|
31
|
+
"scheme": base_scheme,
|
32
|
+
"driver": driver,
|
33
|
+
"user": parsed.username,
|
34
|
+
"password": parsed.password,
|
35
|
+
"host": parsed.hostname,
|
36
|
+
"port": str(parsed.port) if parsed.port else None,
|
37
|
+
"database": parsed.path.lstrip("/") if parsed.path else None,
|
38
|
+
"query": parsed.query,
|
39
|
+
"fragment": parsed.fragment,
|
40
|
+
}
|
41
|
+
|
42
|
+
|
43
|
+
def build_database_uri(
|
44
|
+
scheme: str = "postgresql",
|
45
|
+
driver: Optional[str] = None,
|
46
|
+
user: Optional[str] = None,
|
47
|
+
password: Optional[str] = None,
|
48
|
+
host: Optional[str] = None,
|
49
|
+
port: Optional[str] = None,
|
50
|
+
database: Optional[str] = None,
|
51
|
+
query: Optional[str] = None,
|
52
|
+
fragment: Optional[str] = None,
|
53
|
+
) -> str:
|
54
|
+
"""
|
55
|
+
Build a database URI from components.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
scheme: Base scheme (e.g., "postgresql")
|
59
|
+
driver: Driver name (e.g., "asyncpg", "pg8000")
|
60
|
+
user: Username
|
61
|
+
password: Password
|
62
|
+
host: Hostname
|
63
|
+
port: Port number
|
64
|
+
database: Database name
|
65
|
+
query: Query string
|
66
|
+
fragment: Fragment
|
67
|
+
|
68
|
+
Returns:
|
69
|
+
Complete database URI
|
70
|
+
"""
|
71
|
+
# Combine scheme and driver
|
72
|
+
full_scheme = f"{scheme}+{driver}" if driver else scheme
|
73
|
+
|
74
|
+
# Build netloc (user:password@host:port)
|
75
|
+
netloc_parts = []
|
76
|
+
if user:
|
77
|
+
if password:
|
78
|
+
netloc_parts.append(f"{user}:{password}")
|
79
|
+
else:
|
80
|
+
netloc_parts.append(user)
|
81
|
+
|
82
|
+
if host:
|
83
|
+
if port:
|
84
|
+
netloc_parts.append(f"{host}:{port}")
|
85
|
+
else:
|
86
|
+
netloc_parts.append(host)
|
87
|
+
|
88
|
+
netloc = "@".join(netloc_parts) if netloc_parts else ""
|
89
|
+
|
90
|
+
# Build path
|
91
|
+
path = f"/{database}" if database else ""
|
92
|
+
|
93
|
+
# Build the URI
|
94
|
+
return urlunparse((full_scheme, netloc, path, "", query or "", fragment or ""))
|
95
|
+
|
96
|
+
|
97
|
+
def convert_to_async_uri(uri: str) -> str:
|
98
|
+
"""
|
99
|
+
Convert a database URI to use the asyncpg driver for async operations.
|
100
|
+
|
101
|
+
Args:
|
102
|
+
uri: Original database URI
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
URI with asyncpg driver and ssl parameter adjustments
|
106
|
+
"""
|
107
|
+
components = parse_database_uri(uri)
|
108
|
+
|
109
|
+
# Convert to asyncpg driver
|
110
|
+
components["driver"] = "asyncpg"
|
111
|
+
|
112
|
+
# Build the new URI
|
113
|
+
new_uri = build_database_uri(**components)
|
114
|
+
|
115
|
+
# Replace sslmode= with ssl= for asyncpg compatibility
|
116
|
+
new_uri = new_uri.replace("sslmode=", "ssl=")
|
117
|
+
|
118
|
+
return new_uri
|
119
|
+
|
120
|
+
|
121
|
+
def convert_to_sync_uri(uri: str) -> str:
|
122
|
+
"""
|
123
|
+
Convert a database URI to use the pg8000 driver for sync operations (alembic).
|
124
|
+
|
125
|
+
Args:
|
126
|
+
uri: Original database URI
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
URI with pg8000 driver and sslmode parameter adjustments
|
130
|
+
"""
|
131
|
+
components = parse_database_uri(uri)
|
132
|
+
|
133
|
+
# Convert to pg8000 driver
|
134
|
+
components["driver"] = "pg8000"
|
135
|
+
|
136
|
+
# Build the new URI
|
137
|
+
new_uri = build_database_uri(**components)
|
138
|
+
|
139
|
+
# Replace ssl= with sslmode= for pg8000 compatibility
|
140
|
+
new_uri = new_uri.replace("ssl=", "sslmode=")
|
141
|
+
|
142
|
+
return new_uri
|
143
|
+
|
144
|
+
|
145
|
+
def get_database_uri_for_context(uri: str, context: str = "async") -> str:
|
146
|
+
"""
|
147
|
+
Get the appropriate database URI for a specific context.
|
148
|
+
|
149
|
+
Args:
|
150
|
+
uri: Original database URI
|
151
|
+
context: Context type ("async" for asyncpg, "sync" for pg8000, "alembic" for pg8000)
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
URI formatted for the specified context
|
155
|
+
"""
|
156
|
+
if context in ["async"]:
|
157
|
+
return convert_to_async_uri(uri)
|
158
|
+
elif context in ["sync", "alembic"]:
|
159
|
+
return convert_to_sync_uri(uri)
|
160
|
+
else:
|
161
|
+
raise ValueError(f"Unknown context: {context}. Must be 'async', 'sync', or 'alembic'")
|
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
|
|