letta-nightly 0.13.0.dev20251030104218__py3-none-any.whl → 0.13.1.dev20251031234110__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 +1 -1
- letta/adapters/simple_llm_stream_adapter.py +1 -0
- letta/agents/letta_agent_v2.py +8 -0
- letta/agents/letta_agent_v3.py +120 -27
- letta/agents/temporal/activities/__init__.py +25 -0
- letta/agents/temporal/activities/create_messages.py +26 -0
- letta/agents/temporal/activities/create_step.py +57 -0
- letta/agents/temporal/activities/example_activity.py +9 -0
- letta/agents/temporal/activities/execute_tool.py +130 -0
- letta/agents/temporal/activities/llm_request.py +114 -0
- letta/agents/temporal/activities/prepare_messages.py +27 -0
- letta/agents/temporal/activities/refresh_context.py +160 -0
- letta/agents/temporal/activities/summarize_conversation_history.py +77 -0
- letta/agents/temporal/activities/update_message_ids.py +25 -0
- letta/agents/temporal/activities/update_run.py +43 -0
- letta/agents/temporal/constants.py +59 -0
- letta/agents/temporal/temporal_agent_workflow.py +704 -0
- letta/agents/temporal/types.py +275 -0
- letta/constants.py +8 -0
- letta/errors.py +4 -0
- letta/functions/function_sets/base.py +0 -11
- letta/groups/helpers.py +7 -1
- letta/groups/sleeptime_multi_agent_v4.py +4 -3
- letta/interfaces/anthropic_streaming_interface.py +0 -1
- letta/interfaces/openai_streaming_interface.py +103 -100
- letta/llm_api/anthropic_client.py +57 -12
- letta/llm_api/bedrock_client.py +1 -0
- letta/llm_api/deepseek_client.py +3 -2
- letta/llm_api/google_vertex_client.py +1 -0
- letta/llm_api/groq_client.py +1 -0
- letta/llm_api/llm_client_base.py +15 -1
- letta/llm_api/openai.py +2 -2
- letta/llm_api/openai_client.py +17 -3
- letta/llm_api/xai_client.py +1 -0
- letta/orm/organization.py +4 -0
- letta/orm/sqlalchemy_base.py +7 -0
- letta/otel/tracing.py +131 -4
- letta/schemas/agent_file.py +10 -10
- letta/schemas/block.py +22 -3
- letta/schemas/enums.py +21 -0
- letta/schemas/environment_variables.py +3 -2
- letta/schemas/group.py +3 -3
- letta/schemas/letta_response.py +36 -4
- letta/schemas/llm_batch_job.py +3 -3
- letta/schemas/llm_config.py +27 -3
- letta/schemas/mcp.py +3 -2
- letta/schemas/mcp_server.py +3 -2
- letta/schemas/message.py +167 -49
- letta/schemas/organization.py +2 -1
- letta/schemas/passage.py +2 -1
- letta/schemas/provider_trace.py +2 -1
- letta/schemas/providers/openrouter.py +1 -2
- letta/schemas/run_metrics.py +2 -1
- letta/schemas/sandbox_config.py +3 -1
- letta/schemas/step_metrics.py +2 -1
- letta/schemas/tool_rule.py +2 -2
- letta/schemas/user.py +2 -1
- letta/server/rest_api/app.py +5 -1
- letta/server/rest_api/routers/v1/__init__.py +4 -0
- letta/server/rest_api/routers/v1/agents.py +71 -9
- letta/server/rest_api/routers/v1/blocks.py +7 -7
- letta/server/rest_api/routers/v1/groups.py +40 -0
- letta/server/rest_api/routers/v1/identities.py +2 -2
- letta/server/rest_api/routers/v1/internal_agents.py +31 -0
- letta/server/rest_api/routers/v1/internal_blocks.py +177 -0
- letta/server/rest_api/routers/v1/internal_runs.py +25 -1
- letta/server/rest_api/routers/v1/runs.py +2 -22
- letta/server/rest_api/routers/v1/tools.py +10 -0
- letta/server/server.py +5 -2
- letta/services/agent_manager.py +4 -4
- letta/services/archive_manager.py +16 -0
- letta/services/group_manager.py +44 -0
- letta/services/helpers/run_manager_helper.py +2 -2
- letta/services/lettuce/lettuce_client.py +148 -0
- letta/services/mcp/base_client.py +9 -3
- letta/services/run_manager.py +148 -37
- letta/services/source_manager.py +91 -3
- letta/services/step_manager.py +2 -3
- letta/services/streaming_service.py +52 -13
- letta/services/summarizer/summarizer.py +28 -2
- letta/services/tool_executor/builtin_tool_executor.py +1 -1
- letta/services/tool_executor/core_tool_executor.py +2 -117
- letta/services/tool_schema_generator.py +2 -2
- letta/validators.py +21 -0
- {letta_nightly-0.13.0.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/METADATA +1 -1
- {letta_nightly-0.13.0.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/RECORD +89 -84
- letta/agent.py +0 -1758
- letta/cli/cli_load.py +0 -16
- letta/client/__init__.py +0 -0
- letta/client/streaming.py +0 -95
- letta/client/utils.py +0 -78
- letta/functions/async_composio_toolset.py +0 -109
- letta/functions/composio_helpers.py +0 -96
- letta/helpers/composio_helpers.py +0 -38
- letta/orm/job_messages.py +0 -33
- letta/schemas/providers.py +0 -1617
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -132
- letta/services/tool_executor/composio_tool_executor.py +0 -57
- {letta_nightly-0.13.0.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/WHEEL +0 -0
- {letta_nightly-0.13.0.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.13.0.dev20251030104218.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import copy
|
|
1
2
|
import json
|
|
2
3
|
import logging
|
|
3
4
|
import re
|
|
@@ -19,6 +20,7 @@ from letta.errors import (
|
|
|
19
20
|
LLMConnectionError,
|
|
20
21
|
LLMNotFoundError,
|
|
21
22
|
LLMPermissionDeniedError,
|
|
23
|
+
LLMProviderOverloaded,
|
|
22
24
|
LLMRateLimitError,
|
|
23
25
|
LLMServerError,
|
|
24
26
|
LLMTimeoutError,
|
|
@@ -229,6 +231,7 @@ class AnthropicClient(LLMClientBase):
|
|
|
229
231
|
tools: Optional[List[dict]] = None,
|
|
230
232
|
force_tool_call: Optional[str] = None,
|
|
231
233
|
requires_subsequent_tool_call: bool = False,
|
|
234
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
232
235
|
) -> dict:
|
|
233
236
|
# TODO: This needs to get cleaned up. The logic here is pretty confusing.
|
|
234
237
|
# TODO: I really want to get rid of prefixing, it's a recipe for disaster code maintenance wise
|
|
@@ -334,6 +337,7 @@ class AnthropicClient(LLMClientBase):
|
|
|
334
337
|
# if react, use native content + strip heartbeats
|
|
335
338
|
native_content=is_v1,
|
|
336
339
|
strip_request_heartbeat=is_v1,
|
|
340
|
+
tool_return_truncation_chars=tool_return_truncation_chars,
|
|
337
341
|
)
|
|
338
342
|
|
|
339
343
|
# Ensure first message is user
|
|
@@ -383,25 +387,53 @@ class AnthropicClient(LLMClientBase):
|
|
|
383
387
|
else:
|
|
384
388
|
anthropic_tools = None
|
|
385
389
|
|
|
386
|
-
#
|
|
387
|
-
#
|
|
390
|
+
# Convert final thinking blocks to text to work around token counting endpoint limitation.
|
|
391
|
+
# The token counting endpoint rejects messages where the final content block is thinking,
|
|
392
|
+
# even though the main API supports this with the interleaved-thinking beta.
|
|
393
|
+
# We convert (not strip) to preserve accurate token counts.
|
|
394
|
+
# TODO: Remove this workaround if Anthropic fixes the token counting endpoint.
|
|
388
395
|
thinking_enabled = False
|
|
396
|
+
messages_for_counting = messages
|
|
397
|
+
|
|
389
398
|
if messages and len(messages) > 0:
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
399
|
+
messages_for_counting = copy.deepcopy(messages)
|
|
400
|
+
|
|
401
|
+
# Scan all assistant messages and convert any final thinking blocks to text
|
|
402
|
+
for message in messages_for_counting:
|
|
403
|
+
if message.get("role") == "assistant":
|
|
404
|
+
content = message.get("content")
|
|
405
|
+
|
|
406
|
+
# Check for thinking in any format
|
|
407
|
+
if isinstance(content, list) and len(content) > 0:
|
|
408
|
+
# Check if message has any thinking blocks (to enable thinking mode)
|
|
409
|
+
has_thinking = any(
|
|
410
|
+
isinstance(part, dict) and part.get("type") in {"thinking", "redacted_thinking"} for part in content
|
|
411
|
+
)
|
|
412
|
+
if has_thinking:
|
|
396
413
|
thinking_enabled = True
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
414
|
+
|
|
415
|
+
# If final block is thinking, handle it
|
|
416
|
+
last_block = content[-1]
|
|
417
|
+
if isinstance(last_block, dict) and last_block.get("type") in {"thinking", "redacted_thinking"}:
|
|
418
|
+
if len(content) == 1:
|
|
419
|
+
# Thinking-only message: add text at end (don't convert the thinking)
|
|
420
|
+
# API requires first block to be thinking when thinking is enabled
|
|
421
|
+
content.append({"type": "text", "text": "."})
|
|
422
|
+
else:
|
|
423
|
+
# Multiple blocks: convert final thinking to text
|
|
424
|
+
if last_block["type"] == "thinking":
|
|
425
|
+
content[-1] = {"type": "text", "text": last_block.get("thinking", "")}
|
|
426
|
+
elif last_block["type"] == "redacted_thinking":
|
|
427
|
+
content[-1] = {"type": "text", "text": last_block.get("data", "[redacted]")}
|
|
428
|
+
|
|
429
|
+
elif isinstance(content, str) and "<thinking>" in content:
|
|
430
|
+
# Handle XML-style thinking in string content
|
|
431
|
+
thinking_enabled = True
|
|
400
432
|
|
|
401
433
|
try:
|
|
402
434
|
count_params = {
|
|
403
435
|
"model": model or "claude-3-7-sonnet-20250219",
|
|
404
|
-
"messages":
|
|
436
|
+
"messages": messages_for_counting or [{"role": "user", "content": "hi"}],
|
|
405
437
|
"tools": anthropic_tools or [],
|
|
406
438
|
}
|
|
407
439
|
|
|
@@ -444,6 +476,14 @@ class AnthropicClient(LLMClientBase):
|
|
|
444
476
|
|
|
445
477
|
@trace_method
|
|
446
478
|
def handle_llm_error(self, e: Exception) -> Exception:
|
|
479
|
+
# make sure to check for overflow errors, regardless of error type
|
|
480
|
+
error_str = str(e).lower()
|
|
481
|
+
if "prompt is too long" in error_str or "exceed context limit" in error_str or "exceeds context" in error_str:
|
|
482
|
+
logger.warning(f"[Anthropic] Context window exceeded: {str(e)}")
|
|
483
|
+
return ContextWindowExceededError(
|
|
484
|
+
message=f"Context window exceeded for Anthropic: {str(e)}",
|
|
485
|
+
)
|
|
486
|
+
|
|
447
487
|
if isinstance(e, anthropic.APITimeoutError):
|
|
448
488
|
logger.warning(f"[Anthropic] Request timeout: {e}")
|
|
449
489
|
return LLMTimeoutError(
|
|
@@ -513,6 +553,11 @@ class AnthropicClient(LLMClientBase):
|
|
|
513
553
|
|
|
514
554
|
if isinstance(e, anthropic.APIStatusError):
|
|
515
555
|
logger.warning(f"[Anthropic] API status error: {str(e)}")
|
|
556
|
+
if "overloaded" in str(e).lower():
|
|
557
|
+
return LLMProviderOverloaded(
|
|
558
|
+
message=f"Anthropic API is overloaded: {str(e)}",
|
|
559
|
+
code=ErrorCode.INTERNAL_SERVER_ERROR,
|
|
560
|
+
)
|
|
516
561
|
return LLMServerError(
|
|
517
562
|
message=f"Anthropic API error: {str(e)}",
|
|
518
563
|
code=ErrorCode.INTERNAL_SERVER_ERROR,
|
letta/llm_api/bedrock_client.py
CHANGED
|
@@ -71,6 +71,7 @@ class BedrockClient(AnthropicClient):
|
|
|
71
71
|
tools: Optional[List[dict]] = None,
|
|
72
72
|
force_tool_call: Optional[str] = None,
|
|
73
73
|
requires_subsequent_tool_call: bool = False,
|
|
74
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
74
75
|
) -> dict:
|
|
75
76
|
data = super().build_request_data(agent_type, messages, llm_config, tools, force_tool_call, requires_subsequent_tool_call)
|
|
76
77
|
# remove disallowed fields
|
letta/llm_api/deepseek_client.py
CHANGED
|
@@ -59,7 +59,7 @@ def handle_assistant_message(assistant_message: AssistantMessage) -> AssistantMe
|
|
|
59
59
|
return assistant_message
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
def map_messages_to_deepseek_format(messages: List[ChatMessage]) -> List[_Message]:
|
|
62
|
+
def map_messages_to_deepseek_format(messages: List[ChatMessage]) -> List["_Message"]:
|
|
63
63
|
"""
|
|
64
64
|
Deepeek API has the following constraints: messages must be interleaved between user and assistant messages, ending on a user message.
|
|
65
65
|
Tools are currently unstable for V3 and not supported for R1 in the API: https://api-docs.deepseek.com/guides/function_calling.
|
|
@@ -103,7 +103,7 @@ def map_messages_to_deepseek_format(messages: List[ChatMessage]) -> List[_Messag
|
|
|
103
103
|
|
|
104
104
|
def build_deepseek_chat_completions_request(
|
|
105
105
|
llm_config: LLMConfig,
|
|
106
|
-
messages: List[_Message],
|
|
106
|
+
messages: List["_Message"],
|
|
107
107
|
user_id: Optional[str],
|
|
108
108
|
functions: Optional[list],
|
|
109
109
|
function_call: Optional[str],
|
|
@@ -340,6 +340,7 @@ class DeepseekClient(OpenAIClient):
|
|
|
340
340
|
tools: Optional[List[dict]] = None,
|
|
341
341
|
force_tool_call: Optional[str] = None,
|
|
342
342
|
requires_subsequent_tool_call: bool = False,
|
|
343
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
343
344
|
) -> dict:
|
|
344
345
|
# Override put_inner_thoughts_in_kwargs to False for DeepSeek
|
|
345
346
|
llm_config.put_inner_thoughts_in_kwargs = False
|
|
@@ -291,6 +291,7 @@ class GoogleVertexClient(LLMClientBase):
|
|
|
291
291
|
tools: List[dict],
|
|
292
292
|
force_tool_call: Optional[str] = None,
|
|
293
293
|
requires_subsequent_tool_call: bool = False,
|
|
294
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
294
295
|
) -> dict:
|
|
295
296
|
"""
|
|
296
297
|
Constructs a request object in the expected data format for this client.
|
letta/llm_api/groq_client.py
CHANGED
|
@@ -30,6 +30,7 @@ class GroqClient(OpenAIClient):
|
|
|
30
30
|
tools: Optional[List[dict]] = None,
|
|
31
31
|
force_tool_call: Optional[str] = None,
|
|
32
32
|
requires_subsequent_tool_call: bool = False,
|
|
33
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
33
34
|
) -> dict:
|
|
34
35
|
data = super().build_request_data(agent_type, messages, llm_config, tools, force_tool_call, requires_subsequent_tool_call)
|
|
35
36
|
|
letta/llm_api/llm_client_base.py
CHANGED
|
@@ -47,13 +47,22 @@ class LLMClientBase:
|
|
|
47
47
|
force_tool_call: Optional[str] = None,
|
|
48
48
|
telemetry_manager: Optional["TelemetryManager"] = None,
|
|
49
49
|
step_id: Optional[str] = None,
|
|
50
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
50
51
|
) -> Union[ChatCompletionResponse, Stream[ChatCompletionChunk]]:
|
|
51
52
|
"""
|
|
52
53
|
Issues a request to the downstream model endpoint and parses response.
|
|
53
54
|
If stream=True, returns a Stream[ChatCompletionChunk] that can be iterated over.
|
|
54
55
|
Otherwise returns a ChatCompletionResponse.
|
|
55
56
|
"""
|
|
56
|
-
request_data = self.build_request_data(
|
|
57
|
+
request_data = self.build_request_data(
|
|
58
|
+
agent_type,
|
|
59
|
+
messages,
|
|
60
|
+
llm_config,
|
|
61
|
+
tools,
|
|
62
|
+
force_tool_call,
|
|
63
|
+
requires_subsequent_tool_call=False,
|
|
64
|
+
tool_return_truncation_chars=tool_return_truncation_chars,
|
|
65
|
+
)
|
|
57
66
|
|
|
58
67
|
try:
|
|
59
68
|
log_event(name="llm_request_sent", attributes=request_data)
|
|
@@ -128,9 +137,14 @@ class LLMClientBase:
|
|
|
128
137
|
tools: List[dict],
|
|
129
138
|
force_tool_call: Optional[str] = None,
|
|
130
139
|
requires_subsequent_tool_call: bool = False,
|
|
140
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
131
141
|
) -> dict:
|
|
132
142
|
"""
|
|
133
143
|
Constructs a request object in the expected data format for this client.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
tool_return_truncation_chars: If set, truncates tool return content to this many characters.
|
|
147
|
+
Used during summarization to avoid context window issues.
|
|
134
148
|
"""
|
|
135
149
|
raise NotImplementedError
|
|
136
150
|
|
letta/llm_api/openai.py
CHANGED
|
@@ -624,8 +624,8 @@ def prepare_openai_payload(chat_completion_request: ChatCompletionRequest):
|
|
|
624
624
|
data = chat_completion_request.model_dump(exclude_none=True)
|
|
625
625
|
|
|
626
626
|
# add check otherwise will cause error: "Invalid value for 'parallel_tool_calls': 'parallel_tool_calls' is only allowed when 'tools' are specified."
|
|
627
|
-
if chat_completion_request.tools is not None:
|
|
628
|
-
data["parallel_tool_calls"] =
|
|
627
|
+
if chat_completion_request.tools is not None and chat_completion_request.parallel_tool_calls is not None:
|
|
628
|
+
data["parallel_tool_calls"] = chat_completion_request.parallel_tool_calls
|
|
629
629
|
|
|
630
630
|
# If functions == None, strip from the payload
|
|
631
631
|
if "functions" in data and data["functions"] is None:
|
letta/llm_api/openai_client.py
CHANGED
|
@@ -64,6 +64,14 @@ def is_openai_reasoning_model(model: str) -> bool:
|
|
|
64
64
|
return is_reasoning
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def does_not_support_minimal_reasoning(model: str) -> bool:
|
|
68
|
+
"""Check if the model does not support minimal reasoning effort.
|
|
69
|
+
|
|
70
|
+
Currently, models that contain codex don't support minimal reasoning.
|
|
71
|
+
"""
|
|
72
|
+
return "codex" in model.lower()
|
|
73
|
+
|
|
74
|
+
|
|
67
75
|
def is_openai_5_model(model: str) -> bool:
|
|
68
76
|
"""Utility function to check if the model is a '5' model"""
|
|
69
77
|
return model.startswith("gpt-5")
|
|
@@ -221,6 +229,7 @@ class OpenAIClient(LLMClientBase):
|
|
|
221
229
|
tools: Optional[List[dict]] = None, # Keep as dict for now as per base class
|
|
222
230
|
force_tool_call: Optional[str] = None,
|
|
223
231
|
requires_subsequent_tool_call: bool = False,
|
|
232
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
224
233
|
) -> dict:
|
|
225
234
|
"""
|
|
226
235
|
Constructs a request object in the expected data format for the OpenAI Responses API.
|
|
@@ -228,7 +237,9 @@ class OpenAIClient(LLMClientBase):
|
|
|
228
237
|
if llm_config.put_inner_thoughts_in_kwargs:
|
|
229
238
|
raise ValueError("Inner thoughts in kwargs are not supported for the OpenAI Responses API")
|
|
230
239
|
|
|
231
|
-
openai_messages_list = PydanticMessage.to_openai_responses_dicts_from_list(
|
|
240
|
+
openai_messages_list = PydanticMessage.to_openai_responses_dicts_from_list(
|
|
241
|
+
messages, tool_return_truncation_chars=tool_return_truncation_chars
|
|
242
|
+
)
|
|
232
243
|
# Add multi-modal support for Responses API by rewriting user messages
|
|
233
244
|
# into input_text/input_image parts.
|
|
234
245
|
openai_messages_list = fill_image_content_in_responses_input(openai_messages_list, messages)
|
|
@@ -316,7 +327,7 @@ class OpenAIClient(LLMClientBase):
|
|
|
316
327
|
tool_choice=tool_choice,
|
|
317
328
|
max_output_tokens=llm_config.max_tokens,
|
|
318
329
|
temperature=llm_config.temperature if supports_temperature_param(model) else None,
|
|
319
|
-
parallel_tool_calls=False,
|
|
330
|
+
parallel_tool_calls=llm_config.parallel_tool_calls if tools and supports_parallel_tool_calling(model) else False,
|
|
320
331
|
)
|
|
321
332
|
|
|
322
333
|
# Add verbosity control for GPT-5 models
|
|
@@ -341,7 +352,7 @@ class OpenAIClient(LLMClientBase):
|
|
|
341
352
|
|
|
342
353
|
# Add parallel tool calling
|
|
343
354
|
if tools and supports_parallel_tool_calling(model):
|
|
344
|
-
data.parallel_tool_calls =
|
|
355
|
+
data.parallel_tool_calls = llm_config.parallel_tool_calls
|
|
345
356
|
|
|
346
357
|
# always set user id for openai requests
|
|
347
358
|
if self.actor:
|
|
@@ -369,6 +380,7 @@ class OpenAIClient(LLMClientBase):
|
|
|
369
380
|
tools: Optional[List[dict]] = None, # Keep as dict for now as per base class
|
|
370
381
|
force_tool_call: Optional[str] = None,
|
|
371
382
|
requires_subsequent_tool_call: bool = False,
|
|
383
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
372
384
|
) -> dict:
|
|
373
385
|
"""
|
|
374
386
|
Constructs a request object in the expected data format for the OpenAI API.
|
|
@@ -382,6 +394,7 @@ class OpenAIClient(LLMClientBase):
|
|
|
382
394
|
tools=tools,
|
|
383
395
|
force_tool_call=force_tool_call,
|
|
384
396
|
requires_subsequent_tool_call=requires_subsequent_tool_call,
|
|
397
|
+
tool_return_truncation_chars=tool_return_truncation_chars,
|
|
385
398
|
)
|
|
386
399
|
|
|
387
400
|
if agent_type == AgentType.letta_v1_agent:
|
|
@@ -411,6 +424,7 @@ class OpenAIClient(LLMClientBase):
|
|
|
411
424
|
messages,
|
|
412
425
|
put_inner_thoughts_in_kwargs=llm_config.put_inner_thoughts_in_kwargs,
|
|
413
426
|
use_developer_message=use_developer_message,
|
|
427
|
+
tool_return_truncation_chars=tool_return_truncation_chars,
|
|
414
428
|
)
|
|
415
429
|
]
|
|
416
430
|
|
letta/llm_api/xai_client.py
CHANGED
|
@@ -30,6 +30,7 @@ class XAIClient(OpenAIClient):
|
|
|
30
30
|
tools: Optional[List[dict]] = None,
|
|
31
31
|
force_tool_call: Optional[str] = None,
|
|
32
32
|
requires_subsequent_tool_call: bool = False,
|
|
33
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
33
34
|
) -> dict:
|
|
34
35
|
data = super().build_request_data(agent_type, messages, llm_config, tools, force_tool_call, requires_subsequent_tool_call)
|
|
35
36
|
|
letta/orm/organization.py
CHANGED
|
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
|
|
|
19
19
|
from letta.orm.passage import ArchivalPassage, SourcePassage
|
|
20
20
|
from letta.orm.passage_tag import PassageTag
|
|
21
21
|
from letta.orm.provider import Provider
|
|
22
|
+
from letta.orm.provider_trace import ProviderTrace
|
|
22
23
|
from letta.orm.run import Run
|
|
23
24
|
from letta.orm.sandbox_config import AgentEnvironmentVariable, SandboxConfig, SandboxEnvironmentVariable
|
|
24
25
|
from letta.orm.tool import Tool
|
|
@@ -70,3 +71,6 @@ class Organization(SqlalchemyBase):
|
|
|
70
71
|
)
|
|
71
72
|
jobs: Mapped[List["Job"]] = relationship("Job", back_populates="organization", cascade="all, delete-orphan")
|
|
72
73
|
runs: Mapped[List["Run"]] = relationship("Run", back_populates="organization", cascade="all, delete-orphan")
|
|
74
|
+
provider_traces: Mapped[List["ProviderTrace"]] = relationship(
|
|
75
|
+
"ProviderTrace", back_populates="organization", cascade="all, delete-orphan"
|
|
76
|
+
)
|
letta/orm/sqlalchemy_base.py
CHANGED
|
@@ -9,6 +9,7 @@ from sqlalchemy import Sequence, String, and_, delete, func, or_, select
|
|
|
9
9
|
from sqlalchemy.exc import DBAPIError, IntegrityError, TimeoutError
|
|
10
10
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
11
11
|
from sqlalchemy.orm import Mapped, Session, mapped_column
|
|
12
|
+
from sqlalchemy.orm.exc import StaleDataError
|
|
12
13
|
from sqlalchemy.orm.interfaces import ORMOption
|
|
13
14
|
|
|
14
15
|
from letta.log import get_logger
|
|
@@ -625,6 +626,12 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
625
626
|
if not no_refresh:
|
|
626
627
|
await db_session.refresh(self)
|
|
627
628
|
return self
|
|
629
|
+
except StaleDataError as e:
|
|
630
|
+
# This can occur when using optimistic locking (version_id_col) and:
|
|
631
|
+
# 1. The row doesn't exist (0 rows matched)
|
|
632
|
+
# 2. The version has changed (concurrent update)
|
|
633
|
+
# We convert this to NoResultFound to return a proper 404 error
|
|
634
|
+
raise NoResultFound(f"{self.__class__.__name__} with id '{self.id}' not found or was updated by another transaction") from e
|
|
628
635
|
except (DBAPIError, IntegrityError) as e:
|
|
629
636
|
self._handle_dbapi_error(e)
|
|
630
637
|
|
letta/otel/tracing.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import inspect
|
|
3
|
+
import itertools
|
|
3
4
|
import re
|
|
4
5
|
import time
|
|
5
6
|
import traceback
|
|
@@ -227,11 +228,137 @@ def trace_method(func):
|
|
|
227
228
|
if args and hasattr(args[0], "__class__"):
|
|
228
229
|
param_items = param_items[1:]
|
|
229
230
|
|
|
231
|
+
# Parameters to skip entirely (known to be large)
|
|
232
|
+
SKIP_PARAMS = {
|
|
233
|
+
"agent_state",
|
|
234
|
+
"messages",
|
|
235
|
+
"in_context_messages",
|
|
236
|
+
"message_sequence",
|
|
237
|
+
"content",
|
|
238
|
+
"tool_returns",
|
|
239
|
+
"memory",
|
|
240
|
+
"sources",
|
|
241
|
+
"context",
|
|
242
|
+
"resource_id",
|
|
243
|
+
"source_code",
|
|
244
|
+
"request_data",
|
|
245
|
+
"system",
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
# Max size for parameter value strings (1KB)
|
|
249
|
+
MAX_PARAM_SIZE = 1024
|
|
250
|
+
# Max total size for all parameters (100KB)
|
|
251
|
+
MAX_TOTAL_SIZE = 1024 * 100
|
|
252
|
+
total_size = 0
|
|
253
|
+
|
|
230
254
|
for name, value in param_items:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
255
|
+
try:
|
|
256
|
+
# Check if we've exceeded total size limit
|
|
257
|
+
if total_size > MAX_TOTAL_SIZE:
|
|
258
|
+
span.set_attribute("parameters.truncated", True)
|
|
259
|
+
span.set_attribute("parameters.truncated_reason", f"Total size exceeded {MAX_TOTAL_SIZE} bytes")
|
|
260
|
+
break
|
|
261
|
+
|
|
262
|
+
# Skip parameters known to be large
|
|
263
|
+
if name in SKIP_PARAMS:
|
|
264
|
+
# Try to extract ID for observability
|
|
265
|
+
type_name = type(value).__name__
|
|
266
|
+
id_info = ""
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
# Handle lists/iterables (e.g., messages)
|
|
270
|
+
if hasattr(value, "__iter__") and not isinstance(value, (str, bytes, dict)):
|
|
271
|
+
ids = []
|
|
272
|
+
count = 0
|
|
273
|
+
# Use itertools.islice to avoid converting entire iterable
|
|
274
|
+
for item in itertools.islice(value, 5):
|
|
275
|
+
count += 1
|
|
276
|
+
if hasattr(item, "id"):
|
|
277
|
+
ids.append(str(item.id))
|
|
278
|
+
|
|
279
|
+
# Try to get total count if it's a sized iterable
|
|
280
|
+
total_count = None
|
|
281
|
+
if hasattr(value, "__len__"):
|
|
282
|
+
try:
|
|
283
|
+
total_count = len(value)
|
|
284
|
+
except (TypeError, AttributeError):
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
if ids:
|
|
288
|
+
suffix = ""
|
|
289
|
+
if total_count is not None and total_count > 5:
|
|
290
|
+
suffix = f"... ({total_count} total)"
|
|
291
|
+
elif count == 5:
|
|
292
|
+
suffix = "..."
|
|
293
|
+
id_info = f", ids=[{','.join(ids)}{suffix}]"
|
|
294
|
+
# Handle single objects with id attribute
|
|
295
|
+
elif hasattr(value, "id"):
|
|
296
|
+
id_info = f", id={value.id}"
|
|
297
|
+
except (TypeError, AttributeError, ValueError):
|
|
298
|
+
pass
|
|
299
|
+
|
|
300
|
+
param_value = f"<{type_name} (excluded{id_info})>"
|
|
301
|
+
span.set_attribute(f"parameter.{name}", param_value)
|
|
302
|
+
total_size += len(param_value)
|
|
303
|
+
continue
|
|
304
|
+
|
|
305
|
+
# Try repr first with length limit, fallback to str if needed
|
|
306
|
+
str_value = None
|
|
307
|
+
|
|
308
|
+
# For simple types, use str directly
|
|
309
|
+
if isinstance(value, (str, int, float, bool, type(None))):
|
|
310
|
+
str_value = str(value)
|
|
311
|
+
else:
|
|
312
|
+
# For complex objects, try to get a truncated representation
|
|
313
|
+
try:
|
|
314
|
+
# Test if str() works (some objects have broken __str__)
|
|
315
|
+
try:
|
|
316
|
+
test_str = str(value)
|
|
317
|
+
# If str() works and is reasonable, use repr
|
|
318
|
+
str_value = repr(value)
|
|
319
|
+
except Exception:
|
|
320
|
+
# If str() fails, mark as serialization failed
|
|
321
|
+
raise ValueError("str() failed")
|
|
322
|
+
|
|
323
|
+
# If repr is already too long, try to be smarter
|
|
324
|
+
if len(str_value) > MAX_PARAM_SIZE * 2:
|
|
325
|
+
# For collections, show just the type and size
|
|
326
|
+
if hasattr(value, "__len__"):
|
|
327
|
+
try:
|
|
328
|
+
str_value = f"<{type(value).__name__} with {len(value)} items>"
|
|
329
|
+
except (TypeError, AttributeError):
|
|
330
|
+
str_value = f"<{type(value).__name__}>"
|
|
331
|
+
else:
|
|
332
|
+
str_value = f"<{type(value).__name__}>"
|
|
333
|
+
except (RecursionError, MemoryError, ValueError):
|
|
334
|
+
# Handle cases where repr or str causes issues
|
|
335
|
+
str_value = f"<serialization failed: {type(value).__name__}>"
|
|
336
|
+
except Exception as e:
|
|
337
|
+
# Fallback for any other issues
|
|
338
|
+
str_value = f"<serialization failed: {type(e).__name__}>"
|
|
339
|
+
|
|
340
|
+
# Apply size limit
|
|
341
|
+
original_size = len(str_value)
|
|
342
|
+
if original_size > MAX_PARAM_SIZE:
|
|
343
|
+
str_value = str_value[:MAX_PARAM_SIZE] + f"... (truncated, original size: {original_size} chars)"
|
|
344
|
+
|
|
345
|
+
span.set_attribute(f"parameter.{name}", str_value)
|
|
346
|
+
total_size += len(str_value)
|
|
347
|
+
|
|
348
|
+
except (TypeError, ValueError, AttributeError, RecursionError, MemoryError) as e:
|
|
349
|
+
try:
|
|
350
|
+
error_msg = f"<serialization failed: {type(e).__name__}>"
|
|
351
|
+
span.set_attribute(f"parameter.{name}", error_msg)
|
|
352
|
+
total_size += len(error_msg)
|
|
353
|
+
except Exception:
|
|
354
|
+
# If even the fallback fails, skip this parameter
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
except (TypeError, ValueError, AttributeError) as e:
|
|
358
|
+
logger.debug(f"Failed to add parameters to span: {type(e).__name__}: {e}")
|
|
359
|
+
except Exception as e:
|
|
360
|
+
# Catch-all for any other unexpected exceptions
|
|
361
|
+
logger.debug(f"Unexpected error adding parameters to span: {type(e).__name__}: {e}")
|
|
235
362
|
|
|
236
363
|
@wraps(func)
|
|
237
364
|
async def async_wrapper(*args, **kwargs):
|
letta/schemas/agent_file.py
CHANGED
|
@@ -7,7 +7,7 @@ from pydantic import BaseModel, Field
|
|
|
7
7
|
from letta.helpers.datetime_helpers import get_utc_time
|
|
8
8
|
from letta.schemas.agent import AgentState, CreateAgent
|
|
9
9
|
from letta.schemas.block import Block, CreateBlock
|
|
10
|
-
from letta.schemas.enums import MessageRole
|
|
10
|
+
from letta.schemas.enums import MessageRole, PrimitiveType
|
|
11
11
|
from letta.schemas.file import FileAgent, FileAgentBase, FileMetadata, FileMetadataBase
|
|
12
12
|
from letta.schemas.group import Group, GroupCreate
|
|
13
13
|
from letta.schemas.letta_message import ApprovalReturn
|
|
@@ -42,7 +42,7 @@ class ImportResult:
|
|
|
42
42
|
class MessageSchema(MessageCreate):
|
|
43
43
|
"""Message with human-readable ID for agent file"""
|
|
44
44
|
|
|
45
|
-
__id_prefix__ =
|
|
45
|
+
__id_prefix__ = PrimitiveType.MESSAGE.value
|
|
46
46
|
id: str = Field(..., description="Human-readable identifier for this message in the file")
|
|
47
47
|
|
|
48
48
|
# Override the role field to accept all message roles, not just user/system/assistant
|
|
@@ -96,7 +96,7 @@ class MessageSchema(MessageCreate):
|
|
|
96
96
|
class FileAgentSchema(FileAgentBase):
|
|
97
97
|
"""File-Agent relationship with human-readable ID for agent file"""
|
|
98
98
|
|
|
99
|
-
__id_prefix__ =
|
|
99
|
+
__id_prefix__ = PrimitiveType.FILE_AGENT.value
|
|
100
100
|
id: str = Field(..., description="Human-readable identifier for this file-agent relationship in the file")
|
|
101
101
|
|
|
102
102
|
@classmethod
|
|
@@ -120,7 +120,7 @@ class FileAgentSchema(FileAgentBase):
|
|
|
120
120
|
class AgentSchema(CreateAgent):
|
|
121
121
|
"""Agent with human-readable ID for agent file"""
|
|
122
122
|
|
|
123
|
-
__id_prefix__ =
|
|
123
|
+
__id_prefix__ = PrimitiveType.AGENT.value
|
|
124
124
|
id: str = Field(..., description="Human-readable identifier for this agent in the file")
|
|
125
125
|
in_context_message_ids: List[str] = Field(
|
|
126
126
|
default_factory=list, description="List of message IDs that are currently in the agent's context"
|
|
@@ -198,7 +198,7 @@ class AgentSchema(CreateAgent):
|
|
|
198
198
|
class GroupSchema(GroupCreate):
|
|
199
199
|
"""Group with human-readable ID for agent file"""
|
|
200
200
|
|
|
201
|
-
__id_prefix__ =
|
|
201
|
+
__id_prefix__ = PrimitiveType.GROUP.value
|
|
202
202
|
id: str = Field(..., description="Human-readable identifier for this group in the file")
|
|
203
203
|
|
|
204
204
|
@classmethod
|
|
@@ -220,7 +220,7 @@ class GroupSchema(GroupCreate):
|
|
|
220
220
|
class BlockSchema(CreateBlock):
|
|
221
221
|
"""Block with human-readable ID for agent file"""
|
|
222
222
|
|
|
223
|
-
__id_prefix__ =
|
|
223
|
+
__id_prefix__ = PrimitiveType.BLOCK.value
|
|
224
224
|
id: str = Field(..., description="Human-readable identifier for this block in the file")
|
|
225
225
|
|
|
226
226
|
@classmethod
|
|
@@ -246,7 +246,7 @@ class BlockSchema(CreateBlock):
|
|
|
246
246
|
class FileSchema(FileMetadataBase):
|
|
247
247
|
"""File with human-readable ID for agent file"""
|
|
248
248
|
|
|
249
|
-
__id_prefix__ =
|
|
249
|
+
__id_prefix__ = PrimitiveType.FILE.value
|
|
250
250
|
id: str = Field(..., description="Human-readable identifier for this file in the file")
|
|
251
251
|
|
|
252
252
|
@classmethod
|
|
@@ -276,7 +276,7 @@ class FileSchema(FileMetadataBase):
|
|
|
276
276
|
class SourceSchema(SourceCreate):
|
|
277
277
|
"""Source with human-readable ID for agent file"""
|
|
278
278
|
|
|
279
|
-
__id_prefix__ =
|
|
279
|
+
__id_prefix__ = PrimitiveType.SOURCE.value
|
|
280
280
|
id: str = Field(..., description="Human-readable identifier for this source in the file")
|
|
281
281
|
|
|
282
282
|
@classmethod
|
|
@@ -299,7 +299,7 @@ class SourceSchema(SourceCreate):
|
|
|
299
299
|
class ToolSchema(Tool):
|
|
300
300
|
"""Tool with human-readable ID for agent file"""
|
|
301
301
|
|
|
302
|
-
__id_prefix__ =
|
|
302
|
+
__id_prefix__ = PrimitiveType.TOOL.value
|
|
303
303
|
id: str = Field(..., description="Human-readable identifier for this tool in the file")
|
|
304
304
|
|
|
305
305
|
@classmethod
|
|
@@ -311,7 +311,7 @@ class ToolSchema(Tool):
|
|
|
311
311
|
class MCPServerSchema(BaseModel):
|
|
312
312
|
"""MCP server schema for agent files with remapped ID."""
|
|
313
313
|
|
|
314
|
-
__id_prefix__ =
|
|
314
|
+
__id_prefix__ = PrimitiveType.MCP_SERVER.value
|
|
315
315
|
|
|
316
316
|
id: str = Field(..., description="Human-readable MCP server ID")
|
|
317
317
|
server_type: str
|
letta/schemas/block.py
CHANGED
|
@@ -21,9 +21,9 @@ class BaseBlock(LettaBase, validate_assignment=True):
|
|
|
21
21
|
|
|
22
22
|
project_id: Optional[str] = Field(None, description="The associated project id.")
|
|
23
23
|
# template data (optional)
|
|
24
|
-
template_name: Optional[str] = Field(None, description="Name of the block if it is a template."
|
|
24
|
+
template_name: Optional[str] = Field(None, description="Name of the block if it is a template.")
|
|
25
25
|
is_template: bool = Field(False, description="Whether the block is a template (e.g. saved human/persona options).")
|
|
26
|
-
template_id: Optional[str] = Field(None, description="The id of the template."
|
|
26
|
+
template_id: Optional[str] = Field(None, description="The id of the template.")
|
|
27
27
|
base_template_id: Optional[str] = Field(None, description="The base template id of the block.")
|
|
28
28
|
deployment_id: Optional[str] = Field(None, description="The id of the deployment.")
|
|
29
29
|
entity_id: Optional[str] = Field(None, description="The id of the entity within the template.")
|
|
@@ -102,6 +102,25 @@ class Block(BaseBlock):
|
|
|
102
102
|
last_updated_by_id: Optional[str] = Field(None, description="The id of the user that last updated this Block.")
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
class BlockResponse(Block):
|
|
106
|
+
id: str = Field(
|
|
107
|
+
...,
|
|
108
|
+
description="The id of the block.",
|
|
109
|
+
)
|
|
110
|
+
template_name: Optional[str] = Field(
|
|
111
|
+
None, description="(Deprecated) The name of the block template (if it is a template).", deprecated=True
|
|
112
|
+
)
|
|
113
|
+
template_id: Optional[str] = Field(None, description="(Deprecated) The id of the template.", deprecated=True)
|
|
114
|
+
base_template_id: Optional[str] = Field(None, description="(Deprecated) The base template id of the block.", deprecated=True)
|
|
115
|
+
deployment_id: Optional[str] = Field(None, description="(Deprecated) The id of the deployment.", deprecated=True)
|
|
116
|
+
entity_id: Optional[str] = Field(None, description="(Deprecated) The id of the entity within the template.", deprecated=True)
|
|
117
|
+
preserve_on_migration: Optional[bool] = Field(
|
|
118
|
+
False, description="(Deprecated) Preserve the block on template migration.", deprecated=True
|
|
119
|
+
)
|
|
120
|
+
read_only: bool = Field(False, description="(Deprecated) Whether the agent has read-only access to the block.", deprecated=True)
|
|
121
|
+
hidden: Optional[bool] = Field(None, description="(Deprecated) If set to True, the block will be hidden.", deprecated=True)
|
|
122
|
+
|
|
123
|
+
|
|
105
124
|
class FileBlock(Block):
|
|
106
125
|
file_id: str = Field(..., description="Unique identifier of the file.")
|
|
107
126
|
source_id: str = Field(..., description="Unique identifier of the source.")
|
|
@@ -149,7 +168,7 @@ class CreateBlock(BaseBlock):
|
|
|
149
168
|
project_id: Optional[str] = Field(None, description="The associated project id.")
|
|
150
169
|
# block templates
|
|
151
170
|
is_template: bool = False
|
|
152
|
-
template_name: Optional[str] = Field(None, description="Name of the block if it is a template."
|
|
171
|
+
template_name: Optional[str] = Field(None, description="Name of the block if it is a template.")
|
|
153
172
|
|
|
154
173
|
@model_validator(mode="before")
|
|
155
174
|
@classmethod
|
letta/schemas/enums.py
CHANGED
|
@@ -26,6 +26,27 @@ class PrimitiveType(str, Enum):
|
|
|
26
26
|
STEP = "step"
|
|
27
27
|
IDENTITY = "identity"
|
|
28
28
|
|
|
29
|
+
# Infrastructure types
|
|
30
|
+
MCP_SERVER = "mcp_server"
|
|
31
|
+
MCP_OAUTH = "mcp-oauth"
|
|
32
|
+
FILE_AGENT = "file_agent"
|
|
33
|
+
|
|
34
|
+
# Configuration types
|
|
35
|
+
SANDBOX_ENV = "sandbox-env"
|
|
36
|
+
AGENT_ENV = "agent-env"
|
|
37
|
+
|
|
38
|
+
# Core entity types
|
|
39
|
+
USER = "user"
|
|
40
|
+
ORGANIZATION = "org"
|
|
41
|
+
TOOL_RULE = "tool_rule"
|
|
42
|
+
|
|
43
|
+
# Batch processing types
|
|
44
|
+
BATCH_ITEM = "batch_item"
|
|
45
|
+
BATCH_REQUEST = "batch_req"
|
|
46
|
+
|
|
47
|
+
# Telemetry types
|
|
48
|
+
PROVIDER_TRACE = "provider_trace"
|
|
49
|
+
|
|
29
50
|
|
|
30
51
|
class ProviderType(str, Enum):
|
|
31
52
|
anthropic = "anthropic"
|