letta-nightly 0.5.1.dev20241104104148__py3-none-any.whl → 0.5.1.dev20241106104104__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 (33) hide show
  1. letta/agent.py +37 -5
  2. letta/agent_store/db.py +19 -19
  3. letta/cli/cli.py +3 -3
  4. letta/cli/cli_config.py +2 -2
  5. letta/client/client.py +36 -19
  6. letta/functions/schema_generator.py +48 -7
  7. letta/metadata.py +10 -10
  8. letta/orm/base.py +5 -2
  9. letta/orm/mixins.py +2 -53
  10. letta/orm/organization.py +3 -1
  11. letta/orm/sqlalchemy_base.py +6 -45
  12. letta/orm/tool.py +3 -2
  13. letta/orm/user.py +3 -1
  14. letta/schemas/agent.py +6 -2
  15. letta/schemas/block.py +1 -1
  16. letta/schemas/letta_base.py +2 -0
  17. letta/schemas/memory.py +4 -0
  18. letta/schemas/organization.py +4 -4
  19. letta/schemas/tool.py +14 -11
  20. letta/schemas/user.py +1 -1
  21. letta/server/rest_api/routers/v1/agents.py +6 -1
  22. letta/server/rest_api/routers/v1/organizations.py +2 -1
  23. letta/server/rest_api/routers/v1/tools.py +2 -1
  24. letta/server/rest_api/routers/v1/users.py +2 -2
  25. letta/server/server.py +20 -11
  26. letta/services/organization_manager.py +4 -4
  27. letta/services/tool_manager.py +17 -16
  28. letta/services/user_manager.py +3 -3
  29. {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/METADATA +3 -3
  30. {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/RECORD +33 -33
  31. {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/LICENSE +0 -0
  32. {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/WHEEL +0 -0
  33. {letta_nightly-0.5.1.dev20241104104148.dist-info → letta_nightly-0.5.1.dev20241106104104.dist-info}/entry_points.txt +0 -0
@@ -1,14 +1,11 @@
1
1
  from typing import TYPE_CHECKING, List, Literal, Optional, Type
2
- from uuid import uuid4
3
2
 
4
- from humps import depascalize
5
- from sqlalchemy import Boolean, String, select
3
+ from sqlalchemy import String, select
6
4
  from sqlalchemy.orm import Mapped, mapped_column
7
5
 
8
6
  from letta.log import get_logger
9
7
  from letta.orm.base import Base, CommonSqlalchemyMetaMixins
10
8
  from letta.orm.errors import NoResultFound
11
- from letta.orm.mixins import is_valid_uuid4
12
9
 
13
10
  if TYPE_CHECKING:
14
11
  from pydantic import BaseModel
@@ -24,27 +21,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
24
21
 
25
22
  __order_by_default__ = "created_at"
26
23
 
27
- _id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"{uuid4()}")
28
-
29
- deleted: Mapped[bool] = mapped_column(Boolean, default=False, doc="Is this record deleted? Used for universal soft deletes.")
30
-
31
- @classmethod
32
- def __prefix__(cls) -> str:
33
- return depascalize(cls.__name__)
34
-
35
- @property
36
- def id(self) -> Optional[str]:
37
- if self._id:
38
- return f"{self.__prefix__()}-{self._id}"
39
-
40
- @id.setter
41
- def id(self, value: str) -> None:
42
- if not value:
43
- return
44
- prefix, id_ = value.split("-", 1)
45
- assert prefix == self.__prefix__(), f"{prefix} is not a valid id prefix for {self.__class__.__name__}"
46
- assert is_valid_uuid4(id_), f"{id_} is not a valid uuid4"
47
- self._id = id_
24
+ id: Mapped[str] = mapped_column(String, primary_key=True)
48
25
 
49
26
  @classmethod
50
27
  def list(
@@ -57,11 +34,10 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
57
34
 
58
35
  # Add a cursor condition if provided
59
36
  if cursor:
60
- cursor_uuid = cls.get_uid_from_identifier(cursor) # Assuming the cursor is an _id value
61
- query = query.where(cls._id > cursor_uuid)
37
+ query = query.where(cls.id > cursor)
62
38
 
63
39
  # Add a limit to the query if provided
64
- query = query.order_by(cls._id).limit(limit)
40
+ query = query.order_by(cls.id).limit(limit)
65
41
 
66
42
  # Handle soft deletes if the class has the 'is_deleted' attribute
67
43
  if hasattr(cls, "is_deleted"):
@@ -70,20 +46,6 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
70
46
  # Execute the query and return the results as a list of model instances
71
47
  return list(session.execute(query).scalars())
72
48
 
73
- @classmethod
74
- def get_uid_from_identifier(cls, identifier: str, indifferent: Optional[bool] = False) -> str:
75
- """converts the id into a uuid object
76
- Args:
77
- identifier: the string identifier, such as `organization-xxxx-xx...`
78
- indifferent: if True, will not enforce the prefix check
79
- """
80
- try:
81
- uuid_string = identifier.split("-", 1)[1] if indifferent else identifier.replace(f"{cls.__prefix__()}-", "")
82
- assert is_valid_uuid4(uuid_string)
83
- return uuid_string
84
- except ValueError as e:
85
- raise ValueError(f"{identifier} is not a valid identifier for class {cls.__name__}") from e
86
-
87
49
  @classmethod
88
50
  def read(
89
51
  cls,
@@ -112,8 +74,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
112
74
 
113
75
  # If an identifier is provided, add it to the query conditions
114
76
  if identifier is not None:
115
- identifier = cls.get_uid_from_identifier(identifier)
116
- query = query.where(cls._id == identifier)
77
+ query = query.where(cls.id == identifier)
117
78
  query_conditions.append(f"id='{identifier}'")
118
79
 
119
80
  if kwargs:
@@ -183,7 +144,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
183
144
  org_id = getattr(actor, "organization_id", None)
184
145
  if not org_id:
185
146
  raise ValueError(f"object {actor} has no organization accessor")
186
- return query.where(cls._organization_id == cls.get_uid_from_identifier(org_id, indifferent=True), cls.is_deleted == False)
147
+ return query.where(cls.organization_id == org_id, cls.is_deleted == False)
187
148
 
188
149
  @property
189
150
  def __pydantic_model__(self) -> Type["BaseModel"]:
letta/orm/tool.py CHANGED
@@ -21,13 +21,14 @@ class Tool(SqlalchemyBase, OrganizationMixin):
21
21
  more granular permissions.
22
22
  """
23
23
 
24
- __tablename__ = "tool"
24
+ __tablename__ = "tools"
25
25
  __pydantic_model__ = PydanticTool
26
26
 
27
27
  # Add unique constraint on (name, _organization_id)
28
28
  # An organization should not have multiple tools with the same name
29
- __table_args__ = (UniqueConstraint("name", "_organization_id", name="uix_name_organization"),)
29
+ __table_args__ = (UniqueConstraint("name", "organization_id", name="uix_name_organization"),)
30
30
 
31
+ id: Mapped[str] = mapped_column(String, primary_key=True)
31
32
  name: Mapped[str] = mapped_column(doc="The display name of the tool.")
32
33
  description: Mapped[Optional[str]] = mapped_column(nullable=True, doc="The description of the tool.")
33
34
  tags: Mapped[List] = mapped_column(JSON, doc="Metadata tags used to filter tools.")
letta/orm/user.py CHANGED
@@ -1,5 +1,6 @@
1
1
  from typing import TYPE_CHECKING
2
2
 
3
+ from sqlalchemy import String
3
4
  from sqlalchemy.orm import Mapped, mapped_column, relationship
4
5
 
5
6
  from letta.orm.mixins import OrganizationMixin
@@ -13,9 +14,10 @@ if TYPE_CHECKING:
13
14
  class User(SqlalchemyBase, OrganizationMixin):
14
15
  """User ORM class"""
15
16
 
16
- __tablename__ = "user"
17
+ __tablename__ = "users"
17
18
  __pydantic_model__ = PydanticUser
18
19
 
20
+ id: Mapped[str] = mapped_column(String, primary_key=True)
19
21
  name: Mapped[str] = mapped_column(nullable=False, doc="The display name of the user.")
20
22
 
21
23
  # relationships
letta/schemas/agent.py CHANGED
@@ -1,4 +1,3 @@
1
- import uuid
2
1
  from datetime import datetime
3
2
  from enum import Enum
4
3
  from typing import Dict, List, Optional
@@ -105,7 +104,7 @@ class AgentState(BaseAgent, validate_assignment=True):
105
104
  class CreateAgent(BaseAgent):
106
105
  # all optional as server can generate defaults
107
106
  name: Optional[str] = Field(None, description="The name of the agent.")
108
- message_ids: Optional[List[uuid.UUID]] = Field(None, description="The ids of the messages in the agent's in-context memory.")
107
+ message_ids: Optional[List[str]] = Field(None, description="The ids of the messages in the agent's in-context memory.")
109
108
  memory: Optional[Memory] = Field(None, description="The in-context memory of the agent.")
110
109
  tools: Optional[List[str]] = Field(None, description="The tools used by the agent.")
111
110
  tool_rules: Optional[List[BaseToolRule]] = Field(None, description="The tool rules governing the agent.")
@@ -113,6 +112,11 @@ class CreateAgent(BaseAgent):
113
112
  agent_type: Optional[AgentType] = Field(None, description="The type of agent.")
114
113
  llm_config: Optional[LLMConfig] = Field(None, description="The LLM configuration used by the agent.")
115
114
  embedding_config: Optional[EmbeddingConfig] = Field(None, description="The embedding configuration used by the agent.")
115
+ # Note: if this is None, then we'll populate with the standard "more human than human" initial message sequence
116
+ # If the client wants to make this empty, then the client can set the arg to an empty list
117
+ initial_message_sequence: Optional[List[Message]] = Field(
118
+ None, description="The initial set of messages to put in the agent's in-context memory."
119
+ )
116
120
 
117
121
  @field_validator("name")
118
122
  @classmethod
letta/schemas/block.py CHANGED
@@ -18,7 +18,7 @@ class BaseBlock(LettaBase, validate_assignment=True):
18
18
  limit: int = Field(2000, description="Character limit of the block.")
19
19
 
20
20
  # template data (optional)
21
- name: Optional[str] = Field(None, description="Name of the block if it is a template.")
21
+ template_name: Optional[str] = Field(None, description="Name of the block if it is a template.")
22
22
  template: bool = Field(False, description="Whether the block is a template (e.g. saved human/persona options).")
23
23
 
24
24
  # context window label
@@ -21,6 +21,8 @@ class LettaBase(BaseModel):
21
21
  from_attributes=True,
22
22
  # throw errors if attributes are given that don't belong
23
23
  extra="forbid",
24
+ # handle datetime serialization consistently across all models
25
+ # json_encoders={datetime: lambda dt: (dt.replace(tzinfo=timezone.utc) if dt.tzinfo is None else dt).isoformat()},
24
26
  )
25
27
 
26
28
  # def __id_prefix__(self):
letta/schemas/memory.py CHANGED
@@ -106,6 +106,10 @@ class Memory(BaseModel, validate_assignment=True):
106
106
  # New format
107
107
  obj.prompt_template = state["prompt_template"]
108
108
  for key, value in state["memory"].items():
109
+ # TODO: This is migration code, please take a look at a later time to get rid of this
110
+ if "name" in value:
111
+ value["template_name"] = value["name"]
112
+ value.pop("name")
109
113
  obj.memory[key] = Block(**value)
110
114
  else:
111
115
  # Old format (pre-template)
@@ -4,16 +4,16 @@ from typing import Optional
4
4
  from pydantic import Field
5
5
 
6
6
  from letta.schemas.letta_base import LettaBase
7
- from letta.utils import get_utc_time
7
+ from letta.utils import create_random_username, get_utc_time
8
8
 
9
9
 
10
10
  class OrganizationBase(LettaBase):
11
- __id_prefix__ = "organization"
11
+ __id_prefix__ = "org"
12
12
 
13
13
 
14
14
  class Organization(OrganizationBase):
15
- id: str = Field(..., description="The id of the organization.")
16
- name: str = Field(..., description="The name of the organization.")
15
+ id: str = OrganizationBase.generate_id_field()
16
+ name: str = Field(create_random_username(), description="The name of the organization.")
17
17
  created_at: Optional[datetime] = Field(default_factory=get_utc_time, description="The creation date of the organization.")
18
18
 
19
19
 
letta/schemas/tool.py CHANGED
@@ -8,7 +8,10 @@ from letta.functions.helpers import (
8
8
  generate_crewai_tool_wrapper,
9
9
  generate_langchain_tool_wrapper,
10
10
  )
11
- from letta.functions.schema_generator import generate_schema_from_args_schema
11
+ from letta.functions.schema_generator import (
12
+ generate_schema_from_args_schema_v1,
13
+ generate_schema_from_args_schema_v2,
14
+ )
12
15
  from letta.schemas.letta_base import LettaBase
13
16
  from letta.schemas.openai.chat_completions import ToolCall
14
17
 
@@ -30,21 +33,21 @@ class Tool(BaseTool):
30
33
 
31
34
  """
32
35
 
33
- id: str = Field(..., description="The id of the tool.")
36
+ id: str = BaseTool.generate_id_field()
34
37
  description: Optional[str] = Field(None, description="The description of the tool.")
35
38
  source_type: Optional[str] = Field(None, description="The type of the source code.")
36
39
  module: Optional[str] = Field(None, description="The module of the function.")
37
- organization_id: str = Field(..., description="The unique identifier of the organization associated with the tool.")
38
- name: str = Field(..., description="The name of the function.")
39
- tags: List[str] = Field(..., description="Metadata tags.")
40
+ organization_id: Optional[str] = Field(None, description="The unique identifier of the organization associated with the tool.")
41
+ name: Optional[str] = Field(None, description="The name of the function.")
42
+ tags: List[str] = Field([], description="Metadata tags.")
40
43
 
41
44
  # code
42
45
  source_code: str = Field(..., description="The source code of the function.")
43
- json_schema: Dict = Field(default_factory=dict, description="The JSON schema of the function.")
46
+ json_schema: Optional[Dict] = Field(None, description="The JSON schema of the function.")
44
47
 
45
48
  # metadata fields
46
- created_by_id: str = Field(..., description="The id of the user that made this Tool.")
47
- last_updated_by_id: str = Field(..., description="The id of the user that made this Tool.")
49
+ created_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
50
+ last_updated_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
48
51
 
49
52
  def to_dict(self):
50
53
  """
@@ -97,7 +100,7 @@ class ToolCreate(LettaBase):
97
100
  source_type = "python"
98
101
  tags = ["composio"]
99
102
  wrapper_func_name, wrapper_function_str = generate_composio_tool_wrapper(action)
100
- json_schema = generate_schema_from_args_schema(composio_tool.args_schema, name=wrapper_func_name, description=description)
103
+ json_schema = generate_schema_from_args_schema_v2(composio_tool.args_schema, name=wrapper_func_name, description=description)
101
104
 
102
105
  return cls(
103
106
  name=wrapper_func_name,
@@ -129,7 +132,7 @@ class ToolCreate(LettaBase):
129
132
  tags = ["langchain"]
130
133
  # NOTE: langchain tools may come from different packages
131
134
  wrapper_func_name, wrapper_function_str = generate_langchain_tool_wrapper(langchain_tool, additional_imports_module_attr_map)
132
- json_schema = generate_schema_from_args_schema(langchain_tool.args_schema, name=wrapper_func_name, description=description)
135
+ json_schema = generate_schema_from_args_schema_v1(langchain_tool.args_schema, name=wrapper_func_name, description=description)
133
136
 
134
137
  return cls(
135
138
  name=wrapper_func_name,
@@ -159,7 +162,7 @@ class ToolCreate(LettaBase):
159
162
  source_type = "python"
160
163
  tags = ["crew-ai"]
161
164
  wrapper_func_name, wrapper_function_str = generate_crewai_tool_wrapper(crewai_tool, additional_imports_module_attr_map)
162
- json_schema = generate_schema_from_args_schema(crewai_tool.args_schema, name=wrapper_func_name, description=description)
165
+ json_schema = generate_schema_from_args_schema_v1(crewai_tool.args_schema, name=wrapper_func_name, description=description)
163
166
 
164
167
  return cls(
165
168
  name=wrapper_func_name,
letta/schemas/user.py CHANGED
@@ -21,7 +21,7 @@ class User(UserBase):
21
21
  created_at (datetime): The creation date of the user.
22
22
  """
23
23
 
24
- id: str = Field(..., description="The id of the user.")
24
+ id: str = UserBase.generate_id_field()
25
25
  organization_id: Optional[str] = Field(OrganizationManager.DEFAULT_ORG_ID, description="The organization id of the user")
26
26
  name: str = Field(..., description="The name of the user.")
27
27
  created_at: Optional[datetime] = Field(default_factory=datetime.utcnow, description="The creation date of the user.")
@@ -40,6 +40,7 @@ router = APIRouter(prefix="/agents", tags=["agents"])
40
40
 
41
41
  @router.get("/", response_model=List[AgentState], operation_id="list_agents")
42
42
  def list_agents(
43
+ name: Optional[str] = Query(None, description="Name of the agent"),
43
44
  server: "SyncServer" = Depends(get_letta_server),
44
45
  user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
45
46
  ):
@@ -49,7 +50,11 @@ def list_agents(
49
50
  """
50
51
  actor = server.get_user_or_default(user_id=user_id)
51
52
 
52
- return server.list_agents(user_id=actor.id)
53
+ agents = server.list_agents(user_id=actor.id)
54
+ # TODO: move this logic to the ORM
55
+ if name:
56
+ agents = [a for a in agents if a.name == name]
57
+ return agents
53
58
 
54
59
 
55
60
  @router.get("/{agent_id}/context", response_model=ContextWindowOverview, operation_id="get_agent_context_window")
@@ -38,7 +38,8 @@ def create_org(
38
38
  """
39
39
  Create a new org in the database
40
40
  """
41
- org = server.organization_manager.create_organization(name=request.name)
41
+ org = Organization(**request.model_dump())
42
+ org = server.organization_manager.create_organization(pydantic_org=org)
42
43
  return org
43
44
 
44
45
 
@@ -89,7 +89,8 @@ def create_tool(
89
89
  actor = server.get_user_or_default(user_id=user_id)
90
90
 
91
91
  # Send request to create the tool
92
- return server.tool_manager.create_or_update_tool(tool_create=request, actor=actor)
92
+ tool = Tool(**request.model_dump())
93
+ return server.tool_manager.create_or_update_tool(pydantic_tool=tool, actor=actor)
93
94
 
94
95
 
95
96
  @router.patch("/{tool_id}", response_model=Tool, operation_id="update_tool")
@@ -51,8 +51,8 @@ def create_user(
51
51
  """
52
52
  Create a new user in the database
53
53
  """
54
-
55
- user = server.user_manager.create_user(request)
54
+ user = User(**request.model_dump())
55
+ user = server.user_manager.create_user(user)
56
56
  return user
57
57
 
58
58
 
letta/server/server.py CHANGED
@@ -824,7 +824,7 @@ class SyncServer(Server):
824
824
  source_type = "python"
825
825
  tags = ["memory", "memgpt-base"]
826
826
  tool = self.tool_manager.create_or_update_tool(
827
- ToolCreate(
827
+ Tool(
828
828
  source_code=source_code,
829
829
  source_type=source_type,
830
830
  tags=tags,
@@ -857,7 +857,10 @@ class SyncServer(Server):
857
857
  agent_state=agent_state,
858
858
  tools=tool_objs,
859
859
  # gpt-3.5-turbo tends to omit inner monologue, relax this requirement for now
860
- first_message_verify_mono=True if (llm_config.model is not None and "gpt-4" in llm_config.model) else False,
860
+ first_message_verify_mono=(
861
+ True if (llm_config and llm_config.model is not None and "gpt-4" in llm_config.model) else False
862
+ ),
863
+ initial_message_sequence=request.initial_message_sequence,
861
864
  )
862
865
  elif request.agent_type == AgentType.o1_agent:
863
866
  agent = O1Agent(
@@ -865,7 +868,9 @@ class SyncServer(Server):
865
868
  agent_state=agent_state,
866
869
  tools=tool_objs,
867
870
  # gpt-3.5-turbo tends to omit inner monologue, relax this requirement for now
868
- first_message_verify_mono=True if (llm_config.model is not None and "gpt-4" in llm_config.model) else False,
871
+ first_message_verify_mono=(
872
+ True if (llm_config and llm_config.model is not None and "gpt-4" in llm_config.model) else False
873
+ ),
869
874
  )
870
875
  # rebuilding agent memory on agent create in case shared memory blocks
871
876
  # were specified in the new agent's memory config. we're doing this for two reasons:
@@ -1084,7 +1089,7 @@ class SyncServer(Server):
1084
1089
  id: Optional[str] = None,
1085
1090
  ) -> Optional[List[Block]]:
1086
1091
 
1087
- return self.ms.get_blocks(user_id=user_id, label=label, template=template, name=name, id=id)
1092
+ return self.ms.get_blocks(user_id=user_id, label=label, template=template, template_name=name, id=id)
1088
1093
 
1089
1094
  def get_block(self, block_id: str):
1090
1095
 
@@ -1096,14 +1101,18 @@ class SyncServer(Server):
1096
1101
  return blocks[0]
1097
1102
 
1098
1103
  def create_block(self, request: CreateBlock, user_id: str, update: bool = False) -> Block:
1099
- existing_blocks = self.ms.get_blocks(name=request.name, user_id=user_id, template=request.template, label=request.label)
1100
- if existing_blocks is not None:
1104
+ existing_blocks = self.ms.get_blocks(
1105
+ template_name=request.template_name, user_id=user_id, template=request.template, label=request.label
1106
+ )
1107
+
1108
+ # for templates, update existing block template if exists
1109
+ if existing_blocks is not None and request.template:
1101
1110
  existing_block = existing_blocks[0]
1102
1111
  assert len(existing_blocks) == 1
1103
1112
  if update:
1104
1113
  return self.update_block(UpdateBlock(id=existing_block.id, **vars(request)))
1105
1114
  else:
1106
- raise ValueError(f"Block with name {request.name} already exists")
1115
+ raise ValueError(f"Block with name {request.template_name} already exists")
1107
1116
  block = Block(**vars(request))
1108
1117
  self.ms.create_block(block)
1109
1118
  return block
@@ -1112,7 +1121,7 @@ class SyncServer(Server):
1112
1121
  block = self.get_block(request.id)
1113
1122
  block.limit = request.limit if request.limit is not None else block.limit
1114
1123
  block.value = request.value if request.value is not None else block.value
1115
- block.name = request.name if request.name is not None else block.name
1124
+ block.template_name = request.template_name if request.template_name is not None else block.template_name
1116
1125
  self.ms.update_block(block=block)
1117
1126
  return self.ms.get_block(block_id=request.id)
1118
1127
 
@@ -1757,7 +1766,7 @@ class SyncServer(Server):
1757
1766
  tool_creates += ToolCreate.load_default_composio_tools()
1758
1767
  for tool_create in tool_creates:
1759
1768
  try:
1760
- self.tool_manager.create_or_update_tool(tool_create, actor=actor)
1769
+ self.tool_manager.create_or_update_tool(Tool(**tool_create.model_dump()), actor=actor)
1761
1770
  except Exception as e:
1762
1771
  warnings.warn(f"An error occurred while creating tool {tool_create}: {e}")
1763
1772
  warnings.warn(traceback.format_exc())
@@ -1773,12 +1782,12 @@ class SyncServer(Server):
1773
1782
  for persona_file in list_persona_files():
1774
1783
  text = open(persona_file, "r", encoding="utf-8").read()
1775
1784
  name = os.path.basename(persona_file).replace(".txt", "")
1776
- self.create_block(CreatePersona(user_id=user_id, name=name, value=text, template=True), user_id=user_id, update=True)
1785
+ self.create_block(CreatePersona(user_id=user_id, template_name=name, value=text, template=True), user_id=user_id, update=True)
1777
1786
 
1778
1787
  for human_file in list_human_files():
1779
1788
  text = open(human_file, "r", encoding="utf-8").read()
1780
1789
  name = os.path.basename(human_file).replace(".txt", "")
1781
- self.create_block(CreateHuman(user_id=user_id, name=name, value=text, template=True), user_id=user_id, update=True)
1790
+ self.create_block(CreateHuman(user_id=user_id, template_name=name, value=text, template=True), user_id=user_id, update=True)
1782
1791
 
1783
1792
  def get_agent_message(self, agent_id: str, message_id: str) -> Optional[Message]:
1784
1793
  """Get a single message from the agent's memory"""
@@ -3,13 +3,13 @@ from typing import List, Optional
3
3
  from letta.orm.errors import NoResultFound
4
4
  from letta.orm.organization import Organization as OrganizationModel
5
5
  from letta.schemas.organization import Organization as PydanticOrganization
6
- from letta.utils import create_random_username, enforce_types
6
+ from letta.utils import enforce_types
7
7
 
8
8
 
9
9
  class OrganizationManager:
10
10
  """Manager class to handle business logic related to Organizations."""
11
11
 
12
- DEFAULT_ORG_ID = "organization-00000000-0000-4000-8000-000000000000"
12
+ DEFAULT_ORG_ID = "org-00000000-0000-4000-8000-000000000000"
13
13
  DEFAULT_ORG_NAME = "default_org"
14
14
 
15
15
  def __init__(self):
@@ -37,10 +37,10 @@ class OrganizationManager:
37
37
  raise ValueError(f"Organization with id {org_id} not found.")
38
38
 
39
39
  @enforce_types
40
- def create_organization(self, name: Optional[str] = None) -> PydanticOrganization:
40
+ def create_organization(self, pydantic_org: PydanticOrganization) -> PydanticOrganization:
41
41
  """Create a new organization. If a name is provided, it is used, otherwise, a random one is generated."""
42
42
  with self.session_maker() as session:
43
- org = OrganizationModel(name=name if name else create_random_username())
43
+ org = OrganizationModel(**pydantic_org.model_dump())
44
44
  org.create(session)
45
45
  return org.to_pydantic()
46
46
 
@@ -7,10 +7,9 @@ from letta.functions.functions import derive_openai_json_schema, load_function_s
7
7
 
8
8
  # TODO: Remove this once we translate all of these to the ORM
9
9
  from letta.orm.errors import NoResultFound
10
- from letta.orm.organization import Organization as OrganizationModel
11
10
  from letta.orm.tool import Tool as ToolModel
12
11
  from letta.schemas.tool import Tool as PydanticTool
13
- from letta.schemas.tool import ToolCreate, ToolUpdate
12
+ from letta.schemas.tool import ToolUpdate
14
13
  from letta.schemas.user import User as PydanticUser
15
14
  from letta.utils import enforce_types, printd
16
15
 
@@ -33,20 +32,20 @@ class ToolManager:
33
32
  self.session_maker = db_context
34
33
 
35
34
  @enforce_types
36
- def create_or_update_tool(self, tool_create: ToolCreate, actor: PydanticUser) -> PydanticTool:
35
+ def create_or_update_tool(self, pydantic_tool: PydanticTool, actor: PydanticUser) -> PydanticTool:
37
36
  """Create a new tool based on the ToolCreate schema."""
38
37
  # Derive json_schema
39
- derived_json_schema = tool_create.json_schema or derive_openai_json_schema(
40
- source_code=tool_create.source_code, name=tool_create.name
38
+ derived_json_schema = pydantic_tool.json_schema or derive_openai_json_schema(
39
+ source_code=pydantic_tool.source_code, name=pydantic_tool.name
41
40
  )
42
- derived_name = tool_create.name or derived_json_schema["name"]
41
+ derived_name = pydantic_tool.name or derived_json_schema["name"]
43
42
 
44
43
  try:
45
44
  # NOTE: We use the organization id here
46
45
  # This is important, because even if it's a different user, adding the same tool to the org should not happen
47
46
  tool = self.get_tool_by_name(tool_name=derived_name, actor=actor)
48
47
  # Put to dict and remove fields that should not be reset
49
- update_data = tool_create.model_dump(exclude={"module"}, exclude_unset=True)
48
+ update_data = pydantic_tool.model_dump(exclude={"module"}, exclude_unset=True, exclude_none=True)
50
49
  # Remove redundant update fields
51
50
  update_data = {key: value for key, value in update_data.items() if getattr(tool, key) != value}
52
51
 
@@ -55,22 +54,24 @@ class ToolManager:
55
54
  self.update_tool_by_id(tool.id, ToolUpdate(**update_data), actor)
56
55
  else:
57
56
  printd(
58
- f"`create_or_update_tool` was called with user_id={actor.id}, organization_id={actor.organization_id}, name={tool_create.name}, but found existing tool with nothing to update."
57
+ f"`create_or_update_tool` was called with user_id={actor.id}, organization_id={actor.organization_id}, name={pydantic_tool.name}, but found existing tool with nothing to update."
59
58
  )
60
59
  except NoResultFound:
61
- tool_create.json_schema = derived_json_schema
62
- tool_create.name = derived_name
63
- tool = self.create_tool(tool_create, actor=actor)
60
+ pydantic_tool.json_schema = derived_json_schema
61
+ pydantic_tool.name = derived_name
62
+ tool = self.create_tool(pydantic_tool, actor=actor)
64
63
 
65
64
  return tool
66
65
 
67
66
  @enforce_types
68
- def create_tool(self, tool_create: ToolCreate, actor: PydanticUser) -> PydanticTool:
67
+ def create_tool(self, pydantic_tool: PydanticTool, actor: PydanticUser) -> PydanticTool:
69
68
  """Create a new tool based on the ToolCreate schema."""
70
69
  # Create the tool
71
70
  with self.session_maker() as session:
72
- create_data = tool_create.model_dump()
73
- tool = ToolModel(**create_data, organization_id=actor.organization_id) # Unpack everything directly into ToolModel
71
+ # Set the organization id at the ORM layer
72
+ pydantic_tool.organization_id = actor.organization_id
73
+ tool_data = pydantic_tool.model_dump()
74
+ tool = ToolModel(**tool_data)
74
75
  tool.create(session, actor=actor)
75
76
 
76
77
  return tool.to_pydantic()
@@ -99,7 +100,7 @@ class ToolManager:
99
100
  db_session=session,
100
101
  cursor=cursor,
101
102
  limit=limit,
102
- _organization_id=OrganizationModel.get_uid_from_identifier(actor.organization_id),
103
+ organization_id=actor.organization_id,
103
104
  )
104
105
  return [tool.to_pydantic() for tool in tools]
105
106
 
@@ -176,7 +177,7 @@ class ToolManager:
176
177
  # create to tool
177
178
  tools.append(
178
179
  self.create_or_update_tool(
179
- ToolCreate(
180
+ PydanticTool(
180
181
  name=name,
181
182
  tags=tags,
182
183
  source_type="python",
@@ -4,7 +4,7 @@ from letta.orm.errors import NoResultFound
4
4
  from letta.orm.organization import Organization as OrganizationModel
5
5
  from letta.orm.user import User as UserModel
6
6
  from letta.schemas.user import User as PydanticUser
7
- from letta.schemas.user import UserCreate, UserUpdate
7
+ from letta.schemas.user import UserUpdate
8
8
  from letta.services.organization_manager import OrganizationManager
9
9
  from letta.utils import enforce_types
10
10
 
@@ -42,10 +42,10 @@ class UserManager:
42
42
  return user.to_pydantic()
43
43
 
44
44
  @enforce_types
45
- def create_user(self, user_create: UserCreate) -> PydanticUser:
45
+ def create_user(self, pydantic_user: PydanticUser) -> PydanticUser:
46
46
  """Create a new user if it doesn't already exist."""
47
47
  with self.session_maker() as session:
48
- new_user = UserModel(**user_create.model_dump())
48
+ new_user = UserModel(**pydantic_user.model_dump())
49
49
  new_user.create(session)
50
50
  return new_user.to_pydantic()
51
51
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.5.1.dev20241104104148
3
+ Version: 0.5.1.dev20241106104104
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team
@@ -52,7 +52,7 @@ Requires-Dist: pg8000 (>=1.30.3,<2.0.0) ; extra == "postgres"
52
52
  Requires-Dist: pgvector (>=0.2.3,<0.3.0) ; extra == "postgres"
53
53
  Requires-Dist: pre-commit (>=3.5.0,<4.0.0) ; extra == "dev"
54
54
  Requires-Dist: prettytable (>=3.9.0,<4.0.0)
55
- Requires-Dist: psycopg2 (>=2.9.10,<3.0.0)
55
+ Requires-Dist: psycopg2 (>=2.9.10,<3.0.0) ; extra == "postgres"
56
56
  Requires-Dist: psycopg2-binary (>=2.9.10,<3.0.0) ; extra == "postgres"
57
57
  Requires-Dist: pyautogen (==0.2.22) ; extra == "autogen"
58
58
  Requires-Dist: pydantic (>=2.7.4,<3.0.0)
@@ -99,7 +99,7 @@ Description-Content-Type: text/markdown
99
99
  </h3>
100
100
 
101
101
  **👾 Letta** is an open source framework for building stateful LLM applications. You can use Letta to build **stateful agents** with advanced reasoning capabilities and transparent long-term memory. The Letta framework is white box and model-agnostic.
102
-
102
+
103
103
  [![Discord](https://img.shields.io/discord/1161736243340640419?label=Discord&logo=discord&logoColor=5865F2&style=flat-square&color=5865F2)](https://discord.gg/letta)
104
104
  [![Twitter Follow](https://img.shields.io/badge/Follow-%40Letta__AI-1DA1F2?style=flat-square&logo=x&logoColor=white)](https://twitter.com/Letta_AI)
105
105
  [![arxiv 2310.08560](https://img.shields.io/badge/Research-2310.08560-B31B1B?logo=arxiv&style=flat-square)](https://arxiv.org/abs/2310.08560)