letta-nightly 0.5.5.dev20241122170833__py3-none-any.whl → 0.6.0.dev20241204051808__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.

Files changed (70) hide show
  1. letta/__init__.py +2 -2
  2. letta/agent.py +155 -166
  3. letta/agent_store/chroma.py +2 -0
  4. letta/agent_store/db.py +1 -1
  5. letta/cli/cli.py +12 -8
  6. letta/cli/cli_config.py +1 -1
  7. letta/client/client.py +765 -137
  8. letta/config.py +2 -2
  9. letta/constants.py +10 -14
  10. letta/errors.py +12 -0
  11. letta/functions/function_sets/base.py +38 -1
  12. letta/functions/functions.py +40 -57
  13. letta/functions/helpers.py +0 -4
  14. letta/functions/schema_generator.py +279 -18
  15. letta/helpers/tool_rule_solver.py +6 -5
  16. letta/llm_api/helpers.py +99 -5
  17. letta/llm_api/openai.py +8 -2
  18. letta/local_llm/utils.py +13 -6
  19. letta/log.py +7 -9
  20. letta/main.py +1 -1
  21. letta/metadata.py +53 -38
  22. letta/o1_agent.py +1 -4
  23. letta/orm/__init__.py +2 -0
  24. letta/orm/block.py +7 -3
  25. letta/orm/blocks_agents.py +32 -0
  26. letta/orm/errors.py +8 -0
  27. letta/orm/mixins.py +8 -0
  28. letta/orm/organization.py +8 -1
  29. letta/orm/sandbox_config.py +56 -0
  30. letta/orm/sqlalchemy_base.py +68 -10
  31. letta/persistence_manager.py +1 -0
  32. letta/schemas/agent.py +57 -52
  33. letta/schemas/block.py +85 -26
  34. letta/schemas/blocks_agents.py +32 -0
  35. letta/schemas/enums.py +14 -0
  36. letta/schemas/letta_base.py +10 -1
  37. letta/schemas/letta_request.py +11 -23
  38. letta/schemas/letta_response.py +1 -2
  39. letta/schemas/memory.py +41 -76
  40. letta/schemas/message.py +3 -3
  41. letta/schemas/sandbox_config.py +114 -0
  42. letta/schemas/tool.py +37 -1
  43. letta/schemas/tool_rule.py +13 -5
  44. letta/server/rest_api/app.py +5 -4
  45. letta/server/rest_api/interface.py +12 -19
  46. letta/server/rest_api/routers/openai/assistants/threads.py +2 -3
  47. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +0 -2
  48. letta/server/rest_api/routers/v1/__init__.py +4 -9
  49. letta/server/rest_api/routers/v1/agents.py +145 -61
  50. letta/server/rest_api/routers/v1/blocks.py +50 -5
  51. letta/server/rest_api/routers/v1/sandbox_configs.py +127 -0
  52. letta/server/rest_api/routers/v1/sources.py +8 -1
  53. letta/server/rest_api/routers/v1/tools.py +139 -13
  54. letta/server/rest_api/utils.py +6 -0
  55. letta/server/server.py +397 -340
  56. letta/server/static_files/assets/index-9fa459a2.js +1 -1
  57. letta/services/block_manager.py +23 -2
  58. letta/services/blocks_agents_manager.py +106 -0
  59. letta/services/per_agent_lock_manager.py +18 -0
  60. letta/services/sandbox_config_manager.py +256 -0
  61. letta/services/tool_execution_sandbox.py +352 -0
  62. letta/services/tool_manager.py +16 -22
  63. letta/services/tool_sandbox_env/.gitkeep +0 -0
  64. letta/settings.py +4 -0
  65. letta/utils.py +0 -7
  66. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/METADATA +8 -6
  67. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/RECORD +70 -60
  68. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/LICENSE +0 -0
  69. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.dist-info}/WHEEL +0 -0
  70. {letta_nightly-0.5.5.dev20241122170833.dist-info → letta_nightly-0.6.0.dev20241204051808.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")
@@ -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 NoResultFound
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
- with db_session as session:
103
- session.add(self)
104
- session.commit()
105
- session.refresh(self)
106
- return self
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
- if actor:
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.")
@@ -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, model_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.tool_rule import BaseToolRule
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 AgentState(BaseAgent, validate_assignment=True):
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
- tools: List[str] = Field(..., description="The tools used by the agent.")
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[BaseToolRule]] = Field(default=None, description="The list of tool rules.")
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
- def __init__(self, **data):
81
- super().__init__(**data)
82
- self._internal_memory = self.memory
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
- @property
93
- def memory(self) -> Memory:
94
- return self._internal_memory
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
- @memory.setter
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
- class Config:
103
- arbitrary_types_allowed = True
104
- validate_assignment = True
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
- class CreateAgent(BaseAgent):
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
- memory: Optional[Memory] = Field(None, description="The in-context memory of the agent.")
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[BaseToolRule]] = Field(None, description="The tool rules governing the agent.")
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
- tools: Optional[List[str]] = Field(None, description="The tools used by the agent.")
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(2000, description="Character limit of the block.")
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 BlockCreate(BaseBlock):
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 CreatePersona(BlockCreate):
99
- """Create a persona block"""
99
+ class BlockLabelUpdate(BaseModel):
100
+ """Update the label of a block"""
100
101
 
101
- label: str = "persona"
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 CreateHuman(BlockCreate):
105
- """Create a human block"""
106
-
107
- label: str = "human"
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(2000, description="Character limit of the block.")
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 UpdatePersona(BlockUpdate):
121
- """Update a persona block"""
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 UpdateHuman(BlockUpdate):
127
- """Update a human block"""
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"
@@ -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 [prefix + "-123e4567-e89b-12d3-a456-426614174000"]
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.")
@@ -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
- use_assistant_message: bool = Field(
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="[Only applicable if use_assistant_message is True] The name of the designated message tool.",
16
+ description="The name of the designated message tool.",
36
17
  )
37
- assistant_message_function_kwarg: str = Field(
18
+ assistant_message_tool_kwarg: str = Field(
38
19
  default=DEFAULT_MESSAGE_TOOL_KWARG,
39
- description="[Only applicable if use_assistant_message is True] The name of the message argument in the designated message tool.",
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
  )