letta-nightly 0.10.0.dev20250806104523__py3-none-any.whl → 0.11.0.dev20250807000848__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 (66) hide show
  1. letta/__init__.py +1 -4
  2. letta/agent.py +1 -2
  3. letta/agents/base_agent.py +4 -7
  4. letta/agents/letta_agent.py +59 -51
  5. letta/agents/letta_agent_batch.py +1 -2
  6. letta/agents/voice_agent.py +1 -2
  7. letta/agents/voice_sleeptime_agent.py +1 -3
  8. letta/constants.py +4 -1
  9. letta/embeddings.py +1 -1
  10. letta/functions/function_sets/base.py +0 -1
  11. letta/functions/mcp_client/types.py +4 -0
  12. letta/groups/supervisor_multi_agent.py +1 -1
  13. letta/interfaces/anthropic_streaming_interface.py +16 -24
  14. letta/interfaces/openai_streaming_interface.py +16 -28
  15. letta/llm_api/llm_api_tools.py +3 -3
  16. letta/local_llm/vllm/api.py +3 -0
  17. letta/orm/__init__.py +3 -1
  18. letta/orm/agent.py +8 -0
  19. letta/orm/archive.py +86 -0
  20. letta/orm/archives_agents.py +27 -0
  21. letta/orm/job.py +5 -1
  22. letta/orm/mixins.py +8 -0
  23. letta/orm/organization.py +7 -8
  24. letta/orm/passage.py +12 -10
  25. letta/orm/sqlite_functions.py +2 -2
  26. letta/orm/tool.py +5 -4
  27. letta/schemas/agent.py +4 -2
  28. letta/schemas/agent_file.py +18 -1
  29. letta/schemas/archive.py +44 -0
  30. letta/schemas/embedding_config.py +2 -16
  31. letta/schemas/enums.py +2 -1
  32. letta/schemas/group.py +28 -3
  33. letta/schemas/job.py +4 -0
  34. letta/schemas/llm_config.py +29 -14
  35. letta/schemas/memory.py +9 -3
  36. letta/schemas/npm_requirement.py +12 -0
  37. letta/schemas/passage.py +3 -3
  38. letta/schemas/providers/letta.py +1 -1
  39. letta/schemas/providers/vllm.py +4 -4
  40. letta/schemas/sandbox_config.py +3 -1
  41. letta/schemas/tool.py +10 -38
  42. letta/schemas/tool_rule.py +2 -2
  43. letta/server/db.py +8 -2
  44. letta/server/rest_api/routers/v1/agents.py +9 -8
  45. letta/server/server.py +6 -40
  46. letta/server/startup.sh +3 -0
  47. letta/services/agent_manager.py +92 -31
  48. letta/services/agent_serialization_manager.py +62 -3
  49. letta/services/archive_manager.py +269 -0
  50. letta/services/helpers/agent_manager_helper.py +111 -37
  51. letta/services/job_manager.py +24 -0
  52. letta/services/passage_manager.py +98 -54
  53. letta/services/tool_executor/core_tool_executor.py +0 -1
  54. letta/services/tool_executor/sandbox_tool_executor.py +2 -2
  55. letta/services/tool_executor/tool_execution_manager.py +1 -1
  56. letta/services/tool_manager.py +70 -26
  57. letta/services/tool_sandbox/base.py +2 -2
  58. letta/services/tool_sandbox/local_sandbox.py +5 -1
  59. letta/templates/template_helper.py +8 -0
  60. {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807000848.dist-info}/METADATA +5 -6
  61. {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807000848.dist-info}/RECORD +64 -61
  62. letta/client/client.py +0 -2207
  63. letta/orm/enums.py +0 -21
  64. {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807000848.dist-info}/LICENSE +0 -0
  65. {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807000848.dist-info}/WHEEL +0 -0
  66. {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807000848.dist-info}/entry_points.txt +0 -0
letta/orm/archive.py ADDED
@@ -0,0 +1,86 @@
1
+ import uuid
2
+ from datetime import datetime, timezone
3
+ from typing import TYPE_CHECKING, List, Optional
4
+
5
+ from sqlalchemy import JSON, Index, String
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.archive import Archive as PydanticArchive
11
+ from letta.settings import DatabaseChoice, settings
12
+
13
+ if TYPE_CHECKING:
14
+ from sqlalchemy.ext.asyncio import AsyncSession
15
+ from sqlalchemy.orm import Session
16
+
17
+ from letta.orm.archives_agents import ArchivesAgents
18
+ from letta.orm.organization import Organization
19
+ from letta.schemas.user import User
20
+
21
+
22
+ class Archive(SqlalchemyBase, OrganizationMixin):
23
+ """An archive represents a collection of archival passages that can be shared between agents"""
24
+
25
+ __tablename__ = "archives"
26
+ __pydantic_model__ = PydanticArchive
27
+
28
+ __table_args__ = (
29
+ Index("ix_archives_created_at", "created_at", "id"),
30
+ Index("ix_archives_organization_id", "organization_id"),
31
+ )
32
+
33
+ # archive generates its own id
34
+ # TODO: We want to migrate all the ORM models to do this, so we will need to move this to the SqlalchemyBase
35
+ # TODO: Some still rely on the Pydantic object to do this
36
+ id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"archive-{uuid.uuid4()}")
37
+
38
+ # archive-specific fields
39
+ name: Mapped[str] = mapped_column(String, nullable=False, doc="The name of the archive")
40
+ description: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="A description of the archive")
41
+ metadata_: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, doc="Additional metadata for the archive")
42
+
43
+ # relationships
44
+ archives_agents: Mapped[List["ArchivesAgents"]] = relationship(
45
+ "ArchivesAgents",
46
+ back_populates="archive",
47
+ cascade="all, delete-orphan", # this will delete junction entries when archive is deleted
48
+ lazy="noload",
49
+ )
50
+
51
+ organization: Mapped["Organization"] = relationship("Organization", back_populates="archives", lazy="selectin")
52
+
53
+ def create(
54
+ self,
55
+ db_session: "Session",
56
+ actor: Optional["User"] = None,
57
+ no_commit: bool = False,
58
+ ) -> "Archive":
59
+ """Override create to handle SQLite timestamp issues"""
60
+ # For SQLite, explicitly set timestamps as server_default may not work
61
+ if settings.database_engine == DatabaseChoice.SQLITE:
62
+ now = datetime.now(timezone.utc)
63
+ if not self.created_at:
64
+ self.created_at = now
65
+ if not self.updated_at:
66
+ self.updated_at = now
67
+
68
+ return super().create(db_session, actor=actor, no_commit=no_commit)
69
+
70
+ async def create_async(
71
+ self,
72
+ db_session: "AsyncSession",
73
+ actor: Optional["User"] = None,
74
+ no_commit: bool = False,
75
+ no_refresh: bool = False,
76
+ ) -> "Archive":
77
+ """Override create_async to handle SQLite timestamp issues"""
78
+ # For SQLite, explicitly set timestamps as server_default may not work
79
+ if settings.database_engine == DatabaseChoice.SQLITE:
80
+ now = datetime.now(timezone.utc)
81
+ if not self.created_at:
82
+ self.created_at = now
83
+ if not self.updated_at:
84
+ self.updated_at = now
85
+
86
+ return await super().create_async(db_session, actor=actor, no_commit=no_commit, no_refresh=no_refresh)
@@ -0,0 +1,27 @@
1
+ from datetime import datetime
2
+
3
+ from sqlalchemy import Boolean, DateTime, ForeignKey, String, UniqueConstraint
4
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
5
+
6
+ from letta.orm.base import Base
7
+
8
+
9
+ class ArchivesAgents(Base):
10
+ """Many-to-many relationship between agents and archives"""
11
+
12
+ __tablename__ = "archives_agents"
13
+
14
+ # TODO: Remove this unique constraint when we support multiple archives per agent
15
+ # For now, each agent can only have one archive
16
+ __table_args__ = (UniqueConstraint("agent_id", name="unique_agent_archive"),)
17
+
18
+ agent_id: Mapped[str] = mapped_column(String, ForeignKey("agents.id", ondelete="CASCADE"), primary_key=True)
19
+ archive_id: Mapped[str] = mapped_column(String, ForeignKey("archives.id", ondelete="CASCADE"), primary_key=True)
20
+
21
+ # track when the relationship was created and if agent is owner
22
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default="now()")
23
+ is_owner: Mapped[bool] = mapped_column(Boolean, default=False, doc="Whether this agent created/owns the archive")
24
+
25
+ # relationships
26
+ agent: Mapped["Agent"] = relationship("Agent", back_populates="archives_agents")
27
+ archive: Mapped["Archive"] = relationship("Archive", back_populates="archives_agents")
letta/orm/job.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from datetime import datetime
2
2
  from typing import TYPE_CHECKING, List, Optional
3
3
 
4
- from sqlalchemy import JSON, Index, String
4
+ from sqlalchemy import JSON, BigInteger, Index, String
5
5
  from sqlalchemy.orm import Mapped, mapped_column, relationship
6
6
 
7
7
  from letta.orm.mixins import UserMixin
@@ -46,6 +46,10 @@ class Job(SqlalchemyBase, UserMixin):
46
46
  nullable=True, doc="Optional error message from attempting to POST the callback endpoint."
47
47
  )
48
48
 
49
+ # timing metrics (in nanoseconds for precision)
50
+ ttft_ns: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True, doc="Time to first token in nanoseconds")
51
+ total_duration_ns: Mapped[Optional[int]] = mapped_column(BigInteger, nullable=True, doc="Total run duration in nanoseconds")
52
+
49
53
  # relationships
50
54
  user: Mapped["User"] = relationship("User", back_populates="jobs")
51
55
  job_messages: Mapped[List["JobMessage"]] = relationship("JobMessage", back_populates="job", cascade="all, delete-orphan")
letta/orm/mixins.py CHANGED
@@ -70,3 +70,11 @@ class ProjectMixin(Base):
70
70
  __abstract__ = True
71
71
 
72
72
  project_id: Mapped[str] = mapped_column(String, nullable=True, doc="The associated project id.")
73
+
74
+
75
+ class ArchiveMixin(Base):
76
+ """Mixin for models that belong to an archive."""
77
+
78
+ __abstract__ = True
79
+
80
+ archive_id: Mapped[str] = mapped_column(String, ForeignKey("archives.id", ondelete="CASCADE"))
letta/orm/organization.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, List, Union
1
+ from typing import TYPE_CHECKING, List
2
2
 
3
3
  from sqlalchemy.orm import Mapped, mapped_column, relationship
4
4
 
@@ -8,13 +8,14 @@ from letta.schemas.organization import Organization as PydanticOrganization
8
8
  if TYPE_CHECKING:
9
9
  from letta.orm import Source
10
10
  from letta.orm.agent import Agent
11
+ from letta.orm.archive import Archive
11
12
  from letta.orm.block import Block
12
13
  from letta.orm.group import Group
13
14
  from letta.orm.identity import Identity
14
15
  from letta.orm.llm_batch_items import LLMBatchItem
15
16
  from letta.orm.llm_batch_job import LLMBatchJob
16
17
  from letta.orm.message import Message
17
- from letta.orm.passage import AgentPassage, SourcePassage
18
+ from letta.orm.passage import ArchivalPassage, SourcePassage
18
19
  from letta.orm.provider import Provider
19
20
  from letta.orm.sandbox_config import AgentEnvironmentVariable, SandboxConfig, SandboxEnvironmentVariable
20
21
  from letta.orm.tool import Tool
@@ -52,7 +53,10 @@ class Organization(SqlalchemyBase):
52
53
  source_passages: Mapped[List["SourcePassage"]] = relationship(
53
54
  "SourcePassage", back_populates="organization", cascade="all, delete-orphan"
54
55
  )
55
- agent_passages: Mapped[List["AgentPassage"]] = relationship("AgentPassage", back_populates="organization", cascade="all, delete-orphan")
56
+ archival_passages: Mapped[List["ArchivalPassage"]] = relationship(
57
+ "ArchivalPassage", back_populates="organization", cascade="all, delete-orphan"
58
+ )
59
+ archives: Mapped[List["Archive"]] = relationship("Archive", back_populates="organization", cascade="all, delete-orphan")
56
60
  providers: Mapped[List["Provider"]] = relationship("Provider", back_populates="organization", cascade="all, delete-orphan")
57
61
  identities: Mapped[List["Identity"]] = relationship("Identity", back_populates="organization", cascade="all, delete-orphan")
58
62
  groups: Mapped[List["Group"]] = relationship("Group", back_populates="organization", cascade="all, delete-orphan")
@@ -60,8 +64,3 @@ class Organization(SqlalchemyBase):
60
64
  llm_batch_items: Mapped[List["LLMBatchItem"]] = relationship(
61
65
  "LLMBatchItem", back_populates="organization", cascade="all, delete-orphan"
62
66
  )
63
-
64
- @property
65
- def passages(self) -> List[Union["SourcePassage", "AgentPassage"]]:
66
- """Convenience property to get all passages"""
67
- return self.source_passages + self.agent_passages
letta/orm/passage.py CHANGED
@@ -6,7 +6,7 @@ from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
6
6
  from letta.config import LettaConfig
7
7
  from letta.constants import MAX_EMBEDDING_DIM
8
8
  from letta.orm.custom_columns import CommonVector, EmbeddingConfigColumn
9
- from letta.orm.mixins import AgentMixin, FileMixin, OrganizationMixin, SourceMixin
9
+ from letta.orm.mixins import ArchiveMixin, FileMixin, OrganizationMixin, SourceMixin
10
10
  from letta.orm.sqlalchemy_base import SqlalchemyBase
11
11
  from letta.schemas.passage import Passage as PydanticPassage
12
12
  from letta.settings import DatabaseChoice, settings
@@ -70,26 +70,28 @@ class SourcePassage(BasePassage, FileMixin, SourceMixin):
70
70
  )
71
71
 
72
72
 
73
- class AgentPassage(BasePassage, AgentMixin):
74
- """Passages created by agents as archival memories"""
73
+ class ArchivalPassage(BasePassage, ArchiveMixin):
74
+ """Passages stored in archives as archival memories"""
75
75
 
76
- __tablename__ = "agent_passages"
76
+ __tablename__ = "archival_passages"
77
77
 
78
78
  @declared_attr
79
79
  def organization(cls) -> Mapped["Organization"]:
80
- return relationship("Organization", back_populates="agent_passages", lazy="selectin")
80
+ return relationship("Organization", back_populates="archival_passages", lazy="selectin")
81
81
 
82
82
  @declared_attr
83
83
  def __table_args__(cls):
84
84
  if settings.database_engine is DatabaseChoice.POSTGRES:
85
85
  return (
86
- Index("agent_passages_org_idx", "organization_id"),
87
- Index("ix_agent_passages_org_agent", "organization_id", "agent_id"),
88
- Index("agent_passages_created_at_id_idx", "created_at", "id"),
86
+ Index("archival_passages_org_idx", "organization_id"),
87
+ Index("ix_archival_passages_org_archive", "organization_id", "archive_id"),
88
+ Index("archival_passages_created_at_id_idx", "created_at", "id"),
89
+ Index("ix_archival_passages_archive_id", "archive_id"),
89
90
  {"extend_existing": True},
90
91
  )
91
92
  return (
92
- Index("ix_agent_passages_org_agent", "organization_id", "agent_id"),
93
- Index("agent_passages_created_at_id_idx", "created_at", "id"),
93
+ Index("ix_archival_passages_org_archive", "organization_id", "archive_id"),
94
+ Index("archival_passages_created_at_id_idx", "created_at", "id"),
95
+ Index("ix_archival_passages_archive_id", "archive_id"),
94
96
  {"extend_existing": True},
95
97
  )
@@ -152,7 +152,7 @@ def register_functions(dbapi_connection, connection_record):
152
152
  if is_aiosqlite_connection:
153
153
  # For aiosqlite connections, we cannot use async operations in sync event handlers
154
154
  # The extension will need to be loaded per-connection when actually used
155
- logger.info("Detected aiosqlite connection - sqlite-vec will be loaded per-query")
155
+ logger.debug("Detected aiosqlite connection - sqlite-vec will be loaded per-query")
156
156
  else:
157
157
  # For sync connections
158
158
  # dbapi_connection.enable_load_extension(True)
@@ -173,7 +173,7 @@ def register_functions(dbapi_connection, connection_record):
173
173
  raw_conn = getattr(actual_connection, "_connection", actual_connection)
174
174
  if hasattr(raw_conn, "create_function"):
175
175
  raw_conn.create_function("cosine_distance", 2, cosine_distance)
176
- logger.info("Successfully registered cosine_distance for aiosqlite")
176
+ logger.debug("Successfully registered cosine_distance for aiosqlite")
177
177
  else:
178
178
  dbapi_connection.create_function("cosine_distance", 2, cosine_distance)
179
179
  logger.info("Successfully registered cosine_distance for sync connection")
letta/orm/tool.py CHANGED
@@ -3,11 +3,11 @@ from typing import TYPE_CHECKING, List, Optional
3
3
  from sqlalchemy import JSON, Index, String, UniqueConstraint
4
4
  from sqlalchemy.orm import Mapped, mapped_column, relationship
5
5
 
6
- # TODO everything in functions should live in this model
7
- from letta.orm.enums import ToolType
8
6
  from letta.orm.mixins import OrganizationMixin
9
7
  from letta.orm.sqlalchemy_base import SqlalchemyBase
10
- from letta.schemas.enums import ToolSourceType
8
+
9
+ # TODO everything in functions should live in this model
10
+ from letta.schemas.enums import ToolSourceType, ToolType
11
11
  from letta.schemas.tool import Tool as PydanticTool
12
12
 
13
13
  if TYPE_CHECKING:
@@ -43,11 +43,12 @@ class Tool(SqlalchemyBase, OrganizationMixin):
43
43
  tags: Mapped[List] = mapped_column(JSON, doc="Metadata tags used to filter tools.")
44
44
  source_type: Mapped[ToolSourceType] = mapped_column(String, doc="The type of the source code.", default=ToolSourceType.json)
45
45
  source_code: Mapped[Optional[str]] = mapped_column(String, doc="The source code of the function.")
46
- json_schema: Mapped[Optional[dict]] = mapped_column(JSON, default=lambda: {}, doc="The OAI compatable JSON schema of the function.")
46
+ json_schema: Mapped[Optional[dict]] = mapped_column(JSON, default=lambda: {}, doc="The OAI compatible JSON schema of the function.")
47
47
  args_json_schema: Mapped[Optional[dict]] = mapped_column(JSON, default=lambda: {}, doc="The JSON schema of the function arguments.")
48
48
  pip_requirements: Mapped[Optional[List]] = mapped_column(
49
49
  JSON, nullable=True, doc="Optional list of pip packages required by this tool."
50
50
  )
51
+ npm_requirements: Mapped[list | None] = mapped_column(JSON, doc="Optional list of npm packages required by this tool.")
51
52
  metadata_: Mapped[Optional[dict]] = mapped_column(JSON, default=lambda: {}, doc="A dictionary of additional metadata for the tool.")
52
53
  # relationships
53
54
  organization: Mapped["Organization"] = relationship("Organization", back_populates="tools", lazy="selectin")
letta/schemas/agent.py CHANGED
@@ -186,8 +186,8 @@ class CreateAgent(BaseModel, validate_assignment=True): #
186
186
  include_multi_agent_tools: bool = Field(
187
187
  False, description="If true, attaches the Letta multi-agent tools (e.g. sending a message to another agent)."
188
188
  )
189
- include_base_tool_rules: bool = Field(
190
- True, description="If true, attaches the Letta base tool rules (e.g. deny all tools not explicitly allowed)."
189
+ include_base_tool_rules: Optional[bool] = Field(
190
+ None, description="If true, attaches the Letta base tool rules (e.g. deny all tools not explicitly allowed)."
191
191
  )
192
192
  include_default_source: bool = Field(
193
193
  False, description="If true, automatically creates and attaches a default data source for this agent."
@@ -212,6 +212,7 @@ class CreateAgent(BaseModel, validate_assignment=True): #
212
212
  None, description="The maximum number of tokens to generate for reasoning step. If not set, the model will use its default value."
213
213
  )
214
214
  enable_reasoner: Optional[bool] = Field(False, description="Whether to enable internal extended thinking step for a reasoner model.")
215
+ reasoning: Optional[bool] = Field(None, description="Whether to enable reasoning for this agent.")
215
216
  from_template: Optional[str] = Field(None, description="The template id used to configure the agent")
216
217
  template: bool = Field(False, description="Whether the agent is a template")
217
218
  project: Optional[str] = Field(
@@ -335,6 +336,7 @@ class UpdateAgent(BaseModel):
335
336
  embedding: Optional[str] = Field(
336
337
  None, description="The embedding configuration handle used by the agent, specified in the format provider/model-name."
337
338
  )
339
+ reasoning: Optional[bool] = Field(None, description="Whether to enable reasoning for this agent.")
338
340
  enable_sleeptime: Optional[bool] = Field(None, description="If set to True, memory management will move to a background agent thread.")
339
341
  response_format: Optional[ResponseFormatUnion] = Field(None, description="The response format for the agent.")
340
342
  last_run_completion: Optional[datetime] = Field(None, description="The timestamp when the agent last completed a run.")
@@ -7,7 +7,7 @@ from letta.schemas.agent import AgentState, CreateAgent
7
7
  from letta.schemas.block import Block, CreateBlock
8
8
  from letta.schemas.enums import MessageRole
9
9
  from letta.schemas.file import FileAgent, FileAgentBase, FileMetadata, FileMetadataBase
10
- from letta.schemas.group import GroupCreate
10
+ from letta.schemas.group import Group, GroupCreate
11
11
  from letta.schemas.mcp import MCPServer
12
12
  from letta.schemas.message import Message, MessageCreate
13
13
  from letta.schemas.source import Source, SourceCreate
@@ -99,6 +99,7 @@ class AgentSchema(CreateAgent):
99
99
  )
100
100
  messages: List[MessageSchema] = Field(default_factory=list, description="List of messages in the agent's conversation history")
101
101
  files_agents: List[FileAgentSchema] = Field(default_factory=list, description="List of file-agent relationships for this agent")
102
+ group_ids: List[str] = Field(default_factory=list, description="List of groups that the agent manages")
102
103
 
103
104
  @classmethod
104
105
  async def from_agent_state(
@@ -163,6 +164,7 @@ class AgentSchema(CreateAgent):
163
164
  in_context_message_ids=agent_state.message_ids or [],
164
165
  messages=message_schemas, # Messages will be populated separately by the manager
165
166
  files_agents=[FileAgentSchema.from_file_agent(f) for f in files_agents],
167
+ group_ids=[agent_state.multi_agent_group.id] if agent_state.multi_agent_group else [],
166
168
  **create_agent.model_dump(),
167
169
  )
168
170
 
@@ -173,6 +175,21 @@ class GroupSchema(GroupCreate):
173
175
  __id_prefix__ = "group"
174
176
  id: str = Field(..., description="Human-readable identifier for this group in the file")
175
177
 
178
+ @classmethod
179
+ def from_group(cls, group: Group) -> "GroupSchema":
180
+ """Convert Group to GroupSchema"""
181
+
182
+ create_group = GroupCreate(
183
+ agent_ids=group.agent_ids,
184
+ description=group.description,
185
+ manager_config=group.manager_config,
186
+ project_id=group.project_id,
187
+ shared_block_ids=group.shared_block_ids,
188
+ )
189
+
190
+ # Create GroupSchema with the group's ID (will be remapped later)
191
+ return cls(id=group.id, **create_group.model_dump())
192
+
176
193
 
177
194
  class BlockSchema(CreateBlock):
178
195
  """Block with human-readable ID for agent file"""
@@ -0,0 +1,44 @@
1
+ from datetime import datetime
2
+ from typing import Dict, Optional
3
+
4
+ from pydantic import Field
5
+
6
+ from letta.schemas.letta_base import OrmMetadataBase
7
+
8
+
9
+ class ArchiveBase(OrmMetadataBase):
10
+ __id_prefix__ = "archive"
11
+
12
+ name: str = Field(..., description="The name of the archive")
13
+ description: Optional[str] = Field(None, description="A description of the archive")
14
+ organization_id: str = Field(..., description="The organization this archive belongs to")
15
+ metadata: Optional[Dict] = Field(default_factory=dict, validation_alias="metadata_", description="Additional metadata")
16
+
17
+
18
+ class Archive(ArchiveBase):
19
+ """
20
+ Representation of an archive - a collection of archival passages that can be shared between agents.
21
+
22
+ Parameters:
23
+ id (str): The unique identifier of the archive.
24
+ name (str): The name of the archive.
25
+ description (str): A description of the archive.
26
+ organization_id (str): The organization this archive belongs to.
27
+ created_at (datetime): The creation date of the archive.
28
+ metadata (dict): Additional metadata for the archive.
29
+ """
30
+
31
+ id: str = ArchiveBase.generate_id_field()
32
+ created_at: datetime = Field(..., description="The creation date of the archive")
33
+
34
+
35
+ class ArchiveCreate(ArchiveBase):
36
+ """Create a new archive"""
37
+
38
+
39
+ class ArchiveUpdate(ArchiveBase):
40
+ """Update an existing archive"""
41
+
42
+ name: Optional[str] = Field(None, description="The name of the archive")
43
+ description: Optional[str] = Field(None, description="A description of the archive")
44
+ metadata: Optional[Dict] = Field(None, validation_alias="metadata_", description="Additional metadata")
@@ -6,21 +6,7 @@ from letta.constants import DEFAULT_EMBEDDING_CHUNK_SIZE
6
6
 
7
7
 
8
8
  class EmbeddingConfig(BaseModel):
9
- """
10
-
11
- Embedding model configuration. This object specifies all the information necessary to access an embedding model to usage with Letta, except for secret keys.
12
-
13
- Attributes:
14
- embedding_endpoint_type (str): The endpoint type for the model.
15
- embedding_endpoint (str): The endpoint for the model.
16
- embedding_model (str): The model for the embedding.
17
- embedding_dim (int): The dimension of the embedding.
18
- embedding_chunk_size (int): The chunk size of the embedding.
19
- azure_endpoint (:obj:`str`, optional): The Azure endpoint for the model (Azure only).
20
- azure_version (str): The Azure version for the model (Azure only).
21
- azure_deployment (str): The Azure deployment for the model (Azure only).
22
-
23
- """
9
+ """Configuration for embedding model connection and processing parameters."""
24
10
 
25
11
  embedding_endpoint_type: Literal[
26
12
  "openai",
@@ -77,7 +63,7 @@ class EmbeddingConfig(BaseModel):
77
63
  )
78
64
  elif model_name == "letta":
79
65
  return cls(
80
- embedding_endpoint="https://embeddings.memgpt.ai",
66
+ embedding_endpoint="https://bun-function-production-e310.up.railway.app/v1",
81
67
  embedding_model="BAAI/bge-large-en-v1.5",
82
68
  embedding_dim=1024,
83
69
  embedding_chunk_size=DEFAULT_EMBEDDING_CHUNK_SIZE,
letta/schemas/enums.py CHANGED
@@ -132,7 +132,8 @@ class ToolSourceType(str, Enum):
132
132
  """Defines what a tool was derived from"""
133
133
 
134
134
  python = "python"
135
- json = "json"
135
+ typescript = "typescript"
136
+ json = "json" # TODO (cliandy): is this still valid?
136
137
 
137
138
 
138
139
  class ActorType(str, Enum):
letta/schemas/group.py CHANGED
@@ -15,6 +15,10 @@ class ManagerType(str, Enum):
15
15
  swarm = "swarm"
16
16
 
17
17
 
18
+ class ManagerConfig(BaseModel):
19
+ manager_type: ManagerType = Field(..., description="")
20
+
21
+
18
22
  class GroupBase(LettaBase):
19
23
  __id_prefix__ = "group"
20
24
 
@@ -42,9 +46,30 @@ class Group(GroupBase):
42
46
  description="The desired minimum length of messages in the context window of the convo agent. This is a best effort, and may be off-by-one due to user/assistant interleaving.",
43
47
  )
44
48
 
45
-
46
- class ManagerConfig(BaseModel):
47
- manager_type: ManagerType = Field(..., description="")
49
+ @property
50
+ def manager_config(self) -> ManagerConfig:
51
+ match self.manager_type:
52
+ case ManagerType.round_robin:
53
+ return RoundRobinManager(max_turns=self.max_turns)
54
+ case ManagerType.supervisor:
55
+ return SupervisorManager(manager_agent_id=self.manager_agent_id)
56
+ case ManagerType.dynamic:
57
+ return DynamicManager(
58
+ manager_agent_id=self.manager_agent_id,
59
+ termination_token=self.termination_token,
60
+ max_turns=self.max_turns,
61
+ )
62
+ case ManagerType.sleeptime:
63
+ return SleeptimeManager(
64
+ manager_agent_id=self.manager_agent_id,
65
+ sleeptime_agent_frequency=self.sleeptime_agent_frequency,
66
+ )
67
+ case ManagerType.voice_sleeptime:
68
+ return VoiceSleeptimeManager(
69
+ manager_agent_id=self.manager_agent_id,
70
+ max_message_buffer_length=self.max_message_buffer_length,
71
+ min_message_buffer_length=self.min_message_buffer_length,
72
+ )
48
73
 
49
74
 
50
75
  class RoundRobinManager(ManagerConfig):
letta/schemas/job.py CHANGED
@@ -21,6 +21,10 @@ class JobBase(OrmMetadataBase):
21
21
  callback_status_code: Optional[int] = Field(None, description="HTTP status code returned by the callback endpoint.")
22
22
  callback_error: Optional[str] = Field(None, description="Optional error message from attempting to POST the callback endpoint.")
23
23
 
24
+ # Timing metrics (in nanoseconds for precision)
25
+ ttft_ns: int | None = Field(None, description="Time to first token for a run in nanoseconds")
26
+ total_duration_ns: int | None = Field(None, description="Total run duration in nanoseconds")
27
+
24
28
 
25
29
  class Job(JobBase):
26
30
  """
@@ -10,19 +10,7 @@ logger = get_logger(__name__)
10
10
 
11
11
 
12
12
  class LLMConfig(BaseModel):
13
- """
14
- Configuration for a Language Model (LLM) model. This object specifies all the information necessary to access an LLM model to usage with Letta, except for secret keys.
15
-
16
- Attributes:
17
- model (str): The name of the LLM model.
18
- model_endpoint_type (str): The endpoint type for the model.
19
- model_endpoint (str): The endpoint for the model.
20
- model_wrapper (str): The wrapper for the model. This is used to wrap additional text around the input/output of the model. This is useful for text-to-text completions, such as the Completions API in OpenAI.
21
- context_window (int): The context window size for the model.
22
- put_inner_thoughts_in_kwargs (bool): Puts `inner_thoughts` as a kwarg in the function call if this is set to True. This helps with function calling performance and also the generation of inner thoughts.
23
- temperature (float): The temperature to use when generating text with the model. A higher temperature will result in more random text.
24
- max_tokens (int): The maximum number of tokens to generate.
25
- """
13
+ """Configuration for Language Model (LLM) connection and generation parameters."""
26
14
 
27
15
  model: str = Field(..., description="LLM model name. ")
28
16
  model_endpoint_type: Literal[
@@ -185,7 +173,7 @@ class LLMConfig(BaseModel):
185
173
  model="memgpt-openai",
186
174
  model_endpoint_type="openai",
187
175
  model_endpoint=LETTA_MODEL_ENDPOINT,
188
- context_window=8192,
176
+ context_window=30000,
189
177
  )
190
178
  else:
191
179
  raise ValueError(f"Model {model_name} not supported.")
@@ -196,3 +184,30 @@ class LLMConfig(BaseModel):
196
184
  + (f" [type={self.model_endpoint_type}]" if self.model_endpoint_type else "")
197
185
  + (f" [ip={self.model_endpoint}]" if self.model_endpoint else "")
198
186
  )
187
+
188
+ @classmethod
189
+ def apply_reasoning_setting_to_config(cls, config: "LLMConfig", reasoning: bool):
190
+ if reasoning:
191
+ if (
192
+ config.model_endpoint_type == "anthropic"
193
+ and ("claude-opus-4" in config.model or "claude-sonnet-4" in config.model or "claude-3-7-sonnet" in config.model)
194
+ ) or (
195
+ config.model_endpoint_type == "google_vertex" and ("gemini-2.5-flash" in config.model or "gemini-2.0-pro" in config.model)
196
+ ):
197
+ config.put_inner_thoughts_in_kwargs = False
198
+ config.enable_reasoner = True
199
+ if config.max_reasoning_tokens == 0:
200
+ config.max_reasoning_tokens = 1024
201
+ elif config.model_endpoint_type == "openai" and (
202
+ config.model.startswith("o1") or config.model.startswith("o3") or config.model.startswith("o4")
203
+ ):
204
+ config.put_inner_thoughts_in_kwargs = True
205
+ config.enable_reasoner = True
206
+ if config.reasoning_effort is None:
207
+ config.reasoning_effort = "medium"
208
+ else:
209
+ config.put_inner_thoughts_in_kwargs = True
210
+ config.enable_reasoner = False
211
+ else:
212
+ config.put_inner_thoughts_in_kwargs = False
213
+ config.enable_reasoner = False
letta/schemas/memory.py CHANGED
@@ -1,3 +1,4 @@
1
+ import asyncio
1
2
  import logging
2
3
  from typing import TYPE_CHECKING, List, Optional
3
4
 
@@ -142,11 +143,11 @@ class Memory(BaseModel, validate_assignment=True):
142
143
  """
143
144
  try:
144
145
  # Validate Jinja2 syntax with async enabled
145
- Template(prompt_template, enable_async=True)
146
+ Template(prompt_template)
146
147
 
147
148
  # Validate compatibility with current memory structure - use async rendering
148
- template = Template(prompt_template, enable_async=True)
149
- await template.render_async(blocks=self.blocks, file_blocks=self.file_blocks, sources=[], max_files_open=None)
149
+ template = Template(prompt_template)
150
+ await asyncio.to_thread(template.render, blocks=self.blocks, file_blocks=self.file_blocks, sources=[], max_files_open=None)
150
151
 
151
152
  # If we get here, the template is valid and compatible
152
153
  self.prompt_template = prompt_template
@@ -189,6 +190,11 @@ class Memory(BaseModel, validate_assignment=True):
189
190
  except Exception as e:
190
191
  raise ValueError(f"Prompt template is not compatible with current memory structure: {str(e)}")
191
192
 
193
+ @trace_method
194
+ async def compile_in_thread_async(self, tool_usage_rules=None, sources=None, max_files_open=None) -> str:
195
+ """Compile the memory in a thread"""
196
+ return await asyncio.to_thread(self.compile, tool_usage_rules=tool_usage_rules, sources=sources, max_files_open=max_files_open)
197
+
192
198
  def list_block_labels(self) -> List[str]:
193
199
  """Return a list of the block names held inside the memory object"""
194
200
  # return list(self.memory.keys())
@@ -0,0 +1,12 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class NpmRequirement(BaseModel):
5
+ name: str = Field(..., min_length=1, description="Name of the npm package.")
6
+ version: str | None = Field(None, description="Optional version of the package, following semantic versioning.")
7
+
8
+ def __str__(self) -> str:
9
+ """Return a npm-installable string format."""
10
+ if self.version:
11
+ return f'{self.name}@"{self.version}"'
12
+ return self.name
letta/schemas/passage.py CHANGED
@@ -16,7 +16,7 @@ class PassageBase(OrmMetadataBase):
16
16
 
17
17
  # associated user/agent
18
18
  organization_id: Optional[str] = Field(None, description="The unique identifier of the user associated with the passage.")
19
- agent_id: Optional[str] = Field(None, description="The unique identifier of the agent associated with the passage.")
19
+ archive_id: Optional[str] = Field(None, description="The unique identifier of the archive containing this passage.")
20
20
 
21
21
  # origin data source
22
22
  source_id: Optional[str] = Field(None, description="The data source of the passage.")
@@ -36,8 +36,8 @@ class Passage(PassageBase):
36
36
  embedding (List[float]): The embedding of the passage.
37
37
  embedding_config (EmbeddingConfig): The embedding configuration used by the passage.
38
38
  created_at (datetime): The creation date of the passage.
39
- user_id (str): The unique identifier of the user associated with the passage.
40
- agent_id (str): The unique identifier of the agent associated with the passage.
39
+ organization_id (str): The unique identifier of the organization associated with the passage.
40
+ archive_id (str): The unique identifier of the archive containing this passage.
41
41
  source_id (str): The data source of the passage.
42
42
  file_id (str): The unique identifier of the file associated with the passage.
43
43
  """
@@ -31,7 +31,7 @@ class LettaProvider(Provider):
31
31
  EmbeddingConfig(
32
32
  embedding_model="letta-free", # NOTE: renamed
33
33
  embedding_endpoint_type="hugging-face",
34
- embedding_endpoint="https://embeddings.memgpt.ai",
34
+ embedding_endpoint="https://bun-function-production-e310.up.railway.app/v1",
35
35
  embedding_dim=1024,
36
36
  embedding_chunk_size=DEFAULT_EMBEDDING_CHUNK_SIZE,
37
37
  handle=self.get_handle("letta-free", is_embedding=True),