letta-nightly 0.5.0.dev20241023104105__py3-none-any.whl → 0.5.1.dev20241023193051__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 +1 -1
- letta/constants.py +3 -3
- letta/metadata.py +0 -86
- letta/orm/errors.py +4 -0
- letta/orm/mixins.py +43 -16
- letta/orm/organization.py +7 -14
- letta/orm/sqlalchemy_base.py +3 -13
- letta/orm/user.py +25 -0
- letta/schemas/user.py +13 -6
- letta/server/rest_api/routers/v1/organizations.py +2 -2
- letta/server/rest_api/routers/v1/users.py +6 -7
- letta/server/server.py +47 -66
- letta/services/organization_manager.py +17 -7
- letta/services/user_manager.py +99 -0
- letta/settings.py +5 -0
- {letta_nightly-0.5.0.dev20241023104105.dist-info → letta_nightly-0.5.1.dev20241023193051.dist-info}/METADATA +1 -1
- {letta_nightly-0.5.0.dev20241023104105.dist-info → letta_nightly-0.5.1.dev20241023193051.dist-info}/RECORD +20 -18
- {letta_nightly-0.5.0.dev20241023104105.dist-info → letta_nightly-0.5.1.dev20241023193051.dist-info}/LICENSE +0 -0
- {letta_nightly-0.5.0.dev20241023104105.dist-info → letta_nightly-0.5.1.dev20241023193051.dist-info}/WHEEL +0 -0
- {letta_nightly-0.5.0.dev20241023104105.dist-info → letta_nightly-0.5.1.dev20241023193051.dist-info}/entry_points.txt +0 -0
letta/__init__.py
CHANGED
letta/constants.py
CHANGED
|
@@ -4,13 +4,13 @@ from logging import CRITICAL, DEBUG, ERROR, INFO, NOTSET, WARN, WARNING
|
|
|
4
4
|
LETTA_DIR = os.path.join(os.path.expanduser("~"), ".letta")
|
|
5
5
|
|
|
6
6
|
# Defaults
|
|
7
|
-
DEFAULT_USER_ID = "user-00000000"
|
|
7
|
+
DEFAULT_USER_ID = "user-00000000-0000-4000-8000-000000000000"
|
|
8
8
|
# This UUID follows the UUID4 rules:
|
|
9
9
|
# The 13th character (4) indicates it's version 4.
|
|
10
10
|
# The first character of the third segment (8) ensures the variant is correctly set.
|
|
11
11
|
DEFAULT_ORG_ID = "organization-00000000-0000-4000-8000-000000000000"
|
|
12
|
-
DEFAULT_USER_NAME = "
|
|
13
|
-
DEFAULT_ORG_NAME = "
|
|
12
|
+
DEFAULT_USER_NAME = "default_user"
|
|
13
|
+
DEFAULT_ORG_NAME = "default_org"
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
# String in the error message for when the context window is too large
|
letta/metadata.py
CHANGED
|
@@ -15,7 +15,6 @@ from sqlalchemy import (
|
|
|
15
15
|
String,
|
|
16
16
|
TypeDecorator,
|
|
17
17
|
asc,
|
|
18
|
-
desc,
|
|
19
18
|
or_,
|
|
20
19
|
)
|
|
21
20
|
from sqlalchemy.sql import func
|
|
@@ -31,8 +30,6 @@ from letta.schemas.file import FileMetadata
|
|
|
31
30
|
from letta.schemas.job import Job
|
|
32
31
|
from letta.schemas.llm_config import LLMConfig
|
|
33
32
|
from letta.schemas.memory import Memory
|
|
34
|
-
|
|
35
|
-
# from letta.schemas.message import Message, Passage, Record, RecordType, ToolCall
|
|
36
33
|
from letta.schemas.openai.chat_completions import ToolCall, ToolCallFunction
|
|
37
34
|
from letta.schemas.source import Source
|
|
38
35
|
from letta.schemas.tool import Tool
|
|
@@ -154,25 +151,6 @@ class ToolCallColumn(TypeDecorator):
|
|
|
154
151
|
return value
|
|
155
152
|
|
|
156
153
|
|
|
157
|
-
class UserModel(Base):
|
|
158
|
-
__tablename__ = "users"
|
|
159
|
-
__table_args__ = {"extend_existing": True}
|
|
160
|
-
|
|
161
|
-
id = Column(String, primary_key=True)
|
|
162
|
-
org_id = Column(String)
|
|
163
|
-
name = Column(String, nullable=False)
|
|
164
|
-
created_at = Column(DateTime(timezone=True))
|
|
165
|
-
|
|
166
|
-
# TODO: what is this?
|
|
167
|
-
policies_accepted = Column(Boolean, nullable=False, default=False)
|
|
168
|
-
|
|
169
|
-
def __repr__(self) -> str:
|
|
170
|
-
return f"<User(id='{self.id}' name='{self.name}')>"
|
|
171
|
-
|
|
172
|
-
def to_record(self) -> User:
|
|
173
|
-
return User(id=self.id, name=self.name, created_at=self.created_at, org_id=self.org_id)
|
|
174
|
-
|
|
175
|
-
|
|
176
154
|
# TODO: eventually store providers?
|
|
177
155
|
# class Provider(Base):
|
|
178
156
|
# __tablename__ = "providers"
|
|
@@ -497,15 +475,6 @@ class MetadataStore:
|
|
|
497
475
|
tokens = [r.to_record() for r in results]
|
|
498
476
|
return tokens
|
|
499
477
|
|
|
500
|
-
@enforce_types
|
|
501
|
-
def get_user_from_api_key(self, api_key: str) -> Optional[User]:
|
|
502
|
-
"""Get the user associated with a given API key"""
|
|
503
|
-
token = self.get_api_key(api_key=api_key)
|
|
504
|
-
if token is None:
|
|
505
|
-
raise ValueError(f"Provided token does not exist")
|
|
506
|
-
else:
|
|
507
|
-
return self.get_user(user_id=token.user_id)
|
|
508
|
-
|
|
509
478
|
@enforce_types
|
|
510
479
|
def create_agent(self, agent: AgentState):
|
|
511
480
|
# insert into agent table
|
|
@@ -527,14 +496,6 @@ class MetadataStore:
|
|
|
527
496
|
session.add(SourceModel(**vars(source)))
|
|
528
497
|
session.commit()
|
|
529
498
|
|
|
530
|
-
@enforce_types
|
|
531
|
-
def create_user(self, user: User):
|
|
532
|
-
with self.session_maker() as session:
|
|
533
|
-
if session.query(UserModel).filter(UserModel.id == user.id).count() > 0:
|
|
534
|
-
raise ValueError(f"User with id {user.id} already exists")
|
|
535
|
-
session.add(UserModel(**vars(user)))
|
|
536
|
-
session.commit()
|
|
537
|
-
|
|
538
499
|
@enforce_types
|
|
539
500
|
def create_block(self, block: Block):
|
|
540
501
|
with self.session_maker() as session:
|
|
@@ -573,12 +534,6 @@ class MetadataStore:
|
|
|
573
534
|
session.query(AgentModel).filter(AgentModel.id == agent.id).update(fields)
|
|
574
535
|
session.commit()
|
|
575
536
|
|
|
576
|
-
@enforce_types
|
|
577
|
-
def update_user(self, user: User):
|
|
578
|
-
with self.session_maker() as session:
|
|
579
|
-
session.query(UserModel).filter(UserModel.id == user.id).update(vars(user))
|
|
580
|
-
session.commit()
|
|
581
|
-
|
|
582
537
|
@enforce_types
|
|
583
538
|
def update_source(self, source: Source):
|
|
584
539
|
with self.session_maker() as session:
|
|
@@ -657,23 +612,6 @@ class MetadataStore:
|
|
|
657
612
|
|
|
658
613
|
session.commit()
|
|
659
614
|
|
|
660
|
-
@enforce_types
|
|
661
|
-
def delete_user(self, user_id: str):
|
|
662
|
-
with self.session_maker() as session:
|
|
663
|
-
# delete from users table
|
|
664
|
-
session.query(UserModel).filter(UserModel.id == user_id).delete()
|
|
665
|
-
|
|
666
|
-
# delete associated agents
|
|
667
|
-
session.query(AgentModel).filter(AgentModel.user_id == user_id).delete()
|
|
668
|
-
|
|
669
|
-
# delete associated sources
|
|
670
|
-
session.query(SourceModel).filter(SourceModel.user_id == user_id).delete()
|
|
671
|
-
|
|
672
|
-
# delete associated mappings
|
|
673
|
-
session.query(AgentSourceMappingModel).filter(AgentSourceMappingModel.user_id == user_id).delete()
|
|
674
|
-
|
|
675
|
-
session.commit()
|
|
676
|
-
|
|
677
615
|
@enforce_types
|
|
678
616
|
def list_tools(self, cursor: Optional[str] = None, limit: Optional[int] = 50, user_id: Optional[str] = None) -> List[ToolModel]:
|
|
679
617
|
with self.session_maker() as session:
|
|
@@ -719,30 +657,6 @@ class MetadataStore:
|
|
|
719
657
|
assert len(results) == 1, f"Expected 1 result, got {len(results)}" # should only be one result
|
|
720
658
|
return results[0].to_record()
|
|
721
659
|
|
|
722
|
-
@enforce_types
|
|
723
|
-
def get_user(self, user_id: str) -> Optional[User]:
|
|
724
|
-
with self.session_maker() as session:
|
|
725
|
-
results = session.query(UserModel).filter(UserModel.id == user_id).all()
|
|
726
|
-
if len(results) == 0:
|
|
727
|
-
return None
|
|
728
|
-
assert len(results) == 1, f"Expected 1 result, got {len(results)}"
|
|
729
|
-
return results[0].to_record()
|
|
730
|
-
|
|
731
|
-
@enforce_types
|
|
732
|
-
def get_all_users(self, cursor: Optional[str] = None, limit: Optional[int] = 50):
|
|
733
|
-
with self.session_maker() as session:
|
|
734
|
-
query = session.query(UserModel).order_by(desc(UserModel.id))
|
|
735
|
-
if cursor:
|
|
736
|
-
query = query.filter(UserModel.id < cursor)
|
|
737
|
-
results = query.limit(limit).all()
|
|
738
|
-
if not results:
|
|
739
|
-
return None, []
|
|
740
|
-
user_records = [r.to_record() for r in results]
|
|
741
|
-
next_cursor = user_records[-1].id
|
|
742
|
-
assert isinstance(next_cursor, str)
|
|
743
|
-
|
|
744
|
-
return next_cursor, user_records
|
|
745
|
-
|
|
746
660
|
@enforce_types
|
|
747
661
|
def get_source(
|
|
748
662
|
self, source_id: Optional[str] = None, user_id: Optional[str] = None, source_name: Optional[str] = None
|
letta/orm/errors.py
CHANGED
letta/orm/mixins.py
CHANGED
|
@@ -1,24 +1,35 @@
|
|
|
1
|
-
from typing import Optional
|
|
1
|
+
from typing import Optional
|
|
2
2
|
from uuid import UUID
|
|
3
3
|
|
|
4
|
+
from sqlalchemy import ForeignKey, String
|
|
5
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
6
|
+
|
|
4
7
|
from letta.orm.base import Base
|
|
8
|
+
from letta.orm.errors import MalformedIdError
|
|
5
9
|
|
|
6
10
|
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
def is_valid_uuid4(uuid_string: str) -> bool:
|
|
12
|
+
"""Check if a string is a valid UUID4."""
|
|
13
|
+
try:
|
|
14
|
+
uuid_obj = UUID(uuid_string)
|
|
15
|
+
return uuid_obj.version == 4
|
|
16
|
+
except ValueError:
|
|
17
|
+
return False
|
|
9
18
|
|
|
10
19
|
|
|
11
20
|
def _relation_getter(instance: "Base", prop: str) -> Optional[str]:
|
|
21
|
+
"""Get relation and return id with prefix as a string."""
|
|
12
22
|
prefix = prop.replace("_", "")
|
|
13
23
|
formatted_prop = f"_{prop}_id"
|
|
14
24
|
try:
|
|
15
|
-
|
|
16
|
-
return f"{prefix}-{
|
|
25
|
+
id_ = getattr(instance, formatted_prop) # Get the string id directly
|
|
26
|
+
return f"{prefix}-{id_}"
|
|
17
27
|
except AttributeError:
|
|
18
28
|
return None
|
|
19
29
|
|
|
20
30
|
|
|
21
|
-
def _relation_setter(instance:
|
|
31
|
+
def _relation_setter(instance: "Base", prop: str, value: str) -> None:
|
|
32
|
+
"""Set relation using the id with prefix, ensuring the id is a valid UUIDv4."""
|
|
22
33
|
formatted_prop = f"_{prop}_id"
|
|
23
34
|
prefix = prop.replace("_", "")
|
|
24
35
|
if not value:
|
|
@@ -28,13 +39,29 @@ def _relation_setter(instance: Type["Base"], prop: str, value: str) -> None:
|
|
|
28
39
|
found_prefix, id_ = value.split("-", 1)
|
|
29
40
|
except ValueError as e:
|
|
30
41
|
raise MalformedIdError(f"{value} is not a valid ID.") from e
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
|
|
43
|
+
# Ensure prefix matches
|
|
44
|
+
assert found_prefix == prefix, f"{found_prefix} is not a valid id prefix, expecting {prefix}"
|
|
45
|
+
|
|
46
|
+
# Validate that the id is a valid UUID4 string
|
|
47
|
+
if not is_valid_uuid4(id_):
|
|
48
|
+
raise MalformedIdError(f"Hash segment of {value} is not a valid UUID4")
|
|
49
|
+
|
|
50
|
+
setattr(instance, formatted_prop, id_) # Store id as a string
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class OrganizationMixin(Base):
|
|
54
|
+
"""Mixin for models that belong to an organization."""
|
|
55
|
+
|
|
56
|
+
__abstract__ = True
|
|
57
|
+
|
|
58
|
+
# Changed _organization_id to store string (still a valid UUID4 string)
|
|
59
|
+
_organization_id: Mapped[str] = mapped_column(String, ForeignKey("organization._id"))
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def organization_id(self) -> str:
|
|
63
|
+
return _relation_getter(self, "organization")
|
|
64
|
+
|
|
65
|
+
@organization_id.setter
|
|
66
|
+
def organization_id(self, value: str) -> None:
|
|
67
|
+
_relation_setter(self, "organization", value)
|
letta/orm/organization.py
CHANGED
|
@@ -1,35 +1,28 @@
|
|
|
1
|
-
from typing import TYPE_CHECKING
|
|
1
|
+
from typing import TYPE_CHECKING, List
|
|
2
2
|
|
|
3
|
-
from sqlalchemy.
|
|
4
|
-
from sqlalchemy.orm import Mapped, mapped_column
|
|
3
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
5
4
|
|
|
6
5
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
7
6
|
from letta.schemas.organization import Organization as PydanticOrganization
|
|
8
7
|
|
|
9
8
|
if TYPE_CHECKING:
|
|
10
|
-
|
|
9
|
+
|
|
10
|
+
from letta.orm.user import User
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class Organization(SqlalchemyBase):
|
|
14
14
|
"""The highest level of the object tree. All Entities belong to one and only one Organization."""
|
|
15
15
|
|
|
16
|
-
__tablename__ = "
|
|
16
|
+
__tablename__ = "organization"
|
|
17
17
|
__pydantic_model__ = PydanticOrganization
|
|
18
18
|
|
|
19
19
|
name: Mapped[str] = mapped_column(doc="The display name of the organization.")
|
|
20
20
|
|
|
21
|
+
users: Mapped[List["User"]] = relationship("User", back_populates="organization", cascade="all, delete-orphan")
|
|
22
|
+
|
|
21
23
|
# TODO: Map these relationships later when we actually make these models
|
|
22
24
|
# below is just a suggestion
|
|
23
|
-
# users: Mapped[List["User"]] = relationship("User", back_populates="organization", cascade="all, delete-orphan")
|
|
24
25
|
# agents: Mapped[List["Agent"]] = relationship("Agent", back_populates="organization", cascade="all, delete-orphan")
|
|
25
26
|
# sources: Mapped[List["Source"]] = relationship("Source", back_populates="organization", cascade="all, delete-orphan")
|
|
26
27
|
# tools: Mapped[List["Tool"]] = relationship("Tool", back_populates="organization", cascade="all, delete-orphan")
|
|
27
28
|
# documents: Mapped[List["Document"]] = relationship("Document", back_populates="organization", cascade="all, delete-orphan")
|
|
28
|
-
|
|
29
|
-
@classmethod
|
|
30
|
-
def default(cls, db_session: "Session") -> "Organization":
|
|
31
|
-
"""Get the default org, or create it if it doesn't exist."""
|
|
32
|
-
try:
|
|
33
|
-
return db_session.query(cls).filter(cls.name == "Default Organization").one()
|
|
34
|
-
except NoResultFound:
|
|
35
|
-
return cls(name="Default Organization").create(db_session)
|
letta/orm/sqlalchemy_base.py
CHANGED
|
@@ -8,6 +8,7 @@ from sqlalchemy.orm import Mapped, mapped_column
|
|
|
8
8
|
from letta.log import get_logger
|
|
9
9
|
from letta.orm.base import Base, CommonSqlalchemyMetaMixins
|
|
10
10
|
from letta.orm.errors import NoResultFound
|
|
11
|
+
from letta.orm.mixins import is_valid_uuid4
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
14
|
from pydantic import BaseModel
|
|
@@ -42,7 +43,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
42
43
|
return
|
|
43
44
|
prefix, id_ = value.split("-", 1)
|
|
44
45
|
assert prefix == self.__prefix__(), f"{prefix} is not a valid id prefix for {self.__class__.__name__}"
|
|
45
|
-
assert
|
|
46
|
+
assert is_valid_uuid4(id_), f"{id_} is not a valid uuid4"
|
|
46
47
|
self._id = id_
|
|
47
48
|
|
|
48
49
|
@classmethod
|
|
@@ -78,22 +79,11 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
|
|
|
78
79
|
"""
|
|
79
80
|
try:
|
|
80
81
|
uuid_string = identifier.split("-", 1)[1] if indifferent else identifier.replace(f"{cls.__prefix__()}-", "")
|
|
81
|
-
assert
|
|
82
|
+
assert is_valid_uuid4(uuid_string)
|
|
82
83
|
return uuid_string
|
|
83
84
|
except ValueError as e:
|
|
84
85
|
raise ValueError(f"{identifier} is not a valid identifier for class {cls.__name__}") from e
|
|
85
86
|
|
|
86
|
-
@classmethod
|
|
87
|
-
def is_valid_uuid4(cls, uuid_string: str) -> bool:
|
|
88
|
-
try:
|
|
89
|
-
# Try to create a UUID object from the string
|
|
90
|
-
uuid_obj = UUID(uuid_string)
|
|
91
|
-
# Check if the UUID is version 4
|
|
92
|
-
return uuid_obj.version == 4
|
|
93
|
-
except ValueError:
|
|
94
|
-
# Raised if the string is not a valid UUID
|
|
95
|
-
return False
|
|
96
|
-
|
|
97
87
|
@classmethod
|
|
98
88
|
def read(
|
|
99
89
|
cls,
|
letta/orm/user.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
2
|
+
|
|
3
|
+
from letta.orm.mixins import OrganizationMixin
|
|
4
|
+
from letta.orm.organization import Organization
|
|
5
|
+
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
|
6
|
+
from letta.schemas.user import User as PydanticUser
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class User(SqlalchemyBase, OrganizationMixin):
|
|
10
|
+
"""User ORM class"""
|
|
11
|
+
|
|
12
|
+
__tablename__ = "user"
|
|
13
|
+
__pydantic_model__ = PydanticUser
|
|
14
|
+
|
|
15
|
+
name: Mapped[str] = mapped_column(nullable=False, doc="The display name of the user.")
|
|
16
|
+
|
|
17
|
+
# relationships
|
|
18
|
+
organization: Mapped["Organization"] = relationship("Organization", back_populates="users")
|
|
19
|
+
|
|
20
|
+
# TODO: Add this back later potentially
|
|
21
|
+
# agents: Mapped[List["Agent"]] = relationship(
|
|
22
|
+
# "Agent", secondary="users_agents", back_populates="users", doc="the agents associated with this user."
|
|
23
|
+
# )
|
|
24
|
+
# tokens: Mapped[List["Token"]] = relationship("Token", back_populates="user", doc="the tokens associated with this user.")
|
|
25
|
+
# jobs: Mapped[List["Job"]] = relationship("Job", back_populates="user", doc="the jobs associated with this user.")
|
letta/schemas/user.py
CHANGED
|
@@ -3,6 +3,7 @@ from typing import Optional
|
|
|
3
3
|
|
|
4
4
|
from pydantic import Field
|
|
5
5
|
|
|
6
|
+
from letta.constants import DEFAULT_ORG_ID
|
|
6
7
|
from letta.schemas.letta_base import LettaBase
|
|
7
8
|
|
|
8
9
|
|
|
@@ -20,14 +21,20 @@ class User(UserBase):
|
|
|
20
21
|
created_at (datetime): The creation date of the user.
|
|
21
22
|
"""
|
|
22
23
|
|
|
23
|
-
id: str =
|
|
24
|
-
|
|
25
|
-
..., description="The organization id of the user"
|
|
26
|
-
) # TODO: dont make optional, and pass in default org ID
|
|
24
|
+
id: str = Field(..., description="The id of the user.")
|
|
25
|
+
organization_id: Optional[str] = Field(DEFAULT_ORG_ID, description="The organization id of the user")
|
|
27
26
|
name: str = Field(..., description="The name of the user.")
|
|
28
27
|
created_at: datetime = Field(default_factory=datetime.utcnow, description="The creation date of the user.")
|
|
28
|
+
updated_at: datetime = Field(default_factory=datetime.utcnow, description="The update date of the user.")
|
|
29
|
+
is_deleted: bool = Field(False, description="Whether this user is deleted or not.")
|
|
29
30
|
|
|
30
31
|
|
|
31
32
|
class UserCreate(UserBase):
|
|
32
|
-
name:
|
|
33
|
-
|
|
33
|
+
name: str = Field(..., description="The name of the user.")
|
|
34
|
+
organization_id: str = Field(..., description="The organization id of the user.")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class UserUpdate(UserBase):
|
|
38
|
+
id: str = Field(..., description="The id of the user to update.")
|
|
39
|
+
name: Optional[str] = Field(None, description="The new name of the user.")
|
|
40
|
+
organization_id: Optional[str] = Field(None, description="The new organization id of the user.")
|
|
@@ -42,7 +42,7 @@ def create_org(
|
|
|
42
42
|
return org
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
@router.delete("/", tags=["admin"], response_model=Organization, operation_id="
|
|
45
|
+
@router.delete("/", tags=["admin"], response_model=Organization, operation_id="delete_organization_by_id")
|
|
46
46
|
def delete_org(
|
|
47
47
|
org_id: str = Query(..., description="The org_id key to be deleted."),
|
|
48
48
|
server: "SyncServer" = Depends(get_letta_server),
|
|
@@ -52,7 +52,7 @@ def delete_org(
|
|
|
52
52
|
org = server.organization_manager.get_organization_by_id(org_id=org_id)
|
|
53
53
|
if org is None:
|
|
54
54
|
raise HTTPException(status_code=404, detail=f"Organization does not exist")
|
|
55
|
-
server.organization_manager.
|
|
55
|
+
server.organization_manager.delete_organization_by_id(org_id=org_id)
|
|
56
56
|
except HTTPException:
|
|
57
57
|
raise
|
|
58
58
|
except Exception as e:
|
|
@@ -26,7 +26,7 @@ router = APIRouter(prefix="/users", tags=["users", "admin"])
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
@router.get("/", tags=["admin"], response_model=List[User], operation_id="list_users")
|
|
29
|
-
def
|
|
29
|
+
def list_users(
|
|
30
30
|
cursor: Optional[str] = Query(None),
|
|
31
31
|
limit: Optional[int] = Query(50),
|
|
32
32
|
server: "SyncServer" = Depends(get_letta_server),
|
|
@@ -35,8 +35,7 @@ def get_all_users(
|
|
|
35
35
|
Get a list of all users in the database
|
|
36
36
|
"""
|
|
37
37
|
try:
|
|
38
|
-
next_cursor, users = server.
|
|
39
|
-
# processed_users = [{"user_id": user.id} for user in users]
|
|
38
|
+
next_cursor, users = server.user_manager.list_users(cursor=cursor, limit=limit)
|
|
40
39
|
except HTTPException:
|
|
41
40
|
raise
|
|
42
41
|
except Exception as e:
|
|
@@ -53,7 +52,7 @@ def create_user(
|
|
|
53
52
|
Create a new user in the database
|
|
54
53
|
"""
|
|
55
54
|
|
|
56
|
-
user = server.create_user(request)
|
|
55
|
+
user = server.user_manager.create_user(request)
|
|
57
56
|
return user
|
|
58
57
|
|
|
59
58
|
|
|
@@ -64,10 +63,10 @@ def delete_user(
|
|
|
64
63
|
):
|
|
65
64
|
# TODO make a soft deletion, instead of a hard deletion
|
|
66
65
|
try:
|
|
67
|
-
user = server.
|
|
66
|
+
user = server.user_manager.get_user_by_id(user_id=user_id)
|
|
68
67
|
if user is None:
|
|
69
68
|
raise HTTPException(status_code=404, detail=f"User does not exist")
|
|
70
|
-
server.
|
|
69
|
+
server.user_manager.delete_user_by_id(user_id=user_id)
|
|
71
70
|
except HTTPException:
|
|
72
71
|
raise
|
|
73
72
|
except Exception as e:
|
|
@@ -95,7 +94,7 @@ def get_api_keys(
|
|
|
95
94
|
"""
|
|
96
95
|
Get a list of all API keys for a user
|
|
97
96
|
"""
|
|
98
|
-
if server.
|
|
97
|
+
if server.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
99
98
|
raise HTTPException(status_code=404, detail=f"User does not exist")
|
|
100
99
|
api_keys = server.ms.get_all_api_keys_for_user(user_id=user_id)
|
|
101
100
|
return api_keys
|
letta/server/server.py
CHANGED
|
@@ -44,7 +44,6 @@ from letta.log import get_logger
|
|
|
44
44
|
from letta.memory import get_memory_functions
|
|
45
45
|
from letta.metadata import Base, MetadataStore
|
|
46
46
|
from letta.o1_agent import O1Agent
|
|
47
|
-
from letta.orm.errors import NoResultFound
|
|
48
47
|
from letta.prompts import gpt_system
|
|
49
48
|
from letta.providers import (
|
|
50
49
|
AnthropicProvider,
|
|
@@ -87,6 +86,7 @@ from letta.schemas.tool import Tool, ToolCreate, ToolUpdate
|
|
|
87
86
|
from letta.schemas.usage import LettaUsageStatistics
|
|
88
87
|
from letta.schemas.user import User, UserCreate
|
|
89
88
|
from letta.services.organization_manager import OrganizationManager
|
|
89
|
+
from letta.services.user_manager import UserManager
|
|
90
90
|
from letta.utils import create_random_username, json_dumps, json_loads
|
|
91
91
|
|
|
92
92
|
# from letta.llm_api_tools import openai_get_model_list, azure_openai_get_model_list, smart_urljoin
|
|
@@ -168,7 +168,7 @@ from sqlalchemy.orm import sessionmaker
|
|
|
168
168
|
from letta.config import LettaConfig
|
|
169
169
|
|
|
170
170
|
# NOTE: hack to see if single session management works
|
|
171
|
-
from letta.settings import model_settings, settings
|
|
171
|
+
from letta.settings import model_settings, settings, tool_settings
|
|
172
172
|
|
|
173
173
|
config = LettaConfig.load()
|
|
174
174
|
|
|
@@ -248,6 +248,7 @@ class SyncServer(Server):
|
|
|
248
248
|
|
|
249
249
|
# Managers that interface with data models
|
|
250
250
|
self.organization_manager = OrganizationManager()
|
|
251
|
+
self.user_manager = UserManager()
|
|
251
252
|
|
|
252
253
|
# TODO: this should be removed
|
|
253
254
|
# add global default tools (for admin)
|
|
@@ -576,7 +577,7 @@ class SyncServer(Server):
|
|
|
576
577
|
timestamp: Optional[datetime] = None,
|
|
577
578
|
) -> LettaUsageStatistics:
|
|
578
579
|
"""Process an incoming user message and feed it through the Letta agent"""
|
|
579
|
-
if self.
|
|
580
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
580
581
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
581
582
|
if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
|
|
582
583
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -625,7 +626,7 @@ class SyncServer(Server):
|
|
|
625
626
|
timestamp: Optional[datetime] = None,
|
|
626
627
|
) -> LettaUsageStatistics:
|
|
627
628
|
"""Process an incoming system message and feed it through the Letta agent"""
|
|
628
|
-
if self.
|
|
629
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
629
630
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
630
631
|
if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
|
|
631
632
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -694,7 +695,7 @@ class SyncServer(Server):
|
|
|
694
695
|
|
|
695
696
|
Otherwise, we can pass them in directly.
|
|
696
697
|
"""
|
|
697
|
-
if self.
|
|
698
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
698
699
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
699
700
|
if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
|
|
700
701
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -743,7 +744,7 @@ class SyncServer(Server):
|
|
|
743
744
|
# @LockingServer.agent_lock_decorator
|
|
744
745
|
def run_command(self, user_id: str, agent_id: str, command: str) -> LettaUsageStatistics:
|
|
745
746
|
"""Run a command on the agent"""
|
|
746
|
-
if self.
|
|
747
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
747
748
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
748
749
|
if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
|
|
749
750
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -754,19 +755,12 @@ class SyncServer(Server):
|
|
|
754
755
|
command = command[1:] # strip the prefix
|
|
755
756
|
return self._command(user_id=user_id, agent_id=agent_id, command=command)
|
|
756
757
|
|
|
757
|
-
def list_users_paginated(self, cursor: str, limit: int) -> List[User]:
|
|
758
|
-
"""List all users"""
|
|
759
|
-
# TODO: make this paginated
|
|
760
|
-
next_cursor, users = self.ms.get_all_users(cursor, limit)
|
|
761
|
-
return next_cursor, users
|
|
762
|
-
|
|
763
758
|
def create_user(self, request: UserCreate) -> User:
|
|
764
759
|
"""Create a new user using a config"""
|
|
765
760
|
if not request.name:
|
|
766
761
|
# auto-generate a name
|
|
767
762
|
request.name = create_random_username()
|
|
768
|
-
user =
|
|
769
|
-
self.ms.create_user(user)
|
|
763
|
+
user = self.user_manager.create_user(request)
|
|
770
764
|
logger.debug(f"Created new user from config: {user}")
|
|
771
765
|
|
|
772
766
|
# add default for the user
|
|
@@ -785,7 +779,7 @@ class SyncServer(Server):
|
|
|
785
779
|
interface: Union[AgentInterface, None] = None,
|
|
786
780
|
) -> AgentState:
|
|
787
781
|
"""Create a new agent using a config"""
|
|
788
|
-
if self.
|
|
782
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
789
783
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
790
784
|
|
|
791
785
|
if interface is None:
|
|
@@ -809,7 +803,7 @@ class SyncServer(Server):
|
|
|
809
803
|
raise ValueError(f"Invalid agent type: {request.agent_type}")
|
|
810
804
|
|
|
811
805
|
logger.debug(f"Attempting to find user: {user_id}")
|
|
812
|
-
user = self.
|
|
806
|
+
user = self.user_manager.get_user_by_id(user_id=user_id)
|
|
813
807
|
if not user:
|
|
814
808
|
raise ValueError(f"cannot find user with associated client id: {user_id}")
|
|
815
809
|
|
|
@@ -834,7 +828,8 @@ class SyncServer(Server):
|
|
|
834
828
|
# tool already added
|
|
835
829
|
continue
|
|
836
830
|
source_code = parse_source_code(func)
|
|
837
|
-
|
|
831
|
+
# memory functions are not terminal
|
|
832
|
+
json_schema = generate_schema(func, terminal=False, name=func_name)
|
|
838
833
|
source_type = "python"
|
|
839
834
|
tags = ["memory", "memgpt-base"]
|
|
840
835
|
tool = self.create_tool(
|
|
@@ -912,7 +907,7 @@ class SyncServer(Server):
|
|
|
912
907
|
user_id: str,
|
|
913
908
|
):
|
|
914
909
|
"""Update the agents core memory block, return the new state"""
|
|
915
|
-
if self.
|
|
910
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
916
911
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
917
912
|
if self.ms.get_agent(agent_id=request.id) is None:
|
|
918
913
|
raise ValueError(f"Agent agent_id={request.id} does not exist")
|
|
@@ -974,7 +969,7 @@ class SyncServer(Server):
|
|
|
974
969
|
|
|
975
970
|
def get_tools_from_agent(self, agent_id: str, user_id: Optional[str]) -> List[Tool]:
|
|
976
971
|
"""Get tools from an existing agent"""
|
|
977
|
-
if self.
|
|
972
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
978
973
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
979
974
|
if self.ms.get_agent(agent_id=agent_id) is None:
|
|
980
975
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -990,7 +985,7 @@ class SyncServer(Server):
|
|
|
990
985
|
user_id: str,
|
|
991
986
|
):
|
|
992
987
|
"""Add tools from an existing agent"""
|
|
993
|
-
if self.
|
|
988
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
994
989
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
995
990
|
if self.ms.get_agent(agent_id=agent_id) is None:
|
|
996
991
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -1029,7 +1024,7 @@ class SyncServer(Server):
|
|
|
1029
1024
|
user_id: str,
|
|
1030
1025
|
):
|
|
1031
1026
|
"""Remove tools from an existing agent"""
|
|
1032
|
-
if self.
|
|
1027
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
1033
1028
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
1034
1029
|
if self.ms.get_agent(agent_id=agent_id) is None:
|
|
1035
1030
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -1075,7 +1070,7 @@ class SyncServer(Server):
|
|
|
1075
1070
|
user_id: str,
|
|
1076
1071
|
) -> List[AgentState]:
|
|
1077
1072
|
"""List all available agents to a user"""
|
|
1078
|
-
if self.
|
|
1073
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
1079
1074
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
1080
1075
|
|
|
1081
1076
|
agents_states = self.ms.list_agents(user_id=user_id)
|
|
@@ -1091,7 +1086,7 @@ class SyncServer(Server):
|
|
|
1091
1086
|
if user_id is None:
|
|
1092
1087
|
agents_states = self.ms.list_all_agents()
|
|
1093
1088
|
else:
|
|
1094
|
-
if self.
|
|
1089
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
1095
1090
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
1096
1091
|
|
|
1097
1092
|
agents_states = self.ms.list_agents(user_id=user_id)
|
|
@@ -1231,13 +1226,13 @@ class SyncServer(Server):
|
|
|
1231
1226
|
"""Get the agent state"""
|
|
1232
1227
|
return self.ms.get_agent(agent_id=agent_id, user_id=user_id)
|
|
1233
1228
|
|
|
1234
|
-
def get_user(self, user_id: str) -> User:
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1229
|
+
# def get_user(self, user_id: str) -> User:
|
|
1230
|
+
# """Get the user"""
|
|
1231
|
+
# user = self.user_manager.get_user_by_id(user_id=user_id)
|
|
1232
|
+
# if user is None:
|
|
1233
|
+
# raise ValueError(f"User with user_id {user_id} does not exist")
|
|
1234
|
+
# else:
|
|
1235
|
+
# return user
|
|
1241
1236
|
|
|
1242
1237
|
def get_agent_memory(self, agent_id: str) -> Memory:
|
|
1243
1238
|
"""Return the memory of an agent (core memory)"""
|
|
@@ -1328,7 +1323,7 @@ class SyncServer(Server):
|
|
|
1328
1323
|
|
|
1329
1324
|
def get_agent_archival(self, user_id: str, agent_id: str, start: int, count: int) -> List[Passage]:
|
|
1330
1325
|
"""Paginated query of all messages in agent archival memory"""
|
|
1331
|
-
if self.
|
|
1326
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
1332
1327
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
1333
1328
|
if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
|
|
1334
1329
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -1353,7 +1348,7 @@ class SyncServer(Server):
|
|
|
1353
1348
|
order_by: Optional[str] = "created_at",
|
|
1354
1349
|
reverse: Optional[bool] = False,
|
|
1355
1350
|
) -> List[Passage]:
|
|
1356
|
-
if self.
|
|
1351
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
1357
1352
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
1358
1353
|
if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
|
|
1359
1354
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -1368,7 +1363,7 @@ class SyncServer(Server):
|
|
|
1368
1363
|
return records
|
|
1369
1364
|
|
|
1370
1365
|
def insert_archival_memory(self, user_id: str, agent_id: str, memory_contents: str) -> List[Passage]:
|
|
1371
|
-
if self.
|
|
1366
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
1372
1367
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
1373
1368
|
if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
|
|
1374
1369
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -1383,7 +1378,7 @@ class SyncServer(Server):
|
|
|
1383
1378
|
return [letta_agent.persistence_manager.archival_memory.storage.get(id=passage_id) for passage_id in passage_ids]
|
|
1384
1379
|
|
|
1385
1380
|
def delete_archival_memory(self, user_id: str, agent_id: str, memory_id: str):
|
|
1386
|
-
if self.
|
|
1381
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
1387
1382
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
1388
1383
|
if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
|
|
1389
1384
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -1414,7 +1409,7 @@ class SyncServer(Server):
|
|
|
1414
1409
|
assistant_message_function_name: str = constants.DEFAULT_MESSAGE_TOOL,
|
|
1415
1410
|
assistant_message_function_kwarg: str = constants.DEFAULT_MESSAGE_TOOL_KWARG,
|
|
1416
1411
|
) -> Union[List[Message], List[LettaMessage]]:
|
|
1417
|
-
if self.
|
|
1412
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
1418
1413
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
1419
1414
|
if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
|
|
1420
1415
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -1456,7 +1451,7 @@ class SyncServer(Server):
|
|
|
1456
1451
|
|
|
1457
1452
|
def get_agent_state(self, user_id: str, agent_id: Optional[str], agent_name: Optional[str] = None) -> Optional[AgentState]:
|
|
1458
1453
|
"""Return the config of an agent"""
|
|
1459
|
-
if self.
|
|
1454
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
1460
1455
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
1461
1456
|
if agent_id:
|
|
1462
1457
|
if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
|
|
@@ -1497,7 +1492,7 @@ class SyncServer(Server):
|
|
|
1497
1492
|
|
|
1498
1493
|
def update_agent_core_memory(self, user_id: str, agent_id: str, new_memory_contents: dict) -> Memory:
|
|
1499
1494
|
"""Update the agents core memory block, return the new state"""
|
|
1500
|
-
if self.
|
|
1495
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
1501
1496
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
1502
1497
|
if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
|
|
1503
1498
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -1528,7 +1523,7 @@ class SyncServer(Server):
|
|
|
1528
1523
|
|
|
1529
1524
|
def rename_agent(self, user_id: str, agent_id: str, new_agent_name: str) -> AgentState:
|
|
1530
1525
|
"""Update the name of the agent in the database"""
|
|
1531
|
-
if self.
|
|
1526
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
1532
1527
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
1533
1528
|
if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
|
|
1534
1529
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -1550,13 +1545,9 @@ class SyncServer(Server):
|
|
|
1550
1545
|
assert isinstance(letta_agent.agent_state.id, str)
|
|
1551
1546
|
return letta_agent.agent_state
|
|
1552
1547
|
|
|
1553
|
-
def delete_user(self, user_id: str):
|
|
1554
|
-
# TODO: delete user
|
|
1555
|
-
pass
|
|
1556
|
-
|
|
1557
1548
|
def delete_agent(self, user_id: str, agent_id: str):
|
|
1558
1549
|
"""Delete an agent in the database"""
|
|
1559
|
-
if self.
|
|
1550
|
+
if self.user_manager.get_user_by_id(user_id=user_id) is None:
|
|
1560
1551
|
raise ValueError(f"User user_id={user_id} does not exist")
|
|
1561
1552
|
if self.ms.get_agent(agent_id=agent_id, user_id=user_id) is None:
|
|
1562
1553
|
raise ValueError(f"Agent agent_id={agent_id} does not exist")
|
|
@@ -1585,7 +1576,8 @@ class SyncServer(Server):
|
|
|
1585
1576
|
|
|
1586
1577
|
def api_key_to_user(self, api_key: str) -> str:
|
|
1587
1578
|
"""Decode an API key to a user"""
|
|
1588
|
-
|
|
1579
|
+
token = self.ms.get_api_key(api_key=api_key)
|
|
1580
|
+
user = self.user_manager.get_user_by_id(token.user_id)
|
|
1589
1581
|
if user is None:
|
|
1590
1582
|
raise HTTPException(status_code=403, detail="Invalid credentials")
|
|
1591
1583
|
else:
|
|
@@ -2018,7 +2010,10 @@ class SyncServer(Server):
|
|
|
2018
2010
|
def add_default_external_tools(self, user_id: Optional[str] = None) -> bool:
|
|
2019
2011
|
"""Add default langchain tools. Return true if successful, false otherwise."""
|
|
2020
2012
|
success = True
|
|
2021
|
-
|
|
2013
|
+
if tool_settings.composio_api_key:
|
|
2014
|
+
tools = Tool.load_default_langchain_tools() + Tool.load_default_crewai_tools() + Tool.load_default_composio_tools()
|
|
2015
|
+
else:
|
|
2016
|
+
tools = Tool.load_default_langchain_tools() + Tool.load_default_crewai_tools()
|
|
2022
2017
|
for tool in tools:
|
|
2023
2018
|
try:
|
|
2024
2019
|
self.ms.create_tool(tool)
|
|
@@ -2113,29 +2108,15 @@ class SyncServer(Server):
|
|
|
2113
2108
|
letta_agent = self._get_or_load_agent(agent_id=agent_id)
|
|
2114
2109
|
return letta_agent.retry_message()
|
|
2115
2110
|
|
|
2111
|
+
# TODO: Move a lot of this default logic to the ORM
|
|
2116
2112
|
def get_default_user(self) -> User:
|
|
2113
|
+
self.organization_manager.create_default_organization()
|
|
2114
|
+
user = self.user_manager.create_default_user()
|
|
2117
2115
|
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
# check if default org exists
|
|
2121
|
-
try:
|
|
2122
|
-
self.organization_manager.get_organization_by_id(DEFAULT_ORG_ID)
|
|
2123
|
-
except NoResultFound:
|
|
2124
|
-
self.organization_manager.create_default_organization()
|
|
2125
|
-
|
|
2126
|
-
# check if default user exists
|
|
2127
|
-
try:
|
|
2128
|
-
self.get_user(DEFAULT_USER_ID)
|
|
2129
|
-
except ValueError:
|
|
2130
|
-
user = User(name=DEFAULT_USER_NAME, org_id=DEFAULT_ORG_ID, id=DEFAULT_USER_ID)
|
|
2131
|
-
self.ms.create_user(user)
|
|
2132
|
-
|
|
2133
|
-
# add default data (TODO: move to org)
|
|
2134
|
-
self.add_default_blocks(user.id)
|
|
2135
|
-
self.add_default_tools(module_name="base", user_id=user.id)
|
|
2116
|
+
self.add_default_blocks(user.id)
|
|
2117
|
+
self.add_default_tools(module_name="base", user_id=user.id)
|
|
2136
2118
|
|
|
2137
|
-
|
|
2138
|
-
return self.get_user(DEFAULT_USER_ID)
|
|
2119
|
+
return user
|
|
2139
2120
|
|
|
2140
2121
|
def get_user_or_default(self, user_id: Optional[str]) -> User:
|
|
2141
2122
|
"""Get the user object for user_id if it exists, otherwise return the default user object"""
|
|
@@ -2143,7 +2124,7 @@ class SyncServer(Server):
|
|
|
2143
2124
|
return self.get_default_user()
|
|
2144
2125
|
else:
|
|
2145
2126
|
try:
|
|
2146
|
-
return self.
|
|
2127
|
+
return self.user_manager.get_user_by_id(user_id=user_id)
|
|
2147
2128
|
except ValueError:
|
|
2148
2129
|
raise HTTPException(status_code=404, detail=f"User with id {user_id} not found")
|
|
2149
2130
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
from typing import List, Optional
|
|
2
2
|
|
|
3
|
-
from sqlalchemy.exc import NoResultFound
|
|
4
|
-
|
|
5
3
|
from letta.constants import DEFAULT_ORG_ID, DEFAULT_ORG_NAME
|
|
4
|
+
from letta.orm.errors import NoResultFound
|
|
6
5
|
from letta.orm.organization import Organization
|
|
7
6
|
from letta.schemas.organization import Organization as PydanticOrganization
|
|
8
|
-
from letta.utils import create_random_username
|
|
7
|
+
from letta.utils import create_random_username, enforce_types
|
|
9
8
|
|
|
10
9
|
|
|
11
10
|
class OrganizationManager:
|
|
@@ -20,6 +19,7 @@ class OrganizationManager:
|
|
|
20
19
|
|
|
21
20
|
self.session_maker = db_context
|
|
22
21
|
|
|
22
|
+
@enforce_types
|
|
23
23
|
def get_organization_by_id(self, org_id: str) -> PydanticOrganization:
|
|
24
24
|
"""Fetch an organization by ID."""
|
|
25
25
|
with self.session_maker() as session:
|
|
@@ -29,6 +29,7 @@ class OrganizationManager:
|
|
|
29
29
|
except NoResultFound:
|
|
30
30
|
raise ValueError(f"Organization with id {org_id} not found.")
|
|
31
31
|
|
|
32
|
+
@enforce_types
|
|
32
33
|
def create_organization(self, name: Optional[str] = None) -> PydanticOrganization:
|
|
33
34
|
"""Create a new organization. If a name is provided, it is used, otherwise, a random one is generated."""
|
|
34
35
|
with self.session_maker() as session:
|
|
@@ -36,14 +37,21 @@ class OrganizationManager:
|
|
|
36
37
|
org.create(session)
|
|
37
38
|
return org.to_pydantic()
|
|
38
39
|
|
|
40
|
+
@enforce_types
|
|
39
41
|
def create_default_organization(self) -> PydanticOrganization:
|
|
40
42
|
"""Create the default organization."""
|
|
41
43
|
with self.session_maker() as session:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
44
|
+
# Try to get it first
|
|
45
|
+
try:
|
|
46
|
+
org = Organization.read(db_session=session, identifier=DEFAULT_ORG_ID)
|
|
47
|
+
# If it doesn't exist, make it
|
|
48
|
+
except NoResultFound:
|
|
49
|
+
org = Organization(name=DEFAULT_ORG_NAME, id=DEFAULT_ORG_ID)
|
|
50
|
+
org.create(session)
|
|
51
|
+
|
|
45
52
|
return org.to_pydantic()
|
|
46
53
|
|
|
54
|
+
@enforce_types
|
|
47
55
|
def update_organization_name_using_id(self, org_id: str, name: Optional[str] = None) -> PydanticOrganization:
|
|
48
56
|
"""Update an organization."""
|
|
49
57
|
with self.session_maker() as session:
|
|
@@ -53,12 +61,14 @@ class OrganizationManager:
|
|
|
53
61
|
organization.update(session)
|
|
54
62
|
return organization.to_pydantic()
|
|
55
63
|
|
|
56
|
-
|
|
64
|
+
@enforce_types
|
|
65
|
+
def delete_organization_by_id(self, org_id: str):
|
|
57
66
|
"""Delete an organization by marking it as deleted."""
|
|
58
67
|
with self.session_maker() as session:
|
|
59
68
|
organization = Organization.read(db_session=session, identifier=org_id)
|
|
60
69
|
organization.delete(session)
|
|
61
70
|
|
|
71
|
+
@enforce_types
|
|
62
72
|
def list_organizations(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticOrganization]:
|
|
63
73
|
"""List organizations with pagination based on cursor (org_id) and limit."""
|
|
64
74
|
with self.session_maker() as session:
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from typing import List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
from letta.constants import DEFAULT_ORG_ID, DEFAULT_USER_ID, DEFAULT_USER_NAME
|
|
4
|
+
|
|
5
|
+
# TODO: Remove this once we translate all of these to the ORM
|
|
6
|
+
from letta.metadata import AgentModel, AgentSourceMappingModel, SourceModel
|
|
7
|
+
from letta.orm.errors import NoResultFound
|
|
8
|
+
from letta.orm.organization import Organization as OrganizationModel
|
|
9
|
+
from letta.orm.user import User as UserModel
|
|
10
|
+
from letta.schemas.user import User as PydanticUser
|
|
11
|
+
from letta.schemas.user import UserCreate, UserUpdate
|
|
12
|
+
from letta.utils import enforce_types
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UserManager:
|
|
16
|
+
"""Manager class to handle business logic related to Users."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
# Fetching the db_context similarly as in OrganizationManager
|
|
20
|
+
from letta.server.server import db_context
|
|
21
|
+
|
|
22
|
+
self.session_maker = db_context
|
|
23
|
+
|
|
24
|
+
@enforce_types
|
|
25
|
+
def create_default_user(self, org_id: str = DEFAULT_ORG_ID) -> PydanticUser:
|
|
26
|
+
"""Create the default user."""
|
|
27
|
+
with self.session_maker() as session:
|
|
28
|
+
# Make sure the org id exists
|
|
29
|
+
try:
|
|
30
|
+
OrganizationModel.read(db_session=session, identifier=org_id)
|
|
31
|
+
except NoResultFound:
|
|
32
|
+
raise ValueError(f"No organization with {org_id} exists in the organization table.")
|
|
33
|
+
|
|
34
|
+
# Try to retrieve the user
|
|
35
|
+
try:
|
|
36
|
+
user = UserModel.read(db_session=session, identifier=DEFAULT_USER_ID)
|
|
37
|
+
except NoResultFound:
|
|
38
|
+
# If it doesn't exist, make it
|
|
39
|
+
user = UserModel(id=DEFAULT_USER_ID, name=DEFAULT_USER_NAME, organization_id=org_id)
|
|
40
|
+
user.create(session)
|
|
41
|
+
|
|
42
|
+
return user.to_pydantic()
|
|
43
|
+
|
|
44
|
+
@enforce_types
|
|
45
|
+
def create_user(self, user_create: UserCreate) -> PydanticUser:
|
|
46
|
+
"""Create a new user if it doesn't already exist."""
|
|
47
|
+
with self.session_maker() as session:
|
|
48
|
+
new_user = UserModel(**user_create.model_dump())
|
|
49
|
+
new_user.create(session)
|
|
50
|
+
return new_user.to_pydantic()
|
|
51
|
+
|
|
52
|
+
@enforce_types
|
|
53
|
+
def update_user(self, user_update: UserUpdate) -> PydanticUser:
|
|
54
|
+
"""Update user details."""
|
|
55
|
+
with self.session_maker() as session:
|
|
56
|
+
# Retrieve the existing user by ID
|
|
57
|
+
existing_user = UserModel.read(db_session=session, identifier=user_update.id)
|
|
58
|
+
|
|
59
|
+
# Update only the fields that are provided in UserUpdate
|
|
60
|
+
update_data = user_update.model_dump(exclude_unset=True, exclude_none=True)
|
|
61
|
+
for key, value in update_data.items():
|
|
62
|
+
setattr(existing_user, key, value)
|
|
63
|
+
|
|
64
|
+
# Commit the updated user
|
|
65
|
+
existing_user.update(session)
|
|
66
|
+
return existing_user.to_pydantic()
|
|
67
|
+
|
|
68
|
+
@enforce_types
|
|
69
|
+
def delete_user_by_id(self, user_id: str):
|
|
70
|
+
"""Delete a user and their associated records (agents, sources, mappings)."""
|
|
71
|
+
with self.session_maker() as session:
|
|
72
|
+
# Delete from user table
|
|
73
|
+
user = UserModel.read(db_session=session, identifier=user_id)
|
|
74
|
+
user.delete(session)
|
|
75
|
+
|
|
76
|
+
# TODO: Remove this once we have ORM models for the Agent, Source, and AgentSourceMapping
|
|
77
|
+
# Cascade delete for related models: Agent, Source, AgentSourceMapping
|
|
78
|
+
session.query(AgentModel).filter(AgentModel.user_id == user_id).delete()
|
|
79
|
+
session.query(SourceModel).filter(SourceModel.user_id == user_id).delete()
|
|
80
|
+
session.query(AgentSourceMappingModel).filter(AgentSourceMappingModel.user_id == user_id).delete()
|
|
81
|
+
|
|
82
|
+
session.commit()
|
|
83
|
+
|
|
84
|
+
@enforce_types
|
|
85
|
+
def get_user_by_id(self, user_id: str) -> PydanticUser:
|
|
86
|
+
"""Fetch a user by ID."""
|
|
87
|
+
with self.session_maker() as session:
|
|
88
|
+
try:
|
|
89
|
+
user = UserModel.read(db_session=session, identifier=user_id)
|
|
90
|
+
return user.to_pydantic()
|
|
91
|
+
except NoResultFound:
|
|
92
|
+
raise ValueError(f"User with id {user_id} not found.")
|
|
93
|
+
|
|
94
|
+
@enforce_types
|
|
95
|
+
def list_users(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> Tuple[Optional[str], List[PydanticUser]]:
|
|
96
|
+
"""List users with pagination using cursor (id) and limit."""
|
|
97
|
+
with self.session_maker() as session:
|
|
98
|
+
results = UserModel.list(db_session=session, cursor=cursor, limit=limit)
|
|
99
|
+
return [user.to_pydantic() for user in results]
|
letta/settings.py
CHANGED
|
@@ -7,6 +7,10 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
|
7
7
|
from letta.local_llm.constants import DEFAULT_WRAPPER_NAME
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
class ToolSettings(BaseSettings):
|
|
11
|
+
composio_api_key: Optional[str] = None
|
|
12
|
+
|
|
13
|
+
|
|
10
14
|
class ModelSettings(BaseSettings):
|
|
11
15
|
|
|
12
16
|
# env_prefix='my_prefix_'
|
|
@@ -99,3 +103,4 @@ class TestSettings(Settings):
|
|
|
99
103
|
settings = Settings(_env_parse_none_str="None")
|
|
100
104
|
test_settings = TestSettings()
|
|
101
105
|
model_settings = ModelSettings()
|
|
106
|
+
tool_settings = ToolSettings()
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
letta/__init__.py,sha256=
|
|
1
|
+
letta/__init__.py,sha256=zlZXKr0iQIsC7FTkN9fjTvAgQGPXyr0nnkMJ7_OE1D0,1014
|
|
2
2
|
letta/__main__.py,sha256=6Hs2PV7EYc5Tid4g4OtcLXhqVHiNYTGzSBdoOnW2HXA,29
|
|
3
3
|
letta/agent.py,sha256=PRMDj0vZu5TGj-k2apeWcWYzJnBR36prdMXTKWnRYc8,72997
|
|
4
4
|
letta/agent_store/chroma.py,sha256=upR5zGnGs6I6btulEYbiZdGG87BgKjxUJOQZ4Y-RQ_M,12492
|
|
@@ -17,7 +17,7 @@ letta/client/client.py,sha256=CLOdsr3rVfWHQBPG-NN4sHKKn0Cn0VqIcjtHGxYaix4,93462
|
|
|
17
17
|
letta/client/streaming.py,sha256=bfWlUu7z7EoPfKxBqIarYxGKyrL7Pj79BlliToqcCgI,4592
|
|
18
18
|
letta/client/utils.py,sha256=OJlAKWrldc4I6M1WpcTWNtPJ4wfxlzlZqWLfCozkFtI,2872
|
|
19
19
|
letta/config.py,sha256=j2I90fOh9d9__kOYObwTDLbvVwYR50rIql5nzrvREKg,19161
|
|
20
|
-
letta/constants.py,sha256=
|
|
20
|
+
letta/constants.py,sha256=CLC--_fz5Gibaot0rJObjt53o-KO1LfxiAOJuwplHSI,6821
|
|
21
21
|
letta/credentials.py,sha256=D9mlcPsdDWlIIXQQD8wSPE9M_QvsRrb0p3LB5i9OF5Q,5806
|
|
22
22
|
letta/data_sources/connectors.py,sha256=qO81ASB6V-vDPthfHYtZiyqcQDQPTT0NuD8hVwC6xI0,9907
|
|
23
23
|
letta/data_sources/connectors_helper.py,sha256=2TQjCt74fCgT5sw1AP8PalDEk06jPBbhrPG4HVr-WLs,3371
|
|
@@ -83,7 +83,7 @@ letta/local_llm/webui/settings.py,sha256=gmLHfiOl1u4JmlAZU2d2O8YKF9lafdakyjwR_ft
|
|
|
83
83
|
letta/log.py,sha256=QHquDnL7oUAvdKlAwUlCK9zXKDMUjrU9WA0bxnMsP0Y,2101
|
|
84
84
|
letta/main.py,sha256=yHgM1lltQZvbE8k0QDQMmVyJiWEj07ZTOYIBHDxE_DQ,18709
|
|
85
85
|
letta/memory.py,sha256=6q1x3-PY-PeXzAt6hvP-UF1ajvroPZ7XW-5nLy-JhMo,17657
|
|
86
|
-
letta/metadata.py,sha256=
|
|
86
|
+
letta/metadata.py,sha256=a4XvHD6m52Pu1_Mr0m0yN0mFgo99OLKDtglfRBz6YRs,31548
|
|
87
87
|
letta/o1_agent.py,sha256=0jospImZUKhuQZ0cop0INj8xI6cxhxNffGA8iloHyfU,3114
|
|
88
88
|
letta/openai_backcompat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
89
89
|
letta/openai_backcompat/openai_object.py,sha256=Y1ZS1sATP60qxJiOsjOP3NbwSzuzvkNAvb3DeuhM5Uk,13490
|
|
@@ -91,10 +91,11 @@ letta/orm/__all__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
91
91
|
letta/orm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
92
92
|
letta/orm/base.py,sha256=9k7mwKDApJNpbrk6oXosQ7amlpeYhDzArdYTZ6BhfpQ,2405
|
|
93
93
|
letta/orm/enums.py,sha256=KfHcFt_fR6GUmSlmfsa-TetvmuRxGESNve8MStRYW64,145
|
|
94
|
-
letta/orm/errors.py,sha256=
|
|
95
|
-
letta/orm/mixins.py,sha256=
|
|
96
|
-
letta/orm/organization.py,sha256=
|
|
97
|
-
letta/orm/sqlalchemy_base.py,sha256=
|
|
94
|
+
letta/orm/errors.py,sha256=somsGtotFlb3SDM6tKdZ5TDGwEEP3ppx47ICAvNMnkg,225
|
|
95
|
+
letta/orm/mixins.py,sha256=NtJUSsOv6SM5GY9gXgHdq0smdXEeFv6tWXW_chpW0rw,2157
|
|
96
|
+
letta/orm/organization.py,sha256=afjWs0FU2lOPOHsw_BhDvdmx4f19e1_Jfc0b1w2Bz_8,1316
|
|
97
|
+
letta/orm/sqlalchemy_base.py,sha256=_UBfwpAeuHsPChu6EVV6jHQE_AMPDzE8Loj1a71M2ng,8973
|
|
98
|
+
letta/orm/user.py,sha256=wRo776d-bsKE6ChJkbjO15jHUQ22RiJauhJtXSkOzoc,1079
|
|
98
99
|
letta/persistence_manager.py,sha256=LlLgEDpSafCPAiyKmuq0NvVAnfBkZo6TWbGIKYQjQBs,5200
|
|
99
100
|
letta/personas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
100
101
|
letta/personas/examples/anna_pa.txt,sha256=zgiNdSNhy1HQy58cF_6RFPzcg2i37F9v38YuL1CW40A,1849
|
|
@@ -145,7 +146,7 @@ letta/schemas/passage.py,sha256=eYQMxD_XjHAi72jmqcGBU4wM4VZtSU0XK8uhQxxN3Ug,3563
|
|
|
145
146
|
letta/schemas/source.py,sha256=hB4Ai6Nj8dFdbxv5_Qaf4uN_cmdGmnzgc-4QnHXcV3o,2562
|
|
146
147
|
letta/schemas/tool.py,sha256=m8jWIsPUhekoQcjX7U_Y5vwhhQqSKn748RcXNXRiLGg,10329
|
|
147
148
|
letta/schemas/usage.py,sha256=lvn1ooHwLEdv6gwQpw5PBUbcwn_gwdT6HA-fCiix6sY,817
|
|
148
|
-
letta/schemas/user.py,sha256=
|
|
149
|
+
letta/schemas/user.py,sha256=s1D9QoyBwxsbcZALXg0cJIfKz-3k47UGJeaheiGxusU,1478
|
|
149
150
|
letta/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
150
151
|
letta/server/constants.py,sha256=yAdGbLkzlOU_dLTx0lKDmAnj0ZgRXCEaIcPJWO69eaE,92
|
|
151
152
|
letta/server/rest_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -172,13 +173,13 @@ letta/server/rest_api/routers/v1/blocks.py,sha256=0WekE_yBD2U3jYgPxI0DCFjACWavCA
|
|
|
172
173
|
letta/server/rest_api/routers/v1/health.py,sha256=pKCuVESlVOhGIb4VC4K-H82eZqfghmT6kvj2iOkkKuc,401
|
|
173
174
|
letta/server/rest_api/routers/v1/jobs.py,sha256=a-j0v-5A0un0pVCOHpfeWnzpOWkVDQO6ti42k_qAlZY,2272
|
|
174
175
|
letta/server/rest_api/routers/v1/llms.py,sha256=TcyvSx6MEM3je5F4DysL7ligmssL_pFlJaaO4uL95VY,877
|
|
175
|
-
letta/server/rest_api/routers/v1/organizations.py,sha256=
|
|
176
|
+
letta/server/rest_api/routers/v1/organizations.py,sha256=3XlHPUc6ZdMOVZNdarwDM8ZxYisoHunSZQ0ozzX5orU,2037
|
|
176
177
|
letta/server/rest_api/routers/v1/sources.py,sha256=eY_pk9jRL2Y9yIZdsTjH6EuKsfH1neaTU15MKNL0dvw,8749
|
|
177
178
|
letta/server/rest_api/routers/v1/tools.py,sha256=vxE4b5juoiBiNWmplktuv6GEgenCkKBRov-t6usUJ9A,3665
|
|
178
|
-
letta/server/rest_api/routers/v1/users.py,sha256=
|
|
179
|
+
letta/server/rest_api/routers/v1/users.py,sha256=bxQ-YdevjDqmgNDfbSPAG_4KEVvPNBHD_-Lp1MdeMec,3374
|
|
179
180
|
letta/server/rest_api/static_files.py,sha256=NG8sN4Z5EJ8JVQdj19tkFa9iQ1kBPTab9f_CUxd_u4Q,3143
|
|
180
181
|
letta/server/rest_api/utils.py,sha256=Fc2ZGKzLaBa2sEtSTVjJ8D5M0xIwsWC0CVAOIJaD3rY,2176
|
|
181
|
-
letta/server/server.py,sha256=
|
|
182
|
+
letta/server/server.py,sha256=EBDj5iH2tGr_GsMX6I_yRO6IBYpNpZUZxFY2nGqPDrc,90912
|
|
182
183
|
letta/server/startup.sh,sha256=jeGV7B_PS0hS-tT6o6GpACrUbV9WV1NI2L9aLoUDDtc,311
|
|
183
184
|
letta/server/static_files/assets/index-3ab03d5b.css,sha256=OrA9W4iKJ5h2Wlr7GwdAT4wow0CM8hVit1yOxEL49Qw,54295
|
|
184
185
|
letta/server/static_files/assets/index-d6b3669a.js,sha256=i1nHReU0RPnj-a5W0nNPV4Y9bQ0FOW0ztjMz8a2AE-Y,1821560
|
|
@@ -192,14 +193,15 @@ letta/server/ws_api/interface.py,sha256=TWl9vkcMCnLsUtgsuENZ-ku2oMDA-OUTzLh_yNRo
|
|
|
192
193
|
letta/server/ws_api/protocol.py,sha256=M_-gM5iuDBwa1cuN2IGNCG5GxMJwU2d3XW93XALv9s8,1821
|
|
193
194
|
letta/server/ws_api/server.py,sha256=C2Kv48PCwl46DQFb0ZP30s86KJLQ6dZk2AhWQEZn9pY,6004
|
|
194
195
|
letta/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
195
|
-
letta/services/organization_manager.py,sha256=
|
|
196
|
-
letta/
|
|
196
|
+
letta/services/organization_manager.py,sha256=skEByzSiLgFesBRbm87VKR-TSUK3LhJitTwN1Zl6-Hw,3379
|
|
197
|
+
letta/services/user_manager.py,sha256=q2GiGGv-bMTob2CT17onk38oHkWqU_DQap0sxQMhmkg,4376
|
|
198
|
+
letta/settings.py,sha256=yiYNmnYKj_BdTm0cBEIvQKYGU-lCmFntqsyVfRUy3_k,3411
|
|
197
199
|
letta/streaming_interface.py,sha256=_FPUWy58j50evHcpXyd7zB1wWqeCc71NCFeWh_TBvnw,15736
|
|
198
200
|
letta/streaming_utils.py,sha256=329fsvj1ZN0r0LpQtmMPZ2vSxkDBIUUwvGHZFkjm2I8,11745
|
|
199
201
|
letta/system.py,sha256=buKYPqG5n2x41hVmWpu6JUpyd7vTWED9Km2_M7dLrvk,6960
|
|
200
202
|
letta/utils.py,sha256=SXLEYhyp3gHyIjrxNIKNZZ5ittKo3KOj6zxgC_Trex0,31012
|
|
201
|
-
letta_nightly-0.5.
|
|
202
|
-
letta_nightly-0.5.
|
|
203
|
-
letta_nightly-0.5.
|
|
204
|
-
letta_nightly-0.5.
|
|
205
|
-
letta_nightly-0.5.
|
|
203
|
+
letta_nightly-0.5.1.dev20241023193051.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
|
|
204
|
+
letta_nightly-0.5.1.dev20241023193051.dist-info/METADATA,sha256=2A5lizaE-2kLa_idJ7BxYSzz9PJYEZdkgtvwrFVpF2Q,10660
|
|
205
|
+
letta_nightly-0.5.1.dev20241023193051.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
|
|
206
|
+
letta_nightly-0.5.1.dev20241023193051.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
|
|
207
|
+
letta_nightly-0.5.1.dev20241023193051.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|