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
|
@@ -43,6 +43,7 @@ from letta.schemas.letta_message import (
|
|
|
43
43
|
)
|
|
44
44
|
from letta.schemas.letta_message_content import (
|
|
45
45
|
OmittedReasoningContent,
|
|
46
|
+
ReasoningContent,
|
|
46
47
|
SummarizedReasoningContent,
|
|
47
48
|
SummarizedReasoningContentPart,
|
|
48
49
|
TextContent,
|
|
@@ -51,6 +52,7 @@ from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
|
|
|
51
52
|
from letta.schemas.message import Message
|
|
52
53
|
from letta.schemas.openai.chat_completion_response import FunctionCall, ToolCall
|
|
53
54
|
from letta.server.rest_api.json_parser import OptimisticJSONParser
|
|
55
|
+
from letta.server.rest_api.utils import decrement_message_uuid
|
|
54
56
|
from letta.streaming_utils import (
|
|
55
57
|
FunctionArgumentsStreamHandler,
|
|
56
58
|
JSONInnerThoughtsExtractor,
|
|
@@ -203,7 +205,7 @@ class OpenAIStreamingInterface:
|
|
|
203
205
|
except Exception as e:
|
|
204
206
|
import traceback
|
|
205
207
|
|
|
206
|
-
logger.
|
|
208
|
+
logger.exception("Error processing stream: %s", e)
|
|
207
209
|
if ttft_span:
|
|
208
210
|
ttft_span.add_event(
|
|
209
211
|
name="stop_reason",
|
|
@@ -324,14 +326,14 @@ class OpenAIStreamingInterface:
|
|
|
324
326
|
self.tool_call_name = str(self.function_name_buffer)
|
|
325
327
|
if self.tool_call_name in self.requires_approval_tools:
|
|
326
328
|
tool_call_msg = ApprovalRequestMessage(
|
|
327
|
-
id=self.letta_message_id,
|
|
329
|
+
id=decrement_message_uuid(self.letta_message_id),
|
|
328
330
|
date=datetime.now(timezone.utc),
|
|
329
331
|
tool_call=ToolCallDelta(
|
|
330
332
|
name=self.function_name_buffer,
|
|
331
333
|
arguments=None,
|
|
332
334
|
tool_call_id=self.function_id_buffer,
|
|
333
335
|
),
|
|
334
|
-
otid=Message.generate_otid_from_id(self.letta_message_id,
|
|
336
|
+
otid=Message.generate_otid_from_id(decrement_message_uuid(self.letta_message_id), -1),
|
|
335
337
|
run_id=self.run_id,
|
|
336
338
|
step_id=self.step_id,
|
|
337
339
|
)
|
|
@@ -412,7 +414,7 @@ class OpenAIStreamingInterface:
|
|
|
412
414
|
message_index += 1
|
|
413
415
|
if self.function_name_buffer in self.requires_approval_tools:
|
|
414
416
|
tool_call_msg = ApprovalRequestMessage(
|
|
415
|
-
id=self.letta_message_id,
|
|
417
|
+
id=decrement_message_uuid(self.letta_message_id),
|
|
416
418
|
date=datetime.now(timezone.utc),
|
|
417
419
|
tool_call=ToolCallDelta(
|
|
418
420
|
name=self.function_name_buffer,
|
|
@@ -420,7 +422,7 @@ class OpenAIStreamingInterface:
|
|
|
420
422
|
tool_call_id=self.function_id_buffer,
|
|
421
423
|
),
|
|
422
424
|
# name=name,
|
|
423
|
-
otid=Message.generate_otid_from_id(self.letta_message_id,
|
|
425
|
+
otid=Message.generate_otid_from_id(decrement_message_uuid(self.letta_message_id), -1),
|
|
424
426
|
run_id=self.run_id,
|
|
425
427
|
step_id=self.step_id,
|
|
426
428
|
)
|
|
@@ -451,7 +453,7 @@ class OpenAIStreamingInterface:
|
|
|
451
453
|
message_index += 1
|
|
452
454
|
if self.function_name_buffer in self.requires_approval_tools:
|
|
453
455
|
tool_call_msg = ApprovalRequestMessage(
|
|
454
|
-
id=self.letta_message_id,
|
|
456
|
+
id=decrement_message_uuid(self.letta_message_id),
|
|
455
457
|
date=datetime.now(timezone.utc),
|
|
456
458
|
tool_call=ToolCallDelta(
|
|
457
459
|
name=None,
|
|
@@ -459,7 +461,7 @@ class OpenAIStreamingInterface:
|
|
|
459
461
|
tool_call_id=self.function_id_buffer,
|
|
460
462
|
),
|
|
461
463
|
# name=name,
|
|
462
|
-
otid=Message.generate_otid_from_id(self.letta_message_id,
|
|
464
|
+
otid=Message.generate_otid_from_id(decrement_message_uuid(self.letta_message_id), -1),
|
|
463
465
|
run_id=self.run_id,
|
|
464
466
|
step_id=self.step_id,
|
|
465
467
|
)
|
|
@@ -532,20 +534,31 @@ class SimpleOpenAIStreamingInterface:
|
|
|
532
534
|
|
|
533
535
|
self.requires_approval_tools = requires_approval_tools
|
|
534
536
|
|
|
535
|
-
def get_content(self) -> list[TextContent | OmittedReasoningContent]:
|
|
537
|
+
def get_content(self) -> list[TextContent | OmittedReasoningContent | ReasoningContent]:
|
|
536
538
|
shown_omitted = False
|
|
537
539
|
concat_content = ""
|
|
538
540
|
merged_messages = []
|
|
541
|
+
reasoning_content = []
|
|
542
|
+
|
|
539
543
|
for msg in self.content_messages:
|
|
540
544
|
if isinstance(msg, HiddenReasoningMessage) and not shown_omitted:
|
|
541
545
|
merged_messages.append(OmittedReasoningContent())
|
|
542
546
|
shown_omitted = True
|
|
547
|
+
elif isinstance(msg, ReasoningMessage):
|
|
548
|
+
reasoning_content.append(msg.reasoning)
|
|
543
549
|
elif isinstance(msg, AssistantMessage):
|
|
544
550
|
if isinstance(msg.content, list):
|
|
545
551
|
concat_content += "".join([c.text for c in msg.content])
|
|
546
552
|
else:
|
|
547
553
|
concat_content += msg.content
|
|
548
|
-
|
|
554
|
+
|
|
555
|
+
if reasoning_content:
|
|
556
|
+
combined_reasoning = "".join(reasoning_content)
|
|
557
|
+
merged_messages.append(ReasoningContent(is_native=True, reasoning=combined_reasoning, signature=None))
|
|
558
|
+
|
|
559
|
+
if concat_content:
|
|
560
|
+
merged_messages.append(TextContent(text=concat_content))
|
|
561
|
+
|
|
549
562
|
return merged_messages
|
|
550
563
|
|
|
551
564
|
def get_tool_call_object(self) -> ToolCall:
|
|
@@ -591,6 +604,8 @@ class SimpleOpenAIStreamingInterface:
|
|
|
591
604
|
# For reasoning models, emit a hidden reasoning message before the first chunk
|
|
592
605
|
if not self.emitted_hidden_reasoning and is_openai_reasoning_model(self.model):
|
|
593
606
|
self.emitted_hidden_reasoning = True
|
|
607
|
+
if prev_message_type and prev_message_type != "hidden_reasoning_message":
|
|
608
|
+
message_index += 1
|
|
594
609
|
hidden_message = HiddenReasoningMessage(
|
|
595
610
|
id=self.letta_message_id,
|
|
596
611
|
date=datetime.now(timezone.utc),
|
|
@@ -602,7 +617,6 @@ class SimpleOpenAIStreamingInterface:
|
|
|
602
617
|
)
|
|
603
618
|
self.content_messages.append(hidden_message)
|
|
604
619
|
prev_message_type = hidden_message.message_type
|
|
605
|
-
message_index += 1 # Increment for the next message
|
|
606
620
|
yield hidden_message
|
|
607
621
|
|
|
608
622
|
async for chunk in stream:
|
|
@@ -632,7 +646,7 @@ class SimpleOpenAIStreamingInterface:
|
|
|
632
646
|
except Exception as e:
|
|
633
647
|
import traceback
|
|
634
648
|
|
|
635
|
-
logger.
|
|
649
|
+
logger.exception("Error processing stream: %s", e)
|
|
636
650
|
if ttft_span:
|
|
637
651
|
ttft_span.add_event(
|
|
638
652
|
name="stop_reason",
|
|
@@ -664,9 +678,11 @@ class SimpleOpenAIStreamingInterface:
|
|
|
664
678
|
message_delta = choice.delta
|
|
665
679
|
|
|
666
680
|
if message_delta.content is not None and message_delta.content != "":
|
|
681
|
+
if prev_message_type and prev_message_type != "assistant_message":
|
|
682
|
+
message_index += 1
|
|
667
683
|
assistant_msg = AssistantMessage(
|
|
668
684
|
id=self.letta_message_id,
|
|
669
|
-
content=
|
|
685
|
+
content=message_delta.content,
|
|
670
686
|
date=datetime.now(timezone.utc).isoformat(),
|
|
671
687
|
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
|
672
688
|
run_id=self.run_id,
|
|
@@ -674,9 +690,33 @@ class SimpleOpenAIStreamingInterface:
|
|
|
674
690
|
)
|
|
675
691
|
self.content_messages.append(assistant_msg)
|
|
676
692
|
prev_message_type = assistant_msg.message_type
|
|
677
|
-
message_index += 1 # Increment for the next message
|
|
678
693
|
yield assistant_msg
|
|
679
694
|
|
|
695
|
+
if (
|
|
696
|
+
hasattr(chunk, "choices")
|
|
697
|
+
and len(chunk.choices) > 0
|
|
698
|
+
and hasattr(chunk.choices[0], "delta")
|
|
699
|
+
and hasattr(chunk.choices[0].delta, "reasoning_content")
|
|
700
|
+
):
|
|
701
|
+
delta = chunk.choices[0].delta
|
|
702
|
+
reasoning_content = getattr(delta, "reasoning_content", None)
|
|
703
|
+
if reasoning_content is not None and reasoning_content != "":
|
|
704
|
+
if prev_message_type and prev_message_type != "reasoning_message":
|
|
705
|
+
message_index += 1
|
|
706
|
+
reasoning_msg = ReasoningMessage(
|
|
707
|
+
id=self.letta_message_id,
|
|
708
|
+
date=datetime.now(timezone.utc).isoformat(),
|
|
709
|
+
otid=Message.generate_otid_from_id(self.letta_message_id, message_index),
|
|
710
|
+
source="reasoner_model",
|
|
711
|
+
reasoning=reasoning_content,
|
|
712
|
+
signature=None,
|
|
713
|
+
run_id=self.run_id,
|
|
714
|
+
step_id=self.step_id,
|
|
715
|
+
)
|
|
716
|
+
self.content_messages.append(reasoning_msg)
|
|
717
|
+
prev_message_type = reasoning_msg.message_type
|
|
718
|
+
yield reasoning_msg
|
|
719
|
+
|
|
680
720
|
if message_delta.tool_calls is not None and len(message_delta.tool_calls) > 0:
|
|
681
721
|
tool_call = message_delta.tool_calls[0]
|
|
682
722
|
|
|
@@ -710,7 +750,7 @@ class SimpleOpenAIStreamingInterface:
|
|
|
710
750
|
|
|
711
751
|
if self.requires_approval_tools:
|
|
712
752
|
tool_call_msg = ApprovalRequestMessage(
|
|
713
|
-
id=self.letta_message_id,
|
|
753
|
+
id=decrement_message_uuid(self.letta_message_id),
|
|
714
754
|
date=datetime.now(timezone.utc),
|
|
715
755
|
tool_call=ToolCallDelta(
|
|
716
756
|
name=tool_call.function.name,
|
|
@@ -718,11 +758,13 @@ class SimpleOpenAIStreamingInterface:
|
|
|
718
758
|
tool_call_id=tool_call.id,
|
|
719
759
|
),
|
|
720
760
|
# name=name,
|
|
721
|
-
otid=Message.generate_otid_from_id(self.letta_message_id,
|
|
761
|
+
otid=Message.generate_otid_from_id(decrement_message_uuid(self.letta_message_id), -1),
|
|
722
762
|
run_id=self.run_id,
|
|
723
763
|
step_id=self.step_id,
|
|
724
764
|
)
|
|
725
765
|
else:
|
|
766
|
+
if prev_message_type and prev_message_type != "tool_call_message":
|
|
767
|
+
message_index += 1
|
|
726
768
|
tool_call_delta = ToolCallDelta(
|
|
727
769
|
name=tool_call.function.name,
|
|
728
770
|
arguments=tool_call.function.arguments,
|
|
@@ -738,8 +780,7 @@ class SimpleOpenAIStreamingInterface:
|
|
|
738
780
|
run_id=self.run_id,
|
|
739
781
|
step_id=self.step_id,
|
|
740
782
|
)
|
|
741
|
-
|
|
742
|
-
message_index += 1 # Increment for the next message
|
|
783
|
+
prev_message_type = tool_call_msg.message_type
|
|
743
784
|
yield tool_call_msg
|
|
744
785
|
|
|
745
786
|
|
|
@@ -873,7 +914,7 @@ class SimpleOpenAIResponsesStreamingInterface:
|
|
|
873
914
|
except Exception as e:
|
|
874
915
|
import traceback
|
|
875
916
|
|
|
876
|
-
logger.
|
|
917
|
+
logger.exception("Error processing stream: %s", e)
|
|
877
918
|
if ttft_span:
|
|
878
919
|
ttft_span.add_event(
|
|
879
920
|
name="stop_reason",
|
|
@@ -935,11 +976,9 @@ class SimpleOpenAIResponsesStreamingInterface:
|
|
|
935
976
|
# cache for approval if/elses
|
|
936
977
|
self.tool_call_name = name
|
|
937
978
|
if self.tool_call_name and self.tool_call_name in self.requires_approval_tools:
|
|
938
|
-
if prev_message_type and prev_message_type != "approval_request_message":
|
|
939
|
-
message_index += 1
|
|
940
979
|
yield ApprovalRequestMessage(
|
|
941
|
-
id=self.letta_message_id,
|
|
942
|
-
otid=Message.generate_otid_from_id(self.letta_message_id,
|
|
980
|
+
id=decrement_message_uuid(self.letta_message_id),
|
|
981
|
+
otid=Message.generate_otid_from_id(decrement_message_uuid(self.letta_message_id), -1),
|
|
943
982
|
date=datetime.now(timezone.utc),
|
|
944
983
|
tool_call=ToolCallDelta(
|
|
945
984
|
name=name,
|
|
@@ -949,7 +988,6 @@ class SimpleOpenAIResponsesStreamingInterface:
|
|
|
949
988
|
run_id=self.run_id,
|
|
950
989
|
step_id=self.step_id,
|
|
951
990
|
)
|
|
952
|
-
prev_message_type = "tool_call_message"
|
|
953
991
|
else:
|
|
954
992
|
if prev_message_type and prev_message_type != "tool_call_message":
|
|
955
993
|
message_index += 1
|
|
@@ -1105,11 +1143,9 @@ class SimpleOpenAIResponsesStreamingInterface:
|
|
|
1105
1143
|
delta = event.delta
|
|
1106
1144
|
|
|
1107
1145
|
if self.tool_call_name and self.tool_call_name in self.requires_approval_tools:
|
|
1108
|
-
if prev_message_type and prev_message_type != "approval_request_message":
|
|
1109
|
-
message_index += 1
|
|
1110
1146
|
yield ApprovalRequestMessage(
|
|
1111
|
-
id=self.letta_message_id,
|
|
1112
|
-
otid=Message.generate_otid_from_id(self.letta_message_id,
|
|
1147
|
+
id=decrement_message_uuid(self.letta_message_id),
|
|
1148
|
+
otid=Message.generate_otid_from_id(decrement_message_uuid(self.letta_message_id), -1),
|
|
1113
1149
|
date=datetime.now(timezone.utc),
|
|
1114
1150
|
tool_call=ToolCallDelta(
|
|
1115
1151
|
name=None,
|
|
@@ -1119,7 +1155,6 @@ class SimpleOpenAIResponsesStreamingInterface:
|
|
|
1119
1155
|
run_id=self.run_id,
|
|
1120
1156
|
step_id=self.step_id,
|
|
1121
1157
|
)
|
|
1122
|
-
prev_message_type = "approval_request_message"
|
|
1123
1158
|
else:
|
|
1124
1159
|
if prev_message_type and prev_message_type != "tool_call_message":
|
|
1125
1160
|
message_index += 1
|
|
@@ -439,6 +439,7 @@ class AnthropicClient(LLMClientBase):
|
|
|
439
439
|
llm_config.model.startswith("claude-3-7-sonnet")
|
|
440
440
|
or llm_config.model.startswith("claude-sonnet-4")
|
|
441
441
|
or llm_config.model.startswith("claude-opus-4")
|
|
442
|
+
or llm_config.model.startswith("claude-haiku-4-5")
|
|
442
443
|
)
|
|
443
444
|
|
|
444
445
|
@trace_method
|
|
@@ -575,7 +576,7 @@ class AnthropicClient(LLMClientBase):
|
|
|
575
576
|
reasoning_content = None
|
|
576
577
|
reasoning_content_signature = None
|
|
577
578
|
redacted_reasoning_content = None
|
|
578
|
-
tool_calls =
|
|
579
|
+
tool_calls: list[ToolCall] = []
|
|
579
580
|
|
|
580
581
|
if len(response.content) > 0:
|
|
581
582
|
for content_part in response.content:
|
|
@@ -585,6 +586,8 @@ class AnthropicClient(LLMClientBase):
|
|
|
585
586
|
# hack for incorrect tool format
|
|
586
587
|
tool_input = json.loads(json.dumps(content_part.input))
|
|
587
588
|
if "id" in tool_input and tool_input["id"].startswith("toolu_") and "function" in tool_input:
|
|
589
|
+
if isinstance(tool_input["function"], str):
|
|
590
|
+
tool_input["function"] = json.loads(tool_input["function"])
|
|
588
591
|
arguments = json.dumps(tool_input["function"]["arguments"], indent=2)
|
|
589
592
|
try:
|
|
590
593
|
args_json = json.loads(arguments)
|
|
@@ -594,7 +597,7 @@ class AnthropicClient(LLMClientBase):
|
|
|
594
597
|
arguments = str(tool_input["function"]["arguments"])
|
|
595
598
|
else:
|
|
596
599
|
arguments = json.dumps(tool_input, indent=2)
|
|
597
|
-
tool_calls
|
|
600
|
+
tool_calls.append(
|
|
598
601
|
ToolCall(
|
|
599
602
|
id=content_part.id,
|
|
600
603
|
type="function",
|
|
@@ -603,7 +606,7 @@ class AnthropicClient(LLMClientBase):
|
|
|
603
606
|
arguments=arguments,
|
|
604
607
|
),
|
|
605
608
|
)
|
|
606
|
-
|
|
609
|
+
)
|
|
607
610
|
if content_part.type == "thinking":
|
|
608
611
|
reasoning_content = content_part.thinking
|
|
609
612
|
reasoning_content_signature = content_part.signature
|
|
@@ -623,7 +626,7 @@ class AnthropicClient(LLMClientBase):
|
|
|
623
626
|
reasoning_content=reasoning_content,
|
|
624
627
|
reasoning_content_signature=reasoning_content_signature,
|
|
625
628
|
redacted_reasoning_content=redacted_reasoning_content,
|
|
626
|
-
tool_calls=tool_calls,
|
|
629
|
+
tool_calls=tool_calls or None,
|
|
627
630
|
),
|
|
628
631
|
)
|
|
629
632
|
|
letta/llm_api/deepseek_client.py
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os
|
|
3
3
|
import re
|
|
4
|
-
import warnings
|
|
5
4
|
from typing import List, Optional
|
|
6
5
|
|
|
7
6
|
from openai import AsyncOpenAI, AsyncStream, OpenAI
|
|
@@ -9,10 +8,13 @@ from openai.types.chat.chat_completion import ChatCompletion
|
|
|
9
8
|
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
|
10
9
|
|
|
11
10
|
from letta.llm_api.openai_client import OpenAIClient
|
|
11
|
+
from letta.log import get_logger
|
|
12
12
|
from letta.otel.tracing import trace_method
|
|
13
13
|
from letta.schemas.enums import AgentType
|
|
14
14
|
from letta.schemas.llm_config import LLMConfig
|
|
15
15
|
from letta.schemas.message import Message as PydanticMessage
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
16
18
|
from letta.schemas.openai.chat_completion_request import (
|
|
17
19
|
AssistantMessage,
|
|
18
20
|
ChatCompletionRequest,
|
|
@@ -91,7 +93,7 @@ def map_messages_to_deepseek_format(messages: List[ChatMessage]) -> List[_Messag
|
|
|
91
93
|
merged_message = merge_tool_message(deepseek_messages[-1], message)
|
|
92
94
|
deepseek_messages[-1] = merged_message
|
|
93
95
|
else:
|
|
94
|
-
|
|
96
|
+
logger.warning(f"Skipping message: {message}")
|
|
95
97
|
|
|
96
98
|
# This needs to end on a user message, add a dummy message if the last was assistant
|
|
97
99
|
if deepseek_messages[-1].role == "assistant":
|
|
@@ -127,7 +129,7 @@ def build_deepseek_chat_completions_request(
|
|
|
127
129
|
if llm_config.model:
|
|
128
130
|
model = llm_config.model
|
|
129
131
|
else:
|
|
130
|
-
|
|
132
|
+
logger.warning(f"Model type not set in llm_config: {llm_config.model_dump_json(indent=4)}")
|
|
131
133
|
model = None
|
|
132
134
|
if use_tool_naming:
|
|
133
135
|
if function_call is None:
|
|
@@ -308,7 +310,7 @@ def convert_deepseek_response_to_chatcompletion(
|
|
|
308
310
|
)
|
|
309
311
|
]
|
|
310
312
|
except (json.JSONDecodeError, TypeError, KeyError) as e:
|
|
311
|
-
|
|
313
|
+
logger.error(f"Failed to parse DeepSeek response: {e}")
|
|
312
314
|
tool_calls = response.choices[0].message.tool_calls
|
|
313
315
|
raise ValueError(f"Failed to create valid JSON {content}")
|
|
314
316
|
|
|
@@ -92,10 +92,7 @@ async def google_ai_get_model_list_async(
|
|
|
92
92
|
except httpx.HTTPStatusError as http_err:
|
|
93
93
|
# Handle HTTP errors (e.g., response 4XX, 5XX)
|
|
94
94
|
printd(f"Got HTTPError, exception={http_err}")
|
|
95
|
-
|
|
96
|
-
print(f"HTTP Error: {http_err.response.status_code}")
|
|
97
|
-
# Print the response content (error message from server)
|
|
98
|
-
print(f"Message: {http_err.response.text}")
|
|
95
|
+
logger.error(f"HTTP Error: {http_err.response.status_code}, Message: {http_err.response.text}")
|
|
99
96
|
raise http_err
|
|
100
97
|
|
|
101
98
|
except httpx.RequestError as req_err:
|
|
@@ -136,10 +133,7 @@ def google_ai_get_model_details(base_url: str, api_key: str, model: str, key_in_
|
|
|
136
133
|
except httpx.HTTPStatusError as http_err:
|
|
137
134
|
# Handle HTTP errors (e.g., response 4XX, 5XX)
|
|
138
135
|
printd(f"Got HTTPError, exception={http_err}")
|
|
139
|
-
|
|
140
|
-
print(f"HTTP Error: {http_err.response.status_code}")
|
|
141
|
-
# Print the response content (error message from server)
|
|
142
|
-
print(f"Message: {http_err.response.text}")
|
|
136
|
+
logger.error(f"HTTP Error: {http_err.response.status_code}, Message: {http_err.response.text}")
|
|
143
137
|
raise http_err
|
|
144
138
|
|
|
145
139
|
except httpx.RequestError as req_err:
|
|
@@ -182,10 +176,7 @@ async def google_ai_get_model_details_async(
|
|
|
182
176
|
except httpx.HTTPStatusError as http_err:
|
|
183
177
|
# Handle HTTP errors (e.g., response 4XX, 5XX)
|
|
184
178
|
printd(f"Got HTTPError, exception={http_err}")
|
|
185
|
-
|
|
186
|
-
print(f"HTTP Error: {http_err.response.status_code}")
|
|
187
|
-
# Print the response content (error message from server)
|
|
188
|
-
print(f"Message: {http_err.response.text}")
|
|
179
|
+
logger.error(f"HTTP Error: {http_err.response.status_code}, Message: {http_err.response.text}")
|
|
189
180
|
raise http_err
|
|
190
181
|
|
|
191
182
|
except httpx.RequestError as req_err:
|
|
@@ -128,7 +128,7 @@ class GoogleVertexClient(LLMClientBase):
|
|
|
128
128
|
logger.warning(
|
|
129
129
|
f"Modified heartbeat message with special character warning for retry {retry_count}/{self.MAX_RETRIES}"
|
|
130
130
|
)
|
|
131
|
-
except (json.JSONDecodeError, TypeError):
|
|
131
|
+
except (json.JSONDecodeError, TypeError, AttributeError):
|
|
132
132
|
# Not a JSON message or not a heartbeat, skip modification
|
|
133
133
|
pass
|
|
134
134
|
|
letta/llm_api/helpers.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import copy
|
|
2
2
|
import json
|
|
3
|
-
import warnings
|
|
4
3
|
from collections import OrderedDict
|
|
5
4
|
from typing import Any, List, Union
|
|
6
5
|
|
|
@@ -8,24 +7,77 @@ import requests
|
|
|
8
7
|
|
|
9
8
|
from letta.constants import OPENAI_CONTEXT_WINDOW_ERROR_SUBSTRING
|
|
10
9
|
from letta.helpers.json_helpers import json_dumps
|
|
10
|
+
from letta.log import get_logger
|
|
11
11
|
from letta.schemas.message import Message
|
|
12
12
|
from letta.schemas.openai.chat_completion_response import ChatCompletionResponse, Choice
|
|
13
13
|
from letta.settings import summarizer_settings
|
|
14
14
|
from letta.utils import count_tokens, printd
|
|
15
15
|
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
16
18
|
|
|
17
19
|
def _convert_to_structured_output_helper(property: dict) -> dict:
|
|
18
20
|
"""Convert a single JSON schema property to structured output format (recursive)"""
|
|
19
21
|
|
|
22
|
+
# Handle anyOf structures
|
|
23
|
+
if "anyOf" in property and "type" not in property:
|
|
24
|
+
# Check if this is a simple anyOf that can be flattened to type array
|
|
25
|
+
types = []
|
|
26
|
+
has_complex = False
|
|
27
|
+
for option in property["anyOf"]:
|
|
28
|
+
if "type" in option:
|
|
29
|
+
opt_type = option["type"]
|
|
30
|
+
if opt_type in ["object", "array"]:
|
|
31
|
+
has_complex = True
|
|
32
|
+
break
|
|
33
|
+
types.append(opt_type)
|
|
34
|
+
elif "$ref" in option:
|
|
35
|
+
# Has unresolved $ref, treat as complex
|
|
36
|
+
has_complex = True
|
|
37
|
+
break
|
|
38
|
+
|
|
39
|
+
# If it's simple primitives only (string, null, integer, boolean, etc), flatten to type array
|
|
40
|
+
if not has_complex and types:
|
|
41
|
+
param_description = property.get("description")
|
|
42
|
+
property_dict = {"type": types}
|
|
43
|
+
if param_description is not None:
|
|
44
|
+
property_dict["description"] = param_description
|
|
45
|
+
if "default" in property:
|
|
46
|
+
property_dict["default"] = property["default"]
|
|
47
|
+
# Preserve other fields like enum, format, etc
|
|
48
|
+
for key in ["enum", "format", "pattern", "minLength", "maxLength", "minimum", "maximum"]:
|
|
49
|
+
if key in property:
|
|
50
|
+
property_dict[key] = property[key]
|
|
51
|
+
return property_dict
|
|
52
|
+
|
|
53
|
+
# Otherwise, preserve anyOf and recursively process each option
|
|
54
|
+
property_dict = {"anyOf": [_convert_to_structured_output_helper(opt) for opt in property["anyOf"]]}
|
|
55
|
+
if "description" in property:
|
|
56
|
+
property_dict["description"] = property["description"]
|
|
57
|
+
if "default" in property:
|
|
58
|
+
property_dict["default"] = property["default"]
|
|
59
|
+
if "title" in property:
|
|
60
|
+
property_dict["title"] = property["title"]
|
|
61
|
+
return property_dict
|
|
62
|
+
|
|
20
63
|
if "type" not in property:
|
|
21
|
-
raise ValueError(f"Property {property} is missing a type")
|
|
64
|
+
raise ValueError(f"Property {property} is missing a type and doesn't have anyOf")
|
|
65
|
+
|
|
22
66
|
param_type = property["type"]
|
|
67
|
+
param_description = property.get("description")
|
|
23
68
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
69
|
+
# Handle type arrays (e.g., ["string", "null"])
|
|
70
|
+
if isinstance(param_type, list):
|
|
71
|
+
property_dict = {"type": param_type}
|
|
72
|
+
if param_description is not None:
|
|
73
|
+
property_dict["description"] = param_description
|
|
74
|
+
if "default" in property:
|
|
75
|
+
property_dict["default"] = property["default"]
|
|
76
|
+
# Preserve other fields
|
|
77
|
+
for key in ["enum", "format", "pattern", "minLength", "maxLength", "minimum", "maximum", "title"]:
|
|
78
|
+
if key in property:
|
|
79
|
+
property_dict[key] = property[key]
|
|
80
|
+
return property_dict
|
|
29
81
|
|
|
30
82
|
if param_type == "object":
|
|
31
83
|
if "properties" not in property:
|
|
@@ -39,6 +91,8 @@ def _convert_to_structured_output_helper(property: dict) -> dict:
|
|
|
39
91
|
}
|
|
40
92
|
if param_description is not None:
|
|
41
93
|
property_dict["description"] = param_description
|
|
94
|
+
if "title" in property:
|
|
95
|
+
property_dict["title"] = property["title"]
|
|
42
96
|
return property_dict
|
|
43
97
|
|
|
44
98
|
elif param_type == "array":
|
|
@@ -51,6 +105,8 @@ def _convert_to_structured_output_helper(property: dict) -> dict:
|
|
|
51
105
|
}
|
|
52
106
|
if param_description is not None:
|
|
53
107
|
property_dict["description"] = param_description
|
|
108
|
+
if "title" in property:
|
|
109
|
+
property_dict["title"] = property["title"]
|
|
54
110
|
return property_dict
|
|
55
111
|
|
|
56
112
|
else:
|
|
@@ -59,6 +115,10 @@ def _convert_to_structured_output_helper(property: dict) -> dict:
|
|
|
59
115
|
}
|
|
60
116
|
if param_description is not None:
|
|
61
117
|
property_dict["description"] = param_description
|
|
118
|
+
# Preserve other fields
|
|
119
|
+
for key in ["enum", "format", "pattern", "minLength", "maxLength", "minimum", "maximum", "default", "title"]:
|
|
120
|
+
if key in property:
|
|
121
|
+
property_dict[key] = property[key]
|
|
62
122
|
return property_dict
|
|
63
123
|
|
|
64
124
|
|
|
@@ -66,6 +126,14 @@ def convert_to_structured_output(openai_function: dict, allow_optional: bool = F
|
|
|
66
126
|
"""Convert function call objects to structured output objects.
|
|
67
127
|
|
|
68
128
|
See: https://platform.openai.com/docs/guides/structured-outputs/supported-schemas
|
|
129
|
+
|
|
130
|
+
Supports:
|
|
131
|
+
- Simple type arrays: type: ["string", "null"]
|
|
132
|
+
- anyOf with primitives (flattened to type array)
|
|
133
|
+
- anyOf with complex objects (preserved as anyOf)
|
|
134
|
+
- Nested structures with recursion
|
|
135
|
+
|
|
136
|
+
For OpenAI strict mode, optional fields (not in required) must have explicit default values.
|
|
69
137
|
"""
|
|
70
138
|
description = openai_function.get("description", "")
|
|
71
139
|
|
|
@@ -82,57 +150,19 @@ def convert_to_structured_output(openai_function: dict, allow_optional: bool = F
|
|
|
82
150
|
}
|
|
83
151
|
|
|
84
152
|
for param, details in openai_function["parameters"]["properties"].items():
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if param_type == "object":
|
|
89
|
-
if "properties" not in details:
|
|
90
|
-
raise ValueError(f"Property {param} of type object is missing 'properties'")
|
|
91
|
-
structured_output["parameters"]["properties"][param] = {
|
|
92
|
-
"type": "object",
|
|
93
|
-
"description": param_description,
|
|
94
|
-
"properties": {k: _convert_to_structured_output_helper(v) for k, v in details["properties"].items()},
|
|
95
|
-
"additionalProperties": False,
|
|
96
|
-
"required": list(details["properties"].keys()),
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
elif param_type == "array":
|
|
100
|
-
items_schema = details.get("items")
|
|
101
|
-
prefix_items_schema = details.get("prefixItems")
|
|
102
|
-
|
|
103
|
-
if prefix_items_schema:
|
|
104
|
-
# assume fixed-length tuple — safe fallback to use first type for items
|
|
105
|
-
fallback_item = prefix_items_schema[0] if isinstance(prefix_items_schema, list) else prefix_items_schema
|
|
106
|
-
structured_output["parameters"]["properties"][param] = {
|
|
107
|
-
"type": "array",
|
|
108
|
-
"description": param_description,
|
|
109
|
-
"prefixItems": [_convert_to_structured_output_helper(item) for item in prefix_items_schema],
|
|
110
|
-
"items": _convert_to_structured_output_helper(fallback_item),
|
|
111
|
-
"minItems": details.get("minItems", len(prefix_items_schema)),
|
|
112
|
-
"maxItems": details.get("maxItems", len(prefix_items_schema)),
|
|
113
|
-
}
|
|
114
|
-
elif items_schema:
|
|
115
|
-
structured_output["parameters"]["properties"][param] = {
|
|
116
|
-
"type": "array",
|
|
117
|
-
"description": param_description,
|
|
118
|
-
"items": _convert_to_structured_output_helper(items_schema),
|
|
119
|
-
}
|
|
120
|
-
else:
|
|
121
|
-
raise ValueError(f"Array param '{param}' is missing both 'items' and 'prefixItems'")
|
|
122
|
-
|
|
123
|
-
else:
|
|
124
|
-
prop = {
|
|
125
|
-
"type": param_type,
|
|
126
|
-
"description": param_description,
|
|
127
|
-
}
|
|
128
|
-
if "enum" in details:
|
|
129
|
-
prop["enum"] = details["enum"]
|
|
130
|
-
structured_output["parameters"]["properties"][param] = prop
|
|
153
|
+
# Use the helper for all parameter types - it now handles anyOf, type arrays, objects, arrays, etc.
|
|
154
|
+
structured_output["parameters"]["properties"][param] = _convert_to_structured_output_helper(details)
|
|
131
155
|
|
|
156
|
+
# Determine which fields are required
|
|
157
|
+
# For OpenAI strict mode, ALL fields must be in the required array
|
|
158
|
+
# This is a requirement for strict: true schemas
|
|
132
159
|
if not allow_optional:
|
|
160
|
+
# All fields are required for strict mode
|
|
133
161
|
structured_output["parameters"]["required"] = list(structured_output["parameters"]["properties"].keys())
|
|
134
162
|
else:
|
|
135
|
-
|
|
163
|
+
# Use the input's required list if provided, otherwise empty
|
|
164
|
+
structured_output["parameters"]["required"] = openai_function["parameters"].get("required", [])
|
|
165
|
+
|
|
136
166
|
return structured_output
|
|
137
167
|
|
|
138
168
|
|
|
@@ -269,7 +299,7 @@ def unpack_inner_thoughts_from_kwargs(choice: Choice, inner_thoughts_key: str) -
|
|
|
269
299
|
|
|
270
300
|
if message.role == "assistant" and message.tool_calls and len(message.tool_calls) >= 1:
|
|
271
301
|
if len(message.tool_calls) > 1:
|
|
272
|
-
|
|
302
|
+
logger.warning(f"Unpacking inner thoughts from more than one tool call ({len(message.tool_calls)}) is not supported")
|
|
273
303
|
# TODO support multiple tool calls
|
|
274
304
|
tool_call = message.tool_calls[0]
|
|
275
305
|
|
|
@@ -285,21 +315,20 @@ def unpack_inner_thoughts_from_kwargs(choice: Choice, inner_thoughts_key: str) -
|
|
|
285
315
|
new_choice.message.tool_calls[0].function.arguments = json_dumps(func_args)
|
|
286
316
|
# also replace the message content
|
|
287
317
|
if new_choice.message.content is not None:
|
|
288
|
-
|
|
318
|
+
logger.warning(f"Overwriting existing inner monologue ({new_choice.message.content}) with kwarg ({inner_thoughts})")
|
|
289
319
|
new_choice.message.content = inner_thoughts
|
|
290
320
|
|
|
291
321
|
# update the choice object
|
|
292
322
|
rewritten_choice = new_choice
|
|
293
323
|
else:
|
|
294
|
-
|
|
324
|
+
logger.warning(f"Did not find inner thoughts in tool call: {str(tool_call)}")
|
|
295
325
|
|
|
296
326
|
except json.JSONDecodeError as e:
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
print(f"\nTool call arguments: {tool_call.function.arguments}")
|
|
327
|
+
logger.warning(f"Failed to strip inner thoughts from kwargs: {e}")
|
|
328
|
+
logger.error(f"Failed to strip inner thoughts from kwargs: {e}, Tool call arguments: {tool_call.function.arguments}")
|
|
300
329
|
raise e
|
|
301
330
|
else:
|
|
302
|
-
|
|
331
|
+
logger.warning(f"Did not find tool call in message: {str(message)}")
|
|
303
332
|
|
|
304
333
|
return rewritten_choice
|
|
305
334
|
|