letta-nightly 0.11.6.dev20250903104037__py3-none-any.whl → 0.11.7.dev20250904045700__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.
Files changed (138) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +10 -14
  3. letta/agents/base_agent.py +18 -0
  4. letta/agents/helpers.py +32 -7
  5. letta/agents/letta_agent.py +953 -762
  6. letta/agents/voice_agent.py +1 -1
  7. letta/client/streaming.py +0 -1
  8. letta/constants.py +11 -8
  9. letta/errors.py +9 -0
  10. letta/functions/function_sets/base.py +77 -69
  11. letta/functions/function_sets/builtin.py +41 -22
  12. letta/functions/function_sets/multi_agent.py +1 -2
  13. letta/functions/schema_generator.py +0 -1
  14. letta/helpers/converters.py +8 -3
  15. letta/helpers/datetime_helpers.py +5 -4
  16. letta/helpers/message_helper.py +1 -2
  17. letta/helpers/pinecone_utils.py +0 -1
  18. letta/helpers/tool_rule_solver.py +10 -0
  19. letta/helpers/tpuf_client.py +848 -0
  20. letta/interface.py +8 -8
  21. letta/interfaces/anthropic_streaming_interface.py +7 -0
  22. letta/interfaces/openai_streaming_interface.py +29 -6
  23. letta/llm_api/anthropic_client.py +188 -18
  24. letta/llm_api/azure_client.py +0 -1
  25. letta/llm_api/bedrock_client.py +1 -2
  26. letta/llm_api/deepseek_client.py +319 -5
  27. letta/llm_api/google_vertex_client.py +75 -17
  28. letta/llm_api/groq_client.py +0 -1
  29. letta/llm_api/helpers.py +2 -2
  30. letta/llm_api/llm_api_tools.py +1 -50
  31. letta/llm_api/llm_client.py +6 -8
  32. letta/llm_api/mistral.py +1 -1
  33. letta/llm_api/openai.py +16 -13
  34. letta/llm_api/openai_client.py +31 -16
  35. letta/llm_api/together_client.py +0 -1
  36. letta/llm_api/xai_client.py +0 -1
  37. letta/local_llm/chat_completion_proxy.py +7 -6
  38. letta/local_llm/settings/settings.py +1 -1
  39. letta/orm/__init__.py +1 -0
  40. letta/orm/agent.py +8 -6
  41. letta/orm/archive.py +9 -1
  42. letta/orm/block.py +3 -4
  43. letta/orm/block_history.py +3 -1
  44. letta/orm/group.py +2 -3
  45. letta/orm/identity.py +1 -2
  46. letta/orm/job.py +1 -2
  47. letta/orm/llm_batch_items.py +1 -2
  48. letta/orm/message.py +8 -4
  49. letta/orm/mixins.py +18 -0
  50. letta/orm/organization.py +2 -0
  51. letta/orm/passage.py +8 -1
  52. letta/orm/passage_tag.py +55 -0
  53. letta/orm/sandbox_config.py +1 -3
  54. letta/orm/step.py +1 -2
  55. letta/orm/tool.py +1 -0
  56. letta/otel/resource.py +2 -2
  57. letta/plugins/plugins.py +1 -1
  58. letta/prompts/prompt_generator.py +10 -2
  59. letta/schemas/agent.py +11 -0
  60. letta/schemas/archive.py +4 -0
  61. letta/schemas/block.py +13 -0
  62. letta/schemas/embedding_config.py +0 -1
  63. letta/schemas/enums.py +24 -7
  64. letta/schemas/group.py +12 -0
  65. letta/schemas/letta_message.py +55 -1
  66. letta/schemas/letta_message_content.py +28 -0
  67. letta/schemas/letta_request.py +21 -4
  68. letta/schemas/letta_stop_reason.py +9 -1
  69. letta/schemas/llm_config.py +24 -8
  70. letta/schemas/mcp.py +0 -3
  71. letta/schemas/memory.py +14 -0
  72. letta/schemas/message.py +245 -141
  73. letta/schemas/openai/chat_completion_request.py +2 -1
  74. letta/schemas/passage.py +1 -0
  75. letta/schemas/providers/bedrock.py +1 -1
  76. letta/schemas/providers/openai.py +2 -2
  77. letta/schemas/tool.py +11 -5
  78. letta/schemas/tool_execution_result.py +0 -1
  79. letta/schemas/tool_rule.py +71 -0
  80. letta/serialize_schemas/marshmallow_agent.py +1 -2
  81. letta/server/rest_api/app.py +3 -3
  82. letta/server/rest_api/auth/index.py +0 -1
  83. letta/server/rest_api/interface.py +3 -11
  84. letta/server/rest_api/redis_stream_manager.py +3 -4
  85. letta/server/rest_api/routers/v1/agents.py +143 -84
  86. letta/server/rest_api/routers/v1/blocks.py +1 -1
  87. letta/server/rest_api/routers/v1/folders.py +1 -1
  88. letta/server/rest_api/routers/v1/groups.py +23 -22
  89. letta/server/rest_api/routers/v1/internal_templates.py +68 -0
  90. letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
  91. letta/server/rest_api/routers/v1/sources.py +1 -1
  92. letta/server/rest_api/routers/v1/tools.py +167 -15
  93. letta/server/rest_api/streaming_response.py +4 -3
  94. letta/server/rest_api/utils.py +75 -18
  95. letta/server/server.py +24 -35
  96. letta/services/agent_manager.py +359 -45
  97. letta/services/agent_serialization_manager.py +23 -3
  98. letta/services/archive_manager.py +72 -3
  99. letta/services/block_manager.py +1 -2
  100. letta/services/context_window_calculator/token_counter.py +11 -6
  101. letta/services/file_manager.py +1 -3
  102. letta/services/files_agents_manager.py +2 -4
  103. letta/services/group_manager.py +73 -12
  104. letta/services/helpers/agent_manager_helper.py +5 -5
  105. letta/services/identity_manager.py +8 -3
  106. letta/services/job_manager.py +2 -14
  107. letta/services/llm_batch_manager.py +1 -3
  108. letta/services/mcp/base_client.py +1 -2
  109. letta/services/mcp_manager.py +5 -6
  110. letta/services/message_manager.py +536 -15
  111. letta/services/organization_manager.py +1 -2
  112. letta/services/passage_manager.py +287 -12
  113. letta/services/provider_manager.py +1 -3
  114. letta/services/sandbox_config_manager.py +12 -7
  115. letta/services/source_manager.py +1 -2
  116. letta/services/step_manager.py +0 -1
  117. letta/services/summarizer/summarizer.py +4 -2
  118. letta/services/telemetry_manager.py +1 -3
  119. letta/services/tool_executor/builtin_tool_executor.py +136 -316
  120. letta/services/tool_executor/core_tool_executor.py +231 -74
  121. letta/services/tool_executor/files_tool_executor.py +2 -2
  122. letta/services/tool_executor/mcp_tool_executor.py +0 -1
  123. letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
  124. letta/services/tool_executor/sandbox_tool_executor.py +0 -1
  125. letta/services/tool_executor/tool_execution_sandbox.py +2 -3
  126. letta/services/tool_manager.py +181 -64
  127. letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
  128. letta/services/user_manager.py +1 -2
  129. letta/settings.py +5 -3
  130. letta/streaming_interface.py +3 -3
  131. letta/system.py +1 -1
  132. letta/utils.py +0 -1
  133. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/METADATA +11 -7
  134. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/RECORD +137 -135
  135. letta/llm_api/deepseek.py +0 -303
  136. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/WHEEL +0 -0
  137. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/entry_points.txt +0 -0
  138. {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904045700.dist-info}/licenses/LICENSE +0 -0
letta/llm_api/openai.py CHANGED
@@ -21,11 +21,15 @@ from letta.local_llm.utils import num_tokens_from_functions, num_tokens_from_mes
21
21
  from letta.log import get_logger
22
22
  from letta.otel.tracing import log_event
23
23
  from letta.schemas.llm_config import LLMConfig
24
- from letta.schemas.message import Message as _Message
25
- from letta.schemas.message import MessageRole as _MessageRole
26
- from letta.schemas.openai.chat_completion_request import ChatCompletionRequest
27
- from letta.schemas.openai.chat_completion_request import FunctionCall as ToolFunctionChoiceFunctionCall
28
- from letta.schemas.openai.chat_completion_request import FunctionSchema, Tool, ToolFunctionChoice, cast_message_to_subtype
24
+ from letta.schemas.message import Message as PydanticMessage, MessageRole as _MessageRole
25
+ from letta.schemas.openai.chat_completion_request import (
26
+ ChatCompletionRequest,
27
+ FunctionCall as ToolFunctionChoiceFunctionCall,
28
+ FunctionSchema,
29
+ Tool,
30
+ ToolFunctionChoice,
31
+ cast_message_to_subtype,
32
+ )
29
33
  from letta.schemas.openai.chat_completion_response import (
30
34
  ChatCompletionChunkResponse,
31
35
  ChatCompletionResponse,
@@ -173,7 +177,7 @@ async def openai_get_model_list_async(
173
177
 
174
178
  def build_openai_chat_completions_request(
175
179
  llm_config: LLMConfig,
176
- messages: List[_Message],
180
+ messages: List[PydanticMessage],
177
181
  user_id: Optional[str],
178
182
  functions: Optional[list],
179
183
  function_call: Optional[str],
@@ -197,13 +201,12 @@ def build_openai_chat_completions_request(
197
201
  use_developer_message = accepts_developer_role(llm_config.model)
198
202
 
199
203
  openai_message_list = [
200
- cast_message_to_subtype(
201
- m.to_openai_dict(
202
- put_inner_thoughts_in_kwargs=llm_config.put_inner_thoughts_in_kwargs,
203
- use_developer_message=use_developer_message,
204
- )
204
+ cast_message_to_subtype(m)
205
+ for m in PydanticMessage.to_openai_dicts_from_list(
206
+ messages,
207
+ put_inner_thoughts_in_kwargs=llm_config.put_inner_thoughts_in_kwargs,
208
+ use_developer_message=use_developer_message,
205
209
  )
206
- for m in messages
207
210
  ]
208
211
 
209
212
  if llm_config.model:
@@ -322,7 +325,7 @@ def openai_chat_completions_process_stream(
322
325
 
323
326
  # Create a dummy Message object to get an ID and date
324
327
  # TODO(sarah): add message ID generation function
325
- dummy_message = _Message(
328
+ dummy_message = PydanticMessage(
326
329
  role=_MessageRole.assistant,
327
330
  content=[],
328
331
  agent_id="",
@@ -29,11 +29,14 @@ from letta.schemas.embedding_config import EmbeddingConfig
29
29
  from letta.schemas.letta_message_content import MessageContentType
30
30
  from letta.schemas.llm_config import LLMConfig
31
31
  from letta.schemas.message import Message as PydanticMessage
32
- from letta.schemas.openai.chat_completion_request import ChatCompletionRequest
33
- from letta.schemas.openai.chat_completion_request import FunctionCall as ToolFunctionChoiceFunctionCall
34
- from letta.schemas.openai.chat_completion_request import FunctionSchema
35
- from letta.schemas.openai.chat_completion_request import Tool as OpenAITool
36
- from letta.schemas.openai.chat_completion_request import ToolFunctionChoice, cast_message_to_subtype
32
+ from letta.schemas.openai.chat_completion_request import (
33
+ ChatCompletionRequest,
34
+ FunctionCall as ToolFunctionChoiceFunctionCall,
35
+ FunctionSchema,
36
+ Tool as OpenAITool,
37
+ ToolFunctionChoice,
38
+ cast_message_to_subtype,
39
+ )
37
40
  from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
38
41
  from letta.settings import model_settings
39
42
 
@@ -44,7 +47,7 @@ def is_openai_reasoning_model(model: str) -> bool:
44
47
  """Utility function to check if the model is a 'reasoner'"""
45
48
 
46
49
  # NOTE: needs to be updated with new model releases
47
- is_reasoning = model.startswith("o1") or model.startswith("o3") or model.startswith("o4")
50
+ is_reasoning = model.startswith("o1") or model.startswith("o3") or model.startswith("o4") or model.startswith("gpt-5")
48
51
  return is_reasoning
49
52
 
50
53
 
@@ -176,13 +179,12 @@ class OpenAIClient(LLMClientBase):
176
179
  use_developer_message = accepts_developer_role(llm_config.model)
177
180
 
178
181
  openai_message_list = [
179
- cast_message_to_subtype(
180
- m.to_openai_dict(
181
- put_inner_thoughts_in_kwargs=llm_config.put_inner_thoughts_in_kwargs,
182
- use_developer_message=use_developer_message,
183
- )
182
+ cast_message_to_subtype(m)
183
+ for m in PydanticMessage.to_openai_dicts_from_list(
184
+ messages,
185
+ put_inner_thoughts_in_kwargs=llm_config.put_inner_thoughts_in_kwargs,
186
+ use_developer_message=use_developer_message,
184
187
  )
185
- for m in messages
186
188
  ]
187
189
 
188
190
  if llm_config.model:
@@ -219,6 +221,10 @@ class OpenAIClient(LLMClientBase):
219
221
  if supports_verbosity_control(model) and llm_config.verbosity:
220
222
  data.verbosity = llm_config.verbosity
221
223
 
224
+ # Add reasoning effort control for reasoning models
225
+ if is_openai_reasoning_model(model) and llm_config.reasoning_effort:
226
+ data.reasoning_effort = llm_config.reasoning_effort
227
+
222
228
  if llm_config.frequency_penalty is not None:
223
229
  data.frequency_penalty = llm_config.frequency_penalty
224
230
 
@@ -357,10 +363,19 @@ class OpenAIClient(LLMClientBase):
357
363
  if isinstance(e, openai.BadRequestError):
358
364
  logger.warning(f"[OpenAI] Bad request (400): {str(e)}")
359
365
  # BadRequestError can signify different issues (e.g., invalid args, context length)
360
- # Check message content if finer-grained errors are needed
361
- # Example: if "context_length_exceeded" in str(e): return LLMContextLengthExceededError(...)
362
- # TODO: This is a super soft check. Not sure if we can do better, needs more investigation.
363
- if "This model's maximum context length is" in str(e):
366
+ # Check for context_length_exceeded error code in the error body
367
+ error_code = None
368
+ if e.body and isinstance(e.body, dict):
369
+ error_details = e.body.get("error", {})
370
+ if isinstance(error_details, dict):
371
+ error_code = error_details.get("code")
372
+
373
+ # Check both the error code and message content for context length issues
374
+ if (
375
+ error_code == "context_length_exceeded"
376
+ or "This model's maximum context length is" in str(e)
377
+ or "Input tokens exceed the configured limit" in str(e)
378
+ ):
364
379
  return ContextWindowExceededError(
365
380
  message=f"Bad request to OpenAI (context window exceeded): {str(e)}",
366
381
  )
@@ -12,7 +12,6 @@ from letta.settings import model_settings
12
12
 
13
13
 
14
14
  class TogetherClient(OpenAIClient):
15
-
16
15
  def requires_auto_tool_choice(self, llm_config: LLMConfig) -> bool:
17
16
  return True
18
17
 
@@ -14,7 +14,6 @@ from letta.settings import model_settings
14
14
 
15
15
 
16
16
  class XAIClient(OpenAIClient):
17
-
18
17
  def requires_auto_tool_choice(self, llm_config: LLMConfig) -> bool:
19
18
  return False
20
19
 
@@ -22,6 +22,7 @@ from letta.local_llm.webui.api import get_webui_completion
22
22
  from letta.local_llm.webui.legacy_api import get_webui_completion as get_webui_completion_legacy
23
23
  from letta.otel.tracing import log_event
24
24
  from letta.prompts.gpt_summarize import SYSTEM as SUMMARIZE_SYSTEM_MESSAGE
25
+ from letta.schemas.message import Message as PydanticMessage
25
26
  from letta.schemas.openai.chat_completion_response import ChatCompletionResponse, Choice, Message, ToolCall, UsageStatistics
26
27
  from letta.utils import get_tool_call_id
27
28
 
@@ -61,7 +62,7 @@ def get_chat_completion(
61
62
 
62
63
  # TODO: eventually just process Message object
63
64
  if not isinstance(messages[0], dict):
64
- messages = [m.to_openai_dict() for m in messages]
65
+ messages = PydanticMessage.to_openai_dicts_from_list(messages)
65
66
 
66
67
  if function_call is not None and function_call != "auto":
67
68
  raise ValueError(f"function_call == {function_call} not supported (auto or None only)")
@@ -205,7 +206,7 @@ def get_chat_completion(
205
206
  raise LocalLLMError(f"usage dict in response was missing fields ({usage})")
206
207
 
207
208
  if usage["prompt_tokens"] is None:
208
- printd(f"usage dict was missing prompt_tokens, computing on-the-fly...")
209
+ printd("usage dict was missing prompt_tokens, computing on-the-fly...")
209
210
  usage["prompt_tokens"] = count_tokens(prompt)
210
211
 
211
212
  # NOTE: we should compute on-the-fly anyways since we might have to correct for errors during JSON parsing
@@ -220,7 +221,7 @@ def get_chat_completion(
220
221
 
221
222
  # NOTE: this is the token count that matters most
222
223
  if usage["total_tokens"] is None:
223
- printd(f"usage dict was missing total_tokens, computing on-the-fly...")
224
+ printd("usage dict was missing total_tokens, computing on-the-fly...")
224
225
  usage["total_tokens"] = usage["prompt_tokens"] + usage["completion_tokens"]
225
226
 
226
227
  # unpack with response.choices[0].message.content
@@ -261,9 +262,9 @@ def generate_grammar_and_documentation(
261
262
  ):
262
263
  from letta.utils import printd
263
264
 
264
- assert not (
265
- add_inner_thoughts_top_level and add_inner_thoughts_param_level
266
- ), "Can only place inner thoughts in one location in the grammar generator"
265
+ assert not (add_inner_thoughts_top_level and add_inner_thoughts_param_level), (
266
+ "Can only place inner thoughts in one location in the grammar generator"
267
+ )
267
268
 
268
269
  grammar_function_models = []
269
270
  # create_dynamic_model_from_function will add inner thoughts to the function parameters if add_inner_thoughts is True.
@@ -46,7 +46,7 @@ def get_completions_settings(defaults="simple") -> dict:
46
46
  with open(settings_file, "r", encoding="utf-8") as file:
47
47
  user_settings = json.load(file)
48
48
  if len(user_settings) > 0:
49
- printd(f"Updating base settings with the following user settings:\n{json_dumps(user_settings,indent=2)}")
49
+ printd(f"Updating base settings with the following user settings:\n{json_dumps(user_settings, indent=2)}")
50
50
  settings.update(user_settings)
51
51
  else:
52
52
  printd(f"'{settings_file}' was empty, ignoring...")
letta/orm/__init__.py CHANGED
@@ -22,6 +22,7 @@ from letta.orm.mcp_server import MCPServer
22
22
  from letta.orm.message import Message
23
23
  from letta.orm.organization import Organization
24
24
  from letta.orm.passage import ArchivalPassage, BasePassage, SourcePassage
25
+ from letta.orm.passage_tag import PassageTag
25
26
  from letta.orm.prompt import Prompt
26
27
  from letta.orm.provider import Provider
27
28
  from letta.orm.provider_trace import ProviderTrace
letta/orm/agent.py CHANGED
@@ -10,11 +10,10 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
10
10
  from letta.orm.block import Block
11
11
  from letta.orm.custom_columns import EmbeddingConfigColumn, LLMConfigColumn, ResponseFormatColumn, ToolRulesColumn
12
12
  from letta.orm.identity import Identity
13
- from letta.orm.mixins import OrganizationMixin, ProjectMixin
13
+ from letta.orm.mixins import OrganizationMixin, ProjectMixin, TemplateEntityMixin, TemplateMixin
14
14
  from letta.orm.organization import Organization
15
15
  from letta.orm.sqlalchemy_base import SqlalchemyBase
16
- from letta.schemas.agent import AgentState as PydanticAgentState
17
- from letta.schemas.agent import AgentType, get_prompt_template_for_agent_type
16
+ from letta.schemas.agent import AgentState as PydanticAgentState, AgentType, get_prompt_template_for_agent_type
18
17
  from letta.schemas.embedding_config import EmbeddingConfig
19
18
  from letta.schemas.llm_config import LLMConfig
20
19
  from letta.schemas.memory import Memory
@@ -32,7 +31,7 @@ if TYPE_CHECKING:
32
31
  from letta.orm.tool import Tool
33
32
 
34
33
 
35
- class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, AsyncAttrs):
34
+ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin, TemplateMixin, AsyncAttrs):
36
35
  __tablename__ = "agents"
37
36
  __pydantic_model__ = PydanticAgentState
38
37
  __table_args__ = (Index("ix_agents_created_at", "created_at", "id"),)
@@ -68,8 +67,6 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, AsyncAttrs):
68
67
  embedding_config: Mapped[Optional[EmbeddingConfig]] = mapped_column(
69
68
  EmbeddingConfigColumn, doc="the embedding configuration object for this agent."
70
69
  )
71
- template_id: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The id of the template the agent belongs to.")
72
- base_template_id: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The base template id of the agent.")
73
70
 
74
71
  # Tool rules
75
72
  tool_rules: Mapped[Optional[List[ToolRule]]] = mapped_column(ToolRulesColumn, doc="the tool rules for this agent.")
@@ -103,6 +100,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, AsyncAttrs):
103
100
 
104
101
  # indexing controls
105
102
  hidden: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True, default=None, doc="If set to True, the agent will be hidden.")
103
+ _vector_db_namespace: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="Private field for vector database namespace")
106
104
 
107
105
  # relationships
108
106
  organization: Mapped["Organization"] = relationship("Organization", back_populates="agents", lazy="raise")
@@ -208,6 +206,8 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, AsyncAttrs):
208
206
  "project_id": self.project_id,
209
207
  "template_id": self.template_id,
210
208
  "base_template_id": self.base_template_id,
209
+ "deployment_id": self.deployment_id,
210
+ "entity_id": self.entity_id,
211
211
  "tool_rules": self.tool_rules,
212
212
  "message_buffer_autoclear": self.message_buffer_autoclear,
213
213
  "created_by_id": self.created_by_id,
@@ -296,6 +296,8 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, AsyncAttrs):
296
296
  "project_id": self.project_id,
297
297
  "template_id": self.template_id,
298
298
  "base_template_id": self.base_template_id,
299
+ "deployment_id": self.deployment_id,
300
+ "entity_id": self.entity_id,
299
301
  "tool_rules": self.tool_rules,
300
302
  "message_buffer_autoclear": self.message_buffer_autoclear,
301
303
  "created_by_id": self.created_by_id,
letta/orm/archive.py CHANGED
@@ -2,12 +2,13 @@ import uuid
2
2
  from datetime import datetime, timezone
3
3
  from typing import TYPE_CHECKING, List, Optional
4
4
 
5
- from sqlalchemy import JSON, Index, String
5
+ from sqlalchemy import JSON, Enum, Index, String
6
6
  from sqlalchemy.orm import Mapped, mapped_column, relationship
7
7
 
8
8
  from letta.orm.mixins import OrganizationMixin
9
9
  from letta.orm.sqlalchemy_base import SqlalchemyBase
10
10
  from letta.schemas.archive import Archive as PydanticArchive
11
+ from letta.schemas.enums import VectorDBProvider
11
12
  from letta.settings import DatabaseChoice, settings
12
13
 
13
14
  if TYPE_CHECKING:
@@ -38,7 +39,14 @@ class Archive(SqlalchemyBase, OrganizationMixin):
38
39
  # archive-specific fields
39
40
  name: Mapped[str] = mapped_column(String, nullable=False, doc="The name of the archive")
40
41
  description: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="A description of the archive")
42
+ vector_db_provider: Mapped[VectorDBProvider] = mapped_column(
43
+ Enum(VectorDBProvider),
44
+ nullable=False,
45
+ default=VectorDBProvider.NATIVE,
46
+ doc="The vector database provider used for this archive's passages",
47
+ )
41
48
  metadata_: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, doc="Additional metadata for the archive")
49
+ _vector_db_namespace: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="Private field for vector database namespace")
42
50
 
43
51
  # relationships
44
52
  archives_agents: Mapped[List["ArchivesAgents"]] = relationship(
letta/orm/block.py CHANGED
@@ -6,17 +6,16 @@ from sqlalchemy.orm import Mapped, attributes, declared_attr, mapped_column, rel
6
6
  from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
7
7
  from letta.orm.block_history import BlockHistory
8
8
  from letta.orm.blocks_agents import BlocksAgents
9
- from letta.orm.mixins import OrganizationMixin, ProjectMixin
9
+ from letta.orm.mixins import OrganizationMixin, ProjectMixin, TemplateEntityMixin, TemplateMixin
10
10
  from letta.orm.sqlalchemy_base import SqlalchemyBase
11
- from letta.schemas.block import Block as PydanticBlock
12
- from letta.schemas.block import Human, Persona
11
+ from letta.schemas.block import Block as PydanticBlock, Human, Persona
13
12
 
14
13
  if TYPE_CHECKING:
15
14
  from letta.orm import Organization
16
15
  from letta.orm.identity import Identity
17
16
 
18
17
 
19
- class Block(OrganizationMixin, SqlalchemyBase, ProjectMixin):
18
+ class Block(OrganizationMixin, SqlalchemyBase, ProjectMixin, TemplateEntityMixin, TemplateMixin):
20
19
  """Blocks are sections of the LLM context, representing a specific part of the total Memory"""
21
20
 
22
21
  __tablename__ = "block"
@@ -38,7 +38,9 @@ class BlockHistory(OrganizationMixin, SqlalchemyBase):
38
38
 
39
39
  # Relationships
40
40
  block_id: Mapped[str] = mapped_column(
41
- String, ForeignKey("block.id", ondelete="CASCADE"), nullable=False # History deleted if Block is deleted
41
+ String,
42
+ ForeignKey("block.id", ondelete="CASCADE"),
43
+ nullable=False, # History deleted if Block is deleted
42
44
  )
43
45
 
44
46
  sequence_number: Mapped[int] = mapped_column(
letta/orm/group.py CHANGED
@@ -4,13 +4,12 @@ from typing import List, Optional
4
4
  from sqlalchemy import JSON, ForeignKey, String
5
5
  from sqlalchemy.orm import Mapped, mapped_column, relationship
6
6
 
7
- from letta.orm.mixins import OrganizationMixin, ProjectMixin
7
+ from letta.orm.mixins import OrganizationMixin, ProjectMixin, TemplateMixin
8
8
  from letta.orm.sqlalchemy_base import SqlalchemyBase
9
9
  from letta.schemas.group import Group as PydanticGroup
10
10
 
11
11
 
12
- class Group(SqlalchemyBase, OrganizationMixin, ProjectMixin):
13
-
12
+ class Group(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateMixin):
14
13
  __tablename__ = "groups"
15
14
  __pydantic_model__ = PydanticGroup
16
15
 
letta/orm/identity.py CHANGED
@@ -7,8 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
7
7
 
8
8
  from letta.orm.mixins import OrganizationMixin, ProjectMixin
9
9
  from letta.orm.sqlalchemy_base import SqlalchemyBase
10
- from letta.schemas.identity import Identity as PydanticIdentity
11
- from letta.schemas.identity import IdentityProperty
10
+ from letta.schemas.identity import Identity as PydanticIdentity, IdentityProperty
12
11
 
13
12
 
14
13
  class Identity(SqlalchemyBase, OrganizationMixin, ProjectMixin):
letta/orm/job.py CHANGED
@@ -7,8 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
7
7
  from letta.orm.mixins import UserMixin
8
8
  from letta.orm.sqlalchemy_base import SqlalchemyBase
9
9
  from letta.schemas.enums import JobStatus, JobType
10
- from letta.schemas.job import Job as PydanticJob
11
- from letta.schemas.job import LettaRequestConfig
10
+ from letta.schemas.job import Job as PydanticJob, LettaRequestConfig
12
11
 
13
12
  if TYPE_CHECKING:
14
13
  from letta.orm.job_messages import JobMessage
@@ -9,8 +9,7 @@ from letta.orm.custom_columns import AgentStepStateColumn, BatchRequestResultCol
9
9
  from letta.orm.mixins import AgentMixin, OrganizationMixin
10
10
  from letta.orm.sqlalchemy_base import SqlalchemyBase
11
11
  from letta.schemas.enums import AgentStepStatus, JobStatus
12
- from letta.schemas.llm_batch_job import AgentStepState
13
- from letta.schemas.llm_batch_job import LLMBatchItem as PydanticLLMBatchItem
12
+ from letta.schemas.llm_batch_job import AgentStepState, LLMBatchItem as PydanticLLMBatchItem
14
13
  from letta.schemas.llm_config import LLMConfig
15
14
 
16
15
 
letta/orm/message.py CHANGED
@@ -7,10 +7,8 @@ from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
7
7
  from letta.orm.custom_columns import MessageContentColumn, ToolCallColumn, ToolReturnColumn
8
8
  from letta.orm.mixins import AgentMixin, OrganizationMixin
9
9
  from letta.orm.sqlalchemy_base import SqlalchemyBase
10
- from letta.schemas.letta_message_content import MessageContent
11
- from letta.schemas.letta_message_content import TextContent as PydanticTextContent
12
- from letta.schemas.message import Message as PydanticMessage
13
- from letta.schemas.message import ToolReturn
10
+ from letta.schemas.letta_message_content import MessageContent, TextContent as PydanticTextContent
11
+ from letta.schemas.message import Message as PydanticMessage, ToolReturn
14
12
  from letta.settings import DatabaseChoice, settings
15
13
 
16
14
 
@@ -52,6 +50,12 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
52
50
  is_err: Mapped[Optional[bool]] = mapped_column(
53
51
  nullable=True, doc="Whether this message is part of an error step. Used only for debugging purposes."
54
52
  )
53
+ approval_request_id: Mapped[Optional[str]] = mapped_column(
54
+ nullable=True,
55
+ doc="The id of the approval request if this message is associated with a tool call request.",
56
+ )
57
+ approve: Mapped[Optional[bool]] = mapped_column(nullable=True, doc="Whether tool call is approved.")
58
+ denial_reason: Mapped[Optional[str]] = mapped_column(nullable=True, doc="The reason the tool call request was denied.")
55
59
 
56
60
  # Monotonically increasing sequence for efficient/correct listing
57
61
  sequence_id: Mapped[int] = mapped_column(
letta/orm/mixins.py CHANGED
@@ -78,3 +78,21 @@ class ArchiveMixin(Base):
78
78
  __abstract__ = True
79
79
 
80
80
  archive_id: Mapped[str] = mapped_column(String, ForeignKey("archives.id", ondelete="CASCADE"))
81
+
82
+
83
+ class TemplateMixin(Base):
84
+ """TemplateMixin for models that belong to a template."""
85
+
86
+ __abstract__ = True
87
+
88
+ base_template_id: Mapped[str] = mapped_column(nullable=True, doc="The id of the base template.")
89
+ template_id: Mapped[str] = mapped_column(nullable=True, doc="The id of the template.")
90
+ deployment_id: Mapped[str] = mapped_column(nullable=True, doc="The id of the deployment.")
91
+
92
+
93
+ class TemplateEntityMixin(Base):
94
+ """Mixin for models that belong to an entity (only used for templates)."""
95
+
96
+ __abstract__ = True
97
+
98
+ entity_id: Mapped[str] = mapped_column(nullable=True, doc="The id of the entity within the template.")
letta/orm/organization.py CHANGED
@@ -16,6 +16,7 @@ if TYPE_CHECKING:
16
16
  from letta.orm.llm_batch_job import LLMBatchJob
17
17
  from letta.orm.message import Message
18
18
  from letta.orm.passage import ArchivalPassage, SourcePassage
19
+ from letta.orm.passage_tag import PassageTag
19
20
  from letta.orm.provider import Provider
20
21
  from letta.orm.sandbox_config import AgentEnvironmentVariable, SandboxConfig, SandboxEnvironmentVariable
21
22
  from letta.orm.tool import Tool
@@ -56,6 +57,7 @@ class Organization(SqlalchemyBase):
56
57
  archival_passages: Mapped[List["ArchivalPassage"]] = relationship(
57
58
  "ArchivalPassage", back_populates="organization", cascade="all, delete-orphan"
58
59
  )
60
+ passage_tags: Mapped[List["PassageTag"]] = relationship("PassageTag", back_populates="organization", cascade="all, delete-orphan")
59
61
  archives: Mapped[List["Archive"]] = relationship("Archive", back_populates="organization", cascade="all, delete-orphan")
60
62
  providers: Mapped[List["Provider"]] = relationship("Provider", back_populates="organization", cascade="all, delete-orphan")
61
63
  identities: Mapped[List["Identity"]] = relationship("Identity", back_populates="organization", cascade="all, delete-orphan")
letta/orm/passage.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING
1
+ from typing import TYPE_CHECKING, List, Optional
2
2
 
3
3
  from sqlalchemy import JSON, Column, Index
4
4
  from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
@@ -27,6 +27,8 @@ class BasePassage(SqlalchemyBase, OrganizationMixin):
27
27
  text: Mapped[str] = mapped_column(doc="Passage text content")
28
28
  embedding_config: Mapped[dict] = mapped_column(EmbeddingConfigColumn, doc="Embedding configuration")
29
29
  metadata_: Mapped[dict] = mapped_column(JSON, doc="Additional metadata")
30
+ # dual storage: json column for fast retrieval, junction table for efficient queries
31
+ tags: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True, doc="Tags associated with this passage")
30
32
 
31
33
  # Vector embedding field based on database type
32
34
  if settings.database_engine is DatabaseChoice.POSTGRES:
@@ -75,6 +77,11 @@ class ArchivalPassage(BasePassage, ArchiveMixin):
75
77
 
76
78
  __tablename__ = "archival_passages"
77
79
 
80
+ # junction table for efficient tag queries (complements json column above)
81
+ passage_tags: Mapped[List["PassageTag"]] = relationship(
82
+ "PassageTag", back_populates="passage", cascade="all, delete-orphan", lazy="noload"
83
+ )
84
+
78
85
  @declared_attr
79
86
  def organization(cls) -> Mapped["Organization"]:
80
87
  return relationship("Organization", back_populates="archival_passages", lazy="selectin")
@@ -0,0 +1,55 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from sqlalchemy import ForeignKey, Index, String, UniqueConstraint
4
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
5
+
6
+ from letta.orm.mixins import OrganizationMixin
7
+ from letta.orm.sqlalchemy_base import SqlalchemyBase
8
+
9
+ if TYPE_CHECKING:
10
+ from letta.orm.organization import Organization
11
+ from letta.orm.passage import ArchivalPassage
12
+
13
+
14
+ class PassageTag(SqlalchemyBase, OrganizationMixin):
15
+ """Junction table for tags associated with passages.
16
+
17
+ Design: dual storage approach where tags are stored both in:
18
+ 1. JSON column in passages table (fast retrieval with passage data)
19
+ 2. This junction table (efficient DISTINCT/COUNT queries and filtering)
20
+ """
21
+
22
+ __tablename__ = "passage_tags"
23
+
24
+ __table_args__ = (
25
+ # ensure uniqueness of tag per passage
26
+ UniqueConstraint("passage_id", "tag", name="uq_passage_tag"),
27
+ # indexes for efficient queries
28
+ Index("ix_passage_tags_archive_id", "archive_id"),
29
+ Index("ix_passage_tags_tag", "tag"),
30
+ Index("ix_passage_tags_archive_tag", "archive_id", "tag"),
31
+ Index("ix_passage_tags_org_archive", "organization_id", "archive_id"),
32
+ )
33
+
34
+ # primary key
35
+ id: Mapped[str] = mapped_column(String, primary_key=True, doc="Unique identifier for the tag entry")
36
+
37
+ # tag value
38
+ tag: Mapped[str] = mapped_column(String, nullable=False, doc="The tag value")
39
+
40
+ # foreign keys
41
+ passage_id: Mapped[str] = mapped_column(
42
+ String, ForeignKey("archival_passages.id", ondelete="CASCADE"), nullable=False, doc="ID of the passage this tag belongs to"
43
+ )
44
+
45
+ archive_id: Mapped[str] = mapped_column(
46
+ String,
47
+ ForeignKey("archives.id", ondelete="CASCADE"),
48
+ nullable=False,
49
+ doc="ID of the archive this passage belongs to (denormalized for efficient queries)",
50
+ )
51
+
52
+ # relationships
53
+ passage: Mapped["ArchivalPassage"] = relationship("ArchivalPassage", back_populates="passage_tags", lazy="noload")
54
+
55
+ organization: Mapped["Organization"] = relationship("Organization", back_populates="passage_tags", lazy="selectin")
@@ -1,9 +1,7 @@
1
1
  import uuid
2
2
  from typing import TYPE_CHECKING, Dict, List, Optional
3
3
 
4
- from sqlalchemy import JSON
5
- from sqlalchemy import Enum as SqlEnum
6
- from sqlalchemy import Index, String, UniqueConstraint
4
+ from sqlalchemy import JSON, Enum as SqlEnum, Index, String, UniqueConstraint
7
5
  from sqlalchemy.orm import Mapped, mapped_column, relationship
8
6
 
9
7
  from letta.orm.mixins import AgentMixin, OrganizationMixin, SandboxConfigMixin
letta/orm/step.py CHANGED
@@ -7,7 +7,6 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
7
7
  from letta.orm.mixins import ProjectMixin
8
8
  from letta.orm.sqlalchemy_base import SqlalchemyBase
9
9
  from letta.schemas.enums import StepStatus
10
- from letta.schemas.letta_stop_reason import StopReasonType
11
10
  from letta.schemas.step import Step as PydanticStep
12
11
 
13
12
  if TYPE_CHECKING:
@@ -51,7 +50,7 @@ class Step(SqlalchemyBase, ProjectMixin):
51
50
  prompt_tokens: Mapped[int] = mapped_column(default=0, doc="Number of tokens in the prompt")
52
51
  total_tokens: Mapped[int] = mapped_column(default=0, doc="Total number of tokens processed by the agent")
53
52
  completion_tokens_details: Mapped[Optional[Dict]] = mapped_column(JSON, nullable=True, doc="metadata for the agent.")
54
- stop_reason: Mapped[Optional[StopReasonType]] = mapped_column(None, nullable=True, doc="The stop reason associated with this step.")
53
+ stop_reason: Mapped[Optional[str]] = mapped_column(None, nullable=True, doc="The stop reason associated with this step.")
55
54
  tags: Mapped[Optional[List]] = mapped_column(JSON, doc="Metadata tags.")
56
55
  tid: Mapped[Optional[str]] = mapped_column(None, nullable=True, doc="Transaction ID that processed the step.")
57
56
  trace_id: Mapped[Optional[str]] = mapped_column(None, nullable=True, doc="The trace id of the agent step.")
letta/orm/tool.py CHANGED
@@ -49,6 +49,7 @@ class Tool(SqlalchemyBase, OrganizationMixin):
49
49
  JSON, nullable=True, doc="Optional list of pip packages required by this tool."
50
50
  )
51
51
  npm_requirements: Mapped[list | None] = mapped_column(JSON, doc="Optional list of npm packages required by this tool.")
52
+ default_requires_approval: Mapped[bool] = mapped_column(nullable=True, doc="Whether or not to require approval.")
52
53
  metadata_: Mapped[Optional[dict]] = mapped_column(JSON, default=lambda: {}, doc="A dictionary of additional metadata for the tool.")
53
54
  # relationships
54
55
  organization: Mapped["Organization"] = relationship("Organization", back_populates="tools", lazy="selectin")
letta/otel/resource.py CHANGED
@@ -1,16 +1,16 @@
1
- import os
2
1
  import sys
3
2
  import uuid
4
3
 
5
4
  from opentelemetry.sdk.resources import Resource
6
5
 
7
6
  from letta import __version__ as letta_version
7
+ from letta.settings import settings
8
8
 
9
9
  _resources = {}
10
10
 
11
11
 
12
12
  def get_resource(service_name: str) -> Resource:
13
- _env = os.getenv("LETTA_ENVIRONMENT")
13
+ _env = settings.environment
14
14
  if service_name not in _resources:
15
15
  resource_dict = {
16
16
  "service.name": service_name,
letta/plugins/plugins.py CHANGED
@@ -37,7 +37,7 @@ def get_plugin(plugin_type: str):
37
37
  return plugin
38
38
  elif type(plugin).__name__ == "class":
39
39
  if plugin_register["protocol"] and not isinstance(plugin, type(plugin_register["protocol"])):
40
- raise TypeError(f'{plugin} does not implement {type(plugin_register["protocol"]).__name__}')
40
+ raise TypeError(f"{plugin} does not implement {type(plugin_register['protocol']).__name__}")
41
41
  return plugin()
42
42
  raise TypeError("Unknown plugin type")
43
43