letta-nightly 0.5.5.dev20241122170833__py3-none-any.whl → 0.6.0.dev20241204052927__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +2 -2
- letta/agent.py +155 -166
- letta/agent_store/chroma.py +2 -0
- letta/agent_store/db.py +1 -1
- letta/cli/cli.py +12 -8
- letta/cli/cli_config.py +1 -1
- letta/client/client.py +765 -137
- letta/config.py +2 -2
- letta/constants.py +10 -14
- letta/errors.py +12 -0
- letta/functions/function_sets/base.py +38 -1
- letta/functions/functions.py +40 -57
- letta/functions/helpers.py +0 -4
- letta/functions/schema_generator.py +279 -18
- letta/helpers/tool_rule_solver.py +6 -5
- letta/llm_api/helpers.py +99 -5
- letta/llm_api/openai.py +8 -2
- letta/local_llm/utils.py +13 -6
- letta/log.py +7 -9
- letta/main.py +1 -1
- letta/metadata.py +53 -38
- letta/o1_agent.py +1 -4
- letta/orm/__init__.py +2 -0
- letta/orm/block.py +7 -3
- letta/orm/blocks_agents.py +32 -0
- letta/orm/errors.py +8 -0
- letta/orm/mixins.py +8 -0
- letta/orm/organization.py +8 -1
- letta/orm/sandbox_config.py +56 -0
- letta/orm/sqlalchemy_base.py +68 -10
- letta/persistence_manager.py +1 -0
- letta/schemas/agent.py +57 -52
- letta/schemas/block.py +85 -26
- letta/schemas/blocks_agents.py +32 -0
- letta/schemas/enums.py +14 -0
- letta/schemas/letta_base.py +10 -1
- letta/schemas/letta_request.py +11 -23
- letta/schemas/letta_response.py +1 -2
- letta/schemas/memory.py +41 -76
- letta/schemas/message.py +3 -3
- letta/schemas/sandbox_config.py +114 -0
- letta/schemas/tool.py +37 -1
- letta/schemas/tool_rule.py +13 -5
- letta/server/rest_api/app.py +5 -4
- letta/server/rest_api/interface.py +12 -19
- letta/server/rest_api/routers/openai/assistants/threads.py +2 -3
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -2
- letta/server/rest_api/routers/v1/__init__.py +4 -9
- letta/server/rest_api/routers/v1/agents.py +145 -61
- letta/server/rest_api/routers/v1/blocks.py +50 -5
- letta/server/rest_api/routers/v1/sandbox_configs.py +127 -0
- letta/server/rest_api/routers/v1/sources.py +8 -1
- letta/server/rest_api/routers/v1/tools.py +139 -13
- letta/server/rest_api/utils.py +6 -0
- letta/server/server.py +397 -340
- letta/server/static_files/assets/index-9fa459a2.js +1 -1
- letta/services/block_manager.py +23 -2
- letta/services/blocks_agents_manager.py +106 -0
- letta/services/per_agent_lock_manager.py +18 -0
- letta/services/sandbox_config_manager.py +256 -0
- letta/services/tool_execution_sandbox.py +352 -0
- letta/services/tool_manager.py +16 -22
- letta/services/tool_sandbox_env/.gitkeep +0 -0
- letta/settings.py +4 -0
- letta/utils.py +0 -7
- {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204052927.dist-info}/METADATA +10 -8
- {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204052927.dist-info}/RECORD +70 -60
- {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204052927.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204052927.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204052927.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from sqlalchemy import JSON
|
|
4
|
+
from sqlalchemy import Enum as SqlEnum
|
|
5
|
+
from sqlalchemy import String, UniqueConstraint
|
|
6
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
7
|
+
|
|
8
|
+
from letta.orm.mixins import OrganizationMixin, SandboxConfigMixin
|
|
9
|
+
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
10
|
+
from letta.schemas.sandbox_config import SandboxConfig as PydanticSandboxConfig
|
|
11
|
+
from letta.schemas.sandbox_config import (
|
|
12
|
+
SandboxEnvironmentVariable as PydanticSandboxEnvironmentVariable,
|
|
13
|
+
)
|
|
14
|
+
from letta.schemas.sandbox_config import SandboxType
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from letta.orm.organization import Organization
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SandboxConfig(SqlalchemyBase, OrganizationMixin):
|
|
21
|
+
"""ORM model for sandbox configurations with JSON storage for arbitrary config data."""
|
|
22
|
+
|
|
23
|
+
__tablename__ = "sandbox_configs"
|
|
24
|
+
__pydantic_model__ = PydanticSandboxConfig
|
|
25
|
+
|
|
26
|
+
# For now, we only allow one type of sandbox config per organization
|
|
27
|
+
__table_args__ = (UniqueConstraint("type", "organization_id", name="uix_type_organization"),)
|
|
28
|
+
|
|
29
|
+
id: Mapped[str] = mapped_column(String, primary_key=True, nullable=False)
|
|
30
|
+
type: Mapped[SandboxType] = mapped_column(SqlEnum(SandboxType), nullable=False, doc="The type of sandbox.")
|
|
31
|
+
config: Mapped[Dict] = mapped_column(JSON, nullable=False, doc="The JSON configuration data.")
|
|
32
|
+
|
|
33
|
+
# relationships
|
|
34
|
+
organization: Mapped["Organization"] = relationship("Organization", back_populates="sandbox_configs")
|
|
35
|
+
sandbox_environment_variables: Mapped[List["SandboxEnvironmentVariable"]] = relationship(
|
|
36
|
+
"SandboxEnvironmentVariable", back_populates="sandbox_config", cascade="all, delete-orphan"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SandboxEnvironmentVariable(SqlalchemyBase, OrganizationMixin, SandboxConfigMixin):
|
|
41
|
+
"""ORM model for environment variables associated with sandboxes."""
|
|
42
|
+
|
|
43
|
+
__tablename__ = "sandbox_environment_variables"
|
|
44
|
+
__pydantic_model__ = PydanticSandboxEnvironmentVariable
|
|
45
|
+
|
|
46
|
+
# We cannot have duplicate key names in the same sandbox, the env var would get overwritten
|
|
47
|
+
__table_args__ = (UniqueConstraint("key", "sandbox_config_id", name="uix_key_sandbox_config"),)
|
|
48
|
+
|
|
49
|
+
id: Mapped[str] = mapped_column(String, primary_key=True, nullable=False)
|
|
50
|
+
key: Mapped[str] = mapped_column(String, nullable=False, doc="The name of the environment variable.")
|
|
51
|
+
value: Mapped[str] = mapped_column(String, nullable=False, doc="The value of the environment variable.")
|
|
52
|
+
description: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="An optional description of the environment variable.")
|
|
53
|
+
|
|
54
|
+
# relationships
|
|
55
|
+
organization: Mapped["Organization"] = relationship("Organization", back_populates="sandbox_environment_variables")
|
|
56
|
+
sandbox_config: Mapped["SandboxConfig"] = relationship("SandboxConfig", back_populates="sandbox_environment_variables")
|
letta/orm/sqlalchemy_base.py
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, List, Literal, Optional, Type
|
|
2
2
|
|
|
3
3
|
from sqlalchemy import String, select
|
|
4
|
+
from sqlalchemy.exc import DBAPIError
|
|
4
5
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
5
6
|
|
|
6
7
|
from letta.log import get_logger
|
|
7
8
|
from letta.orm.base import Base, CommonSqlalchemyMetaMixins
|
|
8
|
-
from letta.orm.errors import
|
|
9
|
+
from letta.orm.errors import (
|
|
10
|
+
ForeignKeyConstraintViolationError,
|
|
11
|
+
NoResultFound,
|
|
12
|
+
UniqueConstraintViolationError,
|
|
13
|
+
)
|
|
9
14
|
|
|
10
15
|
if TYPE_CHECKING:
|
|
11
16
|
from pydantic import BaseModel
|
|
12
17
|
from sqlalchemy.orm import Session
|
|
13
18
|
|
|
14
|
-
# from letta.orm.user import User
|
|
15
19
|
|
|
16
20
|
logger = get_logger(__name__)
|
|
17
21
|
|
|
@@ -28,6 +32,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
28
32
|
cls, *, db_session: "Session", cursor: Optional[str] = None, limit: Optional[int] = 50, **kwargs
|
|
29
33
|
) -> List[Type["SqlalchemyBase"]]:
|
|
30
34
|
"""List records with optional cursor (for pagination) and limit."""
|
|
35
|
+
logger.debug(f"Listing {cls.__name__} with kwarg filters {kwargs}")
|
|
31
36
|
with db_session as session:
|
|
32
37
|
# Start with the base query filtered by kwargs
|
|
33
38
|
query = select(cls).filter_by(**kwargs)
|
|
@@ -67,6 +72,8 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
67
72
|
Raises:
|
|
68
73
|
NoResultFound: if the object is not found
|
|
69
74
|
"""
|
|
75
|
+
logger.debug(f"Reading {cls.__name__} with ID: {identifier} with actor={actor}")
|
|
76
|
+
|
|
70
77
|
# Start the query
|
|
71
78
|
query = select(cls)
|
|
72
79
|
# Collect query conditions for better error reporting
|
|
@@ -96,16 +103,22 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
96
103
|
raise NoResultFound(f"{cls.__name__} not found with {conditions_str}")
|
|
97
104
|
|
|
98
105
|
def create(self, db_session: "Session", actor: Optional["User"] = None) -> Type["SqlalchemyBase"]:
|
|
106
|
+
logger.debug(f"Creating {self.__class__.__name__} with ID: {self.id} with actor={actor}")
|
|
107
|
+
|
|
99
108
|
if actor:
|
|
100
109
|
self._set_created_and_updated_by_fields(actor.id)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
110
|
+
try:
|
|
111
|
+
with db_session as session:
|
|
112
|
+
session.add(self)
|
|
113
|
+
session.commit()
|
|
114
|
+
session.refresh(self)
|
|
115
|
+
return self
|
|
116
|
+
except DBAPIError as e:
|
|
117
|
+
self._handle_dbapi_error(e)
|
|
107
118
|
|
|
108
119
|
def delete(self, db_session: "Session", actor: Optional["User"] = None) -> Type["SqlalchemyBase"]:
|
|
120
|
+
logger.debug(f"Soft deleting {self.__class__.__name__} with ID: {self.id} with actor={actor}")
|
|
121
|
+
|
|
109
122
|
if actor:
|
|
110
123
|
self._set_created_and_updated_by_fields(actor.id)
|
|
111
124
|
|
|
@@ -114,8 +127,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
114
127
|
|
|
115
128
|
def hard_delete(self, db_session: "Session", actor: Optional["User"] = None) -> None:
|
|
116
129
|
"""Permanently removes the record from the database."""
|
|
117
|
-
|
|
118
|
-
logger.info(f"User {actor.id} requested hard deletion of {self.__class__.__name__} with ID {self.id}")
|
|
130
|
+
logger.debug(f"Hard deleting {self.__class__.__name__} with ID: {self.id} with actor={actor}")
|
|
119
131
|
|
|
120
132
|
with db_session as session:
|
|
121
133
|
try:
|
|
@@ -129,6 +141,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
129
141
|
logger.info(f"{self.__class__.__name__} with ID {self.id} successfully hard deleted")
|
|
130
142
|
|
|
131
143
|
def update(self, db_session: "Session", actor: Optional["User"] = None) -> Type["SqlalchemyBase"]:
|
|
144
|
+
logger.debug(f"Updating {self.__class__.__name__} with ID: {self.id} with actor={actor}")
|
|
132
145
|
if actor:
|
|
133
146
|
self._set_created_and_updated_by_fields(actor.id)
|
|
134
147
|
|
|
@@ -162,6 +175,51 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
162
175
|
raise ValueError(f"object {actor} has no organization accessor")
|
|
163
176
|
return query.where(cls.organization_id == org_id, cls.is_deleted == False)
|
|
164
177
|
|
|
178
|
+
@classmethod
|
|
179
|
+
def _handle_dbapi_error(cls, e: DBAPIError):
|
|
180
|
+
"""Handle database errors and raise appropriate custom exceptions."""
|
|
181
|
+
orig = e.orig # Extract the original error from the DBAPIError
|
|
182
|
+
error_code = None
|
|
183
|
+
error_message = str(orig) if orig else str(e)
|
|
184
|
+
logger.info(f"Handling DBAPIError: {error_message}")
|
|
185
|
+
|
|
186
|
+
# Handle SQLite-specific errors
|
|
187
|
+
if "UNIQUE constraint failed" in error_message:
|
|
188
|
+
raise UniqueConstraintViolationError(
|
|
189
|
+
f"A unique constraint was violated for {cls.__name__}. Check your input for duplicates: {e}"
|
|
190
|
+
) from e
|
|
191
|
+
|
|
192
|
+
if "FOREIGN KEY constraint failed" in error_message:
|
|
193
|
+
raise ForeignKeyConstraintViolationError(
|
|
194
|
+
f"A foreign key constraint was violated for {cls.__name__}. Check your input for missing or invalid references: {e}"
|
|
195
|
+
) from e
|
|
196
|
+
|
|
197
|
+
# For psycopg2
|
|
198
|
+
if hasattr(orig, "pgcode"):
|
|
199
|
+
error_code = orig.pgcode
|
|
200
|
+
# For pg8000
|
|
201
|
+
elif hasattr(orig, "args") and len(orig.args) > 0:
|
|
202
|
+
# The first argument contains the error details as a dictionary
|
|
203
|
+
err_dict = orig.args[0]
|
|
204
|
+
if isinstance(err_dict, dict):
|
|
205
|
+
error_code = err_dict.get("C") # 'C' is the error code field
|
|
206
|
+
logger.info(f"Extracted error_code: {error_code}")
|
|
207
|
+
|
|
208
|
+
# Handle unique constraint violations
|
|
209
|
+
if error_code == "23505":
|
|
210
|
+
raise UniqueConstraintViolationError(
|
|
211
|
+
f"A unique constraint was violated for {cls.__name__}. Check your input for duplicates: {e}"
|
|
212
|
+
) from e
|
|
213
|
+
|
|
214
|
+
# Handle foreign key violations
|
|
215
|
+
if error_code == "23503":
|
|
216
|
+
raise ForeignKeyConstraintViolationError(
|
|
217
|
+
f"A foreign key constraint was violated for {cls.__name__}. Check your input for missing or invalid references: {e}"
|
|
218
|
+
) from e
|
|
219
|
+
|
|
220
|
+
# Re-raise for other unhandled DBAPI errors
|
|
221
|
+
raise
|
|
222
|
+
|
|
165
223
|
@property
|
|
166
224
|
def __pydantic_model__(self) -> Type["BaseModel"]:
|
|
167
225
|
raise NotImplementedError("Sqlalchemy models must declare a __pydantic_model__ property to be convertable.")
|
letta/persistence_manager.py
CHANGED
|
@@ -121,6 +121,7 @@ class LocalStateManager(PersistenceManager):
|
|
|
121
121
|
# self.messages = [self.messages[0]] + added_messages + self.messages[1:]
|
|
122
122
|
|
|
123
123
|
# add to recall memory
|
|
124
|
+
self.recall_memory.insert_many([m for m in added_messages])
|
|
124
125
|
|
|
125
126
|
def append_to_messages(self, added_messages: List[Message]):
|
|
126
127
|
# first tag with timestamps
|
letta/schemas/agent.py
CHANGED
|
@@ -2,15 +2,18 @@ from datetime import datetime
|
|
|
2
2
|
from enum import Enum
|
|
3
3
|
from typing import Dict, List, Optional
|
|
4
4
|
|
|
5
|
-
from pydantic import BaseModel, Field, field_validator
|
|
5
|
+
from pydantic import BaseModel, Field, field_validator
|
|
6
6
|
|
|
7
|
+
from letta.schemas.block import CreateBlock
|
|
7
8
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
8
9
|
from letta.schemas.letta_base import LettaBase
|
|
9
10
|
from letta.schemas.llm_config import LLMConfig
|
|
10
11
|
from letta.schemas.memory import Memory
|
|
11
12
|
from letta.schemas.message import Message
|
|
12
13
|
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
|
13
|
-
from letta.schemas.
|
|
14
|
+
from letta.schemas.source import Source
|
|
15
|
+
from letta.schemas.tool import Tool
|
|
16
|
+
from letta.schemas.tool_rule import ToolRule
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class BaseAgent(LettaBase, validate_assignment=True):
|
|
@@ -32,23 +35,8 @@ class AgentType(str, Enum):
|
|
|
32
35
|
o1_agent = "o1_agent"
|
|
33
36
|
|
|
34
37
|
|
|
35
|
-
class
|
|
36
|
-
|
|
37
|
-
Representation of an agent's state. This is the state of the agent at a given time, and is persisted in the DB backend. The state has all the information needed to recreate a persisted agent.
|
|
38
|
-
|
|
39
|
-
Parameters:
|
|
40
|
-
id (str): The unique identifier of the agent.
|
|
41
|
-
name (str): The name of the agent (must be unique to the user).
|
|
42
|
-
created_at (datetime): The datetime the agent was created.
|
|
43
|
-
message_ids (List[str]): The ids of the messages in the agent's in-context memory.
|
|
44
|
-
memory (Memory): The in-context memory of the agent.
|
|
45
|
-
tools (List[str]): The tools used by the agent. This includes any memory editing functions specified in `memory`.
|
|
46
|
-
system (str): The system prompt used by the agent.
|
|
47
|
-
llm_config (LLMConfig): The LLM configuration used by the agent.
|
|
48
|
-
embedding_config (EmbeddingConfig): The embedding configuration used by the agent.
|
|
49
|
-
|
|
50
|
-
"""
|
|
51
|
-
|
|
38
|
+
class PersistedAgentState(BaseAgent, validate_assignment=True):
|
|
39
|
+
# NOTE: this has been changed to represent the data stored in the ORM, NOT what is passed around internally or returned to the user
|
|
52
40
|
id: str = BaseAgent.generate_id_field()
|
|
53
41
|
name: str = Field(..., description="The name of the agent.")
|
|
54
42
|
created_at: datetime = Field(..., description="The datetime the agent was created.", default_factory=datetime.now)
|
|
@@ -56,16 +44,12 @@ class AgentState(BaseAgent, validate_assignment=True):
|
|
|
56
44
|
# in-context memory
|
|
57
45
|
message_ids: Optional[List[str]] = Field(default=None, description="The ids of the messages in the agent's in-context memory.")
|
|
58
46
|
|
|
59
|
-
memory: Memory = Field(default_factory=Memory, description="The in-context memory of the agent.")
|
|
60
|
-
|
|
61
47
|
# tools
|
|
62
|
-
|
|
48
|
+
# TODO: move to ORM mapping
|
|
49
|
+
tool_names: List[str] = Field(..., description="The tools used by the agent.")
|
|
63
50
|
|
|
64
51
|
# tool rules
|
|
65
|
-
tool_rules: Optional[List[
|
|
66
|
-
|
|
67
|
-
# tags
|
|
68
|
-
tags: Optional[List[str]] = Field(None, description="The tags associated with the agent.")
|
|
52
|
+
tool_rules: Optional[List[ToolRule]] = Field(default=None, description="The list of tool rules.")
|
|
69
53
|
|
|
70
54
|
# system prompt
|
|
71
55
|
system: str = Field(..., description="The system prompt used by the agent.")
|
|
@@ -77,40 +61,62 @@ class AgentState(BaseAgent, validate_assignment=True):
|
|
|
77
61
|
llm_config: LLMConfig = Field(..., description="The LLM configuration used by the agent.")
|
|
78
62
|
embedding_config: EmbeddingConfig = Field(..., description="The embedding configuration used by the agent.")
|
|
79
63
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
64
|
+
class Config:
|
|
65
|
+
arbitrary_types_allowed = True
|
|
66
|
+
validate_assignment = True
|
|
83
67
|
|
|
84
|
-
@model_validator(mode="after")
|
|
85
|
-
def verify_memory_type(self):
|
|
86
|
-
try:
|
|
87
|
-
assert isinstance(self.memory, Memory)
|
|
88
|
-
except Exception as e:
|
|
89
|
-
raise e
|
|
90
|
-
return self
|
|
91
68
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
69
|
+
class AgentState(PersistedAgentState):
|
|
70
|
+
"""
|
|
71
|
+
Representation of an agent's state. This is the state of the agent at a given time, and is persisted in the DB backend. The state has all the information needed to recreate a persisted agent.
|
|
72
|
+
|
|
73
|
+
Parameters:
|
|
74
|
+
id (str): The unique identifier of the agent.
|
|
75
|
+
name (str): The name of the agent (must be unique to the user).
|
|
76
|
+
created_at (datetime): The datetime the agent was created.
|
|
77
|
+
message_ids (List[str]): The ids of the messages in the agent's in-context memory.
|
|
78
|
+
memory (Memory): The in-context memory of the agent.
|
|
79
|
+
tools (List[str]): The tools used by the agent. This includes any memory editing functions specified in `memory`.
|
|
80
|
+
system (str): The system prompt used by the agent.
|
|
81
|
+
llm_config (LLMConfig): The LLM configuration used by the agent.
|
|
82
|
+
embedding_config (EmbeddingConfig): The embedding configuration used by the agent.
|
|
95
83
|
|
|
96
|
-
|
|
97
|
-
def memory(self, value):
|
|
98
|
-
if not isinstance(value, Memory):
|
|
99
|
-
raise TypeError(f"Expected Memory, got {type(value).__name__}")
|
|
100
|
-
self._internal_memory = value
|
|
84
|
+
"""
|
|
101
85
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
86
|
+
# NOTE: this is what is returned to the client and also what is used to initialize `Agent`
|
|
87
|
+
|
|
88
|
+
# This is an object representing the in-process state of a running `Agent`
|
|
89
|
+
# Field in this object can be theoretically edited by tools, and will be persisted by the ORM
|
|
90
|
+
memory: Memory = Field(..., description="The in-context memory of the agent.")
|
|
91
|
+
tools: List[Tool] = Field(..., description="The tools used by the agent.")
|
|
92
|
+
sources: List[Source] = Field(..., description="The sources used by the agent.")
|
|
93
|
+
tags: List[str] = Field(..., description="The tags associated with the agent.")
|
|
94
|
+
# TODO: add in context message objects
|
|
105
95
|
|
|
96
|
+
def to_persisted_agent_state(self) -> PersistedAgentState:
|
|
97
|
+
# turn back into persisted agent
|
|
98
|
+
data = self.model_dump()
|
|
99
|
+
del data["memory"]
|
|
100
|
+
del data["tools"]
|
|
101
|
+
del data["sources"]
|
|
102
|
+
del data["tags"]
|
|
103
|
+
return PersistedAgentState(**data)
|
|
106
104
|
|
|
107
|
-
|
|
105
|
+
|
|
106
|
+
class CreateAgent(BaseAgent): #
|
|
108
107
|
# all optional as server can generate defaults
|
|
109
108
|
name: Optional[str] = Field(None, description="The name of the agent.")
|
|
110
109
|
message_ids: Optional[List[str]] = Field(None, description="The ids of the messages in the agent's in-context memory.")
|
|
111
|
-
|
|
110
|
+
|
|
111
|
+
# memory creation
|
|
112
|
+
memory_blocks: List[CreateBlock] = Field(
|
|
113
|
+
# [CreateHuman(), CreatePersona()], description="The blocks to create in the agent's in-context memory."
|
|
114
|
+
...,
|
|
115
|
+
description="The blocks to create in the agent's in-context memory.",
|
|
116
|
+
)
|
|
117
|
+
|
|
112
118
|
tools: Optional[List[str]] = Field(None, description="The tools used by the agent.")
|
|
113
|
-
tool_rules: Optional[List[
|
|
119
|
+
tool_rules: Optional[List[ToolRule]] = Field(None, description="The tool rules governing the agent.")
|
|
114
120
|
tags: Optional[List[str]] = Field(None, description="The tags associated with the agent.")
|
|
115
121
|
system: Optional[str] = Field(None, description="The system prompt used by the agent.")
|
|
116
122
|
agent_type: Optional[AgentType] = Field(None, description="The type of agent.")
|
|
@@ -151,7 +157,7 @@ class CreateAgent(BaseAgent):
|
|
|
151
157
|
class UpdateAgentState(BaseAgent):
|
|
152
158
|
id: str = Field(..., description="The id of the agent.")
|
|
153
159
|
name: Optional[str] = Field(None, description="The name of the agent.")
|
|
154
|
-
|
|
160
|
+
tool_names: Optional[List[str]] = Field(None, description="The tools used by the agent.")
|
|
155
161
|
tags: Optional[List[str]] = Field(None, description="The tags associated with the agent.")
|
|
156
162
|
system: Optional[str] = Field(None, description="The system prompt used by the agent.")
|
|
157
163
|
llm_config: Optional[LLMConfig] = Field(None, description="The LLM configuration used by the agent.")
|
|
@@ -159,7 +165,6 @@ class UpdateAgentState(BaseAgent):
|
|
|
159
165
|
|
|
160
166
|
# TODO: determine if these should be editable via this schema?
|
|
161
167
|
message_ids: Optional[List[str]] = Field(None, description="The ids of the messages in the agent's in-context memory.")
|
|
162
|
-
memory: Optional[Memory] = Field(None, description="The in-context memory of the agent.")
|
|
163
168
|
|
|
164
169
|
|
|
165
170
|
class AgentStepResponse(BaseModel):
|
letta/schemas/block.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
|
-
from pydantic import Field, model_validator
|
|
3
|
+
from pydantic import BaseModel, Field, model_validator
|
|
4
4
|
from typing_extensions import Self
|
|
5
5
|
|
|
6
|
+
from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
|
|
6
7
|
from letta.schemas.letta_base import LettaBase
|
|
7
8
|
|
|
8
9
|
# block of the LLM context
|
|
@@ -15,7 +16,7 @@ class BaseBlock(LettaBase, validate_assignment=True):
|
|
|
15
16
|
|
|
16
17
|
# data value
|
|
17
18
|
value: str = Field(..., description="Value of the block.")
|
|
18
|
-
limit: int = Field(
|
|
19
|
+
limit: int = Field(CORE_MEMORY_BLOCK_CHAR_LIMIT, description="Character limit of the block.")
|
|
19
20
|
|
|
20
21
|
# template data (optional)
|
|
21
22
|
template_name: Optional[str] = Field(None, description="Name of the block if it is a template.", alias="name")
|
|
@@ -28,17 +29,20 @@ class BaseBlock(LettaBase, validate_assignment=True):
|
|
|
28
29
|
description: Optional[str] = Field(None, description="Description of the block.")
|
|
29
30
|
metadata_: Optional[dict] = Field({}, description="Metadata of the block.")
|
|
30
31
|
|
|
32
|
+
# def __len__(self):
|
|
33
|
+
# return len(self.value)
|
|
34
|
+
|
|
35
|
+
class Config:
|
|
36
|
+
extra = "ignore" # Ignores extra fields
|
|
37
|
+
|
|
31
38
|
@model_validator(mode="after")
|
|
32
39
|
def verify_char_limit(self) -> Self:
|
|
33
|
-
if len(self.value) > self.limit:
|
|
40
|
+
if self.value and len(self.value) > self.limit:
|
|
34
41
|
error_msg = f"Edit failed: Exceeds {self.limit} character limit (requested {len(self.value)}) - {str(self)}."
|
|
35
42
|
raise ValueError(error_msg)
|
|
36
43
|
|
|
37
44
|
return self
|
|
38
45
|
|
|
39
|
-
# def __len__(self):
|
|
40
|
-
# return len(self.value)
|
|
41
|
-
|
|
42
46
|
def __setattr__(self, name, value):
|
|
43
47
|
"""Run validation if self.value is updated"""
|
|
44
48
|
super().__setattr__(name, value)
|
|
@@ -46,9 +50,6 @@ class BaseBlock(LettaBase, validate_assignment=True):
|
|
|
46
50
|
# run validation
|
|
47
51
|
self.__class__.model_validate(self.model_dump(exclude_unset=True))
|
|
48
52
|
|
|
49
|
-
class Config:
|
|
50
|
-
extra = "ignore" # Ignores extra fields
|
|
51
|
-
|
|
52
53
|
|
|
53
54
|
class Block(BaseBlock):
|
|
54
55
|
"""
|
|
@@ -88,42 +89,100 @@ class Persona(Block):
|
|
|
88
89
|
label: str = "persona"
|
|
89
90
|
|
|
90
91
|
|
|
91
|
-
class
|
|
92
|
-
"""Create a block"""
|
|
93
|
-
|
|
94
|
-
is_template: bool = True
|
|
95
|
-
label: str = Field(..., description="Label of the block.")
|
|
92
|
+
# class CreateBlock(BaseBlock):
|
|
93
|
+
# """Create a block"""
|
|
94
|
+
#
|
|
95
|
+
# is_template: bool = True
|
|
96
|
+
# label: str = Field(..., description="Label of the block.")
|
|
96
97
|
|
|
97
98
|
|
|
98
|
-
class
|
|
99
|
-
"""
|
|
99
|
+
class BlockLabelUpdate(BaseModel):
|
|
100
|
+
"""Update the label of a block"""
|
|
100
101
|
|
|
101
|
-
|
|
102
|
+
current_label: str = Field(..., description="Current label of the block.")
|
|
103
|
+
new_label: str = Field(..., description="New label of the block.")
|
|
102
104
|
|
|
103
105
|
|
|
104
|
-
class
|
|
105
|
-
"""Create a
|
|
106
|
-
|
|
107
|
-
label: str = "
|
|
106
|
+
# class CreatePersona(CreateBlock):
|
|
107
|
+
# """Create a persona block"""
|
|
108
|
+
#
|
|
109
|
+
# label: str = "persona"
|
|
110
|
+
#
|
|
111
|
+
#
|
|
112
|
+
# class CreateHuman(CreateBlock):
|
|
113
|
+
# """Create a human block"""
|
|
114
|
+
#
|
|
115
|
+
# label: str = "human"
|
|
108
116
|
|
|
109
117
|
|
|
110
118
|
class BlockUpdate(BaseBlock):
|
|
111
119
|
"""Update a block"""
|
|
112
120
|
|
|
113
|
-
limit: Optional[int] = Field(
|
|
121
|
+
limit: Optional[int] = Field(CORE_MEMORY_BLOCK_CHAR_LIMIT, description="Character limit of the block.")
|
|
114
122
|
value: Optional[str] = Field(None, description="Value of the block.")
|
|
115
123
|
|
|
116
124
|
class Config:
|
|
117
125
|
extra = "ignore" # Ignores extra fields
|
|
118
126
|
|
|
119
127
|
|
|
120
|
-
class
|
|
121
|
-
"""Update a
|
|
128
|
+
class BlockLimitUpdate(BaseModel):
|
|
129
|
+
"""Update the limit of a block"""
|
|
130
|
+
|
|
131
|
+
label: str = Field(..., description="Label of the block.")
|
|
132
|
+
limit: int = Field(..., description="New limit of the block.")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# class UpdatePersona(BlockUpdate):
|
|
136
|
+
# """Update a persona block"""
|
|
137
|
+
#
|
|
138
|
+
# label: str = "persona"
|
|
139
|
+
#
|
|
140
|
+
#
|
|
141
|
+
# class UpdateHuman(BlockUpdate):
|
|
142
|
+
# """Update a human block"""
|
|
143
|
+
#
|
|
144
|
+
# label: str = "human"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class CreateBlock(BaseBlock):
|
|
148
|
+
"""Create a block"""
|
|
149
|
+
|
|
150
|
+
label: str = Field(..., description="Label of the block.")
|
|
151
|
+
limit: int = Field(CORE_MEMORY_BLOCK_CHAR_LIMIT, description="Character limit of the block.")
|
|
152
|
+
value: str = Field(..., description="Value of the block.")
|
|
153
|
+
|
|
154
|
+
# block templates
|
|
155
|
+
is_template: bool = False
|
|
156
|
+
template_name: Optional[str] = Field(None, description="Name of the block if it is a template.", alias="name")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class CreateHuman(CreateBlock):
|
|
160
|
+
"""Create a human block"""
|
|
161
|
+
|
|
162
|
+
label: str = "human"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class CreatePersona(CreateBlock):
|
|
166
|
+
"""Create a persona block"""
|
|
122
167
|
|
|
123
168
|
label: str = "persona"
|
|
124
169
|
|
|
125
170
|
|
|
126
|
-
class
|
|
127
|
-
"""
|
|
171
|
+
class CreateBlockTemplate(CreateBlock):
|
|
172
|
+
"""Create a block template"""
|
|
173
|
+
|
|
174
|
+
is_template: bool = True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class CreateHumanBlockTemplate(CreateHuman):
|
|
178
|
+
"""Create a human block template"""
|
|
128
179
|
|
|
180
|
+
is_template: bool = True
|
|
129
181
|
label: str = "human"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class CreatePersonaBlockTemplate(CreatePersona):
|
|
185
|
+
"""Create a persona block template"""
|
|
186
|
+
|
|
187
|
+
is_template: bool = True
|
|
188
|
+
label: str = "persona"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
|
|
6
|
+
from letta.schemas.letta_base import LettaBase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BlocksAgentsBase(LettaBase):
|
|
10
|
+
__id_prefix__ = "blocks_agents"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BlocksAgents(BlocksAgentsBase):
|
|
14
|
+
"""
|
|
15
|
+
Schema representing the relationship between blocks and agents.
|
|
16
|
+
|
|
17
|
+
Parameters:
|
|
18
|
+
agent_id (str): The ID of the associated agent.
|
|
19
|
+
block_id (str): The ID of the associated block.
|
|
20
|
+
block_label (str): The label of the block.
|
|
21
|
+
created_at (datetime): The date this relationship was created.
|
|
22
|
+
updated_at (datetime): The date this relationship was last updated.
|
|
23
|
+
is_deleted (bool): Whether this block-agent relationship is deleted or not.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
id: str = BlocksAgentsBase.generate_id_field()
|
|
27
|
+
agent_id: str = Field(..., description="The ID of the associated agent.")
|
|
28
|
+
block_id: str = Field(..., description="The ID of the associated block.")
|
|
29
|
+
block_label: str = Field(..., description="The label of the block.")
|
|
30
|
+
created_at: Optional[datetime] = Field(None, description="The creation date of the association.")
|
|
31
|
+
updated_at: Optional[datetime] = Field(None, description="The update date of the association.")
|
|
32
|
+
is_deleted: bool = Field(False, description="Whether this block-agent relationship is deleted or not.")
|
letta/schemas/enums.py
CHANGED
|
@@ -33,3 +33,17 @@ class MessageStreamStatus(str, Enum):
|
|
|
33
33
|
done_generation = "[DONE_GEN]"
|
|
34
34
|
done_step = "[DONE_STEP]"
|
|
35
35
|
done = "[DONE]"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class ToolRuleType(str, Enum):
|
|
39
|
+
"""
|
|
40
|
+
Type of tool rule.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# note: some of these should be renamed when we do the data migration
|
|
44
|
+
|
|
45
|
+
run_first = "InitToolRule"
|
|
46
|
+
exit_loop = "TerminalToolRule" # reasoning loop should exit
|
|
47
|
+
continue_loop = "continue_loop" # reasoning loop should continue
|
|
48
|
+
constrain_child_tools = "ToolRule"
|
|
49
|
+
require_parent_tools = "require_parent_tools"
|
letta/schemas/letta_base.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import uuid
|
|
2
|
+
from datetime import datetime
|
|
2
3
|
from logging import getLogger
|
|
3
4
|
from typing import Optional
|
|
4
5
|
from uuid import UUID
|
|
@@ -62,7 +63,7 @@ class LettaBase(BaseModel):
|
|
|
62
63
|
@classmethod
|
|
63
64
|
def _id_example(cls, prefix: str):
|
|
64
65
|
"""generates an example id for a given prefix"""
|
|
65
|
-
return
|
|
66
|
+
return f"{prefix}-123e4567-e89b-12d3-a456-426614174000"
|
|
66
67
|
|
|
67
68
|
@classmethod
|
|
68
69
|
def _id_description(cls, prefix: str):
|
|
@@ -80,3 +81,11 @@ class LettaBase(BaseModel):
|
|
|
80
81
|
logger.warning(f"Bare UUIDs are deprecated, please use the full prefixed id ({cls.__id_prefix__})!")
|
|
81
82
|
return f"{cls.__id_prefix__}-{v}"
|
|
82
83
|
return v
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class OrmMetadataBase(LettaBase):
|
|
87
|
+
# metadata fields
|
|
88
|
+
created_by_id: Optional[str] = Field(None, description="The id of the user that made this object.")
|
|
89
|
+
last_updated_by_id: Optional[str] = Field(None, description="The id of the user that made this object.")
|
|
90
|
+
created_at: Optional[datetime] = Field(None, description="The timestamp when the object was created.")
|
|
91
|
+
updated_at: Optional[datetime] = Field(None, description="The timestamp when the object was last updated.")
|
letta/schemas/letta_request.py
CHANGED
|
@@ -8,33 +8,21 @@ from letta.schemas.message import Message, MessageCreate
|
|
|
8
8
|
|
|
9
9
|
class LettaRequest(BaseModel):
|
|
10
10
|
messages: Union[List[MessageCreate], List[Message]] = Field(..., description="The messages to be sent to the agent.")
|
|
11
|
-
run_async: bool = Field(default=False, description="Whether to asynchronously send the messages to the agent.") # TODO: implement
|
|
12
|
-
|
|
13
|
-
stream_steps: bool = Field(
|
|
14
|
-
default=False, description="Flag to determine if the response should be streamed. Set to True for streaming agent steps."
|
|
15
|
-
)
|
|
16
|
-
stream_tokens: bool = Field(
|
|
17
|
-
default=False,
|
|
18
|
-
description="Flag to determine if individual tokens should be streamed. Set to True for token streaming (requires stream_steps = True).",
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
return_message_object: bool = Field(
|
|
22
|
-
default=False,
|
|
23
|
-
description="Set True to return the raw Message object. Set False to return the Message in the format of the Letta API.",
|
|
24
|
-
)
|
|
25
11
|
|
|
26
12
|
# Flags to support the use of AssistantMessage message types
|
|
27
13
|
|
|
28
|
-
|
|
29
|
-
default=False,
|
|
30
|
-
description="[Only applicable if return_message_object is False] If true, returns AssistantMessage objects when the agent calls a designated message tool. If false, return FunctionCallMessage objects for all tool calls.",
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
assistant_message_function_name: str = Field(
|
|
14
|
+
assistant_message_tool_name: str = Field(
|
|
34
15
|
default=DEFAULT_MESSAGE_TOOL,
|
|
35
|
-
description="
|
|
16
|
+
description="The name of the designated message tool.",
|
|
36
17
|
)
|
|
37
|
-
|
|
18
|
+
assistant_message_tool_kwarg: str = Field(
|
|
38
19
|
default=DEFAULT_MESSAGE_TOOL_KWARG,
|
|
39
|
-
description="
|
|
20
|
+
description="The name of the message argument in the designated message tool.",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LettaStreamingRequest(LettaRequest):
|
|
25
|
+
stream_tokens: bool = Field(
|
|
26
|
+
default=False,
|
|
27
|
+
description="Flag to determine if individual tokens should be streamed. Set to True for token streaming (requires stream_steps = True).",
|
|
40
28
|
)
|