letta-nightly 0.5.0.dev20241021104213__py3-none-any.whl → 0.5.0.dev20241023104105__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/__init__.py +7 -2
  2. letta/agent_store/db.py +4 -2
  3. letta/cli/cli_config.py +2 -2
  4. letta/client/client.py +13 -0
  5. letta/constants.py +4 -1
  6. letta/embeddings.py +34 -16
  7. letta/llm_api/azure_openai.py +44 -4
  8. letta/llm_api/helpers.py +45 -19
  9. letta/llm_api/openai.py +24 -5
  10. letta/metadata.py +1 -59
  11. letta/orm/__all__.py +0 -0
  12. letta/orm/__init__.py +0 -0
  13. letta/orm/base.py +75 -0
  14. letta/orm/enums.py +8 -0
  15. letta/orm/errors.py +2 -0
  16. letta/orm/mixins.py +40 -0
  17. letta/orm/organization.py +35 -0
  18. letta/orm/sqlalchemy_base.py +214 -0
  19. letta/schemas/organization.py +3 -3
  20. letta/server/rest_api/interface.py +245 -98
  21. letta/server/rest_api/routers/v1/agents.py +11 -3
  22. letta/server/rest_api/routers/v1/organizations.py +4 -5
  23. letta/server/server.py +10 -25
  24. letta/services/__init__.py +0 -0
  25. letta/services/organization_manager.py +66 -0
  26. letta/streaming_utils.py +270 -0
  27. {letta_nightly-0.5.0.dev20241021104213.dist-info → letta_nightly-0.5.0.dev20241023104105.dist-info}/METADATA +2 -1
  28. {letta_nightly-0.5.0.dev20241021104213.dist-info → letta_nightly-0.5.0.dev20241023104105.dist-info}/RECORD +31 -22
  29. letta/base.py +0 -3
  30. letta/client/admin.py +0 -171
  31. {letta_nightly-0.5.0.dev20241021104213.dist-info → letta_nightly-0.5.0.dev20241023104105.dist-info}/LICENSE +0 -0
  32. {letta_nightly-0.5.0.dev20241021104213.dist-info → letta_nightly-0.5.0.dev20241023104105.dist-info}/WHEEL +0 -0
  33. {letta_nightly-0.5.0.dev20241021104213.dist-info → letta_nightly-0.5.0.dev20241023104105.dist-info}/entry_points.txt +0 -0
letta/orm/mixins.py ADDED
@@ -0,0 +1,40 @@
1
+ from typing import Optional, Type
2
+ from uuid import UUID
3
+
4
+ from letta.orm.base import Base
5
+
6
+
7
+ class MalformedIdError(Exception):
8
+ pass
9
+
10
+
11
+ def _relation_getter(instance: "Base", prop: str) -> Optional[str]:
12
+ prefix = prop.replace("_", "")
13
+ formatted_prop = f"_{prop}_id"
14
+ try:
15
+ uuid_ = getattr(instance, formatted_prop)
16
+ return f"{prefix}-{uuid_}"
17
+ except AttributeError:
18
+ return None
19
+
20
+
21
+ def _relation_setter(instance: Type["Base"], prop: str, value: str) -> None:
22
+ formatted_prop = f"_{prop}_id"
23
+ prefix = prop.replace("_", "")
24
+ if not value:
25
+ setattr(instance, formatted_prop, None)
26
+ return
27
+ try:
28
+ found_prefix, id_ = value.split("-", 1)
29
+ except ValueError as e:
30
+ raise MalformedIdError(f"{value} is not a valid ID.") from e
31
+ assert (
32
+ # TODO: should be able to get this from the Mapped typing, not sure how though
33
+ # prefix = getattr(?, "prefix")
34
+ found_prefix
35
+ == prefix
36
+ ), f"{found_prefix} is not a valid id prefix, expecting {prefix}"
37
+ try:
38
+ setattr(instance, formatted_prop, UUID(id_))
39
+ except ValueError as e:
40
+ raise MalformedIdError("Hash segment of {value} is not a valid UUID") from e
@@ -0,0 +1,35 @@
1
+ from typing import TYPE_CHECKING
2
+
3
+ from sqlalchemy.exc import NoResultFound
4
+ from sqlalchemy.orm import Mapped, mapped_column
5
+
6
+ from letta.orm.sqlalchemy_base import SqlalchemyBase
7
+ from letta.schemas.organization import Organization as PydanticOrganization
8
+
9
+ if TYPE_CHECKING:
10
+ from sqlalchemy.orm import Session
11
+
12
+
13
+ class Organization(SqlalchemyBase):
14
+ """The highest level of the object tree. All Entities belong to one and only one Organization."""
15
+
16
+ __tablename__ = "organizations"
17
+ __pydantic_model__ = PydanticOrganization
18
+
19
+ name: Mapped[str] = mapped_column(doc="The display name of the organization.")
20
+
21
+ # TODO: Map these relationships later when we actually make these models
22
+ # below is just a suggestion
23
+ # users: Mapped[List["User"]] = relationship("User", back_populates="organization", cascade="all, delete-orphan")
24
+ # agents: Mapped[List["Agent"]] = relationship("Agent", back_populates="organization", cascade="all, delete-orphan")
25
+ # sources: Mapped[List["Source"]] = relationship("Source", back_populates="organization", cascade="all, delete-orphan")
26
+ # tools: Mapped[List["Tool"]] = relationship("Tool", back_populates="organization", cascade="all, delete-orphan")
27
+ # 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)
@@ -0,0 +1,214 @@
1
+ from typing import TYPE_CHECKING, List, Literal, Optional, Type, Union
2
+ from uuid import UUID, uuid4
3
+
4
+ from humps import depascalize
5
+ from sqlalchemy import Boolean, String, select
6
+ from sqlalchemy.orm import Mapped, mapped_column
7
+
8
+ from letta.log import get_logger
9
+ from letta.orm.base import Base, CommonSqlalchemyMetaMixins
10
+ from letta.orm.errors import NoResultFound
11
+
12
+ if TYPE_CHECKING:
13
+ from pydantic import BaseModel
14
+ from sqlalchemy.orm import Session
15
+
16
+ # from letta.orm.user import User
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
22
+ __abstract__ = True
23
+
24
+ __order_by_default__ = "created_at"
25
+
26
+ _id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: f"{uuid4()}")
27
+
28
+ deleted: Mapped[bool] = mapped_column(Boolean, default=False, doc="Is this record deleted? Used for universal soft deletes.")
29
+
30
+ @classmethod
31
+ def __prefix__(cls) -> str:
32
+ return depascalize(cls.__name__)
33
+
34
+ @property
35
+ def id(self) -> Optional[str]:
36
+ if self._id:
37
+ return f"{self.__prefix__()}-{self._id}"
38
+
39
+ @id.setter
40
+ def id(self, value: str) -> None:
41
+ if not value:
42
+ return
43
+ prefix, id_ = value.split("-", 1)
44
+ assert prefix == self.__prefix__(), f"{prefix} is not a valid id prefix for {self.__class__.__name__}"
45
+ assert SqlalchemyBase.is_valid_uuid4(id_), f"{id_} is not a valid uuid4"
46
+ self._id = id_
47
+
48
+ @classmethod
49
+ def list(
50
+ cls, *, db_session: "Session", cursor: Optional[str] = None, limit: Optional[int] = 50, **kwargs
51
+ ) -> List[Type["SqlalchemyBase"]]:
52
+ """List records with optional cursor (for pagination) and limit."""
53
+ with db_session as session:
54
+ # Start with the base query filtered by kwargs
55
+ query = select(cls).filter_by(**kwargs)
56
+
57
+ # Add a cursor condition if provided
58
+ if cursor:
59
+ cursor_uuid = cls.get_uid_from_identifier(cursor) # Assuming the cursor is an _id value
60
+ query = query.where(cls._id > cursor_uuid)
61
+
62
+ # Add a limit to the query if provided
63
+ query = query.order_by(cls._id).limit(limit)
64
+
65
+ # Handle soft deletes if the class has the 'is_deleted' attribute
66
+ if hasattr(cls, "is_deleted"):
67
+ query = query.where(cls.is_deleted == False)
68
+
69
+ # Execute the query and return the results as a list of model instances
70
+ return list(session.execute(query).scalars())
71
+
72
+ @classmethod
73
+ def get_uid_from_identifier(cls, identifier: str, indifferent: Optional[bool] = False) -> str:
74
+ """converts the id into a uuid object
75
+ Args:
76
+ identifier: the string identifier, such as `organization-xxxx-xx...`
77
+ indifferent: if True, will not enforce the prefix check
78
+ """
79
+ try:
80
+ uuid_string = identifier.split("-", 1)[1] if indifferent else identifier.replace(f"{cls.__prefix__()}-", "")
81
+ assert SqlalchemyBase.is_valid_uuid4(uuid_string)
82
+ return uuid_string
83
+ except ValueError as e:
84
+ raise ValueError(f"{identifier} is not a valid identifier for class {cls.__name__}") from e
85
+
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
+ @classmethod
98
+ def read(
99
+ cls,
100
+ db_session: "Session",
101
+ identifier: Union[str, UUID],
102
+ actor: Optional["User"] = None,
103
+ access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
104
+ **kwargs,
105
+ ) -> Type["SqlalchemyBase"]:
106
+ """The primary accessor for an ORM record.
107
+ Args:
108
+ db_session: the database session to use when retrieving the record
109
+ identifier: the identifier of the record to read, can be the id string or the UUID object for backwards compatibility
110
+ actor: if specified, results will be scoped only to records the user is able to access
111
+ access: if actor is specified, records will be filtered to the minimum permission level for the actor
112
+ kwargs: additional arguments to pass to the read, used for more complex objects
113
+ Returns:
114
+ The matching object
115
+ Raises:
116
+ NoResultFound: if the object is not found
117
+ """
118
+ del kwargs # arity for more complex reads
119
+ identifier = cls.get_uid_from_identifier(identifier)
120
+ query = select(cls).where(cls._id == identifier)
121
+ # if actor:
122
+ # query = cls.apply_access_predicate(query, actor, access)
123
+ if hasattr(cls, "is_deleted"):
124
+ query = query.where(cls.is_deleted == False)
125
+ if found := db_session.execute(query).scalar():
126
+ return found
127
+ raise NoResultFound(f"{cls.__name__} with id {identifier} not found")
128
+
129
+ def create(self, db_session: "Session") -> Type["SqlalchemyBase"]:
130
+ # self._infer_organization(db_session)
131
+
132
+ with db_session as session:
133
+ session.add(self)
134
+ session.commit()
135
+ session.refresh(self)
136
+ return self
137
+
138
+ def delete(self, db_session: "Session") -> Type["SqlalchemyBase"]:
139
+ self.is_deleted = True
140
+ return self.update(db_session)
141
+
142
+ def update(self, db_session: "Session") -> Type["SqlalchemyBase"]:
143
+ with db_session as session:
144
+ session.add(self)
145
+ session.commit()
146
+ session.refresh(self)
147
+ return self
148
+
149
+ @classmethod
150
+ def read_or_create(cls, *, db_session: "Session", **kwargs) -> Type["SqlalchemyBase"]:
151
+ """get an instance by search criteria or create it if it doesn't exist"""
152
+ try:
153
+ return cls.read(db_session=db_session, identifier=kwargs.get("id", None))
154
+ except NoResultFound:
155
+ clean_kwargs = {k: v for k, v in kwargs.items() if k in cls.__table__.columns}
156
+ return cls(**clean_kwargs).create(db_session=db_session)
157
+
158
+ # TODO: Add back later when access predicates are actually important
159
+ # The idea behind this is that you can add a WHERE clause restricting the actions you can take, e.g. R/W
160
+ # @classmethod
161
+ # def apply_access_predicate(
162
+ # cls,
163
+ # query: "Select",
164
+ # actor: "User",
165
+ # access: List[Literal["read", "write", "admin"]],
166
+ # ) -> "Select":
167
+ # """applies a WHERE clause restricting results to the given actor and access level
168
+ # Args:
169
+ # query: The initial sqlalchemy select statement
170
+ # actor: The user acting on the query. **Note**: this is called 'actor' to identify the
171
+ # person or system acting. Users can act on users, making naming very sticky otherwise.
172
+ # access:
173
+ # what mode of access should the query restrict to? This will be used with granular permissions,
174
+ # but because of how it will impact every query we want to be explicitly calling access ahead of time.
175
+ # Returns:
176
+ # the sqlalchemy select statement restricted to the given access.
177
+ # """
178
+ # del access # entrypoint for row-level permissions. Defaults to "same org as the actor, all permissions" at the moment
179
+ # org_uid = getattr(actor, "_organization_id", getattr(actor.organization, "_id", None))
180
+ # if not org_uid:
181
+ # raise ValueError("object %s has no organization accessor", actor)
182
+ # return query.where(cls._organization_id == org_uid, cls.is_deleted == False)
183
+
184
+ @property
185
+ def __pydantic_model__(self) -> Type["BaseModel"]:
186
+ raise NotImplementedError("Sqlalchemy models must declare a __pydantic_model__ property to be convertable.")
187
+
188
+ def to_pydantic(self) -> Type["BaseModel"]:
189
+ """converts to the basic pydantic model counterpart"""
190
+ return self.__pydantic_model__.model_validate(self)
191
+
192
+ def to_record(self) -> Type["BaseModel"]:
193
+ """Deprecated accessor for to_pydantic"""
194
+ logger.warning("to_record is deprecated, use to_pydantic instead.")
195
+ return self.to_pydantic()
196
+
197
+ # TODO: Look into this later and maybe add back?
198
+ # def _infer_organization(self, db_session: "Session") -> None:
199
+ # """🪄 MAGIC ALERT! 🪄
200
+ # Because so much of the original API is centered around user scopes,
201
+ # this allows us to continue with that scope and then infer the org from the creating user.
202
+ #
203
+ # IF a created_by_id is set, we will use that to infer the organization and magic set it at create time!
204
+ # If not do nothing to the object. Mutates in place.
205
+ # """
206
+ # if self.created_by_id and hasattr(self, "_organization_id"):
207
+ # try:
208
+ # from letta.orm.user import User # to avoid circular import
209
+ #
210
+ # created_by = User.read(db_session, self.created_by_id)
211
+ # except NoResultFound:
212
+ # logger.warning(f"User {self.created_by_id} not found, unable to infer organization.")
213
+ # return
214
+ # self._organization_id = created_by._organization_id
@@ -7,13 +7,13 @@ from letta.schemas.letta_base import LettaBase
7
7
 
8
8
 
9
9
  class OrganizationBase(LettaBase):
10
- __id_prefix__ = "org"
10
+ __id_prefix__ = "organization"
11
11
 
12
12
 
13
13
  class Organization(OrganizationBase):
14
- id: str = OrganizationBase.generate_id_field()
14
+ id: str = Field(..., description="The id of the organization.")
15
15
  name: str = Field(..., description="The name of the organization.")
16
- created_at: datetime = Field(default_factory=datetime.utcnow, description="The creation date of the user.")
16
+ created_at: datetime = Field(default_factory=datetime.utcnow, description="The creation date of the organization.")
17
17
 
18
18
 
19
19
  class OrganizationCreate(OrganizationBase):