letta-nightly 0.12.1.dev20251023104211__py3-none-any.whl → 0.13.0.dev20251024223017__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.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.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.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/licenses/LICENSE +0 -0
|
@@ -47,6 +47,7 @@ class LettaCoreToolExecutor(ToolExecutor):
|
|
|
47
47
|
"core_memory_replace": self.core_memory_replace,
|
|
48
48
|
"memory_replace": self.memory_replace,
|
|
49
49
|
"memory_insert": self.memory_insert,
|
|
50
|
+
"memory_apply_patch": self.memory_apply_patch,
|
|
50
51
|
"memory_str_replace": self.memory_str_replace,
|
|
51
52
|
"memory_str_insert": self.memory_str_insert,
|
|
52
53
|
"memory_rethink": self.memory_rethink,
|
|
@@ -393,6 +394,116 @@ class LettaCoreToolExecutor(ToolExecutor):
|
|
|
393
394
|
# return None
|
|
394
395
|
return success_msg
|
|
395
396
|
|
|
397
|
+
async def memory_apply_patch(self, agent_state: AgentState, actor: User, label: str, patch: str) -> str:
|
|
398
|
+
"""Apply a simplified unified-diff style patch to a memory block, anchored on content and context.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
label: The memory block label to modify.
|
|
402
|
+
patch: Patch text with lines starting with " ", "-", or "+" and optional "@@" hunk headers.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
Success message on clean application; raises ValueError on mismatch/ambiguity.
|
|
406
|
+
"""
|
|
407
|
+
if agent_state.memory.get_block(label).read_only:
|
|
408
|
+
raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
|
|
409
|
+
|
|
410
|
+
# Guardrails: forbid visual line numbers and warning banners
|
|
411
|
+
if MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(patch or ""):
|
|
412
|
+
raise ValueError(
|
|
413
|
+
"Patch contains a line number prefix, which is not allowed. Do not include line numbers (they are for display only)."
|
|
414
|
+
)
|
|
415
|
+
if CORE_MEMORY_LINE_NUMBER_WARNING in (patch or ""):
|
|
416
|
+
raise ValueError("Patch contains the line number warning banner, which is not allowed. Provide only the text to edit.")
|
|
417
|
+
|
|
418
|
+
current_value = str(agent_state.memory.get_block(label).value).expandtabs()
|
|
419
|
+
patch = str(patch).expandtabs()
|
|
420
|
+
|
|
421
|
+
current_lines = current_value.split("\n")
|
|
422
|
+
# Ignore common diff headers
|
|
423
|
+
raw_lines = patch.splitlines()
|
|
424
|
+
patch_lines = [ln for ln in raw_lines if not ln.startswith("*** ") and not ln.startswith("---") and not ln.startswith("+++")]
|
|
425
|
+
|
|
426
|
+
# Split into hunks using '@@' as delimiter
|
|
427
|
+
hunks: list[list[str]] = []
|
|
428
|
+
h: list[str] = []
|
|
429
|
+
for ln in patch_lines:
|
|
430
|
+
if ln.startswith("@@"):
|
|
431
|
+
if h:
|
|
432
|
+
hunks.append(h)
|
|
433
|
+
h = []
|
|
434
|
+
continue
|
|
435
|
+
if ln.startswith(" ") or ln.startswith("-") or ln.startswith("+"):
|
|
436
|
+
h.append(ln)
|
|
437
|
+
elif ln.strip() == "":
|
|
438
|
+
# Treat blank line as context for empty string line
|
|
439
|
+
h.append(" ")
|
|
440
|
+
else:
|
|
441
|
+
# Skip unknown metadata lines
|
|
442
|
+
continue
|
|
443
|
+
if h:
|
|
444
|
+
hunks.append(h)
|
|
445
|
+
|
|
446
|
+
if not hunks:
|
|
447
|
+
raise ValueError("No applicable hunks found in patch. Ensure lines start with ' ', '-', or '+'.")
|
|
448
|
+
|
|
449
|
+
def find_all_subseq(hay: list[str], needle: list[str]) -> list[int]:
|
|
450
|
+
out: list[int] = []
|
|
451
|
+
n = len(needle)
|
|
452
|
+
if n == 0:
|
|
453
|
+
return out
|
|
454
|
+
for i in range(0, len(hay) - n + 1):
|
|
455
|
+
if hay[i : i + n] == needle:
|
|
456
|
+
out.append(i)
|
|
457
|
+
return out
|
|
458
|
+
|
|
459
|
+
# Apply each hunk sequentially against the rolling buffer
|
|
460
|
+
for hunk in hunks:
|
|
461
|
+
expected: list[str] = []
|
|
462
|
+
replacement: list[str] = []
|
|
463
|
+
for ln in hunk:
|
|
464
|
+
if ln.startswith(" "):
|
|
465
|
+
line = ln[1:]
|
|
466
|
+
expected.append(line)
|
|
467
|
+
replacement.append(line)
|
|
468
|
+
elif ln.startswith("-"):
|
|
469
|
+
line = ln[1:]
|
|
470
|
+
expected.append(line)
|
|
471
|
+
elif ln.startswith("+"):
|
|
472
|
+
line = ln[1:]
|
|
473
|
+
replacement.append(line)
|
|
474
|
+
|
|
475
|
+
if not expected and replacement:
|
|
476
|
+
# Pure insertion with no context: append at end
|
|
477
|
+
current_lines = current_lines + replacement
|
|
478
|
+
continue
|
|
479
|
+
|
|
480
|
+
matches = find_all_subseq(current_lines, expected)
|
|
481
|
+
if len(matches) == 0:
|
|
482
|
+
sample = "\n".join(expected[:4])
|
|
483
|
+
raise ValueError(
|
|
484
|
+
"Failed to apply patch: expected hunk context not found in the memory block. "
|
|
485
|
+
f"Verify the target lines exist and try providing more context. Expected start:\n{sample}"
|
|
486
|
+
)
|
|
487
|
+
if len(matches) > 1:
|
|
488
|
+
raise ValueError(
|
|
489
|
+
"Failed to apply patch: hunk context matched multiple places in the memory block. "
|
|
490
|
+
"Please add more unique surrounding context to disambiguate."
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
idx = matches[0]
|
|
494
|
+
end = idx + len(expected)
|
|
495
|
+
current_lines = current_lines[:idx] + replacement + current_lines[end:]
|
|
496
|
+
|
|
497
|
+
new_value = "\n".join(current_lines)
|
|
498
|
+
agent_state.memory.update_block_value(label=label, value=new_value)
|
|
499
|
+
await self.agent_manager.update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
|
|
500
|
+
|
|
501
|
+
return (
|
|
502
|
+
f"The core memory block with label `{label}` has been edited. "
|
|
503
|
+
"Review the changes and make sure they are as expected (correct indentation, no duplicate lines, etc). "
|
|
504
|
+
"Edit the memory block again if necessary."
|
|
505
|
+
)
|
|
506
|
+
|
|
396
507
|
async def memory_insert(
|
|
397
508
|
self,
|
|
398
509
|
agent_state: AgentState,
|
|
@@ -2,6 +2,8 @@ import asyncio
|
|
|
2
2
|
import re
|
|
3
3
|
from typing import Any, Dict, List, Optional
|
|
4
4
|
|
|
5
|
+
from sqlalchemy.exc import NoResultFound
|
|
6
|
+
|
|
5
7
|
from letta.constants import PINECONE_TEXT_FIELD_NAME
|
|
6
8
|
from letta.functions.types import FileOpenRequest
|
|
7
9
|
from letta.helpers.pinecone_utils import search_pinecone_index, should_use_pinecone
|
|
@@ -389,9 +391,9 @@ class LettaFileToolExecutor(ToolExecutor):
|
|
|
389
391
|
|
|
390
392
|
for file_agent in file_agents:
|
|
391
393
|
# Load file content
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
394
|
+
try:
|
|
395
|
+
file = await self.file_manager.get_file_by_id(file_id=file_agent.file_id, actor=self.actor, include_content=True)
|
|
396
|
+
except NoResultFound:
|
|
395
397
|
files_skipped += 1
|
|
396
398
|
self.logger.warning(f"Grep: Skipping file {file_agent.file_name} - no content available")
|
|
397
399
|
continue
|
|
@@ -36,7 +36,7 @@ class SandboxToolExecutor(ToolExecutor):
|
|
|
36
36
|
) -> ToolExecutionResult:
|
|
37
37
|
# Store original memory state
|
|
38
38
|
if agent_state:
|
|
39
|
-
orig_memory_str = agent_state.memory.compile()
|
|
39
|
+
orig_memory_str = agent_state.memory.compile(llm_config=agent_state.llm_config)
|
|
40
40
|
else:
|
|
41
41
|
orig_memory_str = None
|
|
42
42
|
|
|
@@ -89,7 +89,7 @@ class SandboxToolExecutor(ToolExecutor):
|
|
|
89
89
|
|
|
90
90
|
# Verify memory integrity
|
|
91
91
|
if agent_state:
|
|
92
|
-
new_memory_str = agent_state.memory.compile()
|
|
92
|
+
new_memory_str = agent_state.memory.compile(llm_config=agent_state.llm_config)
|
|
93
93
|
assert orig_memory_str == new_memory_str, "Memory should not be modified in a sandbox tool"
|
|
94
94
|
|
|
95
95
|
# Update agent memory if needed
|
|
@@ -142,7 +142,7 @@ class ToolExecutionManager:
|
|
|
142
142
|
)
|
|
143
143
|
except Exception as e:
|
|
144
144
|
status = "error"
|
|
145
|
-
self.logger.
|
|
145
|
+
self.logger.info(f"Error executing tool {function_name}: {str(e)}")
|
|
146
146
|
error_message = get_friendly_error_msg(
|
|
147
147
|
function_name=function_name,
|
|
148
148
|
exception_name=type(e).__name__,
|
letta/services/tool_manager.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import importlib
|
|
2
|
-
import warnings
|
|
3
2
|
from typing import List, Optional, Set, Union
|
|
4
3
|
|
|
5
4
|
from sqlalchemy import and_, func, or_, select
|
|
@@ -13,6 +12,7 @@ from letta.constants import (
|
|
|
13
12
|
BASE_VOICE_SLEEPTIME_TOOLS,
|
|
14
13
|
BUILTIN_TOOLS,
|
|
15
14
|
FILES_TOOLS,
|
|
15
|
+
LETTA_PARALLEL_SAFE_TOOLS,
|
|
16
16
|
LETTA_TOOL_MODULE_NAMES,
|
|
17
17
|
LETTA_TOOL_SET,
|
|
18
18
|
LOCAL_ONLY_MULTI_AGENT_TOOLS,
|
|
@@ -26,7 +26,7 @@ from letta.log import get_logger
|
|
|
26
26
|
from letta.orm.errors import NoResultFound
|
|
27
27
|
from letta.orm.tool import Tool as ToolModel
|
|
28
28
|
from letta.otel.tracing import trace_method
|
|
29
|
-
from letta.schemas.enums import ToolType
|
|
29
|
+
from letta.schemas.enums import PrimitiveType, ToolType
|
|
30
30
|
from letta.schemas.tool import Tool as PydanticTool, ToolCreate, ToolUpdate
|
|
31
31
|
from letta.schemas.user import User as PydanticUser
|
|
32
32
|
from letta.server.db import db_registry
|
|
@@ -35,6 +35,7 @@ from letta.services.mcp.types import SSEServerConfig, StdioServerConfig
|
|
|
35
35
|
from letta.services.tool_schema_generator import generate_schema_for_tool_creation, generate_schema_for_tool_update
|
|
36
36
|
from letta.settings import settings
|
|
37
37
|
from letta.utils import enforce_types, printd
|
|
38
|
+
from letta.validators import raise_on_invalid_id
|
|
38
39
|
|
|
39
40
|
logger = get_logger(__name__)
|
|
40
41
|
|
|
@@ -202,6 +203,7 @@ class ToolManager:
|
|
|
202
203
|
|
|
203
204
|
@enforce_types
|
|
204
205
|
@trace_method
|
|
206
|
+
@raise_on_invalid_id(param_name="tool_id", expected_prefix=PrimitiveType.TOOL)
|
|
205
207
|
async def get_tool_by_id_async(self, tool_id: str, actor: PydanticUser) -> PydanticTool:
|
|
206
208
|
"""Fetch a tool by its ID."""
|
|
207
209
|
async with db_registry.async_session() as session:
|
|
@@ -234,6 +236,7 @@ class ToolManager:
|
|
|
234
236
|
|
|
235
237
|
@enforce_types
|
|
236
238
|
@trace_method
|
|
239
|
+
@raise_on_invalid_id(param_name="tool_id", expected_prefix=PrimitiveType.TOOL)
|
|
237
240
|
async def tool_exists_async(self, tool_id: str, actor: PydanticUser) -> bool:
|
|
238
241
|
"""Check if a tool exists and belongs to the user's organization (lightweight check)."""
|
|
239
242
|
async with db_registry.async_session() as session:
|
|
@@ -506,6 +509,7 @@ class ToolManager:
|
|
|
506
509
|
|
|
507
510
|
@enforce_types
|
|
508
511
|
@trace_method
|
|
512
|
+
@raise_on_invalid_id(param_name="tool_id", expected_prefix=PrimitiveType.TOOL)
|
|
509
513
|
async def update_tool_by_id_async(
|
|
510
514
|
self,
|
|
511
515
|
tool_id: str,
|
|
@@ -603,6 +607,7 @@ class ToolManager:
|
|
|
603
607
|
|
|
604
608
|
@enforce_types
|
|
605
609
|
@trace_method
|
|
610
|
+
# @raise_on_invalid_id This is commented out bc it's called by _list_tools_async, when it encounters malformed tools (i.e. if id is invalid will fail validation on deletion)
|
|
606
611
|
async def delete_tool_by_id_async(self, tool_id: str, actor: PydanticUser) -> None:
|
|
607
612
|
"""Delete a tool by its ID."""
|
|
608
613
|
async with db_registry.async_session() as session:
|
|
@@ -630,7 +635,7 @@ class ToolManager:
|
|
|
630
635
|
module = importlib.import_module(module_name)
|
|
631
636
|
functions_to_schema.update(load_function_set(module))
|
|
632
637
|
except ValueError as e:
|
|
633
|
-
|
|
638
|
+
logger.warning(f"Error loading function set '{module_name}': {e}")
|
|
634
639
|
except Exception as e:
|
|
635
640
|
raise e
|
|
636
641
|
|
|
@@ -662,12 +667,14 @@ class ToolManager:
|
|
|
662
667
|
continue
|
|
663
668
|
|
|
664
669
|
# create pydantic tool for validation and conversion
|
|
670
|
+
parallel_safe = name in LETTA_PARALLEL_SAFE_TOOLS
|
|
665
671
|
pydantic_tool = PydanticTool(
|
|
666
672
|
name=name,
|
|
667
673
|
tags=[tool_type.value],
|
|
668
674
|
source_type="python",
|
|
669
675
|
tool_type=tool_type,
|
|
670
676
|
return_char_limit=BASE_FUNCTION_RETURN_CHAR_LIMIT,
|
|
677
|
+
enable_parallel_execution=parallel_safe,
|
|
671
678
|
)
|
|
672
679
|
|
|
673
680
|
# auto-generate description if not provided
|
|
@@ -57,11 +57,19 @@ class AsyncToolSandboxBase(ABC):
|
|
|
57
57
|
f"Agent attempted to invoke tool {self.tool_name} that does not exist for organization {self.user.organization_id}"
|
|
58
58
|
)
|
|
59
59
|
|
|
60
|
+
# Check for reserved keyword arguments
|
|
61
|
+
tool_arguments = parse_function_arguments(self.tool.source_code, self.tool.name)
|
|
62
|
+
|
|
60
63
|
# TODO: deprecate this
|
|
61
|
-
if "agent_state" in
|
|
64
|
+
if "agent_state" in tool_arguments:
|
|
62
65
|
self.inject_agent_state = True
|
|
63
66
|
else:
|
|
64
67
|
self.inject_agent_state = False
|
|
68
|
+
|
|
69
|
+
# Check for Letta client and agent_id injection
|
|
70
|
+
self.inject_letta_client = "letta_client" in tool_arguments or "client" in tool_arguments
|
|
71
|
+
self.inject_agent_id = "agent_id" in tool_arguments
|
|
72
|
+
|
|
65
73
|
self.is_async_function = self._detect_async_function()
|
|
66
74
|
self._initialized = True
|
|
67
75
|
|
|
@@ -112,12 +120,16 @@ class AsyncToolSandboxBase(ABC):
|
|
|
112
120
|
tool_args += self.initialize_param(param, self.args[param])
|
|
113
121
|
|
|
114
122
|
agent_state_pickle = pickle.dumps(agent_state) if self.inject_agent_state else None
|
|
123
|
+
agent_id = agent_state.id if agent_state else None
|
|
115
124
|
|
|
116
125
|
code = self._render_sandbox_code(
|
|
117
126
|
future_import=future_import,
|
|
118
127
|
inject_agent_state=self.inject_agent_state,
|
|
128
|
+
inject_letta_client=self.inject_letta_client,
|
|
129
|
+
inject_agent_id=self.inject_agent_id,
|
|
119
130
|
schema_imports=schema_code or "",
|
|
120
131
|
agent_state_pickle=agent_state_pickle,
|
|
132
|
+
agent_id=agent_id,
|
|
121
133
|
tool_args=tool_args,
|
|
122
134
|
tool_source_code=self.tool.source_code,
|
|
123
135
|
local_sandbox_result_var_name=self.LOCAL_SANDBOX_RESULT_VAR_NAME,
|
|
@@ -133,8 +145,11 @@ class AsyncToolSandboxBase(ABC):
|
|
|
133
145
|
*,
|
|
134
146
|
future_import: bool,
|
|
135
147
|
inject_agent_state: bool,
|
|
148
|
+
inject_letta_client: bool,
|
|
149
|
+
inject_agent_id: bool,
|
|
136
150
|
schema_imports: str,
|
|
137
151
|
agent_state_pickle: bytes | None,
|
|
152
|
+
agent_id: str | None,
|
|
138
153
|
tool_args: str,
|
|
139
154
|
tool_source_code: str,
|
|
140
155
|
local_sandbox_result_var_name: str,
|
|
@@ -162,6 +177,10 @@ class AsyncToolSandboxBase(ABC):
|
|
|
162
177
|
if inject_agent_state:
|
|
163
178
|
lines.extend(["import letta", "from letta import *"]) # noqa: F401
|
|
164
179
|
|
|
180
|
+
# Import Letta client if needed
|
|
181
|
+
if inject_letta_client:
|
|
182
|
+
lines.append("from letta_client import Letta")
|
|
183
|
+
|
|
165
184
|
if schema_imports:
|
|
166
185
|
lines.append(schema_imports.rstrip())
|
|
167
186
|
|
|
@@ -170,6 +189,34 @@ class AsyncToolSandboxBase(ABC):
|
|
|
170
189
|
else:
|
|
171
190
|
lines.append("agent_state = None")
|
|
172
191
|
|
|
192
|
+
# Initialize Letta client if needed
|
|
193
|
+
if inject_letta_client:
|
|
194
|
+
from letta.settings import settings
|
|
195
|
+
|
|
196
|
+
lines.extend(
|
|
197
|
+
[
|
|
198
|
+
"# Initialize Letta client for tool execution",
|
|
199
|
+
"letta_client = Letta(",
|
|
200
|
+
f" base_url={repr(settings.default_base_url)},",
|
|
201
|
+
f" token={repr(settings.default_token)}",
|
|
202
|
+
")",
|
|
203
|
+
"# Compatibility shim for client.agents.get",
|
|
204
|
+
"try:",
|
|
205
|
+
" _agents = letta_client.agents",
|
|
206
|
+
" if not hasattr(_agents, 'get') and hasattr(_agents, 'retrieve'):",
|
|
207
|
+
" setattr(_agents, 'get', _agents.retrieve)",
|
|
208
|
+
"except Exception:",
|
|
209
|
+
" pass",
|
|
210
|
+
]
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# Set agent_id if needed
|
|
214
|
+
if inject_agent_id:
|
|
215
|
+
if agent_id:
|
|
216
|
+
lines.append(f"agent_id = {repr(agent_id)}")
|
|
217
|
+
else:
|
|
218
|
+
lines.append("agent_id = None")
|
|
219
|
+
|
|
173
220
|
if tool_args:
|
|
174
221
|
lines.append(tool_args.rstrip())
|
|
175
222
|
|
|
@@ -286,9 +333,22 @@ class AsyncToolSandboxBase(ABC):
|
|
|
286
333
|
kwargs.append(name)
|
|
287
334
|
|
|
288
335
|
param_list = [f"{arg}={arg}" for arg in kwargs]
|
|
336
|
+
|
|
337
|
+
# Add reserved keyword arguments
|
|
289
338
|
if self.inject_agent_state:
|
|
290
339
|
param_list.append("agent_state=agent_state")
|
|
291
340
|
|
|
341
|
+
if self.inject_letta_client:
|
|
342
|
+
# Check if the function expects 'client' or 'letta_client'
|
|
343
|
+
tool_arguments = parse_function_arguments(self.tool.source_code, self.tool.name)
|
|
344
|
+
if "client" in tool_arguments:
|
|
345
|
+
param_list.append("client=letta_client")
|
|
346
|
+
elif "letta_client" in tool_arguments:
|
|
347
|
+
param_list.append("letta_client=letta_client")
|
|
348
|
+
|
|
349
|
+
if self.inject_agent_id:
|
|
350
|
+
param_list.append("agent_id=agent_id")
|
|
351
|
+
|
|
292
352
|
params = ", ".join(param_list)
|
|
293
353
|
func_call_str = self.tool.name + "(" + params + ")"
|
|
294
354
|
return func_call_str
|
|
@@ -235,9 +235,7 @@ class AsyncToolSandboxLocal(AsyncToolSandboxBase):
|
|
|
235
235
|
if isinstance(e, TimeoutError):
|
|
236
236
|
raise e
|
|
237
237
|
|
|
238
|
-
logger.
|
|
239
|
-
logger.error(e.__class__.__name__)
|
|
240
|
-
logger.error(e.__traceback__)
|
|
238
|
+
logger.exception(f"Subprocess execution for tool {self.tool_name} encountered an error: {e}")
|
|
241
239
|
func_return = get_friendly_error_msg(
|
|
242
240
|
function_name=self.tool_name,
|
|
243
241
|
exception_name=type(e).__name__,
|
letta/services/user_manager.py
CHANGED
|
@@ -58,7 +58,7 @@ class UserManager:
|
|
|
58
58
|
@enforce_types
|
|
59
59
|
@trace_method
|
|
60
60
|
async def update_actor_async(self, user_update: UserUpdate) -> PydanticUser:
|
|
61
|
-
"""Update user details (async version)."""
|
|
61
|
+
"""Update user details (async version). Raises NoResultFound if not found."""
|
|
62
62
|
async with db_registry.async_session() as session:
|
|
63
63
|
# Retrieve the existing user by ID
|
|
64
64
|
existing_user = await UserModel.read_async(db_session=session, identifier=user_update.id)
|
|
@@ -76,7 +76,7 @@ class UserManager:
|
|
|
76
76
|
@enforce_types
|
|
77
77
|
@trace_method
|
|
78
78
|
async def delete_actor_by_id_async(self, user_id: str):
|
|
79
|
-
"""Delete a user and their associated records (agents, sources, mappings) asynchronously."""
|
|
79
|
+
"""Delete a user and their associated records (agents, sources, mappings) asynchronously. Raises NoResultFound if not found."""
|
|
80
80
|
async with db_registry.async_session() as session:
|
|
81
81
|
# Delete from user table
|
|
82
82
|
user = await UserModel.read_async(db_session=session, identifier=user_id)
|
letta/settings.py
CHANGED
|
@@ -6,10 +6,13 @@ from typing import Optional
|
|
|
6
6
|
from pydantic import AliasChoices, Field
|
|
7
7
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
8
8
|
|
|
9
|
-
from letta.local_llm.constants import DEFAULT_WRAPPER_NAME, INNER_THOUGHTS_KWARG
|
|
10
9
|
from letta.schemas.enums import SandboxType
|
|
11
10
|
from letta.services.summarizer.enums import SummarizationMode
|
|
12
11
|
|
|
12
|
+
# Define constants here to avoid circular import with letta.log
|
|
13
|
+
DEFAULT_WRAPPER_NAME = "chatml"
|
|
14
|
+
INNER_THOUGHTS_KWARG = "thinking"
|
|
15
|
+
|
|
13
16
|
|
|
14
17
|
class ToolSettings(BaseSettings):
|
|
15
18
|
# Sandbox Configurations
|
|
@@ -329,6 +332,10 @@ class Settings(BaseSettings):
|
|
|
329
332
|
file_processing_timeout_minutes: int = 30
|
|
330
333
|
file_processing_timeout_error_message: str = "File processing timed out after {} minutes. Please try again."
|
|
331
334
|
|
|
335
|
+
# Letta client settings for tool execution
|
|
336
|
+
default_base_url: str = Field(default="http://localhost:8283", description="Default base URL for Letta client in tool execution")
|
|
337
|
+
default_token: Optional[str] = Field(default=None, description="Default token for Letta client in tool execution")
|
|
338
|
+
|
|
332
339
|
# enabling letta_agent_v1 architecture
|
|
333
340
|
use_letta_v1_agent: bool = False
|
|
334
341
|
|
|
@@ -374,16 +381,53 @@ class TestSettings(Settings):
|
|
|
374
381
|
|
|
375
382
|
class LogSettings(BaseSettings):
|
|
376
383
|
model_config = SettingsConfigDict(env_prefix="letta_logging_", extra="ignore")
|
|
377
|
-
debug: bool
|
|
378
|
-
json_logging: bool = Field(
|
|
384
|
+
debug: bool = Field(default=False, description="Enable debugging for logging")
|
|
385
|
+
json_logging: bool = Field(
|
|
386
|
+
default=False,
|
|
387
|
+
description="Enable structured JSON logging (recommended).",
|
|
388
|
+
)
|
|
379
389
|
log_level: str | None = Field("WARNING", description="Logging level")
|
|
380
390
|
letta_log_path: Path | None = Field(Path.home() / ".letta" / "logs" / "Letta.log")
|
|
381
|
-
verbose_telemetry_logging: bool = Field(False)
|
|
391
|
+
verbose_telemetry_logging: bool = Field(default=False)
|
|
382
392
|
|
|
383
393
|
|
|
384
394
|
class TelemetrySettings(BaseSettings):
|
|
395
|
+
"""Configuration for telemetry and observability integrations."""
|
|
396
|
+
|
|
385
397
|
model_config = SettingsConfigDict(env_prefix="letta_telemetry_", extra="ignore")
|
|
386
|
-
|
|
398
|
+
|
|
399
|
+
# Google Cloud Profiler
|
|
400
|
+
profiler: bool = Field(default=False, description="Enable Google Cloud Profiler.")
|
|
401
|
+
|
|
402
|
+
# Datadog APM and Profiling
|
|
403
|
+
enable_datadog: bool = Field(default=False, description="Enable Datadog profiling. Environment is pulled from settings.environment.")
|
|
404
|
+
datadog_agent_host: str = Field(
|
|
405
|
+
default="localhost",
|
|
406
|
+
description="Datadog agent hostname or IP address. Use service name for Kubernetes (e.g., 'datadog-cluster-agent').",
|
|
407
|
+
)
|
|
408
|
+
datadog_agent_port: int = Field(default=8126, ge=1, le=65535, description="Datadog trace agent port (typically 8126 for traces).")
|
|
409
|
+
datadog_service_name: str = Field(default="letta-server", description="Service name for Datadog profiling.")
|
|
410
|
+
datadog_profiling_memory_enabled: bool = Field(default=False, description="Enable memory profiling in Datadog.")
|
|
411
|
+
datadog_profiling_heap_enabled: bool = Field(default=False, description="Enable heap profiling in Datadog.")
|
|
412
|
+
|
|
413
|
+
# Datadog Source Code Integration (optional, tightly coupled with profiling)
|
|
414
|
+
# These settings link profiling data and traces to specific Git commits,
|
|
415
|
+
# enabling code navigation directly from Datadog UI to GitHub/GitLab.
|
|
416
|
+
datadog_git_repository_url: str | None = Field(
|
|
417
|
+
default=None,
|
|
418
|
+
validation_alias=AliasChoices("DD_GIT_REPOSITORY_URL", "datadog_git_repository_url"),
|
|
419
|
+
description="Git repository URL (e.g., 'https://github.com/org/repo'). Set at build time.",
|
|
420
|
+
)
|
|
421
|
+
datadog_git_commit_sha: str | None = Field(
|
|
422
|
+
default=None,
|
|
423
|
+
validation_alias=AliasChoices("DD_GIT_COMMIT_SHA", "datadog_git_commit_sha"),
|
|
424
|
+
description="Git commit SHA for the deployed code. Set at build time with 'git rev-parse HEAD'.",
|
|
425
|
+
)
|
|
426
|
+
datadog_main_package: str = Field(
|
|
427
|
+
default="letta",
|
|
428
|
+
validation_alias=AliasChoices("DD_MAIN_PACKAGE", "datadog_main_package"),
|
|
429
|
+
description="Primary Python package name for source code linking. Datadog uses this setting to determine which code is 'yours' vs. third-party dependencies.",
|
|
430
|
+
)
|
|
387
431
|
|
|
388
432
|
|
|
389
433
|
# singleton
|
letta/system.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import json
|
|
2
|
-
import warnings
|
|
3
2
|
from typing import Optional
|
|
4
3
|
|
|
4
|
+
from letta.log import get_logger
|
|
5
|
+
|
|
6
|
+
logger = get_logger(__name__)
|
|
7
|
+
|
|
5
8
|
from .constants import (
|
|
6
9
|
INITIAL_BOOT_MESSAGE,
|
|
7
10
|
INITIAL_BOOT_MESSAGE_SEND_MESSAGE_FIRST_MSG,
|
|
@@ -42,11 +45,17 @@ def get_initial_boot_messages(version, timezone, tool_call_id):
|
|
|
42
45
|
},
|
|
43
46
|
# obligatory function return message
|
|
44
47
|
{
|
|
45
|
-
# "role": "function",
|
|
46
48
|
"role": "tool",
|
|
47
49
|
"name": "send_message", # NOTE: technically not up to spec, this is old functions style
|
|
48
50
|
"content": package_function_response(True, None, timezone),
|
|
49
51
|
"tool_call_id": tool_call_id,
|
|
52
|
+
"tool_returns": [
|
|
53
|
+
{
|
|
54
|
+
"tool_call_id": tool_call_id,
|
|
55
|
+
"status": "success",
|
|
56
|
+
"func_response": package_function_response(True, None, timezone),
|
|
57
|
+
}
|
|
58
|
+
],
|
|
50
59
|
},
|
|
51
60
|
]
|
|
52
61
|
|
|
@@ -154,7 +163,7 @@ def package_system_message(system_message, timezone, message_type="system_alert"
|
|
|
154
163
|
try:
|
|
155
164
|
message_json = json.loads(system_message)
|
|
156
165
|
if "type" in message_json and message_json["type"] == message_type:
|
|
157
|
-
|
|
166
|
+
logger.warning(f"Attempted to pack a system message that is already packed. Not packing: '{system_message}'")
|
|
158
167
|
return system_message
|
|
159
168
|
except:
|
|
160
169
|
pass # do nothing, expected behavior that the message is not JSON
|
|
@@ -245,7 +254,7 @@ def unpack_message(packed_message: str) -> str:
|
|
|
245
254
|
if "type" in message_json and message_json["type"] in ["login", "heartbeat"]:
|
|
246
255
|
# This is a valid user message that the ADE expects, so don't print warning
|
|
247
256
|
return packed_message
|
|
248
|
-
|
|
257
|
+
logger.warning(f"Was unable to find 'message' field in packed message object: '{packed_message}'")
|
|
249
258
|
return packed_message
|
|
250
259
|
else:
|
|
251
260
|
try:
|
|
@@ -254,6 +263,6 @@ def unpack_message(packed_message: str) -> str:
|
|
|
254
263
|
return packed_message
|
|
255
264
|
|
|
256
265
|
if message_type != "user_message":
|
|
257
|
-
|
|
266
|
+
logger.warning(f"Expected type to be 'user_message', but was '{message_type}', so not unpacking: '{packed_message}'")
|
|
258
267
|
return packed_message
|
|
259
268
|
return message_json.get("message")
|
letta/utils.py
CHANGED
|
@@ -41,6 +41,7 @@ from letta.helpers.json_helpers import json_dumps, json_loads
|
|
|
41
41
|
from letta.log import get_logger
|
|
42
42
|
from letta.otel.tracing import log_attributes, trace_method
|
|
43
43
|
from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
|
|
44
|
+
from letta.server.rest_api.dependencies import HeaderParams
|
|
44
45
|
|
|
45
46
|
logger = get_logger(__name__)
|
|
46
47
|
|
|
@@ -1129,6 +1130,29 @@ def safe_create_task(coro, label: str = "background task"):
|
|
|
1129
1130
|
return task
|
|
1130
1131
|
|
|
1131
1132
|
|
|
1133
|
+
@trace_method
|
|
1134
|
+
def safe_create_task_with_return(coro, label: str = "background task"):
|
|
1135
|
+
async def wrapper():
|
|
1136
|
+
try:
|
|
1137
|
+
return await coro
|
|
1138
|
+
except Exception as e:
|
|
1139
|
+
logger.exception(f"{label} failed with {type(e).__name__}: {e}")
|
|
1140
|
+
raise
|
|
1141
|
+
|
|
1142
|
+
task = asyncio.create_task(wrapper())
|
|
1143
|
+
|
|
1144
|
+
# Add task to the set to maintain strong reference
|
|
1145
|
+
_background_tasks.add(task)
|
|
1146
|
+
|
|
1147
|
+
# Log task count to trace
|
|
1148
|
+
log_attributes({"total_background_task_count": get_background_task_count()})
|
|
1149
|
+
|
|
1150
|
+
# Remove task from set when done to prevent memory leaks
|
|
1151
|
+
task.add_done_callback(_background_tasks.discard)
|
|
1152
|
+
|
|
1153
|
+
return task
|
|
1154
|
+
|
|
1155
|
+
|
|
1132
1156
|
def safe_create_shielded_task(coro, label: str = "shielded background task"):
|
|
1133
1157
|
"""
|
|
1134
1158
|
Create a shielded background task that cannot be cancelled externally.
|
|
@@ -1367,7 +1391,7 @@ def fire_and_forget(coro, task_name: Optional[str] = None, error_callback: Optio
|
|
|
1367
1391
|
t.result() # this re-raises exceptions from the task
|
|
1368
1392
|
except Exception as e:
|
|
1369
1393
|
task_desc = f"Background task {task_name}" if task_name else "Background task"
|
|
1370
|
-
logger.
|
|
1394
|
+
logger.exception(f"{task_desc} failed: {str(e)}")
|
|
1371
1395
|
|
|
1372
1396
|
if error_callback:
|
|
1373
1397
|
try:
|
|
@@ -1377,3 +1401,51 @@ def fire_and_forget(coro, task_name: Optional[str] = None, error_callback: Optio
|
|
|
1377
1401
|
|
|
1378
1402
|
task.add_done_callback(callback)
|
|
1379
1403
|
return task
|
|
1404
|
+
|
|
1405
|
+
|
|
1406
|
+
def is_1_0_sdk_version(headers: HeaderParams):
|
|
1407
|
+
"""
|
|
1408
|
+
Check if the SDK version is 1.0.0 or above.
|
|
1409
|
+
1. If sdk_version is provided from stainless (all stainless versions are 1.0.0+)
|
|
1410
|
+
2. If user_agent is provided and in the format
|
|
1411
|
+
@letta-ai/letta-client/version (node) or
|
|
1412
|
+
letta-client/version (python)
|
|
1413
|
+
"""
|
|
1414
|
+
sdk_version = headers.sdk_version
|
|
1415
|
+
if sdk_version:
|
|
1416
|
+
return True
|
|
1417
|
+
|
|
1418
|
+
client = headers.user_agent
|
|
1419
|
+
if "/" not in client:
|
|
1420
|
+
return False
|
|
1421
|
+
|
|
1422
|
+
# Split into parts to validate format
|
|
1423
|
+
parts = client.split("/")
|
|
1424
|
+
|
|
1425
|
+
# Should have at least 2 parts (client-name/version)
|
|
1426
|
+
if len(parts) < 2:
|
|
1427
|
+
return False
|
|
1428
|
+
|
|
1429
|
+
if len(parts) == 3:
|
|
1430
|
+
# Format: @letta-ai/letta-client/version
|
|
1431
|
+
if parts[0] != "@letta-ai" or parts[1] != "letta-client":
|
|
1432
|
+
return False
|
|
1433
|
+
elif len(parts) == 2:
|
|
1434
|
+
# Format: letta-client/version
|
|
1435
|
+
if parts[0] != "letta-client":
|
|
1436
|
+
return False
|
|
1437
|
+
else:
|
|
1438
|
+
return False
|
|
1439
|
+
|
|
1440
|
+
# Extract and validate version
|
|
1441
|
+
maybe_version = parts[-1]
|
|
1442
|
+
if "." not in maybe_version:
|
|
1443
|
+
return False
|
|
1444
|
+
|
|
1445
|
+
# Extract major version (handle alpha/beta suffixes like 1.0.0-alpha.2 or 1.0.0a5)
|
|
1446
|
+
version_base = maybe_version.split("-")[0].split("a")[0].split("b")[0]
|
|
1447
|
+
if "." not in version_base:
|
|
1448
|
+
return False
|
|
1449
|
+
|
|
1450
|
+
major_version = version_base.split(".")[0]
|
|
1451
|
+
return major_version == "1"
|