letta-nightly 0.10.0.dev20250806104523__py3-none-any.whl → 0.11.0.dev20250807104511__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.
- letta/__init__.py +1 -4
- letta/agent.py +1 -2
- letta/agents/base_agent.py +4 -7
- letta/agents/letta_agent.py +59 -51
- letta/agents/letta_agent_batch.py +1 -2
- letta/agents/voice_agent.py +1 -2
- letta/agents/voice_sleeptime_agent.py +1 -3
- letta/constants.py +4 -1
- letta/embeddings.py +1 -1
- letta/functions/function_sets/base.py +0 -1
- letta/functions/mcp_client/types.py +4 -0
- letta/groups/supervisor_multi_agent.py +1 -1
- letta/interfaces/anthropic_streaming_interface.py +16 -24
- letta/interfaces/openai_streaming_interface.py +16 -28
- letta/llm_api/llm_api_tools.py +3 -3
- letta/local_llm/vllm/api.py +3 -0
- letta/orm/__init__.py +3 -1
- letta/orm/agent.py +8 -0
- letta/orm/archive.py +86 -0
- letta/orm/archives_agents.py +27 -0
- letta/orm/job.py +5 -1
- letta/orm/mixins.py +8 -0
- letta/orm/organization.py +7 -8
- letta/orm/passage.py +12 -10
- letta/orm/sqlite_functions.py +2 -2
- letta/orm/tool.py +5 -4
- letta/schemas/agent.py +4 -2
- letta/schemas/agent_file.py +18 -1
- letta/schemas/archive.py +44 -0
- letta/schemas/embedding_config.py +2 -16
- letta/schemas/enums.py +2 -1
- letta/schemas/group.py +28 -3
- letta/schemas/job.py +4 -0
- letta/schemas/llm_config.py +29 -14
- letta/schemas/memory.py +9 -3
- letta/schemas/npm_requirement.py +12 -0
- letta/schemas/passage.py +3 -3
- letta/schemas/providers/letta.py +1 -1
- letta/schemas/providers/vllm.py +4 -4
- letta/schemas/sandbox_config.py +3 -1
- letta/schemas/tool.py +10 -38
- letta/schemas/tool_rule.py +2 -2
- letta/server/db.py +8 -2
- letta/server/rest_api/routers/v1/agents.py +9 -8
- letta/server/server.py +6 -40
- letta/server/startup.sh +3 -0
- letta/services/agent_manager.py +92 -31
- letta/services/agent_serialization_manager.py +62 -3
- letta/services/archive_manager.py +269 -0
- letta/services/helpers/agent_manager_helper.py +111 -37
- letta/services/job_manager.py +24 -0
- letta/services/passage_manager.py +98 -54
- letta/services/tool_executor/core_tool_executor.py +0 -1
- letta/services/tool_executor/sandbox_tool_executor.py +2 -2
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_manager.py +70 -26
- letta/services/tool_sandbox/base.py +2 -2
- letta/services/tool_sandbox/local_sandbox.py +5 -1
- letta/templates/template_helper.py +8 -0
- {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807104511.dist-info}/METADATA +5 -6
- {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807104511.dist-info}/RECORD +64 -61
- letta/client/client.py +0 -2207
- letta/orm/enums.py +0 -21
- {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807104511.dist-info}/LICENSE +0 -0
- {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807104511.dist-info}/WHEEL +0 -0
- {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807104511.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
|
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
|
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
|
-
|
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
|
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
|
74
|
-
"""Passages
|
73
|
+
class ArchivalPassage(BasePassage, ArchiveMixin):
|
74
|
+
"""Passages stored in archives as archival memories"""
|
75
75
|
|
76
|
-
__tablename__ = "
|
76
|
+
__tablename__ = "archival_passages"
|
77
77
|
|
78
78
|
@declared_attr
|
79
79
|
def organization(cls) -> Mapped["Organization"]:
|
80
|
-
return relationship("Organization", back_populates="
|
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("
|
87
|
-
Index("
|
88
|
-
Index("
|
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("
|
93
|
-
Index("
|
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
|
)
|
letta/orm/sqlite_functions.py
CHANGED
@@ -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.
|
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.
|
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
|
-
|
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
|
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
|
-
|
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.")
|
letta/schemas/agent_file.py
CHANGED
@@ -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"""
|
letta/schemas/archive.py
ADDED
@@ -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://
|
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
|
-
|
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
|
-
|
47
|
-
|
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
|
"""
|
letta/schemas/llm_config.py
CHANGED
@@ -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=
|
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
|
146
|
+
Template(prompt_template)
|
146
147
|
|
147
148
|
# Validate compatibility with current memory structure - use async rendering
|
148
|
-
template = Template(prompt_template
|
149
|
-
await template.
|
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
|
-
|
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
|
-
|
40
|
-
|
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
|
"""
|
letta/schemas/providers/letta.py
CHANGED
@@ -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://
|
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),
|