letta-nightly 0.11.6.dev20250903104037__py3-none-any.whl → 0.11.7.dev20250904104046__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- letta/__init__.py +1 -1
- letta/agent.py +10 -14
- letta/agents/base_agent.py +18 -0
- letta/agents/helpers.py +32 -7
- letta/agents/letta_agent.py +953 -762
- letta/agents/voice_agent.py +1 -1
- letta/client/streaming.py +0 -1
- letta/constants.py +11 -8
- letta/errors.py +9 -0
- letta/functions/function_sets/base.py +77 -69
- letta/functions/function_sets/builtin.py +41 -22
- letta/functions/function_sets/multi_agent.py +1 -2
- letta/functions/schema_generator.py +0 -1
- letta/helpers/converters.py +8 -3
- letta/helpers/datetime_helpers.py +5 -4
- letta/helpers/message_helper.py +1 -2
- letta/helpers/pinecone_utils.py +0 -1
- letta/helpers/tool_rule_solver.py +10 -0
- letta/helpers/tpuf_client.py +848 -0
- letta/interface.py +8 -8
- letta/interfaces/anthropic_streaming_interface.py +7 -0
- letta/interfaces/openai_streaming_interface.py +29 -6
- letta/llm_api/anthropic_client.py +188 -18
- letta/llm_api/azure_client.py +0 -1
- letta/llm_api/bedrock_client.py +1 -2
- letta/llm_api/deepseek_client.py +319 -5
- letta/llm_api/google_vertex_client.py +75 -17
- letta/llm_api/groq_client.py +0 -1
- letta/llm_api/helpers.py +2 -2
- letta/llm_api/llm_api_tools.py +1 -50
- letta/llm_api/llm_client.py +6 -8
- letta/llm_api/mistral.py +1 -1
- letta/llm_api/openai.py +16 -13
- letta/llm_api/openai_client.py +31 -16
- letta/llm_api/together_client.py +0 -1
- letta/llm_api/xai_client.py +0 -1
- letta/local_llm/chat_completion_proxy.py +7 -6
- letta/local_llm/settings/settings.py +1 -1
- letta/orm/__init__.py +1 -0
- letta/orm/agent.py +8 -6
- letta/orm/archive.py +9 -1
- letta/orm/block.py +3 -4
- letta/orm/block_history.py +3 -1
- letta/orm/group.py +2 -3
- letta/orm/identity.py +1 -2
- letta/orm/job.py +1 -2
- letta/orm/llm_batch_items.py +1 -2
- letta/orm/message.py +8 -4
- letta/orm/mixins.py +18 -0
- letta/orm/organization.py +2 -0
- letta/orm/passage.py +8 -1
- letta/orm/passage_tag.py +55 -0
- letta/orm/sandbox_config.py +1 -3
- letta/orm/step.py +1 -2
- letta/orm/tool.py +1 -0
- letta/otel/resource.py +2 -2
- letta/plugins/plugins.py +1 -1
- letta/prompts/prompt_generator.py +10 -2
- letta/schemas/agent.py +11 -0
- letta/schemas/archive.py +4 -0
- letta/schemas/block.py +13 -0
- letta/schemas/embedding_config.py +0 -1
- letta/schemas/enums.py +24 -7
- letta/schemas/group.py +12 -0
- letta/schemas/letta_message.py +55 -1
- letta/schemas/letta_message_content.py +28 -0
- letta/schemas/letta_request.py +21 -4
- letta/schemas/letta_stop_reason.py +9 -1
- letta/schemas/llm_config.py +24 -8
- letta/schemas/mcp.py +0 -3
- letta/schemas/memory.py +14 -0
- letta/schemas/message.py +245 -141
- letta/schemas/openai/chat_completion_request.py +2 -1
- letta/schemas/passage.py +1 -0
- letta/schemas/providers/bedrock.py +1 -1
- letta/schemas/providers/openai.py +2 -2
- letta/schemas/tool.py +11 -5
- letta/schemas/tool_execution_result.py +0 -1
- letta/schemas/tool_rule.py +71 -0
- letta/serialize_schemas/marshmallow_agent.py +1 -2
- letta/server/rest_api/app.py +3 -3
- letta/server/rest_api/auth/index.py +0 -1
- letta/server/rest_api/interface.py +3 -11
- letta/server/rest_api/redis_stream_manager.py +3 -4
- letta/server/rest_api/routers/v1/agents.py +143 -84
- letta/server/rest_api/routers/v1/blocks.py +1 -1
- letta/server/rest_api/routers/v1/folders.py +1 -1
- letta/server/rest_api/routers/v1/groups.py +23 -22
- letta/server/rest_api/routers/v1/internal_templates.py +68 -0
- letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
- letta/server/rest_api/routers/v1/sources.py +1 -1
- letta/server/rest_api/routers/v1/tools.py +167 -15
- letta/server/rest_api/streaming_response.py +4 -3
- letta/server/rest_api/utils.py +75 -18
- letta/server/server.py +24 -35
- letta/services/agent_manager.py +359 -45
- letta/services/agent_serialization_manager.py +23 -3
- letta/services/archive_manager.py +72 -3
- letta/services/block_manager.py +1 -2
- letta/services/context_window_calculator/token_counter.py +11 -6
- letta/services/file_manager.py +1 -3
- letta/services/files_agents_manager.py +2 -4
- letta/services/group_manager.py +73 -12
- letta/services/helpers/agent_manager_helper.py +5 -5
- letta/services/identity_manager.py +8 -3
- letta/services/job_manager.py +2 -14
- letta/services/llm_batch_manager.py +1 -3
- letta/services/mcp/base_client.py +1 -2
- letta/services/mcp_manager.py +5 -6
- letta/services/message_manager.py +536 -15
- letta/services/organization_manager.py +1 -2
- letta/services/passage_manager.py +287 -12
- letta/services/provider_manager.py +1 -3
- letta/services/sandbox_config_manager.py +12 -7
- letta/services/source_manager.py +1 -2
- letta/services/step_manager.py +0 -1
- letta/services/summarizer/summarizer.py +4 -2
- letta/services/telemetry_manager.py +1 -3
- letta/services/tool_executor/builtin_tool_executor.py +136 -316
- letta/services/tool_executor/core_tool_executor.py +231 -74
- letta/services/tool_executor/files_tool_executor.py +2 -2
- letta/services/tool_executor/mcp_tool_executor.py +0 -1
- letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
- letta/services/tool_executor/sandbox_tool_executor.py +0 -1
- letta/services/tool_executor/tool_execution_sandbox.py +2 -3
- letta/services/tool_manager.py +181 -64
- letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
- letta/services/user_manager.py +1 -2
- letta/settings.py +5 -3
- letta/streaming_interface.py +3 -3
- letta/system.py +1 -1
- letta/utils.py +0 -1
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/METADATA +11 -7
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/RECORD +137 -135
- letta/llm_api/deepseek.py +0 -303
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/licenses/LICENSE +0 -0
letta/schemas/message.py
CHANGED
@@ -7,11 +7,11 @@ import uuid
|
|
7
7
|
import warnings
|
8
8
|
from collections import OrderedDict
|
9
9
|
from datetime import datetime, timezone
|
10
|
-
from
|
10
|
+
from enum import Enum
|
11
|
+
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
11
12
|
|
12
|
-
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall
|
13
|
-
from
|
14
|
-
from pydantic import BaseModel, Field, field_validator
|
13
|
+
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall, Function as OpenAIFunction
|
14
|
+
from pydantic import BaseModel, Field, field_validator, model_validator
|
15
15
|
|
16
16
|
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, TOOL_CALL_ID_MAX_LEN
|
17
17
|
from letta.helpers.datetime_helpers import get_utc_time, is_utc_datetime
|
@@ -20,6 +20,8 @@ from letta.local_llm.constants import INNER_THOUGHTS_KWARG, INNER_THOUGHTS_KWARG
|
|
20
20
|
from letta.schemas.enums import MessageRole
|
21
21
|
from letta.schemas.letta_base import OrmMetadataBase
|
22
22
|
from letta.schemas.letta_message import (
|
23
|
+
ApprovalRequestMessage,
|
24
|
+
ApprovalResponseMessage,
|
23
25
|
AssistantMessage,
|
24
26
|
HiddenReasoningMessage,
|
25
27
|
LettaMessage,
|
@@ -66,13 +68,21 @@ def add_inner_thoughts_to_tool_call(
|
|
66
68
|
raise e
|
67
69
|
|
68
70
|
|
69
|
-
class
|
70
|
-
|
71
|
+
class MessageCreateType(str, Enum):
|
72
|
+
message = "message"
|
73
|
+
approval = "approval"
|
74
|
+
|
75
|
+
|
76
|
+
class MessageCreateBase(BaseModel):
|
77
|
+
type: MessageCreateType = Field(..., description="The message type to be created.")
|
71
78
|
|
72
79
|
|
73
|
-
class MessageCreate(
|
80
|
+
class MessageCreate(MessageCreateBase):
|
74
81
|
"""Request to create a message"""
|
75
82
|
|
83
|
+
type: Optional[Literal[MessageCreateType.message]] = Field(
|
84
|
+
default=MessageCreateType.message, description="The message type to be created."
|
85
|
+
)
|
76
86
|
# In the simplified format, only allow simple roles
|
77
87
|
role: Literal[
|
78
88
|
MessageRole.user,
|
@@ -98,6 +108,18 @@ class MessageCreate(BaseModel):
|
|
98
108
|
return data
|
99
109
|
|
100
110
|
|
111
|
+
class ApprovalCreate(MessageCreateBase):
|
112
|
+
"""Input to approve or deny a tool call request"""
|
113
|
+
|
114
|
+
type: Literal[MessageCreateType.approval] = Field(default=MessageCreateType.approval, description="The message type to be created.")
|
115
|
+
approve: bool = Field(..., description="Whether the tool has been approved")
|
116
|
+
approval_request_id: str = Field(..., description="The message ID of the approval request")
|
117
|
+
reason: Optional[str] = Field(None, description="An optional explanation for the provided approval status")
|
118
|
+
|
119
|
+
|
120
|
+
MessageCreateUnion = Union[MessageCreate, ApprovalCreate]
|
121
|
+
|
122
|
+
|
101
123
|
class MessageUpdate(BaseModel):
|
102
124
|
"""Request to update a message"""
|
103
125
|
|
@@ -126,6 +148,10 @@ class MessageUpdate(BaseModel):
|
|
126
148
|
return data
|
127
149
|
|
128
150
|
|
151
|
+
class BaseMessage(OrmMetadataBase):
|
152
|
+
__id_prefix__ = "message"
|
153
|
+
|
154
|
+
|
129
155
|
class Message(BaseMessage):
|
130
156
|
"""
|
131
157
|
Letta's internal representation of a message. Includes methods to convert to/from LLM provider formats.
|
@@ -174,13 +200,18 @@ class Message(BaseMessage):
|
|
174
200
|
is_err: Optional[bool] = Field(
|
175
201
|
default=None, description="Whether this message is part of an error step. Used only for debugging purposes."
|
176
202
|
)
|
203
|
+
approval_request_id: Optional[str] = Field(
|
204
|
+
default=None, description="The id of the approval request if this message is associated with a tool call request."
|
205
|
+
)
|
206
|
+
approve: Optional[bool] = Field(default=None, description="Whether tool call is approved.")
|
207
|
+
denial_reason: Optional[str] = Field(default=None, description="The reason the tool call request was denied.")
|
177
208
|
# This overrides the optional base orm schema, created_at MUST exist on all messages objects
|
178
209
|
created_at: datetime = Field(default_factory=get_utc_time, description="The timestamp when the object was created.")
|
179
210
|
|
180
211
|
@field_validator("role")
|
181
212
|
@classmethod
|
182
213
|
def validate_role(cls, v: str) -> str:
|
183
|
-
roles = ["system", "assistant", "user", "tool"]
|
214
|
+
roles = ["system", "assistant", "user", "tool", "approval"]
|
184
215
|
assert v in roles, f"Role must be one of {roles}"
|
185
216
|
return v
|
186
217
|
|
@@ -251,20 +282,77 @@ class Message(BaseMessage):
|
|
251
282
|
include_err: Optional[bool] = None,
|
252
283
|
) -> List[LettaMessage]:
|
253
284
|
"""Convert message object (in DB format) to the style used by the original Letta API"""
|
254
|
-
|
255
|
-
# TODO (cliandy): break this into more manageable pieces
|
285
|
+
messages = []
|
256
286
|
if self.role == MessageRole.assistant:
|
257
|
-
messages = []
|
258
|
-
# Handle reasoning
|
259
287
|
if self.content:
|
260
|
-
|
261
|
-
|
262
|
-
|
288
|
+
messages.extend(self._convert_reasoning_messages())
|
289
|
+
if self.tool_calls is not None:
|
290
|
+
messages.extend(
|
291
|
+
self._convert_tool_call_messages(
|
292
|
+
current_message_count=len(messages),
|
293
|
+
use_assistant_message=use_assistant_message,
|
294
|
+
assistant_message_tool_name=assistant_message_tool_name,
|
295
|
+
assistant_message_tool_kwarg=assistant_message_tool_kwarg,
|
296
|
+
),
|
297
|
+
)
|
298
|
+
elif self.role == MessageRole.tool:
|
299
|
+
messages.append(self._convert_tool_return_message())
|
300
|
+
elif self.role == MessageRole.user:
|
301
|
+
messages.append(self._convert_user_message())
|
302
|
+
elif self.role == MessageRole.system:
|
303
|
+
messages.append(self._convert_system_message())
|
304
|
+
elif self.role == MessageRole.approval:
|
305
|
+
if self.content:
|
306
|
+
messages.extend(self._convert_reasoning_messages())
|
307
|
+
if self.tool_calls is not None:
|
308
|
+
tool_calls = self._convert_tool_call_messages()
|
309
|
+
assert len(tool_calls) == 1
|
310
|
+
approval_request_message = ApprovalRequestMessage(**tool_calls[0].model_dump(exclude={"message_type"}))
|
311
|
+
messages.append(approval_request_message)
|
312
|
+
else:
|
313
|
+
approval_response_message = ApprovalResponseMessage(
|
314
|
+
id=self.id,
|
315
|
+
date=self.created_at,
|
316
|
+
otid=self.otid,
|
317
|
+
approve=self.approve,
|
318
|
+
approval_request_id=self.approval_request_id,
|
319
|
+
reason=self.denial_reason,
|
320
|
+
)
|
321
|
+
messages.append(approval_response_message)
|
322
|
+
else:
|
323
|
+
raise ValueError(f"Unknown role: {self.role}")
|
324
|
+
|
325
|
+
return messages[::-1] if reverse else messages
|
326
|
+
|
327
|
+
def _convert_reasoning_messages(self, current_message_count: int = 0) -> List[LettaMessage]:
|
328
|
+
messages = []
|
329
|
+
# Check for ReACT-style COT inside of TextContent
|
330
|
+
if len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
331
|
+
otid = Message.generate_otid_from_id(self.id, current_message_count + len(messages))
|
332
|
+
messages.append(
|
333
|
+
ReasoningMessage(
|
334
|
+
id=self.id,
|
335
|
+
date=self.created_at,
|
336
|
+
reasoning=self.content[0].text,
|
337
|
+
name=self.name,
|
338
|
+
otid=otid,
|
339
|
+
sender_id=self.sender_id,
|
340
|
+
step_id=self.step_id,
|
341
|
+
is_err=self.is_err,
|
342
|
+
)
|
343
|
+
)
|
344
|
+
# Otherwise, we may have a list of multiple types
|
345
|
+
else:
|
346
|
+
# TODO we can probably collapse these two cases into a single loop
|
347
|
+
for content_part in self.content:
|
348
|
+
otid = Message.generate_otid_from_id(self.id, current_message_count + len(messages))
|
349
|
+
if isinstance(content_part, TextContent):
|
350
|
+
# COT
|
263
351
|
messages.append(
|
264
352
|
ReasoningMessage(
|
265
353
|
id=self.id,
|
266
354
|
date=self.created_at,
|
267
|
-
reasoning=
|
355
|
+
reasoning=content_part.text,
|
268
356
|
name=self.name,
|
269
357
|
otid=otid,
|
270
358
|
sender_id=self.sender_id,
|
@@ -272,127 +360,106 @@ class Message(BaseMessage):
|
|
272
360
|
is_err=self.is_err,
|
273
361
|
)
|
274
362
|
)
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
otid=otid,
|
289
|
-
sender_id=self.sender_id,
|
290
|
-
step_id=self.step_id,
|
291
|
-
is_err=self.is_err,
|
292
|
-
)
|
293
|
-
)
|
294
|
-
elif isinstance(content_part, ReasoningContent):
|
295
|
-
# "native" COT
|
296
|
-
messages.append(
|
297
|
-
ReasoningMessage(
|
298
|
-
id=self.id,
|
299
|
-
date=self.created_at,
|
300
|
-
reasoning=content_part.reasoning,
|
301
|
-
source="reasoner_model", # TODO do we want to tag like this?
|
302
|
-
signature=content_part.signature,
|
303
|
-
name=self.name,
|
304
|
-
otid=otid,
|
305
|
-
step_id=self.step_id,
|
306
|
-
is_err=self.is_err,
|
307
|
-
)
|
308
|
-
)
|
309
|
-
elif isinstance(content_part, RedactedReasoningContent):
|
310
|
-
# "native" redacted/hidden COT
|
311
|
-
messages.append(
|
312
|
-
HiddenReasoningMessage(
|
313
|
-
id=self.id,
|
314
|
-
date=self.created_at,
|
315
|
-
state="redacted",
|
316
|
-
hidden_reasoning=content_part.data,
|
317
|
-
name=self.name,
|
318
|
-
otid=otid,
|
319
|
-
sender_id=self.sender_id,
|
320
|
-
step_id=self.step_id,
|
321
|
-
is_err=self.is_err,
|
322
|
-
)
|
323
|
-
)
|
324
|
-
elif isinstance(content_part, OmittedReasoningContent):
|
325
|
-
# Special case for "hidden reasoning" models like o1/o3
|
326
|
-
# NOTE: we also have to think about how to return this during streaming
|
327
|
-
messages.append(
|
328
|
-
HiddenReasoningMessage(
|
329
|
-
id=self.id,
|
330
|
-
date=self.created_at,
|
331
|
-
state="omitted",
|
332
|
-
name=self.name,
|
333
|
-
otid=otid,
|
334
|
-
step_id=self.step_id,
|
335
|
-
is_err=self.is_err,
|
336
|
-
)
|
337
|
-
)
|
338
|
-
else:
|
339
|
-
warnings.warn(f"Unrecognized content part in assistant message: {content_part}")
|
340
|
-
|
341
|
-
if self.tool_calls is not None:
|
342
|
-
# This is type FunctionCall
|
343
|
-
for tool_call in self.tool_calls:
|
344
|
-
otid = Message.generate_otid_from_id(self.id, len(messages))
|
345
|
-
# If we're supporting using assistant message,
|
346
|
-
# then we want to treat certain function calls as a special case
|
347
|
-
if use_assistant_message and tool_call.function.name == assistant_message_tool_name:
|
348
|
-
# We need to unpack the actual message contents from the function call
|
349
|
-
try:
|
350
|
-
func_args = parse_json(tool_call.function.arguments)
|
351
|
-
message_string = validate_function_response(func_args[assistant_message_tool_kwarg], 0, truncate=False)
|
352
|
-
except KeyError:
|
353
|
-
raise ValueError(f"Function call {tool_call.function.name} missing {assistant_message_tool_kwarg} argument")
|
354
|
-
messages.append(
|
355
|
-
AssistantMessage(
|
356
|
-
id=self.id,
|
357
|
-
date=self.created_at,
|
358
|
-
content=message_string,
|
359
|
-
name=self.name,
|
360
|
-
otid=otid,
|
361
|
-
sender_id=self.sender_id,
|
362
|
-
step_id=self.step_id,
|
363
|
-
is_err=self.is_err,
|
364
|
-
)
|
363
|
+
elif isinstance(content_part, ReasoningContent):
|
364
|
+
# "native" COT
|
365
|
+
messages.append(
|
366
|
+
ReasoningMessage(
|
367
|
+
id=self.id,
|
368
|
+
date=self.created_at,
|
369
|
+
reasoning=content_part.reasoning,
|
370
|
+
source="reasoner_model", # TODO do we want to tag like this?
|
371
|
+
signature=content_part.signature,
|
372
|
+
name=self.name,
|
373
|
+
otid=otid,
|
374
|
+
step_id=self.step_id,
|
375
|
+
is_err=self.is_err,
|
365
376
|
)
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
is_err=self.is_err,
|
381
|
-
)
|
377
|
+
)
|
378
|
+
elif isinstance(content_part, RedactedReasoningContent):
|
379
|
+
# "native" redacted/hidden COT
|
380
|
+
messages.append(
|
381
|
+
HiddenReasoningMessage(
|
382
|
+
id=self.id,
|
383
|
+
date=self.created_at,
|
384
|
+
state="redacted",
|
385
|
+
hidden_reasoning=content_part.data,
|
386
|
+
name=self.name,
|
387
|
+
otid=otid,
|
388
|
+
sender_id=self.sender_id,
|
389
|
+
step_id=self.step_id,
|
390
|
+
is_err=self.is_err,
|
382
391
|
)
|
392
|
+
)
|
393
|
+
elif isinstance(content_part, OmittedReasoningContent):
|
394
|
+
# Special case for "hidden reasoning" models like o1/o3
|
395
|
+
# NOTE: we also have to think about how to return this during streaming
|
396
|
+
messages.append(
|
397
|
+
HiddenReasoningMessage(
|
398
|
+
id=self.id,
|
399
|
+
date=self.created_at,
|
400
|
+
state="omitted",
|
401
|
+
name=self.name,
|
402
|
+
otid=otid,
|
403
|
+
step_id=self.step_id,
|
404
|
+
is_err=self.is_err,
|
405
|
+
)
|
406
|
+
)
|
407
|
+
else:
|
408
|
+
warnings.warn(f"Unrecognized content part in assistant message: {content_part}")
|
409
|
+
return messages
|
383
410
|
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
411
|
+
def _convert_tool_call_messages(
|
412
|
+
self,
|
413
|
+
current_message_count: int = 0,
|
414
|
+
use_assistant_message: bool = False,
|
415
|
+
assistant_message_tool_name: str = DEFAULT_MESSAGE_TOOL,
|
416
|
+
assistant_message_tool_kwarg: str = DEFAULT_MESSAGE_TOOL_KWARG,
|
417
|
+
) -> List[LettaMessage]:
|
418
|
+
messages = []
|
419
|
+
# This is type FunctionCall
|
420
|
+
for tool_call in self.tool_calls:
|
421
|
+
otid = Message.generate_otid_from_id(self.id, current_message_count + len(messages))
|
422
|
+
# If we're supporting using assistant message,
|
423
|
+
# then we want to treat certain function calls as a special case
|
424
|
+
if use_assistant_message and tool_call.function.name == assistant_message_tool_name:
|
425
|
+
# We need to unpack the actual message contents from the function call
|
426
|
+
try:
|
427
|
+
func_args = parse_json(tool_call.function.arguments)
|
428
|
+
message_string = validate_function_response(func_args[assistant_message_tool_kwarg], 0, truncate=False)
|
429
|
+
except KeyError:
|
430
|
+
raise ValueError(f"Function call {tool_call.function.name} missing {assistant_message_tool_kwarg} argument")
|
431
|
+
messages.append(
|
432
|
+
AssistantMessage(
|
433
|
+
id=self.id,
|
434
|
+
date=self.created_at,
|
435
|
+
content=message_string,
|
436
|
+
name=self.name,
|
437
|
+
otid=otid,
|
438
|
+
sender_id=self.sender_id,
|
439
|
+
step_id=self.step_id,
|
440
|
+
is_err=self.is_err,
|
441
|
+
)
|
442
|
+
)
|
443
|
+
else:
|
444
|
+
messages.append(
|
445
|
+
ToolCallMessage(
|
446
|
+
id=self.id,
|
447
|
+
date=self.created_at,
|
448
|
+
tool_call=ToolCall(
|
449
|
+
name=tool_call.function.name,
|
450
|
+
arguments=tool_call.function.arguments,
|
451
|
+
tool_call_id=tool_call.id,
|
452
|
+
),
|
453
|
+
name=self.name,
|
454
|
+
otid=otid,
|
455
|
+
sender_id=self.sender_id,
|
456
|
+
step_id=self.step_id,
|
457
|
+
is_err=self.is_err,
|
458
|
+
)
|
459
|
+
)
|
460
|
+
return messages
|
394
461
|
|
395
|
-
def
|
462
|
+
def _convert_tool_return_message(self) -> ToolReturnMessage:
|
396
463
|
"""Convert tool role message to ToolReturnMessage
|
397
464
|
|
398
465
|
the tool return is packaged as follows:
|
@@ -679,8 +746,10 @@ class Message(BaseMessage):
|
|
679
746
|
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
680
747
|
put_inner_thoughts_in_kwargs: bool = False,
|
681
748
|
use_developer_message: bool = False,
|
682
|
-
) -> dict:
|
749
|
+
) -> dict | None:
|
683
750
|
"""Go from Message class to ChatCompletion message object"""
|
751
|
+
if self.role == "approval" and self.tool_calls is None:
|
752
|
+
return None
|
684
753
|
|
685
754
|
# TODO change to pydantic casting, eg `return SystemMessageModel(self)`
|
686
755
|
# If we only have one content part and it's text, treat it as COT
|
@@ -718,11 +787,11 @@ class Message(BaseMessage):
|
|
718
787
|
"role": self.role,
|
719
788
|
}
|
720
789
|
|
721
|
-
elif self.role == "assistant":
|
790
|
+
elif self.role == "assistant" or self.role == "approval":
|
722
791
|
assert self.tool_calls is not None or text_content is not None
|
723
792
|
openai_message = {
|
724
793
|
"content": None if (put_inner_thoughts_in_kwargs and self.tool_calls is not None) else text_content,
|
725
|
-
"role":
|
794
|
+
"role": "assistant",
|
726
795
|
}
|
727
796
|
|
728
797
|
if self.tool_calls is not None:
|
@@ -771,17 +840,37 @@ class Message(BaseMessage):
|
|
771
840
|
|
772
841
|
return openai_message
|
773
842
|
|
843
|
+
@staticmethod
|
844
|
+
def to_openai_dicts_from_list(
|
845
|
+
messages: List[Message],
|
846
|
+
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
847
|
+
put_inner_thoughts_in_kwargs: bool = False,
|
848
|
+
use_developer_message: bool = False,
|
849
|
+
) -> List[dict]:
|
850
|
+
result = [
|
851
|
+
m.to_openai_dict(
|
852
|
+
max_tool_id_length=max_tool_id_length,
|
853
|
+
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
854
|
+
use_developer_message=use_developer_message,
|
855
|
+
)
|
856
|
+
for m in messages
|
857
|
+
]
|
858
|
+
result = [m for m in result if m is not None]
|
859
|
+
return result
|
860
|
+
|
774
861
|
def to_anthropic_dict(
|
775
862
|
self,
|
776
863
|
inner_thoughts_xml_tag="thinking",
|
777
864
|
put_inner_thoughts_in_kwargs: bool = False,
|
778
|
-
) -> dict:
|
865
|
+
) -> dict | None:
|
779
866
|
"""
|
780
867
|
Convert to an Anthropic message dictionary
|
781
868
|
|
782
869
|
Args:
|
783
870
|
inner_thoughts_xml_tag (str): The XML tag to wrap around inner thoughts
|
784
871
|
"""
|
872
|
+
if self.role == "approval" and self.tool_calls is None:
|
873
|
+
return None
|
785
874
|
|
786
875
|
# Check for COT
|
787
876
|
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
@@ -839,10 +928,10 @@ class Message(BaseMessage):
|
|
839
928
|
"role": self.role,
|
840
929
|
}
|
841
930
|
|
842
|
-
elif self.role == "assistant":
|
931
|
+
elif self.role == "assistant" or self.role == "approval":
|
843
932
|
assert self.tool_calls is not None or text_content is not None
|
844
933
|
anthropic_message = {
|
845
|
-
"role":
|
934
|
+
"role": "assistant",
|
846
935
|
}
|
847
936
|
content = []
|
848
937
|
# COT / reasoning / thinking
|
@@ -880,7 +969,6 @@ class Message(BaseMessage):
|
|
880
969
|
# Tool calling
|
881
970
|
if self.tool_calls is not None:
|
882
971
|
for tool_call in self.tool_calls:
|
883
|
-
|
884
972
|
if put_inner_thoughts_in_kwargs:
|
885
973
|
tool_call_input = add_inner_thoughts_to_tool_call(
|
886
974
|
tool_call,
|
@@ -923,6 +1011,22 @@ class Message(BaseMessage):
|
|
923
1011
|
|
924
1012
|
return anthropic_message
|
925
1013
|
|
1014
|
+
@staticmethod
|
1015
|
+
def to_anthropic_dicts_from_list(
|
1016
|
+
messages: List[Message],
|
1017
|
+
inner_thoughts_xml_tag: str = "thinking",
|
1018
|
+
put_inner_thoughts_in_kwargs: bool = False,
|
1019
|
+
) -> List[dict]:
|
1020
|
+
result = [
|
1021
|
+
m.to_anthropic_dict(
|
1022
|
+
inner_thoughts_xml_tag=inner_thoughts_xml_tag,
|
1023
|
+
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
1024
|
+
)
|
1025
|
+
for m in messages
|
1026
|
+
]
|
1027
|
+
result = [m for m in result if m is not None]
|
1028
|
+
return result
|
1029
|
+
|
926
1030
|
def to_google_ai_dict(self, put_inner_thoughts_in_kwargs: bool = True) -> dict:
|
927
1031
|
"""
|
928
1032
|
Go from Message class to Google AI REST message object
|
@@ -1021,7 +1125,7 @@ class Message(BaseMessage):
|
|
1021
1125
|
assert self.tool_call_id is not None, vars(self)
|
1022
1126
|
|
1023
1127
|
if self.name is None:
|
1024
|
-
warnings.warn(
|
1128
|
+
warnings.warn("Couldn't find function name on tool call, defaulting to tool ID instead.")
|
1025
1129
|
function_name = self.tool_call_id
|
1026
1130
|
else:
|
1027
1131
|
function_name = self.name
|
@@ -50,7 +50,7 @@ def cast_message_to_subtype(m_dict: dict) -> ChatMessage:
|
|
50
50
|
return SystemMessage(**m_dict)
|
51
51
|
elif role == "user":
|
52
52
|
return UserMessage(**m_dict)
|
53
|
-
elif role == "assistant":
|
53
|
+
elif role == "assistant" or role == "approval":
|
54
54
|
return AssistantMessage(**m_dict)
|
55
55
|
elif role == "tool":
|
56
56
|
return ToolMessage(**m_dict)
|
@@ -136,6 +136,7 @@ class ChatCompletionRequest(BaseModel):
|
|
136
136
|
parallel_tool_calls: Optional[bool] = None
|
137
137
|
instructions: Optional[str] = None
|
138
138
|
verbosity: Optional[Literal["low", "medium", "high"]] = None # For verbosity control in GPT-5 models
|
139
|
+
reasoning_effort: Optional[Literal["minimal", "low", "medium", "high"]] = None # For reasoning effort control in reasoning models
|
139
140
|
|
140
141
|
# function-calling related
|
141
142
|
tools: Optional[List[Tool]] = None
|
letta/schemas/passage.py
CHANGED
@@ -25,6 +25,7 @@ class PassageBase(OrmMetadataBase):
|
|
25
25
|
file_id: Optional[str] = Field(None, description="The unique identifier of the file associated with the passage.")
|
26
26
|
file_name: Optional[str] = Field(None, description="The name of the file (only for source passages).")
|
27
27
|
metadata: Optional[Dict] = Field({}, validation_alias="metadata_", description="The metadata of the passage.")
|
28
|
+
tags: Optional[List[str]] = Field(None, description="Tags associated with this passage.")
|
28
29
|
|
29
30
|
|
30
31
|
class Passage(PassageBase):
|
@@ -35,7 +35,7 @@ class BedrockProvider(Provider):
|
|
35
35
|
response = await bedrock.list_inference_profiles()
|
36
36
|
return response["inferenceProfileSummaries"]
|
37
37
|
except Exception as e:
|
38
|
-
logger.error(
|
38
|
+
logger.error("Error getting model list for bedrock: %s", e)
|
39
39
|
raise e
|
40
40
|
|
41
41
|
async def check_api_key(self):
|
@@ -203,7 +203,7 @@ class OpenAIProvider(Provider):
|
|
203
203
|
continue
|
204
204
|
else:
|
205
205
|
logger.debug(
|
206
|
-
|
206
|
+
"Skipping embedding models for %s by default, as we don't assume embeddings are supported."
|
207
207
|
"Please open an issue on GitHub if support is required.",
|
208
208
|
self.base_url,
|
209
209
|
)
|
@@ -227,7 +227,7 @@ class OpenAIProvider(Provider):
|
|
227
227
|
return LLM_MAX_TOKENS[model_name]
|
228
228
|
else:
|
229
229
|
logger.debug(
|
230
|
-
|
230
|
+
"Model %s on %s for provider %s not found in LLM_MAX_TOKENS. Using default of {LLM_MAX_TOKENS['DEFAULT']}",
|
231
231
|
model_name,
|
232
232
|
self.base_url,
|
233
233
|
self.__class__.__name__,
|
letta/schemas/tool.py
CHANGED
@@ -67,6 +67,9 @@ class Tool(BaseTool):
|
|
67
67
|
return_char_limit: int = Field(FUNCTION_RETURN_CHAR_LIMIT, description="The maximum number of characters in the response.")
|
68
68
|
pip_requirements: list[PipRequirement] | None = Field(None, description="Optional list of pip packages required by this tool.")
|
69
69
|
npm_requirements: list[NpmRequirement] | None = Field(None, description="Optional list of npm packages required by this tool.")
|
70
|
+
default_requires_approval: Optional[bool] = Field(
|
71
|
+
None, description="Default value for whether or not executing this tool requires approval."
|
72
|
+
)
|
70
73
|
|
71
74
|
# metadata fields
|
72
75
|
created_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
|
@@ -80,7 +83,8 @@ class Tool(BaseTool):
|
|
80
83
|
"""
|
81
84
|
from letta.functions.helpers import generate_model_from_args_json_schema
|
82
85
|
|
83
|
-
if self.tool_type == ToolType.CUSTOM:
|
86
|
+
if self.tool_type == ToolType.CUSTOM and not self.json_schema:
|
87
|
+
# attempt various fallbacks to get the JSON schema
|
84
88
|
if not self.source_code:
|
85
89
|
logger.error("Custom tool with id=%s is missing source_code field", self.id)
|
86
90
|
raise ValueError(f"Custom tool with id={self.id} is missing source_code field.")
|
@@ -157,7 +161,7 @@ class Tool(BaseTool):
|
|
157
161
|
|
158
162
|
class ToolCreate(LettaBase):
|
159
163
|
description: Optional[str] = Field(None, description="The description of the tool.")
|
160
|
-
tags: List[str] = Field(
|
164
|
+
tags: Optional[List[str]] = Field(None, description="Metadata tags.")
|
161
165
|
source_code: str = Field(..., description="The source code of the function.")
|
162
166
|
source_type: str = Field("python", description="The source type of the function.")
|
163
167
|
json_schema: Optional[Dict] = Field(
|
@@ -167,6 +171,7 @@ class ToolCreate(LettaBase):
|
|
167
171
|
return_char_limit: int = Field(FUNCTION_RETURN_CHAR_LIMIT, description="The maximum number of characters in the response.")
|
168
172
|
pip_requirements: list[PipRequirement] | None = Field(None, description="Optional list of pip packages required by this tool.")
|
169
173
|
npm_requirements: list[NpmRequirement] | None = Field(None, description="Optional list of npm packages required by this tool.")
|
174
|
+
default_requires_approval: Optional[bool] = Field(None, description="Whether or not to require approval before executing this tool.")
|
170
175
|
|
171
176
|
@classmethod
|
172
177
|
def from_mcp(cls, mcp_server_name: str, mcp_tool: MCPTool) -> "ToolCreate":
|
@@ -213,9 +218,9 @@ class ToolCreate(LettaBase):
|
|
213
218
|
composio_action_schemas = composio_toolset.get_action_schemas(actions=[action_name], check_connected_accounts=False)
|
214
219
|
|
215
220
|
assert len(composio_action_schemas) > 0, "User supplied parameters do not match any Composio tools"
|
216
|
-
assert (
|
217
|
-
len(composio_action_schemas)
|
218
|
-
)
|
221
|
+
assert len(composio_action_schemas) == 1, (
|
222
|
+
f"User supplied parameters match too many Composio tools; {len(composio_action_schemas)} > 1"
|
223
|
+
)
|
219
224
|
|
220
225
|
composio_action_schema = composio_action_schemas[0]
|
221
226
|
|
@@ -247,6 +252,7 @@ class ToolUpdate(LettaBase):
|
|
247
252
|
pip_requirements: list[PipRequirement] | None = Field(None, description="Optional list of pip packages required by this tool.")
|
248
253
|
npm_requirements: list[NpmRequirement] | None = Field(None, description="Optional list of npm packages required by this tool.")
|
249
254
|
metadata_: Optional[Dict[str, Any]] = Field(None, description="A dictionary of additional metadata for the tool.")
|
255
|
+
default_requires_approval: Optional[bool] = Field(None, description="Whether or not to require approval before executing this tool.")
|
250
256
|
|
251
257
|
model_config = ConfigDict(extra="ignore") # Allows extra fields without validation errors
|
252
258
|
# TODO: Remove this, and clean usage of ToolUpdate everywhere else
|
@@ -6,7 +6,6 @@ from letta.schemas.agent import AgentState
|
|
6
6
|
|
7
7
|
|
8
8
|
class ToolExecutionResult(BaseModel):
|
9
|
-
|
10
9
|
status: Literal["success", "error"] = Field(..., description="The status of the tool execution and return object")
|
11
10
|
func_return: Optional[Any] = Field(None, description="The function return object")
|
12
11
|
agent_state: Optional[AgentState] = Field(None, description="The agent state")
|