letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.12.0.dev20251009104148__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 (151) hide show
  1. letta/__init__.py +1 -1
  2. letta/adapters/letta_llm_adapter.py +1 -0
  3. letta/adapters/letta_llm_request_adapter.py +0 -1
  4. letta/adapters/letta_llm_stream_adapter.py +7 -2
  5. letta/adapters/simple_llm_request_adapter.py +88 -0
  6. letta/adapters/simple_llm_stream_adapter.py +192 -0
  7. letta/agents/agent_loop.py +6 -0
  8. letta/agents/ephemeral_summary_agent.py +2 -1
  9. letta/agents/helpers.py +142 -6
  10. letta/agents/letta_agent.py +13 -33
  11. letta/agents/letta_agent_batch.py +2 -4
  12. letta/agents/letta_agent_v2.py +87 -77
  13. letta/agents/letta_agent_v3.py +927 -0
  14. letta/agents/voice_agent.py +2 -6
  15. letta/constants.py +8 -4
  16. letta/database_utils.py +161 -0
  17. letta/errors.py +40 -0
  18. letta/functions/function_sets/base.py +84 -4
  19. letta/functions/function_sets/multi_agent.py +0 -3
  20. letta/functions/schema_generator.py +113 -71
  21. letta/groups/dynamic_multi_agent.py +3 -2
  22. letta/groups/helpers.py +1 -2
  23. letta/groups/round_robin_multi_agent.py +3 -2
  24. letta/groups/sleeptime_multi_agent.py +3 -2
  25. letta/groups/sleeptime_multi_agent_v2.py +1 -1
  26. letta/groups/sleeptime_multi_agent_v3.py +17 -17
  27. letta/groups/supervisor_multi_agent.py +84 -80
  28. letta/helpers/converters.py +3 -0
  29. letta/helpers/message_helper.py +4 -0
  30. letta/helpers/tool_rule_solver.py +92 -5
  31. letta/interfaces/anthropic_streaming_interface.py +409 -0
  32. letta/interfaces/gemini_streaming_interface.py +296 -0
  33. letta/interfaces/openai_streaming_interface.py +752 -1
  34. letta/llm_api/anthropic_client.py +127 -16
  35. letta/llm_api/bedrock_client.py +4 -2
  36. letta/llm_api/deepseek_client.py +4 -1
  37. letta/llm_api/google_vertex_client.py +124 -42
  38. letta/llm_api/groq_client.py +4 -1
  39. letta/llm_api/llm_api_tools.py +11 -4
  40. letta/llm_api/llm_client_base.py +6 -2
  41. letta/llm_api/openai.py +32 -2
  42. letta/llm_api/openai_client.py +423 -18
  43. letta/llm_api/xai_client.py +4 -1
  44. letta/main.py +9 -5
  45. letta/memory.py +1 -0
  46. letta/orm/__init__.py +2 -1
  47. letta/orm/agent.py +10 -0
  48. letta/orm/block.py +7 -16
  49. letta/orm/blocks_agents.py +8 -2
  50. letta/orm/files_agents.py +2 -0
  51. letta/orm/job.py +7 -5
  52. letta/orm/mcp_oauth.py +1 -0
  53. letta/orm/message.py +21 -6
  54. letta/orm/organization.py +2 -0
  55. letta/orm/provider.py +6 -2
  56. letta/orm/run.py +71 -0
  57. letta/orm/run_metrics.py +82 -0
  58. letta/orm/sandbox_config.py +7 -1
  59. letta/orm/sqlalchemy_base.py +0 -306
  60. letta/orm/step.py +6 -5
  61. letta/orm/step_metrics.py +5 -5
  62. letta/otel/tracing.py +28 -3
  63. letta/plugins/defaults.py +4 -4
  64. letta/prompts/system_prompts/__init__.py +2 -0
  65. letta/prompts/system_prompts/letta_v1.py +25 -0
  66. letta/schemas/agent.py +3 -2
  67. letta/schemas/agent_file.py +9 -3
  68. letta/schemas/block.py +23 -10
  69. letta/schemas/enums.py +21 -2
  70. letta/schemas/job.py +17 -4
  71. letta/schemas/letta_message_content.py +71 -2
  72. letta/schemas/letta_stop_reason.py +5 -5
  73. letta/schemas/llm_config.py +53 -3
  74. letta/schemas/memory.py +1 -1
  75. letta/schemas/message.py +564 -117
  76. letta/schemas/openai/responses_request.py +64 -0
  77. letta/schemas/providers/__init__.py +2 -0
  78. letta/schemas/providers/anthropic.py +16 -0
  79. letta/schemas/providers/ollama.py +115 -33
  80. letta/schemas/providers/openrouter.py +52 -0
  81. letta/schemas/providers/vllm.py +2 -1
  82. letta/schemas/run.py +48 -42
  83. letta/schemas/run_metrics.py +21 -0
  84. letta/schemas/step.py +2 -2
  85. letta/schemas/step_metrics.py +1 -1
  86. letta/schemas/tool.py +15 -107
  87. letta/schemas/tool_rule.py +88 -5
  88. letta/serialize_schemas/marshmallow_agent.py +1 -0
  89. letta/server/db.py +79 -408
  90. letta/server/rest_api/app.py +61 -10
  91. letta/server/rest_api/dependencies.py +14 -0
  92. letta/server/rest_api/redis_stream_manager.py +19 -8
  93. letta/server/rest_api/routers/v1/agents.py +364 -292
  94. letta/server/rest_api/routers/v1/blocks.py +14 -20
  95. letta/server/rest_api/routers/v1/identities.py +45 -110
  96. letta/server/rest_api/routers/v1/internal_templates.py +21 -0
  97. letta/server/rest_api/routers/v1/jobs.py +23 -6
  98. letta/server/rest_api/routers/v1/messages.py +1 -1
  99. letta/server/rest_api/routers/v1/runs.py +149 -99
  100. letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
  101. letta/server/rest_api/routers/v1/tools.py +281 -594
  102. letta/server/rest_api/routers/v1/voice.py +1 -1
  103. letta/server/rest_api/streaming_response.py +29 -29
  104. letta/server/rest_api/utils.py +122 -64
  105. letta/server/server.py +160 -887
  106. letta/services/agent_manager.py +236 -919
  107. letta/services/agent_serialization_manager.py +16 -0
  108. letta/services/archive_manager.py +0 -100
  109. letta/services/block_manager.py +211 -168
  110. letta/services/context_window_calculator/token_counter.py +1 -1
  111. letta/services/file_manager.py +1 -1
  112. letta/services/files_agents_manager.py +24 -33
  113. letta/services/group_manager.py +0 -142
  114. letta/services/helpers/agent_manager_helper.py +7 -2
  115. letta/services/helpers/run_manager_helper.py +69 -0
  116. letta/services/job_manager.py +96 -411
  117. letta/services/lettuce/__init__.py +6 -0
  118. letta/services/lettuce/lettuce_client_base.py +86 -0
  119. letta/services/mcp_manager.py +38 -6
  120. letta/services/message_manager.py +165 -362
  121. letta/services/organization_manager.py +0 -36
  122. letta/services/passage_manager.py +0 -345
  123. letta/services/provider_manager.py +0 -80
  124. letta/services/run_manager.py +364 -0
  125. letta/services/sandbox_config_manager.py +0 -234
  126. letta/services/step_manager.py +62 -39
  127. letta/services/summarizer/summarizer.py +9 -7
  128. letta/services/telemetry_manager.py +0 -16
  129. letta/services/tool_executor/builtin_tool_executor.py +35 -0
  130. letta/services/tool_executor/core_tool_executor.py +397 -2
  131. letta/services/tool_executor/files_tool_executor.py +3 -3
  132. letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
  133. letta/services/tool_executor/tool_execution_manager.py +6 -8
  134. letta/services/tool_executor/tool_executor_base.py +3 -3
  135. letta/services/tool_manager.py +85 -339
  136. letta/services/tool_sandbox/base.py +24 -13
  137. letta/services/tool_sandbox/e2b_sandbox.py +16 -1
  138. letta/services/tool_schema_generator.py +123 -0
  139. letta/services/user_manager.py +0 -99
  140. letta/settings.py +20 -4
  141. letta/system.py +5 -1
  142. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +3 -5
  143. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +146 -135
  144. letta/agents/temporal/activities/__init__.py +0 -4
  145. letta/agents/temporal/activities/example_activity.py +0 -7
  146. letta/agents/temporal/activities/prepare_messages.py +0 -10
  147. letta/agents/temporal/temporal_agent_workflow.py +0 -56
  148. letta/agents/temporal/types.py +0 -25
  149. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
  150. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
  151. {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/licenses/LICENSE +0 -0
@@ -8,6 +8,7 @@ from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
8
8
  from letta.llm_api.openai_client import OpenAIClient
9
9
  from letta.otel.tracing import trace_method
10
10
  from letta.schemas.embedding_config import EmbeddingConfig
11
+ from letta.schemas.enums import AgentType
11
12
  from letta.schemas.llm_config import LLMConfig
12
13
  from letta.schemas.message import Message as PydanticMessage
13
14
  from letta.settings import model_settings
@@ -23,12 +24,14 @@ class XAIClient(OpenAIClient):
23
24
  @trace_method
24
25
  def build_request_data(
25
26
  self,
27
+ agent_type: AgentType,
26
28
  messages: List[PydanticMessage],
27
29
  llm_config: LLMConfig,
28
30
  tools: Optional[List[dict]] = None,
29
31
  force_tool_call: Optional[str] = None,
32
+ requires_subsequent_tool_call: bool = False,
30
33
  ) -> dict:
31
- data = super().build_request_data(messages, llm_config, tools, force_tool_call)
34
+ data = super().build_request_data(agent_type, messages, llm_config, tools, force_tool_call, requires_subsequent_tool_call)
32
35
 
33
36
  # Specific bug for the mini models (as of Apr 14, 2025)
34
37
  # 400 - {'code': 'Client specified an invalid argument', 'error': 'Argument not supported on this model: presencePenalty'}
letta/main.py CHANGED
@@ -3,12 +3,16 @@ import os
3
3
  import typer
4
4
 
5
5
  from letta.cli.cli import server
6
- from letta.cli.cli_load import app as load_app
7
-
8
- # disable composio print on exit
9
- os.environ["COMPOSIO_DISABLE_VERSION_CHECK"] = "true"
10
6
 
11
7
  app = typer.Typer(pretty_exceptions_enable=False)
8
+
9
+ # Register server as both the default command and as a subcommand
12
10
  app.command(name="server")(server)
13
11
 
14
- app.add_typer(load_app, name="load")
12
+
13
+ # Also make server the default when no command is specified
14
+ @app.callback(invoke_without_command=True)
15
+ def main(ctx: typer.Context):
16
+ if ctx.invoked_subcommand is None:
17
+ # If no subcommand is specified, run the server
18
+ server()
letta/memory.py CHANGED
@@ -91,6 +91,7 @@ def summarize_messages(
91
91
  # TODO: we can just directly call the LLM here?
92
92
  if llm_client:
93
93
  response = llm_client.send_llm_request(
94
+ agent_type=agent_state.agent_type,
94
95
  messages=message_sequence,
95
96
  llm_config=llm_config_no_inner_thoughts,
96
97
  )
letta/orm/__init__.py CHANGED
@@ -15,7 +15,6 @@ from letta.orm.identities_agents import IdentitiesAgents
15
15
  from letta.orm.identities_blocks import IdentitiesBlocks
16
16
  from letta.orm.identity import Identity
17
17
  from letta.orm.job import Job
18
- from letta.orm.job_messages import JobMessage
19
18
  from letta.orm.llm_batch_items import LLMBatchItem
20
19
  from letta.orm.llm_batch_job import LLMBatchJob
21
20
  from letta.orm.mcp_oauth import MCPOAuth
@@ -27,6 +26,8 @@ from letta.orm.passage_tag import PassageTag
27
26
  from letta.orm.prompt import Prompt
28
27
  from letta.orm.provider import Provider
29
28
  from letta.orm.provider_trace import ProviderTrace
29
+ from letta.orm.run import Run
30
+ from letta.orm.run_metrics import RunMetrics
30
31
  from letta.orm.sandbox_config import AgentEnvironmentVariable, SandboxConfig, SandboxEnvironmentVariable
31
32
  from letta.orm.source import Source
32
33
  from letta.orm.sources_agents import SourcesAgents
letta/orm/agent.py CHANGED
@@ -28,6 +28,7 @@ if TYPE_CHECKING:
28
28
  from letta.orm.files_agents import FileAgent
29
29
  from letta.orm.identity import Identity
30
30
  from letta.orm.organization import Organization
31
+ from letta.orm.run import Run
31
32
  from letta.orm.source import Source
32
33
  from letta.orm.tool import Tool
33
34
 
@@ -38,6 +39,8 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
38
39
  __table_args__ = (
39
40
  Index("ix_agents_created_at", "created_at", "id"),
40
41
  Index("ix_agents_organization_id", "organization_id"),
42
+ Index("ix_agents_organization_id_deployment_id", "organization_id", "deployment_id"),
43
+ Index("ix_agents_project_id", "project_id"),
41
44
  )
42
45
 
43
46
  # agent generates its own id
@@ -132,6 +135,13 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin
132
135
  lazy="selectin",
133
136
  doc="Tags associated with the agent.",
134
137
  )
138
+ runs: Mapped[List["Run"]] = relationship(
139
+ "Run",
140
+ back_populates="agent",
141
+ cascade="all, delete-orphan",
142
+ lazy="selectin",
143
+ doc="Runs associated with the agent.",
144
+ )
135
145
  identities: Mapped[List["Identity"]] = relationship(
136
146
  "Identity",
137
147
  secondary="identities_agents",
letta/orm/block.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from typing import TYPE_CHECKING, List, Optional, Type
2
2
 
3
3
  from sqlalchemy import JSON, BigInteger, ForeignKey, Index, Integer, String, UniqueConstraint, event
4
- from sqlalchemy.orm import Mapped, attributes, declared_attr, mapped_column, relationship
4
+ from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
5
5
 
6
6
  from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
7
7
  from letta.orm.block_history import BlockHistory
@@ -25,6 +25,12 @@ class Block(OrganizationMixin, SqlalchemyBase, ProjectMixin, TemplateEntityMixin
25
25
  UniqueConstraint("id", "label", name="unique_block_id_label"),
26
26
  Index("created_at_label_idx", "created_at", "label"),
27
27
  Index("ix_block_label", "label"),
28
+ Index("ix_block_organization_id", "organization_id"),
29
+ Index("ix_block_project_id", "project_id"),
30
+ Index("ix_block_is_template", "is_template"),
31
+ Index("ix_block_hidden", "hidden"),
32
+ Index("ix_block_org_project_template", "organization_id", "project_id", "is_template"),
33
+ Index("ix_block_organization_id_deployment_id", "organization_id", "deployment_id"),
28
34
  )
29
35
 
30
36
  template_name: Mapped[Optional[str]] = mapped_column(
@@ -104,21 +110,6 @@ class Block(OrganizationMixin, SqlalchemyBase, ProjectMixin, TemplateEntityMixin
104
110
  ) # Helps manage potential FK cycles
105
111
 
106
112
 
107
- @event.listens_for(Block, "after_update") # Changed from 'before_update'
108
- def block_before_update(mapper, connection, target):
109
- """Handle updating BlocksAgents when a block's label changes."""
110
- label_history = attributes.get_history(target, "label")
111
- if not label_history.has_changes():
112
- return
113
-
114
- blocks_agents = BlocksAgents.__table__
115
- connection.execute(
116
- blocks_agents.update()
117
- .where(blocks_agents.c.block_id == target.id, blocks_agents.c.block_label == label_history.deleted[0])
118
- .values(block_label=label_history.added[0])
119
- )
120
-
121
-
122
113
  @event.listens_for(Block, "before_insert")
123
114
  @event.listens_for(Block, "before_update")
124
115
  def validate_value_length(mapper, connection, target):
@@ -15,7 +15,13 @@ class BlocksAgents(Base):
15
15
  name="unique_label_per_agent",
16
16
  ),
17
17
  ForeignKeyConstraint(
18
- ["block_id", "block_label"], ["block.id", "block.label"], name="fk_block_id_label", deferrable=True, initially="DEFERRED"
18
+ ["block_id", "block_label"],
19
+ ["block.id", "block.label"],
20
+ name="fk_block_id_label",
21
+ onupdate="CASCADE",
22
+ ondelete="CASCADE",
23
+ deferrable=True,
24
+ initially="IMMEDIATE",
19
25
  ),
20
26
  UniqueConstraint("agent_id", "block_id", name="unique_agent_block"),
21
27
  Index("ix_blocks_agents_block_label_agent_id", "block_label", "agent_id"),
@@ -24,6 +30,6 @@ class BlocksAgents(Base):
24
30
  )
25
31
 
26
32
  # unique agent + block label
27
- agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id"), primary_key=True)
33
+ agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id", ondelete="CASCADE"), primary_key=True)
28
34
  block_id: Mapped[str] = mapped_column(String, primary_key=True)
29
35
  block_label: Mapped[str] = mapped_column(String, primary_key=True)
letta/orm/files_agents.py CHANGED
@@ -32,6 +32,8 @@ class FileAgent(SqlalchemyBase, OrganizationMixin):
32
32
  # helpful indexes for look-ups
33
33
  Index("ix_file_agent", "file_id", "agent_id"),
34
34
  Index("ix_agent_filename", "agent_id", "file_name"),
35
+ # improve lookups by agent alone (e.g., WHERE agent_id IN (...))
36
+ Index("ix_files_agents_agent_id", "agent_id"),
35
37
  )
36
38
  __pydantic_model__ = PydanticFileAgent
37
39
 
letta/orm/job.py CHANGED
@@ -11,10 +11,8 @@ from letta.schemas.job import Job as PydanticJob, LettaRequestConfig
11
11
  from letta.schemas.letta_stop_reason import StopReasonType
12
12
 
13
13
  if TYPE_CHECKING:
14
- from letta.orm.job_messages import JobMessage
15
14
  from letta.orm.message import Message
16
15
  from letta.orm.organization import Organization
17
- from letta.orm.step import Step
18
16
  from letta.orm.user import User
19
17
 
20
18
 
@@ -25,11 +23,17 @@ class Job(SqlalchemyBase, UserMixin):
25
23
 
26
24
  __tablename__ = "jobs"
27
25
  __pydantic_model__ = PydanticJob
28
- __table_args__ = (Index("ix_jobs_created_at", "created_at", "id"),)
26
+ __table_args__ = (
27
+ Index("ix_jobs_created_at", "created_at", "id"),
28
+ Index("ix_jobs_user_id", "user_id"),
29
+ )
29
30
 
30
31
  status: Mapped[JobStatus] = mapped_column(String, default=JobStatus.created, doc="The current status of the job.")
31
32
  completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True, doc="The unix timestamp of when the job was completed.")
32
33
  stop_reason: Mapped[Optional[StopReasonType]] = mapped_column(String, nullable=True, doc="The reason why the job was stopped.")
34
+ background: Mapped[Optional[bool]] = mapped_column(
35
+ Boolean, nullable=True, default=False, doc="Whether the job was created in background mode."
36
+ )
33
37
  metadata_: Mapped[Optional[dict]] = mapped_column(JSON, doc="The metadata of the job.")
34
38
  job_type: Mapped[JobType] = mapped_column(
35
39
  String,
@@ -55,8 +59,6 @@ class Job(SqlalchemyBase, UserMixin):
55
59
 
56
60
  # relationships
57
61
  user: Mapped["User"] = relationship("User", back_populates="jobs")
58
- job_messages: Mapped[List["JobMessage"]] = relationship("JobMessage", back_populates="job", cascade="all, delete-orphan")
59
- steps: Mapped[List["Step"]] = relationship("Step", back_populates="job", cascade="save-update")
60
62
  # organization relationship (nullable for backward compatibility)
61
63
  organization: Mapped[Optional["Organization"]] = relationship("Organization", back_populates="jobs")
62
64
 
letta/orm/mcp_oauth.py CHANGED
@@ -35,6 +35,7 @@ class MCPOAuth(SqlalchemyBase, OrganizationMixin, UserMixin):
35
35
  # OAuth flow data
36
36
  authorization_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth authorization URL")
37
37
  authorization_code: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth authorization code")
38
+ authorization_code_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted OAuth authorization code")
38
39
 
39
40
  # Token data
40
41
  access_token: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="OAuth access token")
letta/orm/message.py CHANGED
@@ -7,7 +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, TextContent as PydanticTextContent
10
+ from letta.schemas.enums import MessageRole
11
+ from letta.schemas.letta_message_content import MessageContent, TextContent, TextContent as PydanticTextContent
11
12
  from letta.schemas.message import Message as PydanticMessage, ToolReturn
12
13
  from letta.settings import DatabaseChoice, settings
13
14
 
@@ -21,6 +22,9 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
21
22
  Index("ix_messages_created_at", "created_at", "id"),
22
23
  Index("ix_messages_agent_sequence", "agent_id", "sequence_id"),
23
24
  Index("ix_messages_org_agent", "organization_id", "agent_id"),
25
+ Index("ix_messages_run_id", "run_id"),
26
+ # Composite index for optimizing the frequently-run query:
27
+ Index("ix_messages_run_sequence", "run_id", "sequence_id"),
24
28
  )
25
29
  __pydantic_model__ = PydanticMessage
26
30
 
@@ -35,6 +39,9 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
35
39
  step_id: Mapped[Optional[str]] = mapped_column(
36
40
  ForeignKey("steps.id", ondelete="SET NULL"), nullable=True, doc="ID of the step that this message belongs to"
37
41
  )
42
+ run_id: Mapped[Optional[str]] = mapped_column(
43
+ ForeignKey("runs.id", ondelete="SET NULL"), nullable=True, doc="ID of the run that this message belongs to"
44
+ )
38
45
  otid: Mapped[Optional[str]] = mapped_column(nullable=True, doc="The offline threading ID associated with this message")
39
46
  tool_returns: Mapped[List[ToolReturn]] = mapped_column(
40
47
  ToolReturnColumn, nullable=True, doc="Tool execution return information for prior tool calls"
@@ -68,11 +75,7 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
68
75
  # Relationships
69
76
  organization: Mapped["Organization"] = relationship("Organization", back_populates="messages", lazy="raise")
70
77
  step: Mapped["Step"] = relationship("Step", back_populates="messages", lazy="selectin")
71
-
72
- # Job relationship
73
- job_message: Mapped[Optional["JobMessage"]] = relationship(
74
- "JobMessage", back_populates="message", uselist=False, cascade="all, delete-orphan", single_parent=True
75
- )
78
+ run: Mapped["Run"] = relationship("Run", back_populates="messages", lazy="selectin")
76
79
 
77
80
  @property
78
81
  def job(self) -> Optional["Job"]:
@@ -87,6 +90,18 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
87
90
  # If there are no tool calls, set tool_calls to None
88
91
  if self.tool_calls is None or len(self.tool_calls) == 0:
89
92
  model.tool_calls = None
93
+
94
+ # Handle legacy case of tool message with single tool return + single text content
95
+ if (
96
+ self.role == MessageRole.tool
97
+ and self.tool_returns
98
+ and len(self.tool_returns) == 1
99
+ and self.content
100
+ and len(self.content) == 1
101
+ and isinstance(self.content[0], TextContent)
102
+ ):
103
+ self.tool_returns[0].func_response = self.content[0].text
104
+
90
105
  return model
91
106
 
92
107
 
letta/orm/organization.py CHANGED
@@ -19,6 +19,7 @@ if TYPE_CHECKING:
19
19
  from letta.orm.passage import ArchivalPassage, SourcePassage
20
20
  from letta.orm.passage_tag import PassageTag
21
21
  from letta.orm.provider import Provider
22
+ from letta.orm.run import Run
22
23
  from letta.orm.sandbox_config import AgentEnvironmentVariable, SandboxConfig, SandboxEnvironmentVariable
23
24
  from letta.orm.tool import Tool
24
25
  from letta.orm.user import User
@@ -68,3 +69,4 @@ class Organization(SqlalchemyBase):
68
69
  "LLMBatchItem", back_populates="organization", cascade="all, delete-orphan"
69
70
  )
70
71
  jobs: Mapped[List["Job"]] = relationship("Job", back_populates="organization", cascade="all, delete-orphan")
72
+ runs: Mapped[List["Run"]] = relationship("Run", back_populates="organization", cascade="all, delete-orphan")
letta/orm/provider.py CHANGED
@@ -1,6 +1,6 @@
1
- from typing import TYPE_CHECKING
1
+ from typing import TYPE_CHECKING, Optional
2
2
 
3
- from sqlalchemy import UniqueConstraint
3
+ from sqlalchemy import Text, UniqueConstraint
4
4
  from sqlalchemy.orm import Mapped, mapped_column, relationship
5
5
 
6
6
  from letta.orm.mixins import OrganizationMixin
@@ -33,5 +33,9 @@ class Provider(SqlalchemyBase, OrganizationMixin):
33
33
  region: Mapped[str] = mapped_column(nullable=True, doc="Region used for requests to the provider.")
34
34
  api_version: Mapped[str] = mapped_column(nullable=True, doc="API version used for requests to the provider.")
35
35
 
36
+ # encrypted columns
37
+ api_key_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted API key or secret key for the provider.")
38
+ access_key_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted access key for the provider.")
39
+
36
40
  # relationships
37
41
  organization: Mapped["Organization"] = relationship("Organization", back_populates="providers")
letta/orm/run.py ADDED
@@ -0,0 +1,71 @@
1
+ import uuid
2
+ from datetime import datetime
3
+ from typing import TYPE_CHECKING, List, Optional
4
+
5
+ from sqlalchemy import JSON, BigInteger, Boolean, DateTime, ForeignKey, Index, String
6
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
7
+
8
+ from letta.orm.mixins import OrganizationMixin, ProjectMixin, TemplateMixin
9
+ from letta.orm.sqlalchemy_base import SqlalchemyBase
10
+ from letta.schemas.enums import RunStatus
11
+ from letta.schemas.job import LettaRequestConfig
12
+ from letta.schemas.letta_stop_reason import StopReasonType
13
+ from letta.schemas.run import Run as PydanticRun
14
+
15
+ if TYPE_CHECKING:
16
+ from letta.orm.agent import Agent
17
+ from letta.orm.message import Message
18
+ from letta.orm.organization import Organization
19
+ from letta.orm.step import Step
20
+
21
+
22
+ class Run(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateMixin):
23
+ """Runs are created when agents process messages and represent a conversation or processing session.
24
+ Unlike Jobs, Runs are specifically tied to agent interactions and message processing.
25
+ """
26
+
27
+ __tablename__ = "runs"
28
+ __pydantic_model__ = PydanticRun
29
+ __table_args__ = (
30
+ Index("ix_runs_created_at", "created_at", "id"),
31
+ Index("ix_runs_agent_id", "agent_id"),
32
+ Index("ix_runs_organization_id", "organization_id"),
33
+ )
34
+
35
+ # Generate run ID with run- prefix
36
+ id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"run-{uuid.uuid4()}")
37
+
38
+ # Core run fields
39
+ status: Mapped[RunStatus] = mapped_column(String, default=RunStatus.created, doc="The current status of the run.")
40
+ completed_at: Mapped[Optional[datetime]] = mapped_column(nullable=True, doc="The unix timestamp of when the run was completed.")
41
+ stop_reason: Mapped[Optional[StopReasonType]] = mapped_column(String, nullable=True, doc="The reason why the run was stopped.")
42
+ background: Mapped[Optional[bool]] = mapped_column(
43
+ Boolean, nullable=True, default=False, doc="Whether the run was created in background mode."
44
+ )
45
+ metadata_: Mapped[Optional[dict]] = mapped_column(JSON, doc="The metadata of the run.")
46
+ request_config: Mapped[Optional[LettaRequestConfig]] = mapped_column(
47
+ JSON, nullable=True, doc="The request configuration for the run, stored as JSON."
48
+ )
49
+
50
+ # Agent relationship - A run belongs to one agent
51
+ agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id"), nullable=False, doc="The agent that owns this run.")
52
+
53
+ # Callback related columns
54
+ callback_url: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="When set, POST to this URL after run completion.")
55
+ callback_sent_at: Mapped[Optional[datetime]] = mapped_column(nullable=True, doc="Timestamp when the callback was last attempted.")
56
+ callback_status_code: Mapped[Optional[int]] = mapped_column(nullable=True, doc="HTTP status code returned by the callback endpoint.")
57
+ callback_error: Mapped[Optional[str]] = mapped_column(
58
+ nullable=True, doc="Optional error message from attempting to POST the callback endpoint."
59
+ )
60
+
61
+ # Timing metrics (in nanoseconds for precision)
62
+ ttft_ns: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True, doc="Time to first token in nanoseconds")
63
+ total_duration_ns: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True, doc="Total run duration in nanoseconds")
64
+
65
+ # Relationships
66
+ agent: Mapped["Agent"] = relationship("Agent", back_populates="runs")
67
+ organization: Mapped[Optional["Organization"]] = relationship("Organization", back_populates="runs")
68
+
69
+ # Steps that are part of this run
70
+ steps: Mapped[List["Step"]] = relationship("Step", back_populates="run", cascade="all, delete-orphan")
71
+ messages: Mapped[List["Message"]] = relationship("Message", back_populates="run", cascade="all, delete-orphan")
@@ -0,0 +1,82 @@
1
+ from datetime import datetime, timezone
2
+ from typing import TYPE_CHECKING, Optional
3
+
4
+ from sqlalchemy import BigInteger, ForeignKey, Integer, String
5
+ from sqlalchemy.ext.asyncio import AsyncSession
6
+ from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
7
+
8
+ from letta.orm.mixins import AgentMixin, OrganizationMixin, ProjectMixin, TemplateMixin
9
+ from letta.orm.sqlalchemy_base import SqlalchemyBase
10
+ from letta.schemas.run_metrics import RunMetrics as PydanticRunMetrics
11
+ from letta.schemas.user import User
12
+ from letta.settings import DatabaseChoice, settings
13
+
14
+ if TYPE_CHECKING:
15
+ from letta.orm.agent import Agent
16
+ from letta.orm.run import Run
17
+ from letta.orm.step import Step
18
+
19
+
20
+ class RunMetrics(SqlalchemyBase, ProjectMixin, AgentMixin, OrganizationMixin, TemplateMixin):
21
+ """Tracks performance metrics for agent steps."""
22
+
23
+ __tablename__ = "run_metrics"
24
+ __pydantic_model__ = PydanticRunMetrics
25
+
26
+ id: Mapped[str] = mapped_column(
27
+ ForeignKey("runs.id", ondelete="CASCADE"),
28
+ primary_key=True,
29
+ doc="The unique identifier of the run this metric belongs to (also serves as PK)",
30
+ )
31
+ run_start_ns: Mapped[Optional[int]] = mapped_column(
32
+ BigInteger,
33
+ nullable=True,
34
+ doc="The timestamp of the start of the run in nanoseconds",
35
+ )
36
+ run_ns: Mapped[Optional[int]] = mapped_column(
37
+ BigInteger,
38
+ nullable=True,
39
+ doc="Total time for the run in nanoseconds",
40
+ )
41
+ num_steps: Mapped[Optional[int]] = mapped_column(
42
+ Integer,
43
+ nullable=True,
44
+ doc="The number of steps in the run",
45
+ )
46
+ run: Mapped[Optional["Run"]] = relationship("Run", foreign_keys=[id])
47
+ agent: Mapped[Optional["Agent"]] = relationship("Agent")
48
+
49
+ def create(
50
+ self,
51
+ db_session: Session,
52
+ actor: Optional[User] = None,
53
+ no_commit: bool = False,
54
+ ) -> "RunMetrics":
55
+ """Override create to handle SQLite timestamp issues"""
56
+ # For SQLite, explicitly set timestamps as server_default may not work
57
+ if settings.database_engine == DatabaseChoice.SQLITE:
58
+ now = datetime.now(timezone.utc)
59
+ if not self.created_at:
60
+ self.created_at = now
61
+ if not self.updated_at:
62
+ self.updated_at = now
63
+
64
+ return super().create(db_session, actor=actor, no_commit=no_commit)
65
+
66
+ async def create_async(
67
+ self,
68
+ db_session: AsyncSession,
69
+ actor: Optional[User] = None,
70
+ no_commit: bool = False,
71
+ no_refresh: bool = False,
72
+ ) -> "RunMetrics":
73
+ """Override create_async to handle SQLite timestamp issues"""
74
+ # For SQLite, explicitly set timestamps as server_default may not work
75
+ if settings.database_engine == DatabaseChoice.SQLITE:
76
+ now = datetime.now(timezone.utc)
77
+ if not self.created_at:
78
+ self.created_at = now
79
+ if not self.updated_at:
80
+ self.updated_at = now
81
+
82
+ return await super().create_async(db_session, actor=actor, no_commit=no_commit, no_refresh=no_refresh)
@@ -1,7 +1,7 @@
1
1
  import uuid
2
2
  from typing import TYPE_CHECKING, Dict, List, Optional
3
3
 
4
- from sqlalchemy import JSON, Enum as SqlEnum, Index, String, UniqueConstraint
4
+ from sqlalchemy import JSON, Enum as SqlEnum, Index, String, Text, UniqueConstraint
5
5
  from sqlalchemy.orm import Mapped, mapped_column, relationship
6
6
 
7
7
  from letta.orm.mixins import AgentMixin, OrganizationMixin, SandboxConfigMixin
@@ -49,6 +49,9 @@ class SandboxEnvironmentVariable(SqlalchemyBase, OrganizationMixin, SandboxConfi
49
49
  value: Mapped[str] = mapped_column(String, nullable=False, doc="The value of the environment variable.")
50
50
  description: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="An optional description of the environment variable.")
51
51
 
52
+ # encrypted columns
53
+ value_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted value of the environment variable.")
54
+
52
55
  # relationships
53
56
  organization: Mapped["Organization"] = relationship("Organization", back_populates="sandbox_environment_variables")
54
57
  sandbox_config: Mapped["SandboxConfig"] = relationship("SandboxConfig", back_populates="sandbox_environment_variables")
@@ -71,5 +74,8 @@ class AgentEnvironmentVariable(SqlalchemyBase, OrganizationMixin, AgentMixin):
71
74
  value: Mapped[str] = mapped_column(String, nullable=False, doc="The value of the environment variable.")
72
75
  description: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="An optional description of the environment variable.")
73
76
 
77
+ # encrypted columns
78
+ value_enc: Mapped[Optional[str]] = mapped_column(Text, nullable=True, doc="Encrypted value of the environment variable.")
79
+
74
80
  organization: Mapped["Organization"] = relationship("Organization", back_populates="agent_environment_variables")
75
81
  agent: Mapped[List["Agent"]] = relationship("Agent", back_populates="tool_exec_environment_variables")