letta-nightly 0.12.1.dev20251023104211__py3-none-any.whl → 0.13.0.dev20251024223017__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +2 -3
- letta/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/simple_llm_request_adapter.py +8 -5
- letta/adapters/simple_llm_stream_adapter.py +22 -6
- letta/agents/agent_loop.py +10 -3
- letta/agents/base_agent.py +4 -1
- letta/agents/helpers.py +41 -9
- letta/agents/letta_agent.py +11 -10
- letta/agents/letta_agent_v2.py +47 -37
- letta/agents/letta_agent_v3.py +395 -300
- letta/agents/voice_agent.py +8 -6
- letta/agents/voice_sleeptime_agent.py +3 -3
- letta/constants.py +30 -7
- letta/errors.py +20 -0
- letta/functions/function_sets/base.py +55 -3
- letta/functions/mcp_client/types.py +33 -57
- letta/functions/schema_generator.py +135 -23
- letta/groups/sleeptime_multi_agent_v3.py +6 -11
- letta/groups/sleeptime_multi_agent_v4.py +227 -0
- letta/helpers/converters.py +78 -4
- letta/helpers/crypto_utils.py +6 -2
- letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
- letta/interfaces/anthropic_streaming_interface.py +3 -4
- letta/interfaces/gemini_streaming_interface.py +4 -6
- letta/interfaces/openai_streaming_interface.py +63 -28
- letta/llm_api/anthropic_client.py +7 -4
- letta/llm_api/deepseek_client.py +6 -4
- letta/llm_api/google_ai_client.py +3 -12
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/helpers.py +90 -61
- letta/llm_api/llm_api_tools.py +4 -1
- letta/llm_api/openai.py +12 -12
- letta/llm_api/openai_client.py +53 -16
- letta/local_llm/constants.py +4 -3
- letta/local_llm/json_parser.py +5 -2
- letta/local_llm/utils.py +2 -3
- letta/log.py +171 -7
- letta/orm/agent.py +43 -9
- letta/orm/archive.py +4 -0
- letta/orm/custom_columns.py +15 -0
- letta/orm/identity.py +11 -11
- letta/orm/mcp_server.py +9 -0
- letta/orm/message.py +6 -1
- letta/orm/run_metrics.py +7 -2
- letta/orm/sqlalchemy_base.py +2 -2
- letta/orm/tool.py +3 -0
- letta/otel/tracing.py +2 -0
- letta/prompts/prompt_generator.py +7 -2
- letta/schemas/agent.py +41 -10
- letta/schemas/agent_file.py +3 -0
- letta/schemas/archive.py +4 -2
- letta/schemas/block.py +2 -1
- letta/schemas/enums.py +36 -3
- letta/schemas/file.py +3 -3
- letta/schemas/folder.py +2 -1
- letta/schemas/group.py +2 -1
- letta/schemas/identity.py +18 -9
- letta/schemas/job.py +3 -1
- letta/schemas/letta_message.py +71 -12
- letta/schemas/letta_request.py +7 -3
- letta/schemas/letta_stop_reason.py +0 -25
- letta/schemas/llm_config.py +8 -2
- letta/schemas/mcp.py +80 -83
- letta/schemas/mcp_server.py +349 -0
- letta/schemas/memory.py +20 -8
- letta/schemas/message.py +212 -67
- letta/schemas/providers/anthropic.py +13 -6
- letta/schemas/providers/azure.py +6 -4
- letta/schemas/providers/base.py +8 -4
- letta/schemas/providers/bedrock.py +6 -2
- letta/schemas/providers/cerebras.py +7 -3
- letta/schemas/providers/deepseek.py +2 -1
- letta/schemas/providers/google_gemini.py +15 -6
- letta/schemas/providers/groq.py +2 -1
- letta/schemas/providers/lmstudio.py +9 -6
- letta/schemas/providers/mistral.py +2 -1
- letta/schemas/providers/openai.py +7 -2
- letta/schemas/providers/together.py +9 -3
- letta/schemas/providers/xai.py +7 -3
- letta/schemas/run.py +7 -2
- letta/schemas/run_metrics.py +2 -1
- letta/schemas/sandbox_config.py +2 -2
- letta/schemas/secret.py +3 -158
- letta/schemas/source.py +2 -2
- letta/schemas/step.py +2 -2
- letta/schemas/tool.py +24 -1
- letta/schemas/usage.py +0 -1
- letta/server/rest_api/app.py +123 -7
- letta/server/rest_api/dependencies.py +3 -0
- letta/server/rest_api/interface.py +7 -4
- letta/server/rest_api/redis_stream_manager.py +16 -1
- letta/server/rest_api/routers/v1/__init__.py +7 -0
- letta/server/rest_api/routers/v1/agents.py +332 -322
- letta/server/rest_api/routers/v1/archives.py +127 -40
- letta/server/rest_api/routers/v1/blocks.py +54 -6
- letta/server/rest_api/routers/v1/chat_completions.py +146 -0
- letta/server/rest_api/routers/v1/folders.py +27 -35
- letta/server/rest_api/routers/v1/groups.py +23 -35
- letta/server/rest_api/routers/v1/identities.py +24 -10
- letta/server/rest_api/routers/v1/internal_runs.py +107 -0
- letta/server/rest_api/routers/v1/internal_templates.py +162 -179
- letta/server/rest_api/routers/v1/jobs.py +15 -27
- letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
- letta/server/rest_api/routers/v1/messages.py +23 -34
- letta/server/rest_api/routers/v1/organizations.py +6 -27
- letta/server/rest_api/routers/v1/providers.py +35 -62
- letta/server/rest_api/routers/v1/runs.py +30 -43
- letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
- letta/server/rest_api/routers/v1/sources.py +26 -42
- letta/server/rest_api/routers/v1/steps.py +16 -29
- letta/server/rest_api/routers/v1/tools.py +17 -13
- letta/server/rest_api/routers/v1/users.py +5 -17
- letta/server/rest_api/routers/v1/voice.py +18 -27
- letta/server/rest_api/streaming_response.py +5 -2
- letta/server/rest_api/utils.py +187 -25
- letta/server/server.py +27 -22
- letta/server/ws_api/server.py +5 -4
- letta/services/agent_manager.py +148 -26
- letta/services/agent_serialization_manager.py +6 -1
- letta/services/archive_manager.py +168 -15
- letta/services/block_manager.py +14 -4
- letta/services/file_manager.py +33 -29
- letta/services/group_manager.py +10 -0
- letta/services/helpers/agent_manager_helper.py +65 -11
- letta/services/identity_manager.py +105 -4
- letta/services/job_manager.py +11 -1
- letta/services/mcp/base_client.py +2 -2
- letta/services/mcp/oauth_utils.py +33 -8
- letta/services/mcp_manager.py +174 -78
- letta/services/mcp_server_manager.py +1331 -0
- letta/services/message_manager.py +109 -4
- letta/services/organization_manager.py +4 -4
- letta/services/passage_manager.py +9 -25
- letta/services/provider_manager.py +91 -15
- letta/services/run_manager.py +72 -15
- letta/services/sandbox_config_manager.py +45 -3
- letta/services/source_manager.py +15 -8
- letta/services/step_manager.py +24 -1
- letta/services/streaming_service.py +581 -0
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/tool_executor/core_tool_executor.py +111 -0
- letta/services/tool_executor/files_tool_executor.py +5 -3
- letta/services/tool_executor/sandbox_tool_executor.py +2 -2
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_manager.py +10 -3
- letta/services/tool_sandbox/base.py +61 -1
- letta/services/tool_sandbox/local_sandbox.py +1 -3
- letta/services/user_manager.py +2 -2
- letta/settings.py +49 -5
- letta/system.py +14 -5
- letta/utils.py +73 -1
- letta/validators.py +105 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/RECORD +157 -151
- letta/schemas/letta_ping.py +0 -28
- letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/licenses/LICENSE +0 -0
letta/schemas/message.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from letta.log import get_logger
|
|
4
|
+
|
|
5
|
+
logger = get_logger(__name__)
|
|
6
|
+
|
|
3
7
|
import copy
|
|
4
8
|
import json
|
|
5
9
|
import re
|
|
6
10
|
import uuid
|
|
7
|
-
import warnings
|
|
8
11
|
from collections import OrderedDict
|
|
9
12
|
from datetime import datetime, timezone
|
|
10
13
|
from enum import Enum
|
|
11
14
|
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
|
12
15
|
|
|
16
|
+
from letta_client import LettaMessageUnion
|
|
13
17
|
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall, Function as OpenAIFunction
|
|
14
18
|
from openai.types.responses import ResponseReasoningItem
|
|
15
19
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
@@ -18,19 +22,22 @@ from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, RE
|
|
|
18
22
|
from letta.helpers.datetime_helpers import get_utc_time, is_utc_datetime
|
|
19
23
|
from letta.helpers.json_helpers import json_dumps
|
|
20
24
|
from letta.local_llm.constants import INNER_THOUGHTS_KWARG, INNER_THOUGHTS_KWARG_VERTEX
|
|
21
|
-
from letta.schemas.enums import MessageRole
|
|
25
|
+
from letta.schemas.enums import MessageRole, PrimitiveType
|
|
22
26
|
from letta.schemas.letta_base import OrmMetadataBase
|
|
23
27
|
from letta.schemas.letta_message import (
|
|
24
28
|
ApprovalRequestMessage,
|
|
25
29
|
ApprovalResponseMessage,
|
|
30
|
+
ApprovalReturn,
|
|
26
31
|
AssistantMessage,
|
|
27
32
|
HiddenReasoningMessage,
|
|
28
33
|
LettaMessage,
|
|
34
|
+
LettaMessageReturnUnion,
|
|
29
35
|
MessageType,
|
|
30
36
|
ReasoningMessage,
|
|
31
37
|
SystemMessage,
|
|
32
38
|
ToolCall,
|
|
33
39
|
ToolCallMessage,
|
|
40
|
+
ToolReturn as LettaToolReturn,
|
|
34
41
|
ToolReturnMessage,
|
|
35
42
|
UserMessage,
|
|
36
43
|
)
|
|
@@ -68,7 +75,7 @@ def add_inner_thoughts_to_tool_call(
|
|
|
68
75
|
updated_tool_call.function.arguments = json_dumps(ordered_args)
|
|
69
76
|
return updated_tool_call
|
|
70
77
|
except json.JSONDecodeError as e:
|
|
71
|
-
|
|
78
|
+
logger.warning(f"Failed to put inner thoughts in kwargs: {e}")
|
|
72
79
|
raise e
|
|
73
80
|
|
|
74
81
|
|
|
@@ -116,9 +123,22 @@ class ApprovalCreate(MessageCreateBase):
|
|
|
116
123
|
"""Input to approve or deny a tool call request"""
|
|
117
124
|
|
|
118
125
|
type: Literal[MessageCreateType.approval] = Field(default=MessageCreateType.approval, description="The message type to be created.")
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
126
|
+
approvals: Optional[List[LettaMessageReturnUnion]] = Field(default=None, description="The list of approval responses")
|
|
127
|
+
approve: Optional[bool] = Field(None, description="Whether the tool has been approved", deprecated=True)
|
|
128
|
+
approval_request_id: Optional[str] = Field(None, description="The message ID of the approval request", deprecated=True)
|
|
129
|
+
reason: Optional[str] = Field(None, description="An optional explanation for the provided approval status", deprecated=True)
|
|
130
|
+
|
|
131
|
+
@model_validator(mode="after")
|
|
132
|
+
def migrate_deprecated_fields(self):
|
|
133
|
+
if not self.approvals and self.approve is not None and self.approval_request_id is not None:
|
|
134
|
+
self.approvals = [
|
|
135
|
+
ApprovalReturn(
|
|
136
|
+
tool_call_id=self.approval_request_id,
|
|
137
|
+
approve=self.approve,
|
|
138
|
+
reason=self.reason,
|
|
139
|
+
)
|
|
140
|
+
]
|
|
141
|
+
return self
|
|
122
142
|
|
|
123
143
|
|
|
124
144
|
MessageCreateUnion = Union[MessageCreate, ApprovalCreate]
|
|
@@ -153,7 +173,7 @@ class MessageUpdate(BaseModel):
|
|
|
153
173
|
|
|
154
174
|
|
|
155
175
|
class BaseMessage(OrmMetadataBase):
|
|
156
|
-
__id_prefix__ =
|
|
176
|
+
__id_prefix__ = PrimitiveType.MESSAGE.value
|
|
157
177
|
|
|
158
178
|
|
|
159
179
|
class Message(BaseMessage):
|
|
@@ -210,6 +230,7 @@ class Message(BaseMessage):
|
|
|
210
230
|
)
|
|
211
231
|
approve: Optional[bool] = Field(default=None, description="Whether tool call is approved.")
|
|
212
232
|
denial_reason: Optional[str] = Field(default=None, description="The reason the tool call request was denied.")
|
|
233
|
+
approvals: Optional[List[ApprovalReturn | ToolReturn]] = Field(default=None, description="The list of approvals for this message.")
|
|
213
234
|
# This overrides the optional base orm schema, created_at MUST exist on all messages objects
|
|
214
235
|
created_at: datetime = Field(default_factory=get_utc_time, description="The timestamp when the object was created.")
|
|
215
236
|
|
|
@@ -313,7 +334,7 @@ class Message(BaseMessage):
|
|
|
313
334
|
),
|
|
314
335
|
)
|
|
315
336
|
elif self.role == MessageRole.tool:
|
|
316
|
-
messages.
|
|
337
|
+
messages.append(self._convert_tool_return_message())
|
|
317
338
|
elif self.role == MessageRole.user:
|
|
318
339
|
messages.append(self._convert_user_message())
|
|
319
340
|
elif self.role == MessageRole.system:
|
|
@@ -322,20 +343,52 @@ class Message(BaseMessage):
|
|
|
322
343
|
if self.content:
|
|
323
344
|
messages.extend(self._convert_reasoning_messages(text_is_assistant_message=text_is_assistant_message))
|
|
324
345
|
if self.tool_calls is not None:
|
|
325
|
-
|
|
326
|
-
assert len(tool_calls) == 1
|
|
327
|
-
approval_request_message = ApprovalRequestMessage(**tool_calls[0].model_dump(exclude={"message_type"}))
|
|
328
|
-
messages.append(approval_request_message)
|
|
346
|
+
messages.append(self._convert_approval_request_message())
|
|
329
347
|
else:
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
348
|
+
if self.approvals:
|
|
349
|
+
first_approval = [a for a in self.approvals if isinstance(a, ApprovalReturn)]
|
|
350
|
+
|
|
351
|
+
def maybe_convert_tool_return_message(maybe_tool_return):
|
|
352
|
+
if isinstance(maybe_tool_return, ToolReturn):
|
|
353
|
+
parsed_data = self._parse_tool_response(maybe_tool_return.func_response)
|
|
354
|
+
return LettaToolReturn(
|
|
355
|
+
tool_call_id=maybe_tool_return.tool_call_id,
|
|
356
|
+
status=maybe_tool_return.status,
|
|
357
|
+
tool_return=parsed_data["message"],
|
|
358
|
+
stdout=maybe_tool_return.stdout,
|
|
359
|
+
stderr=maybe_tool_return.stderr,
|
|
360
|
+
)
|
|
361
|
+
return maybe_tool_return
|
|
362
|
+
|
|
363
|
+
approval_response_message = ApprovalResponseMessage(
|
|
364
|
+
id=self.id,
|
|
365
|
+
date=self.created_at,
|
|
366
|
+
otid=self.otid,
|
|
367
|
+
approvals=[maybe_convert_tool_return_message(approval) for approval in self.approvals],
|
|
368
|
+
run_id=self.run_id,
|
|
369
|
+
# TODO: temporary populate these fields for backwards compatibility
|
|
370
|
+
approve=first_approval[0].approve if first_approval else None,
|
|
371
|
+
approval_request_id=first_approval[0].tool_call_id if first_approval else None,
|
|
372
|
+
reason=first_approval[0].reason if first_approval else None,
|
|
373
|
+
)
|
|
374
|
+
else:
|
|
375
|
+
approval_response_message = ApprovalResponseMessage(
|
|
376
|
+
id=self.id,
|
|
377
|
+
date=self.created_at,
|
|
378
|
+
otid=self.otid,
|
|
379
|
+
approve=self.approve,
|
|
380
|
+
approval_request_id=self.approval_request_id,
|
|
381
|
+
reason=self.denial_reason,
|
|
382
|
+
approvals=[
|
|
383
|
+
# TODO: temporary workaround to populate from legacy fields
|
|
384
|
+
ApprovalReturn(
|
|
385
|
+
tool_call_id=self.approval_request_id,
|
|
386
|
+
approve=self.approve,
|
|
387
|
+
reason=self.denial_reason,
|
|
388
|
+
)
|
|
389
|
+
],
|
|
390
|
+
run_id=self.run_id,
|
|
391
|
+
)
|
|
339
392
|
messages.append(approval_response_message)
|
|
340
393
|
else:
|
|
341
394
|
raise ValueError(f"Unknown role: {self.role}")
|
|
@@ -460,7 +513,7 @@ class Message(BaseMessage):
|
|
|
460
513
|
)
|
|
461
514
|
|
|
462
515
|
else:
|
|
463
|
-
|
|
516
|
+
logger.warning(f"Unrecognized content part in assistant message: {content_part}")
|
|
464
517
|
|
|
465
518
|
return messages
|
|
466
519
|
|
|
@@ -592,7 +645,7 @@ class Message(BaseMessage):
|
|
|
592
645
|
|
|
593
646
|
return messages
|
|
594
647
|
|
|
595
|
-
def _convert_tool_return_message(self) ->
|
|
648
|
+
def _convert_tool_return_message(self) -> ToolReturnMessage:
|
|
596
649
|
"""Convert tool role message to ToolReturnMessage.
|
|
597
650
|
|
|
598
651
|
The tool return is packaged as follows:
|
|
@@ -603,7 +656,7 @@ class Message(BaseMessage):
|
|
|
603
656
|
}
|
|
604
657
|
|
|
605
658
|
Returns:
|
|
606
|
-
|
|
659
|
+
ToolReturnMessage: Converted tool return message
|
|
607
660
|
|
|
608
661
|
Raises:
|
|
609
662
|
ValueError: If message role is not 'tool', parsing fails, or no valid content exists
|
|
@@ -623,27 +676,47 @@ class Message(BaseMessage):
|
|
|
623
676
|
|
|
624
677
|
return self._convert_legacy_tool_return()
|
|
625
678
|
|
|
626
|
-
def _convert_explicit_tool_returns(self) ->
|
|
627
|
-
"""Convert explicit tool returns to ToolReturnMessage
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
for
|
|
679
|
+
def _convert_explicit_tool_returns(self) -> ToolReturnMessage:
|
|
680
|
+
"""Convert explicit tool returns to a single ToolReturnMessage."""
|
|
681
|
+
# build list of all tool return objects
|
|
682
|
+
all_tool_returns = []
|
|
683
|
+
for tool_return in self.tool_returns:
|
|
631
684
|
parsed_data = self._parse_tool_response(tool_return.func_response)
|
|
632
685
|
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
stderr=tool_return.stderr,
|
|
640
|
-
otid_index=index,
|
|
641
|
-
)
|
|
686
|
+
tool_return_obj = LettaToolReturn(
|
|
687
|
+
tool_return=parsed_data["message"],
|
|
688
|
+
status=parsed_data["status"],
|
|
689
|
+
tool_call_id=tool_return.tool_call_id,
|
|
690
|
+
stdout=tool_return.stdout,
|
|
691
|
+
stderr=tool_return.stderr,
|
|
642
692
|
)
|
|
693
|
+
all_tool_returns.append(tool_return_obj)
|
|
694
|
+
|
|
695
|
+
if not all_tool_returns:
|
|
696
|
+
# this should not happen if tool_returns is non-empty, but handle gracefully
|
|
697
|
+
raise ValueError("No tool returns to convert")
|
|
698
|
+
|
|
699
|
+
first_tool_return = all_tool_returns[0]
|
|
643
700
|
|
|
644
|
-
return
|
|
701
|
+
return ToolReturnMessage(
|
|
702
|
+
id=self.id,
|
|
703
|
+
date=self.created_at,
|
|
704
|
+
# deprecated top-level fields populated from first tool return
|
|
705
|
+
tool_return=first_tool_return.tool_return,
|
|
706
|
+
status=first_tool_return.status,
|
|
707
|
+
tool_call_id=first_tool_return.tool_call_id,
|
|
708
|
+
stdout=first_tool_return.stdout,
|
|
709
|
+
stderr=first_tool_return.stderr,
|
|
710
|
+
tool_returns=all_tool_returns,
|
|
711
|
+
name=self.name,
|
|
712
|
+
otid=Message.generate_otid_from_id(self.id, 0),
|
|
713
|
+
sender_id=self.sender_id,
|
|
714
|
+
step_id=self.step_id,
|
|
715
|
+
is_err=self.is_err,
|
|
716
|
+
run_id=self.run_id,
|
|
717
|
+
)
|
|
645
718
|
|
|
646
|
-
def _convert_legacy_tool_return(self) ->
|
|
719
|
+
def _convert_legacy_tool_return(self) -> ToolReturnMessage:
|
|
647
720
|
"""Convert legacy single text content to ToolReturnMessage."""
|
|
648
721
|
if not self._has_single_text_content():
|
|
649
722
|
raise ValueError(f"No valid tool returns to convert: {self}")
|
|
@@ -651,16 +724,14 @@ class Message(BaseMessage):
|
|
|
651
724
|
text_content = self.content[0].text
|
|
652
725
|
parsed_data = self._parse_tool_response(text_content)
|
|
653
726
|
|
|
654
|
-
return
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
)
|
|
663
|
-
]
|
|
727
|
+
return self._create_tool_return_message(
|
|
728
|
+
message_text=parsed_data["message"],
|
|
729
|
+
status=parsed_data["status"],
|
|
730
|
+
tool_call_id=self.tool_call_id,
|
|
731
|
+
stdout=None,
|
|
732
|
+
stderr=None,
|
|
733
|
+
otid_index=0,
|
|
734
|
+
)
|
|
664
735
|
|
|
665
736
|
def _has_single_text_content(self) -> bool:
|
|
666
737
|
"""Check if message has exactly one text content item."""
|
|
@@ -709,9 +780,7 @@ class Message(BaseMessage):
|
|
|
709
780
|
Returns:
|
|
710
781
|
Configured ToolReturnMessage instance
|
|
711
782
|
"""
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
tool_return_obj = ToolReturnSchema(
|
|
783
|
+
tool_return_obj = LettaToolReturn(
|
|
715
784
|
tool_return=message_text,
|
|
716
785
|
status=status,
|
|
717
786
|
tool_call_id=tool_call_id,
|
|
@@ -746,6 +815,28 @@ class Message(BaseMessage):
|
|
|
746
815
|
else:
|
|
747
816
|
raise ValueError(f"Invalid status: {status}")
|
|
748
817
|
|
|
818
|
+
def _convert_approval_request_message(self) -> ApprovalRequestMessage:
|
|
819
|
+
"""Convert approval request message to ApprovalRequestMessage"""
|
|
820
|
+
|
|
821
|
+
def _convert_tool_call(tool_call):
|
|
822
|
+
return ToolCall(
|
|
823
|
+
name=tool_call.function.name,
|
|
824
|
+
arguments=tool_call.function.arguments,
|
|
825
|
+
tool_call_id=tool_call.id,
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
return ApprovalRequestMessage(
|
|
829
|
+
id=self.id,
|
|
830
|
+
date=self.created_at,
|
|
831
|
+
otid=self.otid,
|
|
832
|
+
sender_id=self.sender_id,
|
|
833
|
+
step_id=self.step_id,
|
|
834
|
+
run_id=self.run_id,
|
|
835
|
+
tool_call=_convert_tool_call(self.tool_calls[0]), # backwards compatibility
|
|
836
|
+
tool_calls=[_convert_tool_call(tc) for tc in self.tool_calls],
|
|
837
|
+
name=self.name,
|
|
838
|
+
)
|
|
839
|
+
|
|
749
840
|
def _convert_user_message(self) -> UserMessage:
|
|
750
841
|
"""Convert user role message to UserMessage"""
|
|
751
842
|
# Extract text content
|
|
@@ -816,6 +907,12 @@ class Message(BaseMessage):
|
|
|
816
907
|
[TextContent(text=openai_message_dict["content"])] if openai_message_dict["content"] else []
|
|
817
908
|
)
|
|
818
909
|
|
|
910
|
+
# This is really hacky and this interface is poorly designed, we should auto derive tool_returns instead of passing it in
|
|
911
|
+
if not tool_returns:
|
|
912
|
+
tool_returns = []
|
|
913
|
+
if "tool_returns" in openai_message_dict:
|
|
914
|
+
tool_returns = [ToolReturn(**tr) for tr in openai_message_dict["tool_returns"]]
|
|
915
|
+
|
|
819
916
|
# TODO(caren) bad assumption here that "reasoning_content" always comes before "redacted_reasoning_content"
|
|
820
917
|
if "reasoning_content" in openai_message_dict and openai_message_dict["reasoning_content"]:
|
|
821
918
|
content.append(
|
|
@@ -1099,7 +1196,7 @@ class Message(BaseMessage):
|
|
|
1099
1196
|
if bool(re.match(r"^[^\s<|\\/>]+$", self.name)):
|
|
1100
1197
|
openai_message["name"] = self.name
|
|
1101
1198
|
else:
|
|
1102
|
-
|
|
1199
|
+
logger.warning(f"Using OpenAI with invalid 'name' field (name={self.name} role={self.role}).")
|
|
1103
1200
|
|
|
1104
1201
|
if parse_content_parts and self.content is not None:
|
|
1105
1202
|
for content in self.content:
|
|
@@ -1166,7 +1263,7 @@ class Message(BaseMessage):
|
|
|
1166
1263
|
if bool(re.match(r"^[^\s<|\\/>]+$", self.name)):
|
|
1167
1264
|
user_dict["name"] = self.name
|
|
1168
1265
|
else:
|
|
1169
|
-
|
|
1266
|
+
logger.warning(f"Using OpenAI with invalid 'name' field (name={self.name} role={self.role}).")
|
|
1170
1267
|
|
|
1171
1268
|
message_dicts.append(user_dict)
|
|
1172
1269
|
|
|
@@ -1414,18 +1511,38 @@ class Message(BaseMessage):
|
|
|
1414
1511
|
|
|
1415
1512
|
elif self.role == "tool":
|
|
1416
1513
|
# NOTE: Anthropic uses role "user" for "tool" responses
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1514
|
+
content = []
|
|
1515
|
+
for tool_return in self.tool_returns:
|
|
1516
|
+
if not tool_return.tool_call_id:
|
|
1517
|
+
raise TypeError("Anthropic API requires tool_use_id to be set.")
|
|
1518
|
+
content.append(
|
|
1422
1519
|
{
|
|
1423
1520
|
"type": "tool_result",
|
|
1424
|
-
"tool_use_id":
|
|
1425
|
-
"content":
|
|
1521
|
+
"tool_use_id": tool_return.tool_call_id,
|
|
1522
|
+
"content": tool_return.func_response,
|
|
1426
1523
|
}
|
|
1427
|
-
|
|
1428
|
-
|
|
1524
|
+
)
|
|
1525
|
+
if content:
|
|
1526
|
+
anthropic_message = {
|
|
1527
|
+
"role": "user",
|
|
1528
|
+
"content": content,
|
|
1529
|
+
}
|
|
1530
|
+
else:
|
|
1531
|
+
if not self.tool_call_id:
|
|
1532
|
+
raise TypeError("Anthropic API requires tool_use_id to be set.")
|
|
1533
|
+
|
|
1534
|
+
# This is for legacy reasons
|
|
1535
|
+
anthropic_message = {
|
|
1536
|
+
"role": "user", # NOTE: diff
|
|
1537
|
+
"content": [
|
|
1538
|
+
# TODO support error types etc
|
|
1539
|
+
{
|
|
1540
|
+
"type": "tool_result",
|
|
1541
|
+
"tool_use_id": self.tool_call_id,
|
|
1542
|
+
"content": text_content,
|
|
1543
|
+
}
|
|
1544
|
+
],
|
|
1545
|
+
}
|
|
1429
1546
|
|
|
1430
1547
|
else:
|
|
1431
1548
|
raise ValueError(self.role)
|
|
@@ -1483,7 +1600,7 @@ class Message(BaseMessage):
|
|
|
1483
1600
|
text_content = None
|
|
1484
1601
|
|
|
1485
1602
|
if self.role != "tool" and self.name is not None:
|
|
1486
|
-
|
|
1603
|
+
logger.warning(f"Using Google AI with non-null 'name' field (name={self.name} role={self.role}), not yet supported.")
|
|
1487
1604
|
|
|
1488
1605
|
if self.role == "system":
|
|
1489
1606
|
# NOTE: Gemini API doesn't have a 'system' role, use 'user' instead
|
|
@@ -1603,7 +1720,7 @@ class Message(BaseMessage):
|
|
|
1603
1720
|
assert self.tool_call_id is not None, vars(self)
|
|
1604
1721
|
|
|
1605
1722
|
if self.name is None:
|
|
1606
|
-
|
|
1723
|
+
logger.warning("Couldn't find function name on tool call, defaulting to tool ID instead.")
|
|
1607
1724
|
function_name = self.tool_call_id
|
|
1608
1725
|
else:
|
|
1609
1726
|
function_name = self.name
|
|
@@ -1636,7 +1753,7 @@ class Message(BaseMessage):
|
|
|
1636
1753
|
if "parts" not in google_ai_message or not google_ai_message["parts"]:
|
|
1637
1754
|
# If parts is empty, add a default text part
|
|
1638
1755
|
google_ai_message["parts"] = [{"text": "empty message"}]
|
|
1639
|
-
|
|
1756
|
+
logger.warning(
|
|
1640
1757
|
f"Empty 'parts' detected in message with role '{self.role}'. Added default empty text part. Full message:\n{vars(self)}"
|
|
1641
1758
|
)
|
|
1642
1759
|
|
|
@@ -1697,6 +1814,9 @@ class Message(BaseMessage):
|
|
|
1697
1814
|
# Filter last message if it is a lone approval request without a response - this only occurs for token counting
|
|
1698
1815
|
if messages[-1].role == "approval" and messages[-1].tool_calls is not None and len(messages[-1].tool_calls) > 0:
|
|
1699
1816
|
messages.remove(messages[-1])
|
|
1817
|
+
# Also filter pending tool call message if this turn invoked parallel tool calling
|
|
1818
|
+
if messages and messages[-1].role == "assistant" and messages[-1].tool_calls is not None and len(messages[-1].tool_calls) > 0:
|
|
1819
|
+
messages.remove(messages[-1])
|
|
1700
1820
|
|
|
1701
1821
|
# Filter last message if it is a lone reasoning message without assistant message or tool call
|
|
1702
1822
|
if (
|
|
@@ -1706,6 +1826,28 @@ class Message(BaseMessage):
|
|
|
1706
1826
|
):
|
|
1707
1827
|
messages.remove(messages[-1])
|
|
1708
1828
|
|
|
1829
|
+
# Collapse adjacent tool call and approval messages
|
|
1830
|
+
messages = Message.collapse_tool_call_messages_for_llm_api(messages)
|
|
1831
|
+
|
|
1832
|
+
return messages
|
|
1833
|
+
|
|
1834
|
+
@staticmethod
|
|
1835
|
+
def collapse_tool_call_messages_for_llm_api(
|
|
1836
|
+
messages: List[Message],
|
|
1837
|
+
) -> List[Message]:
|
|
1838
|
+
adjacent_tool_call_approval_messages = []
|
|
1839
|
+
for i in range(len(messages) - 1):
|
|
1840
|
+
if (
|
|
1841
|
+
messages[i].role == MessageRole.assistant
|
|
1842
|
+
and messages[i].tool_calls is not None
|
|
1843
|
+
and messages[i + 1].role == MessageRole.approval
|
|
1844
|
+
and messages[i + 1].tool_calls is not None
|
|
1845
|
+
):
|
|
1846
|
+
adjacent_tool_call_approval_messages.append(i)
|
|
1847
|
+
for i in reversed(adjacent_tool_call_approval_messages):
|
|
1848
|
+
messages[i].content = messages[i].content + messages[i + 1].content
|
|
1849
|
+
messages[i].tool_calls = messages[i].tool_calls + messages[i + 1].tool_calls
|
|
1850
|
+
messages.remove(messages[i + 1])
|
|
1709
1851
|
return messages
|
|
1710
1852
|
|
|
1711
1853
|
@staticmethod
|
|
@@ -1713,6 +1855,9 @@ class Message(BaseMessage):
|
|
|
1713
1855
|
"""
|
|
1714
1856
|
Convert message id to bits and change the list bit to the index
|
|
1715
1857
|
"""
|
|
1858
|
+
if index == -1:
|
|
1859
|
+
return message_id
|
|
1860
|
+
|
|
1716
1861
|
if not 0 <= index < 128:
|
|
1717
1862
|
raise ValueError("Index must be between 0 and 127")
|
|
1718
1863
|
|
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import warnings
|
|
2
1
|
from typing import Literal
|
|
3
2
|
|
|
3
|
+
from letta.log import get_logger
|
|
4
|
+
|
|
5
|
+
logger = get_logger(__name__)
|
|
6
|
+
|
|
4
7
|
import anthropic
|
|
5
8
|
from pydantic import Field
|
|
6
9
|
|
|
10
|
+
from letta.errors import ErrorCode, LLMAuthenticationError, LLMError
|
|
7
11
|
from letta.schemas.enums import ProviderCategory, ProviderType
|
|
8
12
|
from letta.schemas.llm_config import LLMConfig
|
|
9
13
|
from letta.schemas.providers.base import Provider
|
|
14
|
+
from letta.settings import model_settings
|
|
10
15
|
|
|
11
16
|
# https://docs.anthropic.com/claude/docs/models-overview
|
|
12
17
|
# Sadly hardcoded
|
|
@@ -98,8 +103,9 @@ class AnthropicProvider(Provider):
|
|
|
98
103
|
base_url: str = "https://api.anthropic.com/v1"
|
|
99
104
|
|
|
100
105
|
async def check_api_key(self):
|
|
101
|
-
|
|
102
|
-
|
|
106
|
+
api_key = self.get_api_key_secret().get_plaintext()
|
|
107
|
+
if api_key:
|
|
108
|
+
anthropic_client = anthropic.Anthropic(api_key=api_key)
|
|
103
109
|
try:
|
|
104
110
|
# just use a cheap model to count some tokens - as of 5/7/2025 this is faster than fetching the list of models
|
|
105
111
|
anthropic_client.messages.count_tokens(model=MODEL_LIST[-1]["name"], messages=[{"role": "user", "content": "a"}])
|
|
@@ -116,8 +122,9 @@ class AnthropicProvider(Provider):
|
|
|
116
122
|
|
|
117
123
|
NOTE: currently there is no GET /models, so we need to hardcode
|
|
118
124
|
"""
|
|
119
|
-
|
|
120
|
-
|
|
125
|
+
api_key = self.get_api_key_secret().get_plaintext()
|
|
126
|
+
if api_key:
|
|
127
|
+
anthropic_client = anthropic.AsyncAnthropic(api_key=api_key)
|
|
121
128
|
elif model_settings.anthropic_api_key:
|
|
122
129
|
anthropic_client = anthropic.AsyncAnthropic()
|
|
123
130
|
else:
|
|
@@ -145,7 +152,7 @@ class AnthropicProvider(Provider):
|
|
|
145
152
|
model["context_window"] = model_library[model["id"]]
|
|
146
153
|
else:
|
|
147
154
|
# On fallback, we can set 200k (generally safe), but we should warn the user
|
|
148
|
-
|
|
155
|
+
logger.warning(f"Couldn't find context window size for model {model['id']}, defaulting to 200,000")
|
|
149
156
|
model["context_window"] = 200000
|
|
150
157
|
|
|
151
158
|
# Optional override: enable 1M context for Sonnet 4/4.5 when flag is set
|
letta/schemas/providers/azure.py
CHANGED
|
@@ -60,7 +60,8 @@ class AzureProvider(Provider):
|
|
|
60
60
|
def azure_openai_get_deployed_model_list(self) -> list:
|
|
61
61
|
"""https://learn.microsoft.com/en-us/rest/api/azureopenai/models/list?view=rest-azureopenai-2023-05-15&tabs=HTTP"""
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
api_key = self.get_api_key_secret().get_plaintext()
|
|
64
|
+
client = AzureOpenAI(api_key=api_key, api_version=self.api_version, azure_endpoint=self.base_url)
|
|
64
65
|
|
|
65
66
|
try:
|
|
66
67
|
models_list = client.models.list()
|
|
@@ -71,8 +72,8 @@ class AzureProvider(Provider):
|
|
|
71
72
|
|
|
72
73
|
# https://xxx.openai.azure.com/openai/models?api-version=xxx
|
|
73
74
|
headers = {"Content-Type": "application/json"}
|
|
74
|
-
if
|
|
75
|
-
headers["api-key"] = f"{
|
|
75
|
+
if api_key is not None:
|
|
76
|
+
headers["api-key"] = f"{api_key}"
|
|
76
77
|
|
|
77
78
|
# 2. Get all the deployed models
|
|
78
79
|
url = self.get_azure_deployment_list_endpoint()
|
|
@@ -165,7 +166,8 @@ class AzureProvider(Provider):
|
|
|
165
166
|
return AZURE_MODEL_TO_CONTEXT_LENGTH.get(model_name, llm_default)
|
|
166
167
|
|
|
167
168
|
async def check_api_key(self):
|
|
168
|
-
|
|
169
|
+
api_key = self.get_api_key_secret().get_plaintext()
|
|
170
|
+
if not api_key:
|
|
169
171
|
raise ValueError("No API key provided")
|
|
170
172
|
|
|
171
173
|
try:
|
letta/schemas/providers/base.py
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
|
|
3
|
+
from letta.log import get_logger
|
|
4
|
+
|
|
5
|
+
logger = get_logger(__name__)
|
|
6
|
+
|
|
3
7
|
from pydantic import BaseModel, Field, model_validator
|
|
4
8
|
|
|
5
9
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
6
10
|
from letta.schemas.embedding_config_overrides import EMBEDDING_HANDLE_OVERRIDES
|
|
7
|
-
from letta.schemas.enums import ProviderCategory, ProviderType
|
|
11
|
+
from letta.schemas.enums import PrimitiveType, ProviderCategory, ProviderType
|
|
8
12
|
from letta.schemas.letta_base import LettaBase
|
|
9
13
|
from letta.schemas.llm_config import LLMConfig
|
|
10
14
|
from letta.schemas.llm_config_overrides import LLM_HANDLE_OVERRIDES
|
|
@@ -13,7 +17,7 @@ from letta.settings import model_settings
|
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
class ProviderBase(LettaBase):
|
|
16
|
-
__id_prefix__ =
|
|
20
|
+
__id_prefix__ = PrimitiveType.PROVIDER.value
|
|
17
21
|
|
|
18
22
|
|
|
19
23
|
class Provider(ProviderBase):
|
|
@@ -90,7 +94,7 @@ class Provider(ProviderBase):
|
|
|
90
94
|
import asyncio
|
|
91
95
|
import warnings
|
|
92
96
|
|
|
93
|
-
|
|
97
|
+
logger.warning("list_llm_models is deprecated, use list_llm_models_async instead", stacklevel=2)
|
|
94
98
|
|
|
95
99
|
# Simplified asyncio handling - just use asyncio.run()
|
|
96
100
|
# This works in most contexts and avoids complex event loop detection
|
|
@@ -115,7 +119,7 @@ class Provider(ProviderBase):
|
|
|
115
119
|
import asyncio
|
|
116
120
|
import warnings
|
|
117
121
|
|
|
118
|
-
|
|
122
|
+
logger.warning("list_embedding_models is deprecated, use list_embedding_models_async instead", stacklevel=2)
|
|
119
123
|
|
|
120
124
|
# Simplified asyncio handling - just use asyncio.run()
|
|
121
125
|
# This works in most contexts and avoids complex event loop detection
|
|
@@ -25,11 +25,15 @@ class BedrockProvider(Provider):
|
|
|
25
25
|
from aioboto3.session import Session
|
|
26
26
|
|
|
27
27
|
try:
|
|
28
|
+
# Decrypt credentials before using
|
|
29
|
+
access_key = self.get_access_key_secret().get_plaintext()
|
|
30
|
+
secret_key = self.get_api_key_secret().get_plaintext()
|
|
31
|
+
|
|
28
32
|
session = Session()
|
|
29
33
|
async with session.client(
|
|
30
34
|
"bedrock",
|
|
31
|
-
aws_access_key_id=
|
|
32
|
-
aws_secret_access_key=
|
|
35
|
+
aws_access_key_id=access_key,
|
|
36
|
+
aws_secret_access_key=secret_key,
|
|
33
37
|
region_name=self.region,
|
|
34
38
|
) as bedrock:
|
|
35
39
|
response = await bedrock.list_inference_profiles()
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import warnings
|
|
2
1
|
from typing import Literal
|
|
3
2
|
|
|
3
|
+
from letta.log import get_logger
|
|
4
|
+
|
|
5
|
+
logger = get_logger(__name__)
|
|
6
|
+
|
|
4
7
|
from pydantic import Field
|
|
5
8
|
|
|
6
9
|
from letta.schemas.enums import ProviderCategory, ProviderType
|
|
@@ -38,7 +41,8 @@ class CerebrasProvider(OpenAIProvider):
|
|
|
38
41
|
async def list_llm_models_async(self) -> list[LLMConfig]:
|
|
39
42
|
from letta.llm_api.openai import openai_get_model_list_async
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
api_key = self.get_api_key_secret().get_plaintext()
|
|
45
|
+
response = await openai_get_model_list_async(self.base_url, api_key=api_key)
|
|
42
46
|
|
|
43
47
|
if "data" in response:
|
|
44
48
|
data = response["data"]
|
|
@@ -57,7 +61,7 @@ class CerebrasProvider(OpenAIProvider):
|
|
|
57
61
|
context_window_size = self.get_model_context_window_size(model_name)
|
|
58
62
|
|
|
59
63
|
if not context_window_size:
|
|
60
|
-
|
|
64
|
+
logger.warning(f"Couldn't find context window size for model {model_name}")
|
|
61
65
|
continue
|
|
62
66
|
|
|
63
67
|
# Cerebras supports function calling
|
|
@@ -34,7 +34,8 @@ class DeepSeekProvider(OpenAIProvider):
|
|
|
34
34
|
async def list_llm_models_async(self) -> list[LLMConfig]:
|
|
35
35
|
from letta.llm_api.openai import openai_get_model_list_async
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
api_key = self.get_api_key_secret().get_plaintext()
|
|
38
|
+
response = await openai_get_model_list_async(self.base_url, api_key=api_key)
|
|
38
39
|
data = response.get("data", response)
|
|
39
40
|
|
|
40
41
|
configs = []
|