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
@@ -1,3 +1,4 @@
1
+ import json
1
2
  from urllib.parse import urljoin
2
3
 
3
4
  from letta.local_llm.settings.settings import get_completions_settings
@@ -6,6 +7,73 @@ from letta.utils import count_tokens
6
7
 
7
8
  LMSTUDIO_API_CHAT_SUFFIX = "/v1/chat/completions"
8
9
  LMSTUDIO_API_COMPLETIONS_SUFFIX = "/v1/completions"
10
+ LMSTUDIO_API_CHAT_COMPLETIONS_SUFFIX = "/v1/chat/completions"
11
+
12
+
13
+ def get_lmstudio_completion_chatcompletions(endpoint, auth_type, auth_key, model, messages):
14
+ """
15
+ This is the request we need to send
16
+
17
+ {
18
+ "model": "deepseek-r1-distill-qwen-7b",
19
+ "messages": [
20
+ { "role": "system", "content": "Always answer in rhymes. Today is Thursday" },
21
+ { "role": "user", "content": "What day is it today?" },
22
+ { "role": "user", "content": "What day is it today?" }],
23
+ "temperature": 0.7,
24
+ "max_tokens": -1,
25
+ "stream": false
26
+ """
27
+ from letta.utils import printd
28
+
29
+ URI = endpoint + LMSTUDIO_API_CHAT_COMPLETIONS_SUFFIX
30
+ request = {"model": model, "messages": messages}
31
+
32
+ response = post_json_auth_request(uri=URI, json_payload=request, auth_type=auth_type, auth_key=auth_key)
33
+
34
+ # Get the reasoning from the model
35
+ if response.status_code == 200:
36
+ result_full = response.json()
37
+ result_reasoning = result_full["choices"][0]["message"].get("reasoning_content")
38
+ result = result_full["choices"][0]["message"]["content"]
39
+ usage = result_full["usage"]
40
+
41
+ # See if result is json
42
+ try:
43
+ function_call = json.loads(result)
44
+ if "function" in function_call and "params" in function_call:
45
+ return result, usage, result_reasoning
46
+ else:
47
+ print("Did not get json on without json constraint, attempting with json decoding")
48
+ except Exception as e:
49
+ print(f"Did not get json on without json constraint, attempting with json decoding: {e}")
50
+
51
+ request["messages"].append({"role": "assistant", "content": result_reasoning})
52
+ request["messages"].append({"role": "user", "content": ""}) # last message must be user
53
+ # Now run with json decoding to get the function
54
+ request["response_format"] = {
55
+ "type": "json_schema",
56
+ "json_schema": {
57
+ "name": "function_call",
58
+ "strict": "true",
59
+ "schema": {
60
+ "type": "object",
61
+ "properties": {"function": {"type": "string"}, "params": {"type": "object"}},
62
+ "required": ["function", "params"],
63
+ },
64
+ },
65
+ }
66
+
67
+ response = post_json_auth_request(uri=URI, json_payload=request, auth_type=auth_type, auth_key=auth_key)
68
+ if response.status_code == 200:
69
+ result_full = response.json()
70
+ printd(f"JSON API response:\n{result_full}")
71
+ result = result_full["choices"][0]["message"]["content"]
72
+ # add usage with previous call, merge with prev usage
73
+ for key, value in result_full["usage"].items():
74
+ usage[key] += value
75
+
76
+ return result, usage, result_reasoning
9
77
 
10
78
 
11
79
  def get_lmstudio_completion(endpoint, auth_type, auth_key, prompt, context_window, api="completions"):
@@ -24,7 +92,8 @@ def get_lmstudio_completion(endpoint, auth_type, auth_key, prompt, context_windo
24
92
  # This controls how LM studio handles context overflow
25
93
  # In Letta we handle this ourselves, so this should be disabled
26
94
  # "context_overflow_policy": 0,
27
- "lmstudio": {"context_overflow_policy": 0}, # 0 = stop at limit
95
+ # "lmstudio": {"context_overflow_policy": 0}, # 0 = stop at limit
96
+ # "lmstudio": {"context_overflow_policy": "stopAtLimit"}, # https://github.com/letta-ai/letta/issues/1782
28
97
  "stream": False,
29
98
  "model": "local model",
30
99
  }
@@ -72,6 +141,11 @@ def get_lmstudio_completion(endpoint, auth_type, auth_key, prompt, context_windo
72
141
  elif api == "completions":
73
142
  result = result_full["choices"][0]["text"]
74
143
  usage = result_full.get("usage", None)
144
+ elif api == "chat/completions":
145
+ result = result_full["choices"][0]["content"]
146
+ result_full["choices"][0]["reasoning_content"]
147
+ usage = result_full.get("usage", None)
148
+
75
149
  else:
76
150
  # Example error: msg={"error":"Context length exceeded. Tokens in context: 8000, Context length: 8000"}
77
151
  if "context length" in str(response.text).lower():
letta/orm/__init__.py CHANGED
@@ -4,6 +4,8 @@ from letta.orm.base import Base
4
4
  from letta.orm.block import Block
5
5
  from letta.orm.blocks_agents import BlocksAgents
6
6
  from letta.orm.file import FileMetadata
7
+ from letta.orm.identities_agents import IdentitiesAgents
8
+ from letta.orm.identity import Identity
7
9
  from letta.orm.job import Job
8
10
  from letta.orm.job_messages import JobMessage
9
11
  from letta.orm.message import Message
letta/orm/agent.py CHANGED
@@ -6,6 +6,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
6
6
 
7
7
  from letta.orm.block import Block
8
8
  from letta.orm.custom_columns import EmbeddingConfigColumn, LLMConfigColumn, ToolRulesColumn
9
+ from letta.orm.identity import Identity
9
10
  from letta.orm.message import Message
10
11
  from letta.orm.mixins import OrganizationMixin
11
12
  from letta.orm.organization import Organization
@@ -15,10 +16,11 @@ from letta.schemas.agent import AgentType
15
16
  from letta.schemas.embedding_config import EmbeddingConfig
16
17
  from letta.schemas.llm_config import LLMConfig
17
18
  from letta.schemas.memory import Memory
18
- from letta.schemas.tool_rule import TerminalToolRule, ToolRule
19
+ from letta.schemas.tool_rule import ToolRule
19
20
 
20
21
  if TYPE_CHECKING:
21
22
  from letta.orm.agents_tags import AgentsTags
23
+ from letta.orm.identity import Identity
22
24
  from letta.orm.organization import Organization
23
25
  from letta.orm.source import Source
24
26
  from letta.orm.tool import Tool
@@ -119,14 +121,18 @@ class Agent(SqlalchemyBase, OrganizationMixin):
119
121
  viewonly=True, # Ensures SQLAlchemy doesn't attempt to manage this relationship
120
122
  doc="All passages derived created by this agent.",
121
123
  )
124
+ identities: Mapped[List["Identity"]] = relationship(
125
+ "Identity",
126
+ secondary="identities_agents",
127
+ lazy="selectin",
128
+ back_populates="agents",
129
+ passive_deletes=True,
130
+ )
122
131
 
123
132
  def to_pydantic(self) -> PydanticAgentState:
124
133
  """converts to the basic pydantic model counterpart"""
125
134
  # add default rule for having send_message be a terminal tool
126
135
  tool_rules = self.tool_rules
127
- if not tool_rules:
128
- tool_rules = [TerminalToolRule(tool_name="send_message"), TerminalToolRule(tool_name="send_message_to_agent_async")]
129
-
130
136
  state = {
131
137
  "id": self.id,
132
138
  "organization_id": self.organization_id,
@@ -151,6 +157,7 @@ class Agent(SqlalchemyBase, OrganizationMixin):
151
157
  "project_id": self.project_id,
152
158
  "template_id": self.template_id,
153
159
  "base_template_id": self.base_template_id,
160
+ "identity_ids": [identity.id for identity in self.identities],
154
161
  "message_buffer_autoclear": self.message_buffer_autoclear,
155
162
  }
156
163
 
@@ -1,159 +1,80 @@
1
- import base64
2
- from typing import List, 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
1
  from sqlalchemy import JSON
8
2
  from sqlalchemy.types import BINARY, TypeDecorator
9
3
 
10
- from letta.schemas.embedding_config import EmbeddingConfig
11
- from letta.schemas.enums import ToolRuleType
12
- from letta.schemas.llm_config import LLMConfig
13
- from letta.schemas.tool_rule import ChildToolRule, ConditionalToolRule, InitToolRule, TerminalToolRule
4
+ from letta.helpers.converters import (
5
+ deserialize_embedding_config,
6
+ deserialize_llm_config,
7
+ deserialize_tool_calls,
8
+ deserialize_tool_rules,
9
+ deserialize_vector,
10
+ serialize_embedding_config,
11
+ serialize_llm_config,
12
+ serialize_tool_calls,
13
+ serialize_tool_rules,
14
+ serialize_vector,
15
+ )
14
16
 
15
17
 
16
- class EmbeddingConfigColumn(TypeDecorator):
17
- """Custom type for storing EmbeddingConfig as JSON."""
18
+ class LLMConfigColumn(TypeDecorator):
19
+ """Custom SQLAlchemy column type for storing LLMConfig as JSON."""
18
20
 
19
21
  impl = JSON
20
22
  cache_ok = True
21
23
 
22
- def load_dialect_impl(self, dialect):
23
- return dialect.type_descriptor(JSON())
24
-
25
24
  def process_bind_param(self, value, dialect):
26
- if value and isinstance(value, EmbeddingConfig):
27
- return value.model_dump()
28
- return value
25
+ return serialize_llm_config(value)
29
26
 
30
27
  def process_result_value(self, value, dialect):
31
- if value:
32
- return EmbeddingConfig(**value)
33
- return value
28
+ return deserialize_llm_config(value)
34
29
 
35
30
 
36
- class LLMConfigColumn(TypeDecorator):
37
- """Custom type for storing LLMConfig as JSON."""
31
+ class EmbeddingConfigColumn(TypeDecorator):
32
+ """Custom SQLAlchemy column type for storing EmbeddingConfig as JSON."""
38
33
 
39
34
  impl = JSON
40
35
  cache_ok = True
41
36
 
42
- def load_dialect_impl(self, dialect):
43
- return dialect.type_descriptor(JSON())
44
-
45
37
  def process_bind_param(self, value, dialect):
46
- if value and isinstance(value, LLMConfig):
47
- return value.model_dump()
48
- return value
38
+ return serialize_embedding_config(value)
49
39
 
50
40
  def process_result_value(self, value, dialect):
51
- if value:
52
- return LLMConfig(**value)
53
- return value
41
+ return deserialize_embedding_config(value)
54
42
 
55
43
 
56
44
  class ToolRulesColumn(TypeDecorator):
57
- """Custom type for storing a list of ToolRules as JSON"""
45
+ """Custom SQLAlchemy column type for storing a list of ToolRules as JSON."""
58
46
 
59
47
  impl = JSON
60
48
  cache_ok = True
61
49
 
62
- def load_dialect_impl(self, dialect):
63
- return dialect.type_descriptor(JSON())
64
-
65
50
  def process_bind_param(self, value, dialect):
66
- """Convert a list of ToolRules to JSON-serializable format."""
67
- if value:
68
- data = [rule.model_dump() for rule in value]
69
- for d in data:
70
- d["type"] = d["type"].value
71
-
72
- for d in data:
73
- assert not (d["type"] == "ToolRule" and "children" not in d), "ToolRule does not have children field"
74
- return data
75
- return value
76
-
77
- def process_result_value(self, value, dialect) -> List[Union[ChildToolRule, InitToolRule, TerminalToolRule]]:
78
- """Convert JSON back to a list of ToolRules."""
79
- if value:
80
- return [self.deserialize_tool_rule(rule_data) for rule_data in value]
81
- return value
82
-
83
- @staticmethod
84
- def deserialize_tool_rule(data: dict) -> Union[ChildToolRule, InitToolRule, TerminalToolRule, ConditionalToolRule]:
85
- """Deserialize a dictionary to the appropriate ToolRule subclass based on the 'type'."""
86
- rule_type = ToolRuleType(data.get("type")) # Remove 'type' field if it exists since it is a class var
87
- if rule_type == ToolRuleType.run_first or rule_type == "InitToolRule":
88
- data["type"] = ToolRuleType.run_first
89
- return InitToolRule(**data)
90
- elif rule_type == ToolRuleType.exit_loop or rule_type == "TerminalToolRule":
91
- data["type"] = ToolRuleType.exit_loop
92
- return TerminalToolRule(**data)
93
- elif rule_type == ToolRuleType.constrain_child_tools or rule_type == "ToolRule":
94
- data["type"] = ToolRuleType.constrain_child_tools
95
- rule = ChildToolRule(**data)
96
- return rule
97
- elif rule_type == ToolRuleType.conditional:
98
- rule = ConditionalToolRule(**data)
99
- return rule
100
- else:
101
- raise ValueError(f"Unknown tool rule type: {rule_type}")
51
+ return serialize_tool_rules(value)
52
+
53
+ def process_result_value(self, value, dialect):
54
+ return deserialize_tool_rules(value)
102
55
 
103
56
 
104
57
  class ToolCallColumn(TypeDecorator):
58
+ """Custom SQLAlchemy column type for storing OpenAI ToolCall objects as JSON."""
105
59
 
106
60
  impl = JSON
107
61
  cache_ok = True
108
62
 
109
- def load_dialect_impl(self, dialect):
110
- return dialect.type_descriptor(JSON())
111
-
112
63
  def process_bind_param(self, value, dialect):
113
- if value:
114
- values = []
115
- for v in value:
116
- if isinstance(v, OpenAIToolCall):
117
- values.append(v.model_dump())
118
- else:
119
- values.append(v)
120
- return values
121
-
122
- return value
64
+ return serialize_tool_calls(value)
123
65
 
124
66
  def process_result_value(self, value, dialect):
125
- if value:
126
- tools = []
127
- for tool_value in value:
128
- if "function" in tool_value:
129
- tool_call_function = OpenAIFunction(**tool_value["function"])
130
- del tool_value["function"]
131
- else:
132
- tool_call_function = None
133
- tools.append(OpenAIToolCall(function=tool_call_function, **tool_value))
134
- return tools
135
- return value
67
+ return deserialize_tool_calls(value)
136
68
 
137
69
 
138
70
  class CommonVector(TypeDecorator):
139
- """Common type for representing vectors in SQLite"""
71
+ """Custom SQLAlchemy column type for storing vectors in SQLite."""
140
72
 
141
73
  impl = BINARY
142
74
  cache_ok = True
143
75
 
144
- def load_dialect_impl(self, dialect):
145
- return dialect.type_descriptor(BINARY())
146
-
147
76
  def process_bind_param(self, value, dialect):
148
- if value is None:
149
- return value
150
- if isinstance(value, list):
151
- value = np.array(value, dtype=np.float32)
152
- return base64.b64encode(value.tobytes())
77
+ return serialize_vector(value)
153
78
 
154
79
  def process_result_value(self, value, dialect):
155
- if not value:
156
- return value
157
- if dialect.name == "sqlite":
158
- value = base64.b64decode(value)
159
- return np.frombuffer(value, dtype=np.float32)
80
+ return deserialize_vector(value, dialect)
@@ -0,0 +1,13 @@
1
+ from sqlalchemy import ForeignKey, String
2
+ from sqlalchemy.orm import Mapped, mapped_column
3
+
4
+ from letta.orm.base import Base
5
+
6
+
7
+ class IdentitiesAgents(Base):
8
+ """Identities may have one or many agents associated with them."""
9
+
10
+ __tablename__ = "identities_agents"
11
+
12
+ identity_id: Mapped[str] = mapped_column(String, ForeignKey("identities.id", ondelete="CASCADE"), primary_key=True)
13
+ agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id", ondelete="CASCADE"), primary_key=True)
letta/orm/identity.py ADDED
@@ -0,0 +1,60 @@
1
+ import uuid
2
+ from typing import List, Optional
3
+
4
+ from sqlalchemy import String, UniqueConstraint
5
+ from sqlalchemy.dialects.postgresql import JSONB
6
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
7
+
8
+ from letta.orm.mixins import OrganizationMixin
9
+ from letta.orm.sqlalchemy_base import SqlalchemyBase
10
+ from letta.schemas.identity import Identity as PydanticIdentity
11
+ from letta.schemas.identity import IdentityProperty
12
+
13
+
14
+ class Identity(SqlalchemyBase, OrganizationMixin):
15
+ """Identity ORM class"""
16
+
17
+ __tablename__ = "identities"
18
+ __pydantic_model__ = PydanticIdentity
19
+ __table_args__ = (
20
+ UniqueConstraint(
21
+ "identifier_key",
22
+ "project_id",
23
+ "organization_id",
24
+ name="unique_identifier_without_project",
25
+ postgresql_nulls_not_distinct=True,
26
+ ),
27
+ )
28
+
29
+ id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"identity-{uuid.uuid4()}")
30
+ identifier_key: Mapped[str] = mapped_column(nullable=False, doc="External, user-generated identifier key of the identity.")
31
+ name: Mapped[str] = mapped_column(nullable=False, doc="The name of the identity.")
32
+ identity_type: Mapped[str] = mapped_column(nullable=False, doc="The type of the identity.")
33
+ project_id: Mapped[Optional[str]] = mapped_column(nullable=True, doc="The project id of the identity.")
34
+ properties: Mapped[List["IdentityProperty"]] = mapped_column(
35
+ JSONB, nullable=False, default=list, doc="List of properties associated with the identity"
36
+ )
37
+
38
+ # relationships
39
+ organization: Mapped["Organization"] = relationship("Organization", back_populates="identities")
40
+ agents: Mapped[List["Agent"]] = relationship(
41
+ "Agent", secondary="identities_agents", lazy="selectin", passive_deletes=True, back_populates="identities"
42
+ )
43
+
44
+ @property
45
+ def agent_ids(self) -> List[str]:
46
+ """Get just the agent IDs without loading the full agent objects"""
47
+ return [agent.id for agent in self.agents]
48
+
49
+ def to_pydantic(self) -> PydanticIdentity:
50
+ state = {
51
+ "id": self.id,
52
+ "identifier_key": self.identifier_key,
53
+ "name": self.name,
54
+ "identity_type": self.identity_type,
55
+ "project_id": self.project_id,
56
+ "agent_ids": self.agent_ids,
57
+ "organization_id": self.organization_id,
58
+ "properties": self.properties,
59
+ }
60
+ return PydanticIdentity(**state)
letta/orm/organization.py CHANGED
@@ -9,6 +9,7 @@ if TYPE_CHECKING:
9
9
 
10
10
  from letta.orm.agent import Agent
11
11
  from letta.orm.file import FileMetadata
12
+ from letta.orm.identity import Identity
12
13
  from letta.orm.provider import Provider
13
14
  from letta.orm.sandbox_config import AgentEnvironmentVariable
14
15
  from letta.orm.tool import Tool
@@ -47,6 +48,7 @@ class Organization(SqlalchemyBase):
47
48
  )
48
49
  agent_passages: Mapped[List["AgentPassage"]] = relationship("AgentPassage", back_populates="organization", cascade="all, delete-orphan")
49
50
  providers: Mapped[List["Provider"]] = relationship("Provider", back_populates="organization", cascade="all, delete-orphan")
51
+ identities: Mapped[List["Identity"]] = relationship("Identity", back_populates="organization", cascade="all, delete-orphan")
50
52
 
51
53
  @property
52
54
  def passages(self) -> List[Union["SourcePassage", "AgentPassage"]]:
@@ -68,6 +68,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
68
68
  access_type: AccessType = AccessType.ORGANIZATION,
69
69
  join_model: Optional[Base] = None,
70
70
  join_conditions: Optional[Union[Tuple, List]] = None,
71
+ identifier_keys: Optional[List[str]] = None,
71
72
  **kwargs,
72
73
  ) -> List["SqlalchemyBase"]:
73
74
  """
@@ -143,6 +144,9 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
143
144
  # Group by primary key and all necessary columns to avoid JSON comparison
144
145
  query = query.group_by(cls.id)
145
146
 
147
+ if identifier_keys and hasattr(cls, "identities"):
148
+ query = query.join(cls.identities).filter(cls.identities.property.mapper.class_.identifier_key.in_(identifier_keys))
149
+
146
150
  # Apply filtering logic from kwargs
147
151
  for key, value in kwargs.items():
148
152
  if "." in key:
letta/schemas/agent.py CHANGED
@@ -83,6 +83,7 @@ class AgentState(OrmMetadataBase, validate_assignment=True):
83
83
  project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
84
84
  template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
85
85
  base_template_id: Optional[str] = Field(None, description="The base template id of the agent.")
86
+ identity_ids: List[str] = Field([], description="The ids of the identities associated with this agent.")
86
87
 
87
88
  # An advanced configuration that makes it so this agent does not remember any previous messages
88
89
  message_buffer_autoclear: bool = Field(
@@ -129,6 +130,9 @@ class CreateAgent(BaseModel, validate_assignment=True): #
129
130
  include_multi_agent_tools: bool = Field(
130
131
  False, description="If true, attaches the Letta multi-agent tools (e.g. sending a message to another agent)."
131
132
  )
133
+ include_base_tool_rules: bool = Field(
134
+ True, description="If true, attaches the Letta base tool rules (e.g. deny all tools not explicitly allowed)."
135
+ )
132
136
  description: Optional[str] = Field(None, description="The description of the agent.")
133
137
  metadata: Optional[Dict] = Field(None, description="The metadata of the agent.")
134
138
  model: Optional[str] = Field(
@@ -143,7 +147,11 @@ class CreateAgent(BaseModel, validate_assignment=True): #
143
147
  embedding_chunk_size: Optional[int] = Field(DEFAULT_EMBEDDING_CHUNK_SIZE, description="The embedding chunk size used by the agent.")
144
148
  from_template: Optional[str] = Field(None, description="The template id used to configure the agent")
145
149
  template: bool = Field(False, description="Whether the agent is a template")
146
- project: Optional[str] = Field(None, description="The project slug that the agent will be associated with.")
150
+ project: Optional[str] = Field(
151
+ None,
152
+ deprecated=True,
153
+ description="Deprecated: Project should now be passed via the X-Project header instead of in the request body. If using the sdk, this can be done via the new x_project field below.",
154
+ )
147
155
  tool_exec_environment_variables: Optional[Dict[str, str]] = Field(
148
156
  None, description="The environment variables for tool execution specific to this agent."
149
157
  )
@@ -151,6 +159,7 @@ class CreateAgent(BaseModel, validate_assignment=True): #
151
159
  project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
152
160
  template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
153
161
  base_template_id: Optional[str] = Field(None, description="The base template id of the agent.")
162
+ identity_ids: Optional[List[str]] = Field(None, description="The ids of the identities associated with this agent.")
154
163
  message_buffer_autoclear: bool = Field(
155
164
  False,
156
165
  description="If set to True, the agent will not remember previous messages (though the agent will still retain state via core memory blocks and archival/recall memory). Not recommended unless you have an advanced use case.",
@@ -225,6 +234,7 @@ class UpdateAgent(BaseModel):
225
234
  project_id: Optional[str] = Field(None, description="The id of the project the agent belongs to.")
226
235
  template_id: Optional[str] = Field(None, description="The id of the template the agent belongs to.")
227
236
  base_template_id: Optional[str] = Field(None, description="The base template id of the agent.")
237
+ identity_ids: Optional[List[str]] = Field(None, description="The ids of the identities associated with this agent.")
228
238
  message_buffer_autoclear: Optional[bool] = Field(
229
239
  None,
230
240
  description="If set to True, the agent will not remember previous messages (though the agent will still retain state via core memory blocks and archival/recall memory). Not recommended unless you have an advanced use case.",
@@ -0,0 +1,67 @@
1
+ from enum import Enum
2
+ from typing import List, Optional, Union
3
+
4
+ from pydantic import Field
5
+
6
+ from letta.schemas.letta_base import LettaBase
7
+
8
+
9
+ class IdentityType(str, Enum):
10
+ """
11
+ Enum to represent the type of the identity.
12
+ """
13
+
14
+ org = "org"
15
+ user = "user"
16
+ other = "other"
17
+
18
+
19
+ class IdentityPropertyType(str, Enum):
20
+ """
21
+ Enum to represent the type of the identity property.
22
+ """
23
+
24
+ string = "string"
25
+ number = "number"
26
+ boolean = "boolean"
27
+ json = "json"
28
+
29
+
30
+ class IdentityBase(LettaBase):
31
+ __id_prefix__ = "identity"
32
+
33
+
34
+ class IdentityProperty(LettaBase):
35
+ """A property of an identity"""
36
+
37
+ key: str = Field(..., description="The key of the property")
38
+ value: Union[str, int, float, bool, dict] = Field(..., description="The value of the property")
39
+ type: IdentityPropertyType = Field(..., description="The type of the property")
40
+
41
+
42
+ class Identity(IdentityBase):
43
+ id: str = IdentityBase.generate_id_field()
44
+ identifier_key: str = Field(..., description="External, user-generated identifier key of the identity.")
45
+ name: str = Field(..., description="The name of the identity.")
46
+ identity_type: IdentityType = Field(..., description="The type of the identity.")
47
+ project_id: Optional[str] = Field(None, description="The project id of the identity, if applicable.")
48
+ agent_ids: List[str] = Field(..., description="The IDs of the agents associated with the identity.")
49
+ organization_id: Optional[str] = Field(None, description="The organization id of the user")
50
+ properties: List[IdentityProperty] = Field(default_factory=list, description="List of properties associated with the identity")
51
+
52
+
53
+ class IdentityCreate(LettaBase):
54
+ identifier_key: str = Field(..., description="External, user-generated identifier key of the identity.")
55
+ name: str = Field(..., description="The name of the identity.")
56
+ identity_type: IdentityType = Field(..., description="The type of the identity.")
57
+ project_id: Optional[str] = Field(None, description="The project id of the identity, if applicable.")
58
+ agent_ids: Optional[List[str]] = Field(None, description="The agent ids that are associated with the identity.")
59
+ properties: Optional[List[IdentityProperty]] = Field(None, description="List of properties associated with the identity.")
60
+
61
+
62
+ class IdentityUpdate(LettaBase):
63
+ identifier_key: Optional[str] = Field(None, description="External, user-generated identifier key of the identity.")
64
+ name: Optional[str] = Field(None, description="The name of the identity.")
65
+ identity_type: Optional[IdentityType] = Field(None, description="The type of the identity.")
66
+ agent_ids: Optional[List[str]] = Field(None, description="The agent ids that are associated with the identity.")
67
+ properties: Optional[List[IdentityProperty]] = Field(None, description="List of properties associated with the identity.")
@@ -33,6 +33,7 @@ class LLMConfig(BaseModel):
33
33
  "webui-legacy",
34
34
  "lmstudio",
35
35
  "lmstudio-legacy",
36
+ "lmstudio-chatcompletions",
36
37
  "llamacpp",
37
38
  "koboldcpp",
38
39
  "vllm",
@@ -40,6 +41,7 @@ class LLMConfig(BaseModel):
40
41
  "mistral",
41
42
  "together", # completions endpoint
42
43
  "bedrock",
44
+ "deepseek",
43
45
  ] = Field(..., description="The endpoint type for the model.")
44
46
  model_endpoint: Optional[str] = Field(None, description="The endpoint for the model.")
45
47
  model_wrapper: Optional[str] = Field(None, description="The wrapper for the model.")
letta/schemas/message.py CHANGED
@@ -647,7 +647,7 @@ class Message(BaseMessage):
647
647
  # role: str ('user' or 'model')
648
648
 
649
649
  if self.role != "tool" and self.name is not None:
650
- raise UserWarning(f"Using Google AI with non-null 'name' field ({self.name}) not yet supported.")
650
+ warnings.warn(f"Using Google AI with non-null 'name' field ({self.name}) not yet supported.")
651
651
 
652
652
  if self.role == "system":
653
653
  # NOTE: Gemini API doesn't have a 'system' role, use 'user' instead
@@ -39,6 +39,7 @@ class Message(BaseModel):
39
39
  tool_calls: Optional[List[ToolCall]] = None
40
40
  role: str
41
41
  function_call: Optional[FunctionCall] = None # Deprecated
42
+ reasoning_content: Optional[str] = None # Used in newer reasoning APIs
42
43
 
43
44
 
44
45
  class Choice(BaseModel):
@@ -115,6 +116,7 @@ class MessageDelta(BaseModel):
115
116
  """
116
117
 
117
118
  content: Optional[str] = None
119
+ reasoning_content: Optional[str] = None
118
120
  tool_calls: Optional[List[ToolCallDelta]] = None
119
121
  role: Optional[str] = None
120
122
  function_call: Optional[FunctionCallDelta] = None # Deprecated