letta-nightly 0.12.1.dev20251024104217__py3-none-any.whl → 0.13.0.dev20251025104015__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.

Files changed (159) hide show
  1. letta/__init__.py +2 -3
  2. letta/adapters/letta_llm_adapter.py +1 -0
  3. letta/adapters/simple_llm_request_adapter.py +8 -5
  4. letta/adapters/simple_llm_stream_adapter.py +22 -6
  5. letta/agents/agent_loop.py +10 -3
  6. letta/agents/base_agent.py +4 -1
  7. letta/agents/helpers.py +41 -9
  8. letta/agents/letta_agent.py +11 -10
  9. letta/agents/letta_agent_v2.py +47 -37
  10. letta/agents/letta_agent_v3.py +395 -300
  11. letta/agents/voice_agent.py +8 -6
  12. letta/agents/voice_sleeptime_agent.py +3 -3
  13. letta/constants.py +30 -7
  14. letta/errors.py +20 -0
  15. letta/functions/function_sets/base.py +55 -3
  16. letta/functions/mcp_client/types.py +33 -57
  17. letta/functions/schema_generator.py +135 -23
  18. letta/groups/sleeptime_multi_agent_v3.py +6 -11
  19. letta/groups/sleeptime_multi_agent_v4.py +227 -0
  20. letta/helpers/converters.py +78 -4
  21. letta/helpers/crypto_utils.py +6 -2
  22. letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
  23. letta/interfaces/anthropic_streaming_interface.py +3 -4
  24. letta/interfaces/gemini_streaming_interface.py +4 -6
  25. letta/interfaces/openai_streaming_interface.py +63 -28
  26. letta/llm_api/anthropic_client.py +7 -4
  27. letta/llm_api/deepseek_client.py +6 -4
  28. letta/llm_api/google_ai_client.py +3 -12
  29. letta/llm_api/google_vertex_client.py +1 -1
  30. letta/llm_api/helpers.py +90 -61
  31. letta/llm_api/llm_api_tools.py +4 -1
  32. letta/llm_api/openai.py +12 -12
  33. letta/llm_api/openai_client.py +53 -16
  34. letta/local_llm/constants.py +4 -3
  35. letta/local_llm/json_parser.py +5 -2
  36. letta/local_llm/utils.py +2 -3
  37. letta/log.py +171 -7
  38. letta/orm/agent.py +43 -9
  39. letta/orm/archive.py +4 -0
  40. letta/orm/custom_columns.py +15 -0
  41. letta/orm/identity.py +11 -11
  42. letta/orm/mcp_server.py +9 -0
  43. letta/orm/message.py +6 -1
  44. letta/orm/run_metrics.py +7 -2
  45. letta/orm/sqlalchemy_base.py +2 -2
  46. letta/orm/tool.py +3 -0
  47. letta/otel/tracing.py +2 -0
  48. letta/prompts/prompt_generator.py +7 -2
  49. letta/schemas/agent.py +41 -10
  50. letta/schemas/agent_file.py +3 -0
  51. letta/schemas/archive.py +4 -2
  52. letta/schemas/block.py +2 -1
  53. letta/schemas/enums.py +36 -3
  54. letta/schemas/file.py +3 -3
  55. letta/schemas/folder.py +2 -1
  56. letta/schemas/group.py +2 -1
  57. letta/schemas/identity.py +18 -9
  58. letta/schemas/job.py +3 -1
  59. letta/schemas/letta_message.py +71 -12
  60. letta/schemas/letta_request.py +7 -3
  61. letta/schemas/letta_stop_reason.py +0 -25
  62. letta/schemas/llm_config.py +8 -2
  63. letta/schemas/mcp.py +80 -83
  64. letta/schemas/mcp_server.py +349 -0
  65. letta/schemas/memory.py +20 -8
  66. letta/schemas/message.py +212 -67
  67. letta/schemas/providers/anthropic.py +13 -6
  68. letta/schemas/providers/azure.py +6 -4
  69. letta/schemas/providers/base.py +8 -4
  70. letta/schemas/providers/bedrock.py +6 -2
  71. letta/schemas/providers/cerebras.py +7 -3
  72. letta/schemas/providers/deepseek.py +2 -1
  73. letta/schemas/providers/google_gemini.py +15 -6
  74. letta/schemas/providers/groq.py +2 -1
  75. letta/schemas/providers/lmstudio.py +9 -6
  76. letta/schemas/providers/mistral.py +2 -1
  77. letta/schemas/providers/openai.py +7 -2
  78. letta/schemas/providers/together.py +9 -3
  79. letta/schemas/providers/xai.py +7 -3
  80. letta/schemas/run.py +7 -2
  81. letta/schemas/run_metrics.py +2 -1
  82. letta/schemas/sandbox_config.py +2 -2
  83. letta/schemas/secret.py +3 -158
  84. letta/schemas/source.py +2 -2
  85. letta/schemas/step.py +2 -2
  86. letta/schemas/tool.py +24 -1
  87. letta/schemas/usage.py +0 -1
  88. letta/server/rest_api/app.py +123 -7
  89. letta/server/rest_api/dependencies.py +3 -0
  90. letta/server/rest_api/interface.py +7 -4
  91. letta/server/rest_api/redis_stream_manager.py +16 -1
  92. letta/server/rest_api/routers/v1/__init__.py +7 -0
  93. letta/server/rest_api/routers/v1/agents.py +332 -322
  94. letta/server/rest_api/routers/v1/archives.py +127 -40
  95. letta/server/rest_api/routers/v1/blocks.py +54 -6
  96. letta/server/rest_api/routers/v1/chat_completions.py +146 -0
  97. letta/server/rest_api/routers/v1/folders.py +27 -35
  98. letta/server/rest_api/routers/v1/groups.py +23 -35
  99. letta/server/rest_api/routers/v1/identities.py +24 -10
  100. letta/server/rest_api/routers/v1/internal_runs.py +107 -0
  101. letta/server/rest_api/routers/v1/internal_templates.py +162 -179
  102. letta/server/rest_api/routers/v1/jobs.py +15 -27
  103. letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
  104. letta/server/rest_api/routers/v1/messages.py +23 -34
  105. letta/server/rest_api/routers/v1/organizations.py +6 -27
  106. letta/server/rest_api/routers/v1/providers.py +35 -62
  107. letta/server/rest_api/routers/v1/runs.py +30 -43
  108. letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
  109. letta/server/rest_api/routers/v1/sources.py +26 -42
  110. letta/server/rest_api/routers/v1/steps.py +16 -29
  111. letta/server/rest_api/routers/v1/tools.py +17 -13
  112. letta/server/rest_api/routers/v1/users.py +5 -17
  113. letta/server/rest_api/routers/v1/voice.py +18 -27
  114. letta/server/rest_api/streaming_response.py +5 -2
  115. letta/server/rest_api/utils.py +187 -25
  116. letta/server/server.py +27 -22
  117. letta/server/ws_api/server.py +5 -4
  118. letta/services/agent_manager.py +148 -26
  119. letta/services/agent_serialization_manager.py +6 -1
  120. letta/services/archive_manager.py +168 -15
  121. letta/services/block_manager.py +14 -4
  122. letta/services/file_manager.py +33 -29
  123. letta/services/group_manager.py +10 -0
  124. letta/services/helpers/agent_manager_helper.py +65 -11
  125. letta/services/identity_manager.py +105 -4
  126. letta/services/job_manager.py +11 -1
  127. letta/services/mcp/base_client.py +2 -2
  128. letta/services/mcp/oauth_utils.py +33 -8
  129. letta/services/mcp_manager.py +174 -78
  130. letta/services/mcp_server_manager.py +1331 -0
  131. letta/services/message_manager.py +109 -4
  132. letta/services/organization_manager.py +4 -4
  133. letta/services/passage_manager.py +9 -25
  134. letta/services/provider_manager.py +91 -15
  135. letta/services/run_manager.py +72 -15
  136. letta/services/sandbox_config_manager.py +45 -3
  137. letta/services/source_manager.py +15 -8
  138. letta/services/step_manager.py +24 -1
  139. letta/services/streaming_service.py +581 -0
  140. letta/services/summarizer/summarizer.py +1 -1
  141. letta/services/tool_executor/core_tool_executor.py +111 -0
  142. letta/services/tool_executor/files_tool_executor.py +5 -3
  143. letta/services/tool_executor/sandbox_tool_executor.py +2 -2
  144. letta/services/tool_executor/tool_execution_manager.py +1 -1
  145. letta/services/tool_manager.py +10 -3
  146. letta/services/tool_sandbox/base.py +61 -1
  147. letta/services/tool_sandbox/local_sandbox.py +1 -3
  148. letta/services/user_manager.py +2 -2
  149. letta/settings.py +49 -5
  150. letta/system.py +14 -5
  151. letta/utils.py +73 -1
  152. letta/validators.py +105 -0
  153. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/METADATA +4 -2
  154. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/RECORD +157 -151
  155. letta/schemas/letta_ping.py +0 -28
  156. letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
  157. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/WHEEL +0 -0
  158. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.dist-info}/entry_points.txt +0 -0
  159. {letta_nightly-0.12.1.dev20251024104217.dist-info → letta_nightly-0.13.0.dev20251025104015.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.error("Error processing stream: %s\n%s", e, traceback.format_exc())
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, message_index),
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, message_index),
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, message_index),
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
- merged_messages.append(TextContent(text=concat_content))
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.error("Error processing stream: %s\n%s", e, traceback.format_exc())
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=[TextContent(text=message_delta.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, message_index),
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
- prev_message_type = tool_call_msg.message_type
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.error("Error processing stream: %s\n%s", e, traceback.format_exc())
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, message_index),
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, message_index),
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 = None
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
 
@@ -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
- print(f"Skipping message: {message}")
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
- warnings.warn(f"Model type not set in llm_config: {llm_config.model_dump_json(indent=4)}")
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
- print(e)
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
- # Print the HTTP status code
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
- # Print the HTTP status code
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
- # Print the HTTP status code
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
- if "description" not in property:
25
- # raise ValueError(f"Property {property} is missing a description")
26
- param_description = None
27
- else:
28
- param_description = property["description"]
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
- param_type = details["type"]
86
- param_description = details.get("description", "")
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
- raise NotImplementedError("Optional parameter handling is not implemented.")
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
- warnings.warn(f"Unpacking inner thoughts from more than one tool call ({len(message.tool_calls)}) is not supported")
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
- warnings.warn(f"Overwriting existing inner monologue ({new_choice.message.content}) with kwarg ({inner_thoughts})")
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
- warnings.warn(f"Did not find inner thoughts in tool call: {str(tool_call)}")
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
- warnings.warn(f"Failed to strip inner thoughts from kwargs: {e}")
298
- print(f"\nFailed to strip inner thoughts from kwargs: {e}")
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
- warnings.warn(f"Did not find tool call in message: {str(message)}")
331
+ logger.warning(f"Did not find tool call in message: {str(message)}")
303
332
 
304
333
  return rewritten_choice
305
334