letta-nightly 0.6.27.dev20250220104103__py3-none-any.whl → 0.6.29.dev20250221033538__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 (66) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +19 -2
  3. letta/client/client.py +2 -0
  4. letta/constants.py +2 -0
  5. letta/functions/schema_generator.py +6 -6
  6. letta/helpers/converters.py +153 -0
  7. letta/helpers/tool_rule_solver.py +11 -1
  8. letta/llm_api/anthropic.py +10 -5
  9. letta/llm_api/aws_bedrock.py +1 -1
  10. letta/llm_api/deepseek.py +303 -0
  11. letta/llm_api/helpers.py +20 -10
  12. letta/llm_api/llm_api_tools.py +85 -2
  13. letta/llm_api/openai.py +16 -1
  14. letta/local_llm/chat_completion_proxy.py +15 -2
  15. letta/local_llm/lmstudio/api.py +75 -1
  16. letta/orm/__init__.py +2 -0
  17. letta/orm/agent.py +11 -4
  18. letta/orm/custom_columns.py +31 -110
  19. letta/orm/identities_agents.py +13 -0
  20. letta/orm/identity.py +60 -0
  21. letta/orm/organization.py +2 -0
  22. letta/orm/sqlalchemy_base.py +4 -0
  23. letta/schemas/agent.py +11 -1
  24. letta/schemas/identity.py +67 -0
  25. letta/schemas/llm_config.py +2 -0
  26. letta/schemas/message.py +1 -1
  27. letta/schemas/openai/chat_completion_response.py +2 -0
  28. letta/schemas/providers.py +72 -1
  29. letta/schemas/tool_rule.py +9 -1
  30. letta/serialize_schemas/__init__.py +1 -0
  31. letta/serialize_schemas/agent.py +36 -0
  32. letta/serialize_schemas/base.py +12 -0
  33. letta/serialize_schemas/custom_fields.py +69 -0
  34. letta/serialize_schemas/message.py +15 -0
  35. letta/server/db.py +111 -0
  36. letta/server/rest_api/app.py +8 -0
  37. letta/server/rest_api/chat_completions_interface.py +45 -21
  38. letta/server/rest_api/interface.py +114 -9
  39. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +98 -24
  40. letta/server/rest_api/routers/v1/__init__.py +2 -0
  41. letta/server/rest_api/routers/v1/agents.py +14 -3
  42. letta/server/rest_api/routers/v1/identities.py +121 -0
  43. letta/server/rest_api/utils.py +183 -4
  44. letta/server/server.py +23 -117
  45. letta/services/agent_manager.py +53 -6
  46. letta/services/block_manager.py +1 -1
  47. letta/services/identity_manager.py +156 -0
  48. letta/services/job_manager.py +1 -1
  49. letta/services/message_manager.py +1 -1
  50. letta/services/organization_manager.py +1 -1
  51. letta/services/passage_manager.py +1 -1
  52. letta/services/provider_manager.py +1 -1
  53. letta/services/sandbox_config_manager.py +1 -1
  54. letta/services/source_manager.py +1 -1
  55. letta/services/step_manager.py +1 -1
  56. letta/services/tool_manager.py +1 -1
  57. letta/services/user_manager.py +1 -1
  58. letta/settings.py +3 -0
  59. letta/streaming_interface.py +6 -2
  60. letta/tracing.py +205 -0
  61. letta/utils.py +4 -0
  62. {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/METADATA +9 -2
  63. {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/RECORD +66 -52
  64. {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/LICENSE +0 -0
  65. {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/WHEEL +0 -0
  66. {letta_nightly-0.6.27.dev20250220104103.dist-info → letta_nightly-0.6.29.dev20250221033538.dist-info}/entry_points.txt +0 -0
letta/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.6.27"
1
+ __version__ = "0.6.29"
2
2
 
3
3
  # import clients
4
4
  from letta.client.client import LocalClient, RESTClient, create_client
letta/agent.py CHANGED
@@ -60,6 +60,7 @@ from letta.services.tool_manager import ToolManager
60
60
  from letta.settings import summarizer_settings
61
61
  from letta.streaming_interface import StreamingRefreshCLIInterface
62
62
  from letta.system import get_heartbeat, get_token_limit_warning, package_function_response, package_summarize_message, package_user_message
63
+ from letta.tracing import trace_method
63
64
  from letta.utils import (
64
65
  count_tokens,
65
66
  get_friendly_error_msg,
@@ -309,6 +310,7 @@ class Agent(BaseAgent):
309
310
  # Return updated messages
310
311
  return messages
311
312
 
313
+ @trace_method("Get AI Reply")
312
314
  def _get_ai_reply(
313
315
  self,
314
316
  message_sequence: List[Message],
@@ -320,6 +322,7 @@ class Agent(BaseAgent):
320
322
  max_delay: float = 10.0, # max delay between retries
321
323
  step_count: Optional[int] = None,
322
324
  last_function_failed: bool = False,
325
+ put_inner_thoughts_first: bool = True,
323
326
  ) -> ChatCompletionResponse:
324
327
  """Get response from LLM API with robust retry mechanism."""
325
328
  log_telemetry(self.logger, "_get_ai_reply start")
@@ -365,6 +368,7 @@ class Agent(BaseAgent):
365
368
  force_tool_call=force_tool_call,
366
369
  stream=stream,
367
370
  stream_interface=self.interface,
371
+ put_inner_thoughts_first=put_inner_thoughts_first,
368
372
  )
369
373
  log_telemetry(self.logger, "_get_ai_reply create finish")
370
374
 
@@ -399,6 +403,7 @@ class Agent(BaseAgent):
399
403
  log_telemetry(self.logger, "_handle_ai_response finish catch-all exception")
400
404
  raise Exception("Retries exhausted and no valid response received.")
401
405
 
406
+ @trace_method("Handle AI Response")
402
407
  def _handle_ai_response(
403
408
  self,
404
409
  response_message: ChatCompletionMessage, # TODO should we eventually move the Message creation outside of this function?
@@ -492,7 +497,10 @@ class Agent(BaseAgent):
492
497
  try:
493
498
  raw_function_args = function_call.arguments
494
499
  function_args = parse_json(raw_function_args)
495
- except Exception:
500
+ if not isinstance(function_args, dict):
501
+ raise ValueError(f"Function arguments are not a dictionary: {function_args} (raw={raw_function_args})")
502
+ except Exception as e:
503
+ print(e)
496
504
  error_msg = f"Error parsing JSON for function '{function_name}' arguments: {function_call.arguments}"
497
505
  function_response = "None" # more like "never ran?"
498
506
  messages = self._handle_function_error_response(
@@ -627,15 +635,22 @@ class Agent(BaseAgent):
627
635
  elif self.tool_rules_solver.is_terminal_tool(function_name):
628
636
  heartbeat_request = False
629
637
 
638
+ # if continue tool rule, then must request a heartbeat
639
+ # TODO: dont even include heartbeats in the args
640
+ if self.tool_rules_solver.is_continue_tool(function_name):
641
+ heartbeat_request = True
642
+
630
643
  log_telemetry(self.logger, "_handle_ai_response finish")
631
644
  return messages, heartbeat_request, function_failed
632
645
 
646
+ @trace_method("Agent Step")
633
647
  def step(
634
648
  self,
635
649
  messages: Union[Message, List[Message]],
636
650
  # additional args
637
651
  chaining: bool = True,
638
652
  max_chaining_steps: Optional[int] = None,
653
+ put_inner_thoughts_first: bool = True,
639
654
  **kwargs,
640
655
  ) -> LettaUsageStatistics:
641
656
  """Run Agent.step in a loop, handling chaining via heartbeat requests and function failures"""
@@ -650,6 +665,7 @@ class Agent(BaseAgent):
650
665
  kwargs["last_function_failed"] = function_failed
651
666
  step_response = self.inner_step(
652
667
  messages=next_input_message,
668
+ put_inner_thoughts_first=put_inner_thoughts_first,
653
669
  **kwargs,
654
670
  )
655
671
 
@@ -731,9 +747,9 @@ class Agent(BaseAgent):
731
747
  metadata: Optional[dict] = None,
732
748
  summarize_attempt_count: int = 0,
733
749
  last_function_failed: bool = False,
750
+ put_inner_thoughts_first: bool = True,
734
751
  ) -> AgentStepResponse:
735
752
  """Runs a single step in the agent loop (generates at most one LLM call)"""
736
-
737
753
  try:
738
754
 
739
755
  # Extract job_id from metadata if present
@@ -766,6 +782,7 @@ class Agent(BaseAgent):
766
782
  stream=stream,
767
783
  step_count=step_count,
768
784
  last_function_failed=last_function_failed,
785
+ put_inner_thoughts_first=put_inner_thoughts_first,
769
786
  )
770
787
  if not response:
771
788
  # EDGE CASE: Function call failed AND there's no tools left for agent to call -> return early
letta/client/client.py CHANGED
@@ -2351,6 +2351,7 @@ class LocalClient(AbstractClient):
2351
2351
  tool_rules: Optional[List[BaseToolRule]] = None,
2352
2352
  include_base_tools: Optional[bool] = True,
2353
2353
  include_multi_agent_tools: bool = False,
2354
+ include_base_tool_rules: bool = True,
2354
2355
  # metadata
2355
2356
  metadata: Optional[Dict] = {"human:": DEFAULT_HUMAN, "persona": DEFAULT_PERSONA},
2356
2357
  description: Optional[str] = None,
@@ -2402,6 +2403,7 @@ class LocalClient(AbstractClient):
2402
2403
  "tool_rules": tool_rules,
2403
2404
  "include_base_tools": include_base_tools,
2404
2405
  "include_multi_agent_tools": include_multi_agent_tools,
2406
+ "include_base_tool_rules": include_base_tool_rules,
2405
2407
  "system": system,
2406
2408
  "agent_type": agent_type,
2407
2409
  "llm_config": llm_config if llm_config else self._default_llm_config,
letta/constants.py CHANGED
@@ -86,6 +86,8 @@ NON_USER_MSG_PREFIX = "[This is an automated system message hidden from the user
86
86
  # The max amount of tokens supported by the underlying model (eg 8k for gpt-4 and Mistral 7B)
87
87
  LLM_MAX_TOKENS = {
88
88
  "DEFAULT": 8192,
89
+ "deepseek-chat": 64000,
90
+ "deepseek-reasoner": 64000,
89
91
  ## OpenAI models: https://platform.openai.com/docs/models/overview
90
92
  # "o1-preview
91
93
  "chatgpt-4o-latest": 128000,
@@ -394,12 +394,12 @@ def generate_schema(function, name: Optional[str] = None, description: Optional[
394
394
  # append the heartbeat
395
395
  # TODO: don't hard-code
396
396
  # TODO: if terminal, don't include this
397
- if function.__name__ not in ["send_message"]:
398
- schema["parameters"]["properties"]["request_heartbeat"] = {
399
- "type": "boolean",
400
- "description": "Request an immediate heartbeat after function execution. Set to `True` if you want to send a follow-up message or run a follow-up function.",
401
- }
402
- schema["parameters"]["required"].append("request_heartbeat")
397
+ # if function.__name__ not in ["send_message"]:
398
+ schema["parameters"]["properties"]["request_heartbeat"] = {
399
+ "type": "boolean",
400
+ "description": "Request an immediate heartbeat after function execution. Set to `True` if you want to send a follow-up message or run a follow-up function.",
401
+ }
402
+ schema["parameters"]["required"].append("request_heartbeat")
403
403
 
404
404
  return schema
405
405
 
@@ -0,0 +1,153 @@
1
+ import base64
2
+ from typing import Any, Dict, List, Optional, Union
3
+
4
+ import numpy as np
5
+ from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall
6
+ from openai.types.chat.chat_completion_message_tool_call import Function as OpenAIFunction
7
+ from sqlalchemy import Dialect
8
+
9
+ from letta.schemas.embedding_config import EmbeddingConfig
10
+ from letta.schemas.enums import ToolRuleType
11
+ from letta.schemas.llm_config import LLMConfig
12
+ from letta.schemas.tool_rule import ChildToolRule, ConditionalToolRule, ContinueToolRule, InitToolRule, TerminalToolRule, ToolRule
13
+
14
+ # --------------------------
15
+ # LLMConfig Serialization
16
+ # --------------------------
17
+
18
+
19
+ def serialize_llm_config(config: Union[Optional[LLMConfig], Dict]) -> Optional[Dict]:
20
+ """Convert an LLMConfig object into a JSON-serializable dictionary."""
21
+ if config and isinstance(config, LLMConfig):
22
+ return config.model_dump()
23
+ return config
24
+
25
+
26
+ def deserialize_llm_config(data: Optional[Dict]) -> Optional[LLMConfig]:
27
+ """Convert a dictionary back into an LLMConfig object."""
28
+ return LLMConfig(**data) if data else None
29
+
30
+
31
+ # --------------------------
32
+ # EmbeddingConfig Serialization
33
+ # --------------------------
34
+
35
+
36
+ def serialize_embedding_config(config: Union[Optional[EmbeddingConfig], Dict]) -> Optional[Dict]:
37
+ """Convert an EmbeddingConfig object into a JSON-serializable dictionary."""
38
+ if config and isinstance(config, EmbeddingConfig):
39
+ return config.model_dump()
40
+ return config
41
+
42
+
43
+ def deserialize_embedding_config(data: Optional[Dict]) -> Optional[EmbeddingConfig]:
44
+ """Convert a dictionary back into an EmbeddingConfig object."""
45
+ return EmbeddingConfig(**data) if data else None
46
+
47
+
48
+ # --------------------------
49
+ # ToolRule Serialization
50
+ # --------------------------
51
+
52
+
53
+ def serialize_tool_rules(tool_rules: Optional[List[ToolRule]]) -> List[Dict[str, Any]]:
54
+ """Convert a list of ToolRules into a JSON-serializable format."""
55
+
56
+ if not tool_rules:
57
+ return []
58
+
59
+ data = [{**rule.model_dump(), "type": rule.type.value} for rule in tool_rules] # Convert Enum to string for JSON compatibility
60
+
61
+ # Validate ToolRule structure
62
+ for rule_data in data:
63
+ if rule_data["type"] == ToolRuleType.constrain_child_tools.value and "children" not in rule_data:
64
+ raise ValueError(f"Invalid ToolRule serialization: 'children' field missing for rule {rule_data}")
65
+
66
+ return data
67
+
68
+
69
+ def deserialize_tool_rules(data: Optional[List[Dict]]) -> List[Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule]]:
70
+ """Convert a list of dictionaries back into ToolRule objects."""
71
+ if not data:
72
+ return []
73
+
74
+ return [deserialize_tool_rule(rule_data) for rule_data in data]
75
+
76
+
77
+ def deserialize_tool_rule(data: Dict) -> Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule, ContinueToolRule]:
78
+ """Deserialize a dictionary to the appropriate ToolRule subclass based on 'type'."""
79
+ rule_type = ToolRuleType(data.get("type"))
80
+
81
+ if rule_type == ToolRuleType.run_first:
82
+ return InitToolRule(**data)
83
+ elif rule_type == ToolRuleType.exit_loop:
84
+ return TerminalToolRule(**data)
85
+ elif rule_type == ToolRuleType.constrain_child_tools:
86
+ return ChildToolRule(**data)
87
+ elif rule_type == ToolRuleType.conditional:
88
+ return ConditionalToolRule(**data)
89
+ elif rule_type == ToolRuleType.continue_loop:
90
+ return ContinueToolRule(**data)
91
+ raise ValueError(f"Unknown ToolRule type: {rule_type}")
92
+
93
+
94
+ # --------------------------
95
+ # ToolCall Serialization
96
+ # --------------------------
97
+
98
+
99
+ def serialize_tool_calls(tool_calls: Optional[List[Union[OpenAIToolCall, dict]]]) -> List[Dict]:
100
+ """Convert a list of OpenAI ToolCall objects into JSON-serializable format."""
101
+ if not tool_calls:
102
+ return []
103
+
104
+ serialized_calls = []
105
+ for call in tool_calls:
106
+ if isinstance(call, OpenAIToolCall):
107
+ serialized_calls.append(call.model_dump())
108
+ elif isinstance(call, dict):
109
+ serialized_calls.append(call) # Already a dictionary, leave it as-is
110
+ else:
111
+ raise TypeError(f"Unexpected tool call type: {type(call)}")
112
+
113
+ return serialized_calls
114
+
115
+
116
+ def deserialize_tool_calls(data: Optional[List[Dict]]) -> List[OpenAIToolCall]:
117
+ """Convert a JSON list back into OpenAIToolCall objects."""
118
+ if not data:
119
+ return []
120
+
121
+ calls = []
122
+ for item in data:
123
+ func_data = item.pop("function", None)
124
+ tool_call_function = OpenAIFunction(**func_data) if func_data else None
125
+ calls.append(OpenAIToolCall(function=tool_call_function, **item))
126
+
127
+ return calls
128
+
129
+
130
+ # --------------------------
131
+ # Vector Serialization
132
+ # --------------------------
133
+
134
+
135
+ def serialize_vector(vector: Optional[Union[List[float], np.ndarray]]) -> Optional[bytes]:
136
+ """Convert a NumPy array or list into a base64-encoded byte string."""
137
+ if vector is None:
138
+ return None
139
+ if isinstance(vector, list):
140
+ vector = np.array(vector, dtype=np.float32)
141
+
142
+ return base64.b64encode(vector.tobytes())
143
+
144
+
145
+ def deserialize_vector(data: Optional[bytes], dialect: Dialect) -> Optional[np.ndarray]:
146
+ """Convert a base64-encoded byte string back into a NumPy array."""
147
+ if not data:
148
+ return None
149
+
150
+ if dialect.name == "sqlite":
151
+ data = base64.b64decode(data)
152
+
153
+ return np.frombuffer(data, dtype=np.float32)
@@ -4,7 +4,7 @@ from typing import List, Optional, Union
4
4
  from pydantic import BaseModel, Field
5
5
 
6
6
  from letta.schemas.enums import ToolRuleType
7
- from letta.schemas.tool_rule import BaseToolRule, ChildToolRule, ConditionalToolRule, InitToolRule, TerminalToolRule
7
+ from letta.schemas.tool_rule import BaseToolRule, ChildToolRule, ConditionalToolRule, ContinueToolRule, InitToolRule, TerminalToolRule
8
8
 
9
9
 
10
10
  class ToolRuleValidationError(Exception):
@@ -18,6 +18,9 @@ class ToolRulesSolver(BaseModel):
18
18
  init_tool_rules: List[InitToolRule] = Field(
19
19
  default_factory=list, description="Initial tool rules to be used at the start of tool execution."
20
20
  )
21
+ continue_tool_rules: List[ContinueToolRule] = Field(
22
+ default_factory=list, description="Continue tool rules to be used to continue tool execution."
23
+ )
21
24
  tool_rules: List[Union[ChildToolRule, ConditionalToolRule]] = Field(
22
25
  default_factory=list, description="Standard tool rules for controlling execution sequence and allowed transitions."
23
26
  )
@@ -43,6 +46,9 @@ class ToolRulesSolver(BaseModel):
43
46
  elif rule.type == ToolRuleType.exit_loop:
44
47
  assert isinstance(rule, TerminalToolRule)
45
48
  self.terminal_tool_rules.append(rule)
49
+ elif rule.type == ToolRuleType.continue_loop:
50
+ assert isinstance(rule, ContinueToolRule)
51
+ self.continue_tool_rules.append(rule)
46
52
 
47
53
  def update_tool_usage(self, tool_name: str):
48
54
  """Update the internal state to track the last tool called."""
@@ -80,6 +86,10 @@ class ToolRulesSolver(BaseModel):
80
86
  """Check if the tool has children tools"""
81
87
  return any(rule.tool_name == tool_name for rule in self.tool_rules)
82
88
 
89
+ def is_continue_tool(self, tool_name):
90
+ """Check if the tool is defined as a continue tool in the tool rules."""
91
+ return any(rule.tool_name == tool_name for rule in self.continue_tool_rules)
92
+
83
93
  def validate_conditional_tool(self, rule: ConditionalToolRule):
84
94
  """
85
95
  Validate a conditional tool rule
@@ -519,6 +519,7 @@ def _prepare_anthropic_request(
519
519
  prefix_fill: bool = True,
520
520
  # if true, put COT inside the tool calls instead of inside the content
521
521
  put_inner_thoughts_in_kwargs: bool = False,
522
+ bedrock: bool = False,
522
523
  ) -> dict:
523
524
  """Prepare the request data for Anthropic API format."""
524
525
 
@@ -606,10 +607,11 @@ def _prepare_anthropic_request(
606
607
  # NOTE: cannot prefill with tools for opus:
607
608
  # Your API request included an `assistant` message in the final position, which would pre-fill the `assistant` response. When using tools with "claude-3-opus-20240229"
608
609
  if prefix_fill and not put_inner_thoughts_in_kwargs and "opus" not in data["model"]:
609
- data["messages"].append(
610
- # Start the thinking process for the assistant
611
- {"role": "assistant", "content": f"<{inner_thoughts_xml_tag}>"},
612
- )
610
+ if not bedrock: # not support for bedrock
611
+ data["messages"].append(
612
+ # Start the thinking process for the assistant
613
+ {"role": "assistant", "content": f"<{inner_thoughts_xml_tag}>"},
614
+ )
613
615
 
614
616
  # Validate max_tokens
615
617
  assert "max_tokens" in data, data
@@ -651,13 +653,16 @@ def anthropic_bedrock_chat_completions_request(
651
653
  inner_thoughts_xml_tag: Optional[str] = "thinking",
652
654
  ) -> ChatCompletionResponse:
653
655
  """Make a chat completion request to Anthropic via AWS Bedrock."""
654
- data = _prepare_anthropic_request(data, inner_thoughts_xml_tag)
656
+ data = _prepare_anthropic_request(data, inner_thoughts_xml_tag, bedrock=True)
655
657
 
656
658
  # Get the client
657
659
  client = get_bedrock_client()
658
660
 
659
661
  # Make the request
660
662
  try:
663
+ # bedrock does not support certain args
664
+ data["tool_choice"] = {"type": "any"}
665
+
661
666
  response = client.messages.create(**data)
662
667
  return convert_anthropic_response_to_chatcompletion(response=response, inner_thoughts_xml_tag=inner_thoughts_xml_tag)
663
668
  except PermissionDeniedError:
@@ -10,7 +10,7 @@ def has_valid_aws_credentials() -> bool:
10
10
  """
11
11
  Check if AWS credentials are properly configured.
12
12
  """
13
- valid_aws_credentials = os.getenv("AWS_ACCESS_KEY_ID") and os.getenv("AWS_SECRET_ACCESS_KEY") and os.getenv("AWS_REGION")
13
+ valid_aws_credentials = os.getenv("AWS_ACCESS_KEY") and os.getenv("AWS_SECRET_ACCESS_KEY") and os.getenv("AWS_REGION")
14
14
  return valid_aws_credentials
15
15
 
16
16