letta-nightly 0.12.1.dev20251024104217__py3-none-any.whl → 0.13.0.dev20251025104015__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.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +2 -3
- letta/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/simple_llm_request_adapter.py +8 -5
- letta/adapters/simple_llm_stream_adapter.py +22 -6
- letta/agents/agent_loop.py +10 -3
- letta/agents/base_agent.py +4 -1
- letta/agents/helpers.py +41 -9
- letta/agents/letta_agent.py +11 -10
- letta/agents/letta_agent_v2.py +47 -37
- letta/agents/letta_agent_v3.py +395 -300
- letta/agents/voice_agent.py +8 -6
- letta/agents/voice_sleeptime_agent.py +3 -3
- letta/constants.py +30 -7
- letta/errors.py +20 -0
- letta/functions/function_sets/base.py +55 -3
- letta/functions/mcp_client/types.py +33 -57
- letta/functions/schema_generator.py +135 -23
- letta/groups/sleeptime_multi_agent_v3.py +6 -11
- letta/groups/sleeptime_multi_agent_v4.py +227 -0
- letta/helpers/converters.py +78 -4
- letta/helpers/crypto_utils.py +6 -2
- letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
- letta/interfaces/anthropic_streaming_interface.py +3 -4
- letta/interfaces/gemini_streaming_interface.py +4 -6
- letta/interfaces/openai_streaming_interface.py +63 -28
- letta/llm_api/anthropic_client.py +7 -4
- letta/llm_api/deepseek_client.py +6 -4
- letta/llm_api/google_ai_client.py +3 -12
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/helpers.py +90 -61
- letta/llm_api/llm_api_tools.py +4 -1
- letta/llm_api/openai.py +12 -12
- letta/llm_api/openai_client.py +53 -16
- letta/local_llm/constants.py +4 -3
- letta/local_llm/json_parser.py +5 -2
- letta/local_llm/utils.py +2 -3
- letta/log.py +171 -7
- letta/orm/agent.py +43 -9
- letta/orm/archive.py +4 -0
- letta/orm/custom_columns.py +15 -0
- letta/orm/identity.py +11 -11
- letta/orm/mcp_server.py +9 -0
- letta/orm/message.py +6 -1
- letta/orm/run_metrics.py +7 -2
- letta/orm/sqlalchemy_base.py +2 -2
- letta/orm/tool.py +3 -0
- letta/otel/tracing.py +2 -0
- letta/prompts/prompt_generator.py +7 -2
- letta/schemas/agent.py +41 -10
- letta/schemas/agent_file.py +3 -0
- letta/schemas/archive.py +4 -2
- letta/schemas/block.py +2 -1
- letta/schemas/enums.py +36 -3
- letta/schemas/file.py +3 -3
- letta/schemas/folder.py +2 -1
- letta/schemas/group.py +2 -1
- letta/schemas/identity.py +18 -9
- letta/schemas/job.py +3 -1
- letta/schemas/letta_message.py +71 -12
- letta/schemas/letta_request.py +7 -3
- letta/schemas/letta_stop_reason.py +0 -25
- letta/schemas/llm_config.py +8 -2
- letta/schemas/mcp.py +80 -83
- letta/schemas/mcp_server.py +349 -0
- letta/schemas/memory.py +20 -8
- letta/schemas/message.py +212 -67
- letta/schemas/providers/anthropic.py +13 -6
- letta/schemas/providers/azure.py +6 -4
- letta/schemas/providers/base.py +8 -4
- letta/schemas/providers/bedrock.py +6 -2
- letta/schemas/providers/cerebras.py +7 -3
- letta/schemas/providers/deepseek.py +2 -1
- letta/schemas/providers/google_gemini.py +15 -6
- letta/schemas/providers/groq.py +2 -1
- letta/schemas/providers/lmstudio.py +9 -6
- letta/schemas/providers/mistral.py +2 -1
- letta/schemas/providers/openai.py +7 -2
- letta/schemas/providers/together.py +9 -3
- letta/schemas/providers/xai.py +7 -3
- letta/schemas/run.py +7 -2
- letta/schemas/run_metrics.py +2 -1
- letta/schemas/sandbox_config.py +2 -2
- letta/schemas/secret.py +3 -158
- letta/schemas/source.py +2 -2
- letta/schemas/step.py +2 -2
- letta/schemas/tool.py +24 -1
- letta/schemas/usage.py +0 -1
- letta/server/rest_api/app.py +123 -7
- letta/server/rest_api/dependencies.py +3 -0
- letta/server/rest_api/interface.py +7 -4
- letta/server/rest_api/redis_stream_manager.py +16 -1
- letta/server/rest_api/routers/v1/__init__.py +7 -0
- letta/server/rest_api/routers/v1/agents.py +332 -322
- letta/server/rest_api/routers/v1/archives.py +127 -40
- letta/server/rest_api/routers/v1/blocks.py +54 -6
- letta/server/rest_api/routers/v1/chat_completions.py +146 -0
- letta/server/rest_api/routers/v1/folders.py +27 -35
- letta/server/rest_api/routers/v1/groups.py +23 -35
- letta/server/rest_api/routers/v1/identities.py +24 -10
- letta/server/rest_api/routers/v1/internal_runs.py +107 -0
- letta/server/rest_api/routers/v1/internal_templates.py +162 -179
- letta/server/rest_api/routers/v1/jobs.py +15 -27
- letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
- letta/server/rest_api/routers/v1/messages.py +23 -34
- letta/server/rest_api/routers/v1/organizations.py +6 -27
- letta/server/rest_api/routers/v1/providers.py +35 -62
- letta/server/rest_api/routers/v1/runs.py +30 -43
- letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
- letta/server/rest_api/routers/v1/sources.py +26 -42
- letta/server/rest_api/routers/v1/steps.py +16 -29
- letta/server/rest_api/routers/v1/tools.py +17 -13
- letta/server/rest_api/routers/v1/users.py +5 -17
- letta/server/rest_api/routers/v1/voice.py +18 -27
- letta/server/rest_api/streaming_response.py +5 -2
- letta/server/rest_api/utils.py +187 -25
- letta/server/server.py +27 -22
- letta/server/ws_api/server.py +5 -4
- letta/services/agent_manager.py +148 -26
- letta/services/agent_serialization_manager.py +6 -1
- letta/services/archive_manager.py +168 -15
- letta/services/block_manager.py +14 -4
- letta/services/file_manager.py +33 -29
- letta/services/group_manager.py +10 -0
- letta/services/helpers/agent_manager_helper.py +65 -11
- letta/services/identity_manager.py +105 -4
- letta/services/job_manager.py +11 -1
- letta/services/mcp/base_client.py +2 -2
- letta/services/mcp/oauth_utils.py +33 -8
- letta/services/mcp_manager.py +174 -78
- letta/services/mcp_server_manager.py +1331 -0
- letta/services/message_manager.py +109 -4
- letta/services/organization_manager.py +4 -4
- letta/services/passage_manager.py +9 -25
- letta/services/provider_manager.py +91 -15
- letta/services/run_manager.py +72 -15
- letta/services/sandbox_config_manager.py +45 -3
- letta/services/source_manager.py +15 -8
- letta/services/step_manager.py +24 -1
- letta/services/streaming_service.py +581 -0
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/tool_executor/core_tool_executor.py +111 -0
- letta/services/tool_executor/files_tool_executor.py +5 -3
- letta/services/tool_executor/sandbox_tool_executor.py +2 -2
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_manager.py +10 -3
- letta/services/tool_sandbox/base.py +61 -1
- letta/services/tool_sandbox/local_sandbox.py +1 -3
- letta/services/user_manager.py +2 -2
- letta/settings.py +49 -5
- letta/system.py +14 -5
- letta/utils.py +73 -1
- letta/validators.py +105 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/METADATA +4 -2
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/RECORD +157 -151
- letta/schemas/letta_ping.py +0 -28
- letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/WHEEL +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/licenses/LICENSE +0 -0
letta/agents/voice_agent.py
CHANGED
|
@@ -36,9 +36,9 @@ from letta.server.rest_api.utils import (
|
|
|
36
36
|
)
|
|
37
37
|
from letta.services.agent_manager import AgentManager
|
|
38
38
|
from letta.services.block_manager import BlockManager
|
|
39
|
-
from letta.services.job_manager import JobManager
|
|
40
39
|
from letta.services.message_manager import MessageManager
|
|
41
40
|
from letta.services.passage_manager import PassageManager
|
|
41
|
+
from letta.services.run_manager import RunManager
|
|
42
42
|
from letta.services.summarizer.enums import SummarizationMode
|
|
43
43
|
from letta.services.summarizer.summarizer import Summarizer
|
|
44
44
|
from letta.services.tool_executor.tool_execution_manager import ToolExecutionManager
|
|
@@ -63,7 +63,7 @@ class VoiceAgent(BaseAgent):
|
|
|
63
63
|
message_manager: MessageManager,
|
|
64
64
|
agent_manager: AgentManager,
|
|
65
65
|
block_manager: BlockManager,
|
|
66
|
-
|
|
66
|
+
run_manager: RunManager,
|
|
67
67
|
passage_manager: PassageManager,
|
|
68
68
|
actor: User,
|
|
69
69
|
):
|
|
@@ -73,7 +73,7 @@ class VoiceAgent(BaseAgent):
|
|
|
73
73
|
|
|
74
74
|
# Summarizer settings
|
|
75
75
|
self.block_manager = block_manager
|
|
76
|
-
self.
|
|
76
|
+
self.run_manager = run_manager
|
|
77
77
|
self.passage_manager = passage_manager
|
|
78
78
|
# TODO: This is not guaranteed to exist!
|
|
79
79
|
self.summary_block_label = "human"
|
|
@@ -99,7 +99,7 @@ class VoiceAgent(BaseAgent):
|
|
|
99
99
|
agent_manager=self.agent_manager,
|
|
100
100
|
actor=self.actor,
|
|
101
101
|
block_manager=self.block_manager,
|
|
102
|
-
|
|
102
|
+
run_manager=self.run_manager,
|
|
103
103
|
passage_manager=self.passage_manager,
|
|
104
104
|
target_block_label=self.summary_block_label,
|
|
105
105
|
),
|
|
@@ -153,6 +153,7 @@ class VoiceAgent(BaseAgent):
|
|
|
153
153
|
archival_memory_size=self.num_archival_memories,
|
|
154
154
|
sources=agent_state.sources,
|
|
155
155
|
max_files_open=agent_state.max_files_open,
|
|
156
|
+
llm_config=agent_state.llm_config,
|
|
156
157
|
)
|
|
157
158
|
letta_message_db_queue = create_input_messages(
|
|
158
159
|
input_messages=input_messages, agent_id=agent_state.id, timezone=agent_state.timezone, actor=self.actor
|
|
@@ -437,13 +438,14 @@ class VoiceAgent(BaseAgent):
|
|
|
437
438
|
)
|
|
438
439
|
|
|
439
440
|
# Use ToolExecutionManager for modern tool execution
|
|
440
|
-
|
|
441
|
+
# Decrypt environment variable values
|
|
442
|
+
sandbox_env_vars = {var.key: var.get_value_secret().get_plaintext() for var in agent_state.secrets}
|
|
441
443
|
tool_execution_manager = ToolExecutionManager(
|
|
442
444
|
agent_state=agent_state,
|
|
443
445
|
message_manager=self.message_manager,
|
|
444
446
|
agent_manager=self.agent_manager,
|
|
445
447
|
block_manager=self.block_manager,
|
|
446
|
-
|
|
448
|
+
run_manager=self.run_manager,
|
|
447
449
|
passage_manager=self.passage_manager,
|
|
448
450
|
sandbox_env_vars=sandbox_env_vars,
|
|
449
451
|
actor=self.actor,
|
|
@@ -14,9 +14,9 @@ from letta.schemas.tool_rule import ChildToolRule, ContinueToolRule, InitToolRul
|
|
|
14
14
|
from letta.schemas.user import User
|
|
15
15
|
from letta.services.agent_manager import AgentManager
|
|
16
16
|
from letta.services.block_manager import BlockManager
|
|
17
|
-
from letta.services.job_manager import JobManager
|
|
18
17
|
from letta.services.message_manager import MessageManager
|
|
19
18
|
from letta.services.passage_manager import PassageManager
|
|
19
|
+
from letta.services.run_manager import RunManager
|
|
20
20
|
from letta.services.summarizer.enums import SummarizationMode
|
|
21
21
|
from letta.services.summarizer.summarizer import Summarizer
|
|
22
22
|
from letta.types import JsonDict
|
|
@@ -34,7 +34,7 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
|
34
34
|
message_manager: MessageManager,
|
|
35
35
|
agent_manager: AgentManager,
|
|
36
36
|
block_manager: BlockManager,
|
|
37
|
-
|
|
37
|
+
run_manager: RunManager,
|
|
38
38
|
passage_manager: PassageManager,
|
|
39
39
|
target_block_label: str,
|
|
40
40
|
actor: User,
|
|
@@ -44,7 +44,7 @@ class VoiceSleeptimeAgent(LettaAgent):
|
|
|
44
44
|
message_manager=message_manager,
|
|
45
45
|
agent_manager=agent_manager,
|
|
46
46
|
block_manager=block_manager,
|
|
47
|
-
job_manager=
|
|
47
|
+
job_manager=run_manager,
|
|
48
48
|
passage_manager=passage_manager,
|
|
49
49
|
actor=actor,
|
|
50
50
|
)
|
letta/constants.py
CHANGED
|
@@ -33,8 +33,6 @@ LETTA_TOOL_MODULE_NAMES = [
|
|
|
33
33
|
DEFAULT_ORG_ID = "org-00000000-0000-4000-8000-000000000000"
|
|
34
34
|
DEFAULT_ORG_NAME = "default_org"
|
|
35
35
|
|
|
36
|
-
AGENT_ID_PATTERN = re.compile(r"^agent-[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", re.IGNORECASE)
|
|
37
|
-
|
|
38
36
|
# String in the error message for when the context window is too large
|
|
39
37
|
# Example full message:
|
|
40
38
|
# This model's maximum context length is 8192 tokens. However, your messages resulted in 8198 tokens (7450 in the messages, 748 in the functions). Please reduce the length of the messages or functions.
|
|
@@ -127,10 +125,10 @@ LOCAL_ONLY_MULTI_AGENT_TOOLS = ["send_message_to_agent_async"]
|
|
|
127
125
|
|
|
128
126
|
# Used to catch if line numbers are pushed in
|
|
129
127
|
# MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX = re.compile(r"^Line \d+: ", re.MULTILINE)
|
|
130
|
-
#
|
|
128
|
+
# Updated to match new arrow format: "1→ content"
|
|
131
129
|
# shared constant for both memory_insert and memory_replace
|
|
132
130
|
MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX = re.compile(
|
|
133
|
-
r"^[ \t]
|
|
131
|
+
r"^[ \t]*\d+→[ \t]*", # match number followed by arrow, with optional whitespace
|
|
134
132
|
re.MULTILINE,
|
|
135
133
|
)
|
|
136
134
|
|
|
@@ -157,6 +155,16 @@ LETTA_TOOL_SET = set(
|
|
|
157
155
|
+ FILES_TOOLS
|
|
158
156
|
)
|
|
159
157
|
|
|
158
|
+
LETTA_PARALLEL_SAFE_TOOLS = {
|
|
159
|
+
"conversation_search",
|
|
160
|
+
"archival_memory_search",
|
|
161
|
+
"run_code",
|
|
162
|
+
"web_search",
|
|
163
|
+
"fetch_webpage",
|
|
164
|
+
"grep_files",
|
|
165
|
+
"semantic_search_files",
|
|
166
|
+
}
|
|
167
|
+
|
|
160
168
|
|
|
161
169
|
def FUNCTION_RETURN_VALUE_TRUNCATED(return_str, return_char: int, return_char_limit: int):
|
|
162
170
|
return (
|
|
@@ -202,9 +210,7 @@ ERROR_MESSAGE_PREFIX = "Error"
|
|
|
202
210
|
|
|
203
211
|
NON_USER_MSG_PREFIX = "[This is an automated system message hidden from the user] "
|
|
204
212
|
|
|
205
|
-
CORE_MEMORY_LINE_NUMBER_WARNING = (
|
|
206
|
-
"# NOTE: Line numbers shown below are to help during editing. Do NOT include line number prefixes in your memory edit tool calls."
|
|
207
|
-
)
|
|
213
|
+
CORE_MEMORY_LINE_NUMBER_WARNING = "# NOTE: Line numbers shown below (with arrows like '1→') are to help during editing. Do NOT include line number prefixes in your memory edit tool calls."
|
|
208
214
|
|
|
209
215
|
|
|
210
216
|
# Constants to do with summarization / conversation length window
|
|
@@ -318,6 +324,23 @@ LLM_MAX_TOKENS = {
|
|
|
318
324
|
"gemini-2.0-flash-thinking-exp-1219": 1048576,
|
|
319
325
|
"gemini-2.5-flash-preview-tts": 32768,
|
|
320
326
|
"gemini-2.5-pro-preview-tts": 65536,
|
|
327
|
+
# gemini 2.5 stable releases
|
|
328
|
+
"gemini-2.5-flash": 1048576,
|
|
329
|
+
"gemini-2.5-flash-lite": 1048576,
|
|
330
|
+
"gemini-2.5-pro": 1048576,
|
|
331
|
+
"gemini-2.5-pro-preview-06-05": 1048576,
|
|
332
|
+
"gemini-2.5-flash-lite-preview-06-17": 1048576,
|
|
333
|
+
"gemini-2.5-flash-image": 1048576,
|
|
334
|
+
"gemini-2.5-flash-image-preview": 1048576,
|
|
335
|
+
"gemini-2.5-flash-preview-09-2025": 1048576,
|
|
336
|
+
"gemini-2.5-flash-lite-preview-09-2025": 1048576,
|
|
337
|
+
"gemini-2.5-computer-use-preview-10-2025": 1048576,
|
|
338
|
+
# gemini latest aliases
|
|
339
|
+
"gemini-flash-latest": 1048576,
|
|
340
|
+
"gemini-flash-lite-latest": 1048576,
|
|
341
|
+
"gemini-pro-latest": 1048576,
|
|
342
|
+
# gemini specialized models
|
|
343
|
+
"gemini-robotics-er-1.5-preview": 1048576,
|
|
321
344
|
}
|
|
322
345
|
# The error message that Letta will receive
|
|
323
346
|
# MESSAGE_SUMMARY_WARNING_STR = f"Warning: the conversation history will soon reach its maximum length and be trimmed. Make sure to save any important information from the conversation to your memory before it is removed."
|
letta/errors.py
CHANGED
|
@@ -19,6 +19,7 @@ class ErrorCode(Enum):
|
|
|
19
19
|
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
|
|
20
20
|
TIMEOUT = "TIMEOUT"
|
|
21
21
|
CONFLICT = "CONFLICT"
|
|
22
|
+
EXPIRED = "EXPIRED"
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
class LettaError(Exception):
|
|
@@ -97,6 +98,10 @@ class LettaUserNotFoundError(LettaError):
|
|
|
97
98
|
"""Error raised when a user is not found."""
|
|
98
99
|
|
|
99
100
|
|
|
101
|
+
class LettaUnsupportedFileUploadError(LettaError):
|
|
102
|
+
"""Error raised when an unsupported file upload is attempted."""
|
|
103
|
+
|
|
104
|
+
|
|
100
105
|
class LettaInvalidArgumentError(LettaError):
|
|
101
106
|
"""Error raised when an invalid argument is provided."""
|
|
102
107
|
|
|
@@ -137,10 +142,25 @@ class LettaMCPTimeoutError(LettaMCPError):
|
|
|
137
142
|
super().__init__(message=message, code=ErrorCode.TIMEOUT, details=details)
|
|
138
143
|
|
|
139
144
|
|
|
145
|
+
class LettaServiceUnavailableError(LettaError):
|
|
146
|
+
"""Error raised when a required service is unavailable."""
|
|
147
|
+
|
|
148
|
+
def __init__(self, message: str, service_name: Optional[str] = None):
|
|
149
|
+
details = {"service_name": service_name} if service_name else {}
|
|
150
|
+
super().__init__(message=message, code=ErrorCode.INTERNAL_SERVER_ERROR, details=details)
|
|
151
|
+
|
|
152
|
+
|
|
140
153
|
class LettaUnexpectedStreamCancellationError(LettaError):
|
|
141
154
|
"""Error raised when a streaming request is terminated unexpectedly."""
|
|
142
155
|
|
|
143
156
|
|
|
157
|
+
class LettaExpiredError(LettaError):
|
|
158
|
+
"""Error raised when a resource has expired."""
|
|
159
|
+
|
|
160
|
+
def __init__(self, message: str):
|
|
161
|
+
super().__init__(message=message, code=ErrorCode.EXPIRED)
|
|
162
|
+
|
|
163
|
+
|
|
144
164
|
class LLMError(LettaError):
|
|
145
165
|
pass
|
|
146
166
|
|
|
@@ -294,6 +294,7 @@ SNIPPET_LINES: int = 4
|
|
|
294
294
|
def memory_replace(agent_state: "AgentState", label: str, old_str: str, new_str: str) -> str: # type: ignore
|
|
295
295
|
"""
|
|
296
296
|
The memory_replace command allows you to replace a specific string in a memory block with a new string. This is used for making precise edits.
|
|
297
|
+
Do NOT attempt to replace long strings, e.g. do not attempt to replace the entire contents of a memory block with a new string.
|
|
297
298
|
|
|
298
299
|
Args:
|
|
299
300
|
label (str): Section of the memory to be edited, identified by its label.
|
|
@@ -311,10 +312,10 @@ def memory_replace(agent_state: "AgentState", label: str, old_str: str, new_str:
|
|
|
311
312
|
memory_replace(label="human", old_str="Their name is Alice", new_str="")
|
|
312
313
|
|
|
313
314
|
# Bad example - do NOT add (view-only) line numbers to the args
|
|
314
|
-
memory_replace(label="human", old_str="
|
|
315
|
+
memory_replace(label="human", old_str="1: Their name is Alice", new_str="1: Their name is Bob")
|
|
315
316
|
|
|
316
|
-
# Bad example - do NOT include the
|
|
317
|
-
memory_replace(label="human", old_str="# NOTE: Line numbers shown below are to help during editing. Do NOT include line number prefixes in your memory edit tool calls.\\
|
|
317
|
+
# Bad example - do NOT include the line number warning either
|
|
318
|
+
memory_replace(label="human", old_str="# NOTE: Line numbers shown below (with arrows like '1→') are to help during editing. Do NOT include line number prefixes in your memory edit tool calls.\\n1→ Their name is Alice", new_str="1→ Their name is Bob")
|
|
318
319
|
|
|
319
320
|
# Good example - no line numbers or line number warning (they are view-only), just the text
|
|
320
321
|
memory_replace(label="human", old_str="Their name is Alice", new_str="Their name is Bob")
|
|
@@ -449,6 +450,57 @@ def memory_insert(agent_state: "AgentState", label: str, new_str: str, insert_li
|
|
|
449
450
|
return success_msg
|
|
450
451
|
|
|
451
452
|
|
|
453
|
+
def memory_apply_patch(agent_state: "AgentState", label: str, patch: str) -> str: # type: ignore
|
|
454
|
+
"""
|
|
455
|
+
Apply a unified-diff style patch to a memory block by anchoring on content and context (not line numbers).
|
|
456
|
+
|
|
457
|
+
The patch format is a simplified unified diff that supports one or more hunks. Each hunk may optionally
|
|
458
|
+
start with a line beginning with `@@` and then contains lines that begin with one of:
|
|
459
|
+
- " " (space): context lines that must match the current memory content
|
|
460
|
+
- "-": lines to remove (must match exactly in the current content)
|
|
461
|
+
- "+": lines to add
|
|
462
|
+
|
|
463
|
+
Notes:
|
|
464
|
+
- Do not include line number prefixes like "Line 12:" anywhere in the patch. Line numbers are for display only.
|
|
465
|
+
- Do not include the line-number warning banner. Provide only the text to edit.
|
|
466
|
+
- Tabs are normalized to spaces for matching consistency.
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
label (str): The memory block to edit, identified by its label.
|
|
470
|
+
patch (str): The simplified unified-diff patch text composed of context (" "), deletion ("-"), and addition ("+") lines. Optional
|
|
471
|
+
lines beginning with "@@" can be used to delimit hunks. Do not include visual line numbers or warning banners.
|
|
472
|
+
|
|
473
|
+
Examples:
|
|
474
|
+
Simple replacement:
|
|
475
|
+
label="human",
|
|
476
|
+
patch:
|
|
477
|
+
@@
|
|
478
|
+
-Their name is Alice
|
|
479
|
+
+Their name is Bob
|
|
480
|
+
|
|
481
|
+
Replacement with surrounding context for disambiguation:
|
|
482
|
+
label="persona",
|
|
483
|
+
patch:
|
|
484
|
+
@@
|
|
485
|
+
Persona:
|
|
486
|
+
-Friendly and curious
|
|
487
|
+
+Friendly, curious, and precise
|
|
488
|
+
Likes: Hiking
|
|
489
|
+
|
|
490
|
+
Insertion (no deletions) between two context lines:
|
|
491
|
+
label="todos",
|
|
492
|
+
patch:
|
|
493
|
+
@@
|
|
494
|
+
- [ ] Step 1: Gather requirements
|
|
495
|
+
+ [ ] Step 1.5: Clarify stakeholders
|
|
496
|
+
- [ ] Step 2: Draft design
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
str: A success message if the patch applied cleanly; raises ValueError otherwise.
|
|
500
|
+
"""
|
|
501
|
+
raise NotImplementedError("This should never be invoked directly. Contact Letta if you see this error message.")
|
|
502
|
+
|
|
503
|
+
|
|
452
504
|
def memory_rethink(agent_state: "AgentState", label: str, new_memory: str) -> None:
|
|
453
505
|
"""
|
|
454
506
|
The memory_rethink command allows you to completely rewrite the contents of a memory block. Use this tool to make large sweeping changes (e.g. when you want to condense or reorganize the memory blocks), do NOT use this tool to make small precise edits (e.g. add or remove a line, replace a specific string, etc).
|
|
@@ -129,9 +129,9 @@ class BaseServerConfig(BaseModel):
|
|
|
129
129
|
raise NotImplementedError
|
|
130
130
|
|
|
131
131
|
|
|
132
|
-
class
|
|
132
|
+
class HTTPBasedServerConfig(BaseServerConfig):
|
|
133
133
|
"""
|
|
134
|
-
|
|
134
|
+
Base configuration for HTTP-based MCP servers (SSE and Streamable HTTP).
|
|
135
135
|
|
|
136
136
|
Authentication can be provided in multiple ways:
|
|
137
137
|
1. Using auth_header + auth_token: Will add a specific header with the token
|
|
@@ -141,11 +141,10 @@ class SSEServerConfig(BaseServerConfig):
|
|
|
141
141
|
Example: custom_headers={"X-API-Key": "abc123", "X-Custom-Header": "value"}
|
|
142
142
|
"""
|
|
143
143
|
|
|
144
|
-
|
|
145
|
-
server_url: str = Field(..., description="The URL of the server (MCP SSE client will connect to this URL)")
|
|
144
|
+
server_url: str = Field(..., description="The URL of the server")
|
|
146
145
|
auth_header: Optional[str] = Field(None, description="The name of the authentication header (e.g., 'Authorization')")
|
|
147
146
|
auth_token: Optional[str] = Field(None, description="The authentication token or API key value")
|
|
148
|
-
custom_headers: Optional[dict[str, str]] = Field(None, description="Custom HTTP headers to include with
|
|
147
|
+
custom_headers: Optional[dict[str, str]] = Field(None, description="Custom HTTP headers to include with requests")
|
|
149
148
|
|
|
150
149
|
def resolve_token(self) -> Optional[str]:
|
|
151
150
|
"""
|
|
@@ -170,13 +169,13 @@ class SSEServerConfig(BaseServerConfig):
|
|
|
170
169
|
|
|
171
170
|
self.custom_headers = super().resolve_custom_headers(self.custom_headers, environment_variables)
|
|
172
171
|
|
|
173
|
-
def
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
"url": self.server_url,
|
|
177
|
-
}
|
|
172
|
+
def _build_headers_dict(self) -> Optional[dict[str, str]]:
|
|
173
|
+
"""
|
|
174
|
+
Build headers dictionary from custom_headers and auth_header/auth_token.
|
|
178
175
|
|
|
179
|
-
|
|
176
|
+
Returns:
|
|
177
|
+
Dictionary of headers or None if no headers are configured
|
|
178
|
+
"""
|
|
180
179
|
if self.custom_headers is not None or (self.auth_header is not None and self.auth_token is not None):
|
|
181
180
|
headers = self.custom_headers.copy() if self.custom_headers else {}
|
|
182
181
|
|
|
@@ -184,6 +183,24 @@ class SSEServerConfig(BaseServerConfig):
|
|
|
184
183
|
if self.auth_header is not None and self.auth_token is not None:
|
|
185
184
|
headers[self.auth_header] = self.auth_token
|
|
186
185
|
|
|
186
|
+
return headers
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class SSEServerConfig(HTTPBasedServerConfig):
|
|
191
|
+
"""Configuration for an MCP server using SSE"""
|
|
192
|
+
|
|
193
|
+
type: MCPServerType = MCPServerType.SSE
|
|
194
|
+
|
|
195
|
+
def to_dict(self) -> dict:
|
|
196
|
+
values = {
|
|
197
|
+
"transport": "sse",
|
|
198
|
+
"url": self.server_url,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Handle custom headers using shared method
|
|
202
|
+
headers = self._build_headers_dict()
|
|
203
|
+
if headers:
|
|
187
204
|
values["headers"] = headers
|
|
188
205
|
|
|
189
206
|
return values
|
|
@@ -210,46 +227,10 @@ class StdioServerConfig(BaseServerConfig):
|
|
|
210
227
|
return values
|
|
211
228
|
|
|
212
229
|
|
|
213
|
-
class StreamableHTTPServerConfig(
|
|
214
|
-
"""
|
|
215
|
-
Configuration for an MCP server using Streamable HTTP
|
|
216
|
-
|
|
217
|
-
Authentication can be provided in multiple ways:
|
|
218
|
-
1. Using auth_header + auth_token: Will add a specific header with the token
|
|
219
|
-
Example: auth_header="Authorization", auth_token="Bearer abc123"
|
|
220
|
-
|
|
221
|
-
2. Using the custom_headers dict: For more complex authentication scenarios
|
|
222
|
-
Example: custom_headers={"X-API-Key": "abc123", "X-Custom-Header": "value"}
|
|
223
|
-
"""
|
|
230
|
+
class StreamableHTTPServerConfig(HTTPBasedServerConfig):
|
|
231
|
+
"""Configuration for an MCP server using Streamable HTTP"""
|
|
224
232
|
|
|
225
233
|
type: MCPServerType = MCPServerType.STREAMABLE_HTTP
|
|
226
|
-
server_url: str = Field(..., description="The URL path for the streamable HTTP server (e.g., 'example/mcp')")
|
|
227
|
-
auth_header: Optional[str] = Field(None, description="The name of the authentication header (e.g., 'Authorization')")
|
|
228
|
-
auth_token: Optional[str] = Field(None, description="The authentication token or API key value")
|
|
229
|
-
custom_headers: Optional[dict[str, str]] = Field(None, description="Custom HTTP headers to include with streamable HTTP requests")
|
|
230
|
-
|
|
231
|
-
def resolve_token(self) -> Optional[str]:
|
|
232
|
-
"""
|
|
233
|
-
Extract token for storage if auth_header/auth_token are provided
|
|
234
|
-
and not already in custom_headers.
|
|
235
|
-
|
|
236
|
-
Returns:
|
|
237
|
-
The resolved token (without Bearer prefix) if it should be stored separately, None otherwise
|
|
238
|
-
"""
|
|
239
|
-
if self.auth_token and self.auth_header:
|
|
240
|
-
# Check if custom_headers already has the auth header
|
|
241
|
-
if not self.custom_headers or self.auth_header not in self.custom_headers:
|
|
242
|
-
# Strip Bearer prefix if present
|
|
243
|
-
if self.auth_token.startswith(f"{MCP_AUTH_TOKEN_BEARER_PREFIX} "):
|
|
244
|
-
return self.auth_token[len(f"{MCP_AUTH_TOKEN_BEARER_PREFIX} ") :]
|
|
245
|
-
return self.auth_token
|
|
246
|
-
return None
|
|
247
|
-
|
|
248
|
-
def resolve_environment_variables(self, environment_variables: Optional[Dict[str, str]] = None) -> None:
|
|
249
|
-
if self.auth_token and super().is_templated_tool_variable(self.auth_token):
|
|
250
|
-
self.auth_token = super().get_tool_variable(self.auth_token, environment_variables)
|
|
251
|
-
|
|
252
|
-
self.custom_headers = super().resolve_custom_headers(self.custom_headers, environment_variables)
|
|
253
234
|
|
|
254
235
|
def model_post_init(self, __context) -> None:
|
|
255
236
|
"""Validate the server URL format."""
|
|
@@ -275,14 +256,9 @@ class StreamableHTTPServerConfig(BaseServerConfig):
|
|
|
275
256
|
"url": self.server_url,
|
|
276
257
|
}
|
|
277
258
|
|
|
278
|
-
# Handle custom headers
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
# Add auth header if specified
|
|
283
|
-
if self.auth_header is not None and self.auth_token is not None:
|
|
284
|
-
headers[self.auth_header] = self.auth_token
|
|
285
|
-
|
|
259
|
+
# Handle custom headers using shared method
|
|
260
|
+
headers = self._build_headers_dict()
|
|
261
|
+
if headers:
|
|
286
262
|
values["headers"] = headers
|
|
287
263
|
|
|
288
264
|
return values
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import inspect
|
|
2
|
-
import warnings
|
|
3
2
|
from typing import Any, Dict, List, Optional, Tuple, Type, Union, get_args, get_origin
|
|
4
3
|
|
|
5
4
|
from docstring_parser import parse
|
|
@@ -101,7 +100,7 @@ def type_to_json_schema_type(py_type) -> dict:
|
|
|
101
100
|
args = get_args(py_type)
|
|
102
101
|
if len(args) == 0:
|
|
103
102
|
# is this correct
|
|
104
|
-
|
|
103
|
+
logger.warning("Defaulting to string type for untyped List")
|
|
105
104
|
return {
|
|
106
105
|
"type": "array",
|
|
107
106
|
"items": {"type": "string"},
|
|
@@ -662,6 +661,16 @@ def normalize_mcp_schema(schema: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
662
661
|
# Handle anyOf (complex union types)
|
|
663
662
|
if "anyOf" in prop_schema:
|
|
664
663
|
for option in prop_schema["anyOf"]:
|
|
664
|
+
# Add explicit type to $ref options for flattening support
|
|
665
|
+
if "$ref" in option and "type" not in option:
|
|
666
|
+
if defs and option["$ref"].startswith("#/$defs/"):
|
|
667
|
+
def_name = option["$ref"].split("/")[-1]
|
|
668
|
+
if def_name in defs and "type" in defs[def_name]:
|
|
669
|
+
option["type"] = defs[def_name]["type"]
|
|
670
|
+
# Default to object if type can't be resolved
|
|
671
|
+
if "type" not in option:
|
|
672
|
+
option["type"] = "object"
|
|
673
|
+
# Recursively normalize object types
|
|
665
674
|
if isinstance(option, dict) and option.get("type") == "object":
|
|
666
675
|
normalize_object_schema(option, defs)
|
|
667
676
|
|
|
@@ -710,28 +719,131 @@ def generate_tool_schema_for_mcp(
|
|
|
710
719
|
# Normalise so downstream code can treat it consistently.
|
|
711
720
|
parameters_schema.setdefault("required", [])
|
|
712
721
|
|
|
713
|
-
#
|
|
714
|
-
|
|
722
|
+
# Get $defs for $ref resolution
|
|
723
|
+
defs = parameters_schema.get("$defs", {})
|
|
724
|
+
|
|
725
|
+
def deduplicate_anyof(anyof_list):
|
|
726
|
+
"""
|
|
727
|
+
Deduplicate entries in an anyOf array based on their content.
|
|
728
|
+
|
|
729
|
+
Rules:
|
|
730
|
+
1. Remove exact duplicates (same type, same properties)
|
|
731
|
+
2. For duplicate types with different metadata (e.g., format):
|
|
732
|
+
- Keep the most specific version (with format/constraints)
|
|
733
|
+
- If one has format and others don't, keep only the one with format
|
|
734
|
+
"""
|
|
735
|
+
if not anyof_list:
|
|
736
|
+
return anyof_list
|
|
737
|
+
|
|
738
|
+
seen = []
|
|
739
|
+
result = []
|
|
740
|
+
|
|
741
|
+
for item in anyof_list:
|
|
742
|
+
if not isinstance(item, dict):
|
|
743
|
+
if item not in seen:
|
|
744
|
+
seen.append(item)
|
|
745
|
+
result.append(item)
|
|
746
|
+
continue
|
|
747
|
+
|
|
748
|
+
# Create a hashable representation for comparison
|
|
749
|
+
# Sort keys to ensure consistent comparison
|
|
750
|
+
item_type = item.get("type")
|
|
751
|
+
item_format = item.get("format")
|
|
752
|
+
|
|
753
|
+
# Check if we've seen this exact item
|
|
754
|
+
is_duplicate = False
|
|
755
|
+
for existing_idx, existing in enumerate(result):
|
|
756
|
+
if not isinstance(existing, dict):
|
|
757
|
+
continue
|
|
758
|
+
|
|
759
|
+
existing_type = existing.get("type")
|
|
760
|
+
existing_format = existing.get("format")
|
|
761
|
+
|
|
762
|
+
# Exact match - skip this item
|
|
763
|
+
if item == existing:
|
|
764
|
+
is_duplicate = True
|
|
765
|
+
break
|
|
766
|
+
|
|
767
|
+
# Same type with different format handling
|
|
768
|
+
if item_type and item_type == existing_type:
|
|
769
|
+
# Both have same type
|
|
770
|
+
if item_format and not existing_format:
|
|
771
|
+
# New item has format, existing doesn't - replace existing with new
|
|
772
|
+
result[existing_idx] = item
|
|
773
|
+
is_duplicate = True
|
|
774
|
+
break
|
|
775
|
+
elif not item_format and existing_format:
|
|
776
|
+
# Existing has format, new doesn't - keep existing, skip new
|
|
777
|
+
is_duplicate = True
|
|
778
|
+
break
|
|
779
|
+
elif item_format == existing_format:
|
|
780
|
+
# Same type and format (or both None) - compare full objects
|
|
781
|
+
# Prefer the one with more properties/constraints
|
|
782
|
+
if len(item) >= len(existing):
|
|
783
|
+
result[existing_idx] = item
|
|
784
|
+
is_duplicate = True
|
|
785
|
+
break
|
|
786
|
+
|
|
787
|
+
if not is_duplicate:
|
|
788
|
+
result.append(item)
|
|
789
|
+
|
|
790
|
+
return result
|
|
791
|
+
|
|
792
|
+
def inline_ref(schema_node, defs, depth=0, max_depth=10):
|
|
793
|
+
"""
|
|
794
|
+
Recursively inline all $ref references in a schema node.
|
|
795
|
+
Returns a new schema with all $refs replaced by their definitions.
|
|
796
|
+
"""
|
|
797
|
+
if depth > max_depth:
|
|
798
|
+
return schema_node # Prevent infinite recursion
|
|
799
|
+
|
|
800
|
+
if not isinstance(schema_node, dict):
|
|
801
|
+
return schema_node
|
|
802
|
+
|
|
803
|
+
# Make a copy to avoid modifying the original
|
|
804
|
+
result = schema_node.copy()
|
|
805
|
+
|
|
806
|
+
# If this node has a $ref, resolve it and merge
|
|
807
|
+
if "$ref" in result:
|
|
808
|
+
ref_path = result["$ref"]
|
|
809
|
+
if ref_path.startswith("#/$defs/"):
|
|
810
|
+
def_name = ref_path.split("/")[-1]
|
|
811
|
+
if def_name in defs:
|
|
812
|
+
# Get the referenced schema
|
|
813
|
+
ref_schema = defs[def_name].copy()
|
|
814
|
+
# Remove the $ref
|
|
815
|
+
del result["$ref"]
|
|
816
|
+
# Merge the referenced schema into result
|
|
817
|
+
# The referenced schema properties take precedence
|
|
818
|
+
for key, value in ref_schema.items():
|
|
819
|
+
if key not in result:
|
|
820
|
+
result[key] = value
|
|
821
|
+
# Recursively inline any $refs in the merged schema
|
|
822
|
+
result = inline_ref(result, defs, depth + 1, max_depth)
|
|
823
|
+
|
|
824
|
+
# Recursively process nested structures
|
|
825
|
+
if "anyOf" in result:
|
|
826
|
+
# Inline refs in each anyOf option
|
|
827
|
+
result["anyOf"] = [inline_ref(opt, defs, depth + 1, max_depth) for opt in result["anyOf"]]
|
|
828
|
+
# Deduplicate anyOf entries
|
|
829
|
+
result["anyOf"] = deduplicate_anyof(result["anyOf"])
|
|
830
|
+
if "properties" in result and isinstance(result["properties"], dict):
|
|
831
|
+
result["properties"] = {
|
|
832
|
+
prop_name: inline_ref(prop_schema, defs, depth + 1, max_depth) for prop_name, prop_schema in result["properties"].items()
|
|
833
|
+
}
|
|
834
|
+
if "items" in result:
|
|
835
|
+
result["items"] = inline_ref(result["items"], defs, depth + 1, max_depth)
|
|
836
|
+
|
|
837
|
+
return result
|
|
838
|
+
|
|
839
|
+
# Process properties to inline all $refs while keeping anyOf structure
|
|
715
840
|
if "properties" in parameters_schema:
|
|
716
|
-
for field_name
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
if "type" in option:
|
|
723
|
-
types.append(option["type"])
|
|
724
|
-
# Capture format if present (e.g., uuid format for strings)
|
|
725
|
-
if "format" in option and not format_value:
|
|
726
|
-
format_value = option["format"]
|
|
727
|
-
if types:
|
|
728
|
-
# Deduplicate types using set
|
|
729
|
-
field_props["type"] = list(dict.fromkeys(types))
|
|
730
|
-
# Only add format if the field is not optional (doesn't have null type)
|
|
731
|
-
if format_value and len(field_props["type"]) == 1 and "null" not in field_props["type"]:
|
|
732
|
-
field_props["format"] = format_value
|
|
733
|
-
# Remove the anyOf since we've flattened it
|
|
734
|
-
del field_props["anyOf"]
|
|
841
|
+
for field_name in list(parameters_schema["properties"].keys()):
|
|
842
|
+
field_props = parameters_schema["properties"][field_name]
|
|
843
|
+
|
|
844
|
+
# Inline all $refs in this property (recursively)
|
|
845
|
+
field_props = inline_ref(field_props, defs)
|
|
846
|
+
parameters_schema["properties"][field_name] = field_props
|
|
735
847
|
|
|
736
848
|
# For strict mode: heal optional fields by making them required with null type
|
|
737
849
|
if strict and field_name not in parameters_schema["required"]:
|