letta-nightly 0.13.0.dev20251031104146__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.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/METADATA +1 -1
- {letta_nightly-0.13.0.dev20251031104146.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.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/WHEEL +0 -0
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.13.0.dev20251031104146.dist-info → letta_nightly-0.13.1.dev20251031234110.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,7 @@ from typing import Optional
|
|
|
2
2
|
|
|
3
3
|
from pydantic import Field
|
|
4
4
|
|
|
5
|
+
from letta.schemas.enums import PrimitiveType
|
|
5
6
|
from letta.schemas.letta_base import LettaBase, OrmMetadataBase
|
|
6
7
|
from letta.schemas.secret import Secret
|
|
7
8
|
from letta.settings import settings
|
|
@@ -52,7 +53,7 @@ class EnvironmentVariableUpdateBase(LettaBase):
|
|
|
52
53
|
|
|
53
54
|
# Environment Variable
|
|
54
55
|
class SandboxEnvironmentVariableBase(EnvironmentVariableBase):
|
|
55
|
-
__id_prefix__ =
|
|
56
|
+
__id_prefix__ = PrimitiveType.SANDBOX_ENV.value
|
|
56
57
|
sandbox_config_id: str = Field(..., description="The ID of the sandbox config this environment variable belongs to.")
|
|
57
58
|
|
|
58
59
|
|
|
@@ -70,7 +71,7 @@ class SandboxEnvironmentVariableUpdate(EnvironmentVariableUpdateBase):
|
|
|
70
71
|
|
|
71
72
|
# Agent-Specific Environment Variable
|
|
72
73
|
class AgentEnvironmentVariableBase(EnvironmentVariableBase):
|
|
73
|
-
__id_prefix__ =
|
|
74
|
+
__id_prefix__ = PrimitiveType.AGENT_ENV.value
|
|
74
75
|
agent_id: str = Field(..., description="The ID of the agent this environment variable belongs to.")
|
|
75
76
|
|
|
76
77
|
|
letta/schemas/group.py
CHANGED
|
@@ -34,7 +34,7 @@ class Group(GroupBase):
|
|
|
34
34
|
template_id: Optional[str] = Field(None, description="The id of the template.")
|
|
35
35
|
base_template_id: Optional[str] = Field(None, description="The base template id.")
|
|
36
36
|
deployment_id: Optional[str] = Field(None, description="The id of the deployment.")
|
|
37
|
-
shared_block_ids: List[str] = Field([], description="")
|
|
37
|
+
shared_block_ids: List[str] = Field([], description="", deprecated=True)
|
|
38
38
|
# Pattern fields
|
|
39
39
|
manager_agent_id: Optional[str] = Field(None, description="")
|
|
40
40
|
termination_token: Optional[str] = Field(None, description="")
|
|
@@ -174,7 +174,7 @@ class GroupCreate(BaseModel):
|
|
|
174
174
|
description: str = Field(..., description="")
|
|
175
175
|
manager_config: ManagerConfigUnion = Field(RoundRobinManager(), description="")
|
|
176
176
|
project_id: Optional[str] = Field(None, description="The associated project id.")
|
|
177
|
-
shared_block_ids: List[str] = Field([], description="")
|
|
177
|
+
shared_block_ids: List[str] = Field([], description="", deprecated=True)
|
|
178
178
|
hidden: Optional[bool] = Field(
|
|
179
179
|
None,
|
|
180
180
|
description="If set to True, the group will be hidden.",
|
|
@@ -194,4 +194,4 @@ class GroupUpdate(BaseModel):
|
|
|
194
194
|
description: Optional[str] = Field(None, description="")
|
|
195
195
|
manager_config: Optional[ManagerConfigUpdateUnion] = Field(None, description="")
|
|
196
196
|
project_id: Optional[str] = Field(None, description="The associated project id.")
|
|
197
|
-
shared_block_ids: Optional[List[str]] = Field(None, description="")
|
|
197
|
+
shared_block_ids: Optional[List[str]] = Field(None, description="", deprecated=True)
|
letta/schemas/letta_response.py
CHANGED
|
@@ -4,11 +4,24 @@ import re
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from typing import List, Union
|
|
6
6
|
|
|
7
|
-
from pydantic import BaseModel, Field
|
|
7
|
+
from pydantic import BaseModel, Field, RootModel
|
|
8
8
|
|
|
9
9
|
from letta.helpers.json_helpers import json_dumps
|
|
10
10
|
from letta.schemas.enums import JobStatus, MessageStreamStatus
|
|
11
|
-
from letta.schemas.letta_message import
|
|
11
|
+
from letta.schemas.letta_message import (
|
|
12
|
+
ApprovalRequestMessage,
|
|
13
|
+
ApprovalResponseMessage,
|
|
14
|
+
AssistantMessage,
|
|
15
|
+
HiddenReasoningMessage,
|
|
16
|
+
LettaMessage,
|
|
17
|
+
LettaMessageUnion,
|
|
18
|
+
LettaPing,
|
|
19
|
+
ReasoningMessage,
|
|
20
|
+
SystemMessage,
|
|
21
|
+
ToolCallMessage,
|
|
22
|
+
ToolReturnMessage,
|
|
23
|
+
UserMessage,
|
|
24
|
+
)
|
|
12
25
|
from letta.schemas.letta_stop_reason import LettaStopReason
|
|
13
26
|
from letta.schemas.message import Message
|
|
14
27
|
from letta.schemas.usage import LettaUsageStatistics
|
|
@@ -170,8 +183,27 @@ class LettaResponse(BaseModel):
|
|
|
170
183
|
return html_output
|
|
171
184
|
|
|
172
185
|
|
|
173
|
-
# The streaming response
|
|
174
|
-
LettaStreamingResponse
|
|
186
|
+
# The streaming response can be any of the individual message types, plus metadata types
|
|
187
|
+
class LettaStreamingResponse(RootModel):
|
|
188
|
+
"""
|
|
189
|
+
Streaming response type for Server-Sent Events (SSE) endpoints.
|
|
190
|
+
Each event in the stream will be one of these types.
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
root: Union[
|
|
194
|
+
SystemMessage,
|
|
195
|
+
UserMessage,
|
|
196
|
+
ReasoningMessage,
|
|
197
|
+
HiddenReasoningMessage,
|
|
198
|
+
ToolCallMessage,
|
|
199
|
+
ToolReturnMessage,
|
|
200
|
+
AssistantMessage,
|
|
201
|
+
ApprovalRequestMessage,
|
|
202
|
+
ApprovalResponseMessage,
|
|
203
|
+
LettaPing,
|
|
204
|
+
LettaStopReason,
|
|
205
|
+
LettaUsageStatistics,
|
|
206
|
+
] = Field(..., discriminator="message_type")
|
|
175
207
|
|
|
176
208
|
|
|
177
209
|
class LettaBatchResponse(BaseModel):
|
letta/schemas/llm_batch_job.py
CHANGED
|
@@ -5,7 +5,7 @@ from anthropic.types.beta.messages import BetaMessageBatch, BetaMessageBatchIndi
|
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
7
|
from letta.helpers import ToolRulesSolver
|
|
8
|
-
from letta.schemas.enums import AgentStepStatus, JobStatus, ProviderType
|
|
8
|
+
from letta.schemas.enums import AgentStepStatus, JobStatus, PrimitiveType, ProviderType
|
|
9
9
|
from letta.schemas.letta_base import OrmMetadataBase
|
|
10
10
|
from letta.schemas.llm_config import LLMConfig
|
|
11
11
|
|
|
@@ -16,7 +16,7 @@ class AgentStepState(BaseModel):
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
class LLMBatchItemBase(OrmMetadataBase, validate_assignment=True):
|
|
19
|
-
__id_prefix__ =
|
|
19
|
+
__id_prefix__ = PrimitiveType.BATCH_ITEM.value
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
class LLMBatchItem(LLMBatchItemBase, validate_assignment=True):
|
|
@@ -47,7 +47,7 @@ class LLMBatchJob(OrmMetadataBase, validate_assignment=True):
|
|
|
47
47
|
Each job corresponds to one API call that sends multiple messages to the LLM provider, and aggregates responses across all agent submissions.
|
|
48
48
|
"""
|
|
49
49
|
|
|
50
|
-
__id_prefix__ =
|
|
50
|
+
__id_prefix__ = PrimitiveType.BATCH_REQUEST.value
|
|
51
51
|
|
|
52
52
|
id: Optional[str] = Field(None, description="The id of the batch job. Assigned by the database.")
|
|
53
53
|
status: JobStatus = Field(..., description="The current status of the batch (e.g., created, in_progress, done).")
|
letta/schemas/llm_config.py
CHANGED
|
@@ -3,6 +3,7 @@ from typing import TYPE_CHECKING, Literal, Optional
|
|
|
3
3
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
4
4
|
|
|
5
5
|
from letta.constants import LETTA_MODEL_ENDPOINT
|
|
6
|
+
from letta.errors import LettaInvalidArgumentError
|
|
6
7
|
from letta.log import get_logger
|
|
7
8
|
from letta.schemas.enums import AgentType, ProviderCategory
|
|
8
9
|
|
|
@@ -163,6 +164,24 @@ class LLMConfig(BaseModel):
|
|
|
163
164
|
|
|
164
165
|
return values
|
|
165
166
|
|
|
167
|
+
@model_validator(mode="before")
|
|
168
|
+
@classmethod
|
|
169
|
+
def validate_codex_reasoning_effort(cls, values):
|
|
170
|
+
"""
|
|
171
|
+
Validate that gpt-5-codex models do not use 'minimal' reasoning effort.
|
|
172
|
+
Codex models require at least 'low' reasoning effort.
|
|
173
|
+
"""
|
|
174
|
+
from letta.llm_api.openai_client import does_not_support_minimal_reasoning
|
|
175
|
+
|
|
176
|
+
model = values.get("model")
|
|
177
|
+
reasoning_effort = values.get("reasoning_effort")
|
|
178
|
+
|
|
179
|
+
if model and does_not_support_minimal_reasoning(model) and reasoning_effort == "minimal":
|
|
180
|
+
raise LettaInvalidArgumentError(
|
|
181
|
+
f"Model '{model}' does not support 'minimal' reasoning effort. Please use 'low', 'medium', or 'high' instead."
|
|
182
|
+
)
|
|
183
|
+
return values
|
|
184
|
+
|
|
166
185
|
@classmethod
|
|
167
186
|
def default_config(cls, model_name: str):
|
|
168
187
|
"""
|
|
@@ -277,6 +296,8 @@ class LLMConfig(BaseModel):
|
|
|
277
296
|
- Google Gemini (2.5 family): force disabled until native reasoning supported
|
|
278
297
|
- All others: disabled (no simulated reasoning via kwargs)
|
|
279
298
|
"""
|
|
299
|
+
from letta.llm_api.openai_client import does_not_support_minimal_reasoning
|
|
300
|
+
|
|
280
301
|
# V1 agent policy: do not allow simulated reasoning for non-native models
|
|
281
302
|
if agent_type is not None and agent_type == AgentType.letta_v1_agent:
|
|
282
303
|
# OpenAI native reasoning models: always on
|
|
@@ -284,7 +305,8 @@ class LLMConfig(BaseModel):
|
|
|
284
305
|
config.put_inner_thoughts_in_kwargs = False
|
|
285
306
|
config.enable_reasoner = True
|
|
286
307
|
if config.reasoning_effort is None:
|
|
287
|
-
|
|
308
|
+
# Codex models cannot use "minimal" reasoning effort
|
|
309
|
+
if config.model.startswith("gpt-5") and not does_not_support_minimal_reasoning(config.model):
|
|
288
310
|
config.reasoning_effort = "minimal"
|
|
289
311
|
else:
|
|
290
312
|
config.reasoning_effort = "medium"
|
|
@@ -324,7 +346,8 @@ class LLMConfig(BaseModel):
|
|
|
324
346
|
config.enable_reasoner = True
|
|
325
347
|
if config.reasoning_effort is None:
|
|
326
348
|
# GPT-5 models default to minimal, others to medium
|
|
327
|
-
|
|
349
|
+
# Codex models cannot use "minimal" reasoning effort
|
|
350
|
+
if config.model.startswith("gpt-5") and not does_not_support_minimal_reasoning(config.model):
|
|
328
351
|
config.reasoning_effort = "minimal"
|
|
329
352
|
else:
|
|
330
353
|
config.reasoning_effort = "medium"
|
|
@@ -357,7 +380,8 @@ class LLMConfig(BaseModel):
|
|
|
357
380
|
config.put_inner_thoughts_in_kwargs = False
|
|
358
381
|
if config.reasoning_effort is None:
|
|
359
382
|
# GPT-5 models default to minimal, others to medium
|
|
360
|
-
|
|
383
|
+
# Codex models cannot use "minimal" reasoning effort
|
|
384
|
+
if config.model.startswith("gpt-5") and not does_not_support_minimal_reasoning(config.model):
|
|
361
385
|
config.reasoning_effort = "minimal"
|
|
362
386
|
else:
|
|
363
387
|
config.reasoning_effort = "medium"
|
letta/schemas/mcp.py
CHANGED
|
@@ -13,13 +13,14 @@ from letta.functions.mcp_client.types import (
|
|
|
13
13
|
StreamableHTTPServerConfig,
|
|
14
14
|
)
|
|
15
15
|
from letta.orm.mcp_oauth import OAuthSessionStatus
|
|
16
|
+
from letta.schemas.enums import PrimitiveType
|
|
16
17
|
from letta.schemas.letta_base import LettaBase
|
|
17
18
|
from letta.schemas.secret import Secret
|
|
18
19
|
from letta.settings import settings
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
class BaseMCPServer(LettaBase):
|
|
22
|
-
__id_prefix__ =
|
|
23
|
+
__id_prefix__ = PrimitiveType.MCP_SERVER.value
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
class MCPServer(BaseMCPServer):
|
|
@@ -178,7 +179,7 @@ UpdateMCPServer = Union[UpdateSSEMCPServer, UpdateStdioMCPServer, UpdateStreamab
|
|
|
178
179
|
|
|
179
180
|
# OAuth-related schemas
|
|
180
181
|
class BaseMCPOAuth(LettaBase):
|
|
181
|
-
__id_prefix__ =
|
|
182
|
+
__id_prefix__ = PrimitiveType.MCP_OAUTH.value
|
|
182
183
|
|
|
183
184
|
|
|
184
185
|
class MCPOAuthSession(BaseMCPOAuth):
|
letta/schemas/mcp_server.py
CHANGED
|
@@ -13,12 +13,13 @@ from letta.functions.mcp_client.types import (
|
|
|
13
13
|
StreamableHTTPServerConfig,
|
|
14
14
|
)
|
|
15
15
|
from letta.orm.mcp_oauth import OAuthSessionStatus
|
|
16
|
+
from letta.schemas.enums import PrimitiveType
|
|
16
17
|
from letta.schemas.letta_base import LettaBase
|
|
17
18
|
from letta.schemas.secret import Secret
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
class BaseMCPServer(LettaBase):
|
|
21
|
-
__id_prefix__ =
|
|
22
|
+
__id_prefix__ = PrimitiveType.MCP_SERVER.value
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
# Create Schemas (for POST requests)
|
|
@@ -101,7 +102,7 @@ UpdateMCPServerUnion = Union[UpdateStdioMCPServer, UpdateSSEMCPServer, UpdateStr
|
|
|
101
102
|
|
|
102
103
|
# OAuth-related schemas
|
|
103
104
|
class BaseMCPOAuth(LettaBase):
|
|
104
|
-
__id_prefix__ =
|
|
105
|
+
__id_prefix__ = PrimitiveType.MCP_OAUTH.value
|
|
105
106
|
|
|
106
107
|
|
|
107
108
|
class MCPOAuthSession(BaseMCPOAuth):
|
letta/schemas/message.py
CHANGED
|
@@ -13,7 +13,6 @@ from datetime import datetime, timezone
|
|
|
13
13
|
from enum import Enum
|
|
14
14
|
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
|
15
15
|
|
|
16
|
-
from letta_client import LettaMessageUnion
|
|
17
16
|
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall, Function as OpenAIFunction
|
|
18
17
|
from openai.types.responses import ResponseReasoningItem
|
|
19
18
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
@@ -57,6 +56,14 @@ from letta.system import unpack_message
|
|
|
57
56
|
from letta.utils import parse_json, validate_function_response
|
|
58
57
|
|
|
59
58
|
|
|
59
|
+
def truncate_tool_return(content: Optional[str], limit: Optional[int]) -> Optional[str]:
|
|
60
|
+
if limit is None or content is None:
|
|
61
|
+
return content
|
|
62
|
+
if len(content) <= limit:
|
|
63
|
+
return content
|
|
64
|
+
return content[:limit] + f"... [truncated {len(content) - limit} chars]"
|
|
65
|
+
|
|
66
|
+
|
|
60
67
|
def add_inner_thoughts_to_tool_call(
|
|
61
68
|
tool_call: OpenAIToolCall,
|
|
62
69
|
inner_thoughts: str,
|
|
@@ -1091,6 +1098,7 @@ class Message(BaseMessage):
|
|
|
1091
1098
|
# if true, then treat the content field as AssistantMessage
|
|
1092
1099
|
native_content: bool = False,
|
|
1093
1100
|
strip_request_heartbeat: bool = False,
|
|
1101
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1094
1102
|
) -> dict | None:
|
|
1095
1103
|
"""Go from Message class to ChatCompletion message object"""
|
|
1096
1104
|
assert not (native_content and put_inner_thoughts_in_kwargs), "native_content and put_inner_thoughts_in_kwargs cannot both be true"
|
|
@@ -1139,8 +1147,14 @@ class Message(BaseMessage):
|
|
|
1139
1147
|
assert self.tool_calls is not None or text_content is not None, vars(self)
|
|
1140
1148
|
except AssertionError as e:
|
|
1141
1149
|
# relax check if this message only contains reasoning content
|
|
1142
|
-
if self.content is not None and len(self.content) > 0
|
|
1143
|
-
|
|
1150
|
+
if self.content is not None and len(self.content) > 0:
|
|
1151
|
+
# Check if all non-empty content is reasoning-related
|
|
1152
|
+
all_reasoning = all(
|
|
1153
|
+
isinstance(c, (ReasoningContent, SummarizedReasoningContent, OmittedReasoningContent, RedactedReasoningContent))
|
|
1154
|
+
for c in self.content
|
|
1155
|
+
)
|
|
1156
|
+
if all_reasoning:
|
|
1157
|
+
return None
|
|
1144
1158
|
raise e
|
|
1145
1159
|
|
|
1146
1160
|
# if native content, then put it directly inside the content
|
|
@@ -1181,12 +1195,26 @@ class Message(BaseMessage):
|
|
|
1181
1195
|
tool_call_dict["id"] = tool_call_dict["id"][:max_tool_id_length]
|
|
1182
1196
|
|
|
1183
1197
|
elif self.role == "tool":
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1198
|
+
# Handle tool returns - if tool_returns exists, use the first one
|
|
1199
|
+
if self.tool_returns and len(self.tool_returns) > 0:
|
|
1200
|
+
tool_return = self.tool_returns[0]
|
|
1201
|
+
if not tool_return.tool_call_id:
|
|
1202
|
+
raise TypeError("OpenAI API requires tool_call_id to be set.")
|
|
1203
|
+
func_response = truncate_tool_return(tool_return.func_response, tool_return_truncation_chars)
|
|
1204
|
+
openai_message = {
|
|
1205
|
+
"content": func_response,
|
|
1206
|
+
"role": self.role,
|
|
1207
|
+
"tool_call_id": tool_return.tool_call_id[:max_tool_id_length] if max_tool_id_length else tool_return.tool_call_id,
|
|
1208
|
+
}
|
|
1209
|
+
else:
|
|
1210
|
+
# Legacy fallback for old message format
|
|
1211
|
+
assert self.tool_call_id is not None, vars(self)
|
|
1212
|
+
legacy_content = truncate_tool_return(text_content, tool_return_truncation_chars)
|
|
1213
|
+
openai_message = {
|
|
1214
|
+
"content": legacy_content,
|
|
1215
|
+
"role": self.role,
|
|
1216
|
+
"tool_call_id": self.tool_call_id[:max_tool_id_length] if max_tool_id_length else self.tool_call_id,
|
|
1217
|
+
}
|
|
1190
1218
|
|
|
1191
1219
|
else:
|
|
1192
1220
|
raise ValueError(self.role)
|
|
@@ -1215,22 +1243,42 @@ class Message(BaseMessage):
|
|
|
1215
1243
|
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
|
1216
1244
|
put_inner_thoughts_in_kwargs: bool = False,
|
|
1217
1245
|
use_developer_message: bool = False,
|
|
1246
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1218
1247
|
) -> List[dict]:
|
|
1219
1248
|
messages = Message.filter_messages_for_llm_api(messages)
|
|
1220
|
-
result = [
|
|
1221
|
-
|
|
1249
|
+
result: List[dict] = []
|
|
1250
|
+
|
|
1251
|
+
for m in messages:
|
|
1252
|
+
# Special case: OpenAI Chat Completions requires a separate tool message per tool_call_id
|
|
1253
|
+
# If we have multiple explicit tool_returns on a single Message, expand into one dict per return
|
|
1254
|
+
if m.role == MessageRole.tool and m.tool_returns and len(m.tool_returns) > 0:
|
|
1255
|
+
for tr in m.tool_returns:
|
|
1256
|
+
if not tr.tool_call_id:
|
|
1257
|
+
raise TypeError("ToolReturn came back without a tool_call_id.")
|
|
1258
|
+
result.append(
|
|
1259
|
+
{
|
|
1260
|
+
"content": tr.func_response,
|
|
1261
|
+
"role": "tool",
|
|
1262
|
+
"tool_call_id": tr.tool_call_id[:max_tool_id_length] if max_tool_id_length else tr.tool_call_id,
|
|
1263
|
+
}
|
|
1264
|
+
)
|
|
1265
|
+
continue
|
|
1266
|
+
|
|
1267
|
+
d = m.to_openai_dict(
|
|
1222
1268
|
max_tool_id_length=max_tool_id_length,
|
|
1223
1269
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
|
1224
1270
|
use_developer_message=use_developer_message,
|
|
1271
|
+
tool_return_truncation_chars=tool_return_truncation_chars,
|
|
1225
1272
|
)
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1273
|
+
if d is not None:
|
|
1274
|
+
result.append(d)
|
|
1275
|
+
|
|
1229
1276
|
return result
|
|
1230
1277
|
|
|
1231
1278
|
def to_openai_responses_dicts(
|
|
1232
1279
|
self,
|
|
1233
1280
|
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
|
1281
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1234
1282
|
) -> List[dict]:
|
|
1235
1283
|
"""Go from Message class to ChatCompletion message object"""
|
|
1236
1284
|
|
|
@@ -1306,15 +1354,31 @@ class Message(BaseMessage):
|
|
|
1306
1354
|
)
|
|
1307
1355
|
|
|
1308
1356
|
elif self.role == "tool":
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1357
|
+
# Handle tool returns - similar pattern to Anthropic
|
|
1358
|
+
if self.tool_returns:
|
|
1359
|
+
for tool_return in self.tool_returns:
|
|
1360
|
+
if not tool_return.tool_call_id:
|
|
1361
|
+
raise TypeError("OpenAI Responses API requires tool_call_id to be set.")
|
|
1362
|
+
func_response = truncate_tool_return(tool_return.func_response, tool_return_truncation_chars)
|
|
1363
|
+
message_dicts.append(
|
|
1364
|
+
{
|
|
1365
|
+
"type": "function_call_output",
|
|
1366
|
+
"call_id": tool_return.tool_call_id[:max_tool_id_length] if max_tool_id_length else tool_return.tool_call_id,
|
|
1367
|
+
"output": func_response,
|
|
1368
|
+
}
|
|
1369
|
+
)
|
|
1370
|
+
else:
|
|
1371
|
+
# Legacy fallback for old message format
|
|
1372
|
+
assert self.tool_call_id is not None, vars(self)
|
|
1373
|
+
assert len(self.content) == 1 and isinstance(self.content[0], TextContent), vars(self)
|
|
1374
|
+
legacy_output = truncate_tool_return(self.content[0].text, tool_return_truncation_chars)
|
|
1375
|
+
message_dicts.append(
|
|
1376
|
+
{
|
|
1377
|
+
"type": "function_call_output",
|
|
1378
|
+
"call_id": self.tool_call_id[:max_tool_id_length] if max_tool_id_length else self.tool_call_id,
|
|
1379
|
+
"output": legacy_output,
|
|
1380
|
+
}
|
|
1381
|
+
)
|
|
1318
1382
|
|
|
1319
1383
|
else:
|
|
1320
1384
|
raise ValueError(self.role)
|
|
@@ -1325,11 +1389,16 @@ class Message(BaseMessage):
|
|
|
1325
1389
|
def to_openai_responses_dicts_from_list(
|
|
1326
1390
|
messages: List[Message],
|
|
1327
1391
|
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
|
1392
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1328
1393
|
) -> List[dict]:
|
|
1329
1394
|
messages = Message.filter_messages_for_llm_api(messages)
|
|
1330
1395
|
result = []
|
|
1331
1396
|
for message in messages:
|
|
1332
|
-
result.extend(
|
|
1397
|
+
result.extend(
|
|
1398
|
+
message.to_openai_responses_dicts(
|
|
1399
|
+
max_tool_id_length=max_tool_id_length, tool_return_truncation_chars=tool_return_truncation_chars
|
|
1400
|
+
)
|
|
1401
|
+
)
|
|
1333
1402
|
return result
|
|
1334
1403
|
|
|
1335
1404
|
def to_anthropic_dict(
|
|
@@ -1340,6 +1409,7 @@ class Message(BaseMessage):
|
|
|
1340
1409
|
# if true, then treat the content field as AssistantMessage
|
|
1341
1410
|
native_content: bool = False,
|
|
1342
1411
|
strip_request_heartbeat: bool = False,
|
|
1412
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1343
1413
|
) -> dict | None:
|
|
1344
1414
|
"""
|
|
1345
1415
|
Convert to an Anthropic message dictionary
|
|
@@ -1515,11 +1585,12 @@ class Message(BaseMessage):
|
|
|
1515
1585
|
for tool_return in self.tool_returns:
|
|
1516
1586
|
if not tool_return.tool_call_id:
|
|
1517
1587
|
raise TypeError("Anthropic API requires tool_use_id to be set.")
|
|
1588
|
+
func_response = truncate_tool_return(tool_return.func_response, tool_return_truncation_chars)
|
|
1518
1589
|
content.append(
|
|
1519
1590
|
{
|
|
1520
1591
|
"type": "tool_result",
|
|
1521
1592
|
"tool_use_id": tool_return.tool_call_id,
|
|
1522
|
-
"content":
|
|
1593
|
+
"content": func_response,
|
|
1523
1594
|
}
|
|
1524
1595
|
)
|
|
1525
1596
|
if content:
|
|
@@ -1532,6 +1603,7 @@ class Message(BaseMessage):
|
|
|
1532
1603
|
raise TypeError("Anthropic API requires tool_use_id to be set.")
|
|
1533
1604
|
|
|
1534
1605
|
# This is for legacy reasons
|
|
1606
|
+
legacy_content = truncate_tool_return(text_content, tool_return_truncation_chars)
|
|
1535
1607
|
anthropic_message = {
|
|
1536
1608
|
"role": "user", # NOTE: diff
|
|
1537
1609
|
"content": [
|
|
@@ -1539,7 +1611,7 @@ class Message(BaseMessage):
|
|
|
1539
1611
|
{
|
|
1540
1612
|
"type": "tool_result",
|
|
1541
1613
|
"tool_use_id": self.tool_call_id,
|
|
1542
|
-
"content":
|
|
1614
|
+
"content": legacy_content,
|
|
1543
1615
|
}
|
|
1544
1616
|
],
|
|
1545
1617
|
}
|
|
@@ -1558,6 +1630,7 @@ class Message(BaseMessage):
|
|
|
1558
1630
|
# if true, then treat the content field as AssistantMessage
|
|
1559
1631
|
native_content: bool = False,
|
|
1560
1632
|
strip_request_heartbeat: bool = False,
|
|
1633
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1561
1634
|
) -> List[dict]:
|
|
1562
1635
|
messages = Message.filter_messages_for_llm_api(messages)
|
|
1563
1636
|
result = [
|
|
@@ -1567,6 +1640,7 @@ class Message(BaseMessage):
|
|
|
1567
1640
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
|
1568
1641
|
native_content=native_content,
|
|
1569
1642
|
strip_request_heartbeat=strip_request_heartbeat,
|
|
1643
|
+
tool_return_truncation_chars=tool_return_truncation_chars,
|
|
1570
1644
|
)
|
|
1571
1645
|
for m in messages
|
|
1572
1646
|
]
|
|
@@ -1580,6 +1654,7 @@ class Message(BaseMessage):
|
|
|
1580
1654
|
# if true, then treat the content field as AssistantMessage
|
|
1581
1655
|
native_content: bool = False,
|
|
1582
1656
|
strip_request_heartbeat: bool = False,
|
|
1657
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1583
1658
|
) -> dict | None:
|
|
1584
1659
|
"""
|
|
1585
1660
|
Go from Message class to Google AI REST message object
|
|
@@ -1717,34 +1792,75 @@ class Message(BaseMessage):
|
|
|
1717
1792
|
|
|
1718
1793
|
elif self.role == "tool":
|
|
1719
1794
|
# NOTE: Significantly different tool calling format, more similar to function calling format
|
|
1720
|
-
assert self.tool_call_id is not None, vars(self)
|
|
1721
1795
|
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1796
|
+
# Handle tool returns - similar pattern to Anthropic
|
|
1797
|
+
if self.tool_returns:
|
|
1798
|
+
parts = []
|
|
1799
|
+
for tool_return in self.tool_returns:
|
|
1800
|
+
if not tool_return.tool_call_id:
|
|
1801
|
+
raise TypeError("Google AI API requires tool_call_id to be set.")
|
|
1802
|
+
|
|
1803
|
+
# Use the function name if available, otherwise use tool_call_id
|
|
1804
|
+
function_name = self.name if self.name else tool_return.tool_call_id
|
|
1805
|
+
|
|
1806
|
+
# Truncate the tool return if needed
|
|
1807
|
+
func_response = truncate_tool_return(tool_return.func_response, tool_return_truncation_chars)
|
|
1808
|
+
|
|
1809
|
+
# NOTE: Google AI API wants the function response as JSON only, no string
|
|
1810
|
+
try:
|
|
1811
|
+
function_response = parse_json(func_response)
|
|
1812
|
+
except:
|
|
1813
|
+
function_response = {"function_response": func_response}
|
|
1814
|
+
|
|
1815
|
+
parts.append(
|
|
1816
|
+
{
|
|
1817
|
+
"functionResponse": {
|
|
1818
|
+
"name": function_name,
|
|
1819
|
+
"response": {
|
|
1820
|
+
"name": function_name, # NOTE: name twice... why?
|
|
1821
|
+
"content": function_response,
|
|
1822
|
+
},
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
)
|
|
1826
|
+
|
|
1827
|
+
google_ai_message = {
|
|
1828
|
+
"role": "function",
|
|
1829
|
+
"parts": parts,
|
|
1830
|
+
}
|
|
1725
1831
|
else:
|
|
1726
|
-
|
|
1832
|
+
# Legacy fallback for old message format
|
|
1833
|
+
assert self.tool_call_id is not None, vars(self)
|
|
1727
1834
|
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1835
|
+
if self.name is None:
|
|
1836
|
+
logger.warning("Couldn't find function name on tool call, defaulting to tool ID instead.")
|
|
1837
|
+
function_name = self.tool_call_id
|
|
1838
|
+
else:
|
|
1839
|
+
function_name = self.name
|
|
1733
1840
|
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1841
|
+
# Truncate the legacy content if needed
|
|
1842
|
+
legacy_content = truncate_tool_return(text_content, tool_return_truncation_chars)
|
|
1843
|
+
|
|
1844
|
+
# NOTE: Google AI API wants the function response as JSON only, no string
|
|
1845
|
+
try:
|
|
1846
|
+
function_response = parse_json(legacy_content)
|
|
1847
|
+
except:
|
|
1848
|
+
function_response = {"function_response": legacy_content}
|
|
1849
|
+
|
|
1850
|
+
google_ai_message = {
|
|
1851
|
+
"role": "function",
|
|
1852
|
+
"parts": [
|
|
1853
|
+
{
|
|
1854
|
+
"functionResponse": {
|
|
1855
|
+
"name": function_name,
|
|
1856
|
+
"response": {
|
|
1857
|
+
"name": function_name, # NOTE: name twice... why?
|
|
1858
|
+
"content": function_response,
|
|
1859
|
+
},
|
|
1860
|
+
}
|
|
1744
1861
|
}
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
}
|
|
1862
|
+
],
|
|
1863
|
+
}
|
|
1748
1864
|
|
|
1749
1865
|
else:
|
|
1750
1866
|
raise ValueError(self.role)
|
|
@@ -1765,6 +1881,7 @@ class Message(BaseMessage):
|
|
|
1765
1881
|
current_model: str,
|
|
1766
1882
|
put_inner_thoughts_in_kwargs: bool = True,
|
|
1767
1883
|
native_content: bool = False,
|
|
1884
|
+
tool_return_truncation_chars: Optional[int] = None,
|
|
1768
1885
|
):
|
|
1769
1886
|
messages = Message.filter_messages_for_llm_api(messages)
|
|
1770
1887
|
result = [
|
|
@@ -1772,6 +1889,7 @@ class Message(BaseMessage):
|
|
|
1772
1889
|
current_model=current_model,
|
|
1773
1890
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
|
1774
1891
|
native_content=native_content,
|
|
1892
|
+
tool_return_truncation_chars=tool_return_truncation_chars,
|
|
1775
1893
|
)
|
|
1776
1894
|
for m in messages
|
|
1777
1895
|
]
|
letta/schemas/organization.py
CHANGED
|
@@ -4,12 +4,13 @@ from typing import Optional
|
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
|
|
6
6
|
from letta.helpers.datetime_helpers import get_utc_time
|
|
7
|
+
from letta.schemas.enums import PrimitiveType
|
|
7
8
|
from letta.schemas.letta_base import LettaBase
|
|
8
9
|
from letta.utils import create_random_username
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
class OrganizationBase(LettaBase):
|
|
12
|
-
__id_prefix__ =
|
|
13
|
+
__id_prefix__ = PrimitiveType.ORGANIZATION.value
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
class Organization(OrganizationBase):
|
letta/schemas/passage.py
CHANGED
|
@@ -6,11 +6,12 @@ from pydantic import Field, field_validator
|
|
|
6
6
|
from letta.constants import MAX_EMBEDDING_DIM
|
|
7
7
|
from letta.helpers.datetime_helpers import get_utc_time
|
|
8
8
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
9
|
+
from letta.schemas.enums import PrimitiveType
|
|
9
10
|
from letta.schemas.letta_base import OrmMetadataBase
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class PassageBase(OrmMetadataBase):
|
|
13
|
-
__id_prefix__ =
|
|
14
|
+
__id_prefix__ = PrimitiveType.PASSAGE.value
|
|
14
15
|
|
|
15
16
|
is_deleted: bool = Field(False, description="Whether this passage is deleted or not.")
|
|
16
17
|
|
letta/schemas/provider_trace.py
CHANGED
|
@@ -6,11 +6,12 @@ from typing import Any, Dict, Optional
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
8
|
from letta.helpers.datetime_helpers import get_utc_time
|
|
9
|
+
from letta.schemas.enums import PrimitiveType
|
|
9
10
|
from letta.schemas.letta_base import OrmMetadataBase
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class BaseProviderTrace(OrmMetadataBase):
|
|
13
|
-
__id_prefix__ =
|
|
14
|
+
__id_prefix__ = PrimitiveType.PROVIDER_TRACE.value
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class ProviderTraceCreate(BaseModel):
|