letta-nightly 0.5.1.dev20241028104150__py3-none-any.whl → 0.5.1.dev20241029104141__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/cli/cli.py CHANGED
@@ -218,9 +218,7 @@ def run(
218
218
  )
219
219
 
220
220
  # create agent
221
- tools = [
222
- server.tool_manager.get_tool_by_name_and_user_id(tool_name=tool_name, user_id=client.user_id) for tool_name in agent_state.tools
223
- ]
221
+ tools = [server.tool_manager.get_tool_by_name(tool_name=tool_name, actor=client.user) for tool_name in agent_state.tools]
224
222
  letta_agent = Agent(agent_state=agent_state, interface=interface(), tools=tools)
225
223
 
226
224
  else: # create new agent
@@ -300,7 +298,7 @@ def run(
300
298
  )
301
299
  assert isinstance(agent_state.memory, Memory), f"Expected Memory, got {type(agent_state.memory)}"
302
300
  typer.secho(f"-> 🛠️ {len(agent_state.tools)} tools: {', '.join([t for t in agent_state.tools])}", fg=typer.colors.WHITE)
303
- tools = [server.tool_manager.get_tool_by_name_and_user_id(tool_name, user_id=client.user_id) for tool_name in agent_state.tools]
301
+ tools = [server.tool_manager.get_tool_by_name(tool_name, actor=client.user) for tool_name in agent_state.tools]
304
302
 
305
303
  letta_agent = Agent(
306
304
  interface=interface(),
letta/client/client.py CHANGED
@@ -276,10 +276,10 @@ class AbstractClient(object):
276
276
  ) -> List[Message]:
277
277
  raise NotImplementedError
278
278
 
279
- def list_models(self) -> List[LLMConfig]:
279
+ def list_model_configs(self) -> List[LLMConfig]:
280
280
  raise NotImplementedError
281
281
 
282
- def list_embedding_models(self) -> List[EmbeddingConfig]:
282
+ def list_embedding_configs(self) -> List[EmbeddingConfig]:
283
283
  raise NotImplementedError
284
284
 
285
285
 
@@ -1234,32 +1234,6 @@ class RESTClient(AbstractClient):
1234
1234
  assert response.status_code == 200, f"Failed to detach source from agent: {response.text}"
1235
1235
  return Source(**response.json())
1236
1236
 
1237
- # server configuration commands
1238
-
1239
- def list_models(self):
1240
- """
1241
- List available LLM models
1242
-
1243
- Returns:
1244
- models (List[LLMConfig]): List of LLM models
1245
- """
1246
- response = requests.get(f"{self.base_url}/{self.api_prefix}/models", headers=self.headers)
1247
- if response.status_code != 200:
1248
- raise ValueError(f"Failed to list models: {response.text}")
1249
- return [LLMConfig(**model) for model in response.json()]
1250
-
1251
- def list_embedding_models(self):
1252
- """
1253
- List available embedding models
1254
-
1255
- Returns:
1256
- models (List[EmbeddingConfig]): List of embedding models
1257
- """
1258
- response = requests.get(f"{self.base_url}/{self.api_prefix}/models/embedding", headers=self.headers)
1259
- if response.status_code != 200:
1260
- raise ValueError(f"Failed to list embedding models: {response.text}")
1261
- return [EmbeddingConfig(**model) for model in response.json()]
1262
-
1263
1237
  # tools
1264
1238
 
1265
1239
  def get_tool_id(self, tool_name: str):
@@ -1546,6 +1520,9 @@ class LocalClient(AbstractClient):
1546
1520
  # get default user
1547
1521
  self.user_id = self.server.user_manager.DEFAULT_USER_ID
1548
1522
 
1523
+ self.user = self.server.get_user_or_default(self.user_id)
1524
+ self.organization = self.server.get_organization_or_default(self.org_id)
1525
+
1549
1526
  # agents
1550
1527
  def list_agents(self) -> List[AgentState]:
1551
1528
  self.interface.clear()
@@ -1648,7 +1625,7 @@ class LocalClient(AbstractClient):
1648
1625
  llm_config=llm_config if llm_config else self._default_llm_config,
1649
1626
  embedding_config=embedding_config if embedding_config else self._default_embedding_config,
1650
1627
  ),
1651
- user_id=self.user_id,
1628
+ actor=self.user,
1652
1629
  )
1653
1630
  return agent_state
1654
1631
 
@@ -1720,7 +1697,7 @@ class LocalClient(AbstractClient):
1720
1697
  message_ids=message_ids,
1721
1698
  memory=memory,
1722
1699
  ),
1723
- user_id=self.user_id,
1700
+ actor=self.user,
1724
1701
  )
1725
1702
  return agent_state
1726
1703
 
@@ -2198,24 +2175,22 @@ class LocalClient(AbstractClient):
2198
2175
  def load_langchain_tool(self, langchain_tool: "LangChainBaseTool", additional_imports_module_attr_map: dict[str, str] = None) -> Tool:
2199
2176
  tool_create = ToolCreate.from_langchain(
2200
2177
  langchain_tool=langchain_tool,
2201
- user_id=self.user_id,
2202
2178
  organization_id=self.org_id,
2203
2179
  additional_imports_module_attr_map=additional_imports_module_attr_map,
2204
2180
  )
2205
- return self.server.tool_manager.create_or_update_tool(tool_create)
2181
+ return self.server.tool_manager.create_or_update_tool(tool_create, actor=self.user)
2206
2182
 
2207
2183
  def load_crewai_tool(self, crewai_tool: "CrewAIBaseTool", additional_imports_module_attr_map: dict[str, str] = None) -> Tool:
2208
2184
  tool_create = ToolCreate.from_crewai(
2209
2185
  crewai_tool=crewai_tool,
2210
2186
  additional_imports_module_attr_map=additional_imports_module_attr_map,
2211
- user_id=self.user_id,
2212
2187
  organization_id=self.org_id,
2213
2188
  )
2214
- return self.server.tool_manager.create_or_update_tool(tool_create)
2189
+ return self.server.tool_manager.create_or_update_tool(tool_create, actor=self.user)
2215
2190
 
2216
2191
  def load_composio_tool(self, action: "ActionType") -> Tool:
2217
- tool_create = ToolCreate.from_composio(action=action, user_id=self.user_id, organization_id=self.org_id)
2218
- return self.server.tool_manager.create_or_update_tool(tool_create)
2192
+ tool_create = ToolCreate.from_composio(action=action, organization_id=self.org_id)
2193
+ return self.server.tool_manager.create_or_update_tool(tool_create, actor=self.user)
2219
2194
 
2220
2195
  # TODO: Use the above function `add_tool` here as there is duplicate logic
2221
2196
  def create_tool(
@@ -2250,14 +2225,13 @@ class LocalClient(AbstractClient):
2250
2225
  # call server function
2251
2226
  return self.server.tool_manager.create_or_update_tool(
2252
2227
  ToolCreate(
2253
- user_id=self.user_id,
2254
- organization_id=self.org_id,
2255
2228
  source_type=source_type,
2256
2229
  source_code=source_code,
2257
2230
  name=name,
2258
2231
  tags=tags,
2259
2232
  terminal=terminal,
2260
2233
  ),
2234
+ actor=self.user,
2261
2235
  )
2262
2236
 
2263
2237
  def update_tool(
@@ -2289,7 +2263,7 @@ class LocalClient(AbstractClient):
2289
2263
  # Filter out any None values from the dictionary
2290
2264
  update_data = {key: value for key, value in update_data.items() if value is not None}
2291
2265
 
2292
- return self.server.tool_manager.update_tool_by_id(id, ToolUpdate(**update_data))
2266
+ return self.server.tool_manager.update_tool_by_id(tool_id=id, tool_update=ToolUpdate(**update_data), actor=self.user)
2293
2267
 
2294
2268
  def list_tools(self, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[Tool]:
2295
2269
  """
@@ -2298,7 +2272,7 @@ class LocalClient(AbstractClient):
2298
2272
  Returns:
2299
2273
  tools (List[Tool]): List of tools
2300
2274
  """
2301
- return self.server.tool_manager.list_tools_for_org(cursor=cursor, limit=limit, organization_id=self.org_id)
2275
+ return self.server.tool_manager.list_tools(cursor=cursor, limit=limit, actor=self.user)
2302
2276
 
2303
2277
  def get_tool(self, id: str) -> Optional[Tool]:
2304
2278
  """
@@ -2310,7 +2284,7 @@ class LocalClient(AbstractClient):
2310
2284
  Returns:
2311
2285
  tool (Tool): Tool
2312
2286
  """
2313
- return self.server.tool_manager.get_tool_by_id(id)
2287
+ return self.server.tool_manager.get_tool_by_id(id, actor=self.user)
2314
2288
 
2315
2289
  def delete_tool(self, id: str):
2316
2290
  """
@@ -2319,7 +2293,7 @@ class LocalClient(AbstractClient):
2319
2293
  Args:
2320
2294
  id (str): ID of the tool
2321
2295
  """
2322
- return self.server.tool_manager.delete_tool_by_id(id)
2296
+ return self.server.tool_manager.delete_tool_by_id(id, user_id=self.user_id)
2323
2297
 
2324
2298
  def get_tool_id(self, name: str) -> Optional[str]:
2325
2299
  """
@@ -2331,7 +2305,7 @@ class LocalClient(AbstractClient):
2331
2305
  Returns:
2332
2306
  id (str): ID of the tool (`None` if not found)
2333
2307
  """
2334
- tool = self.server.tool_manager.get_tool_by_name_and_org_id(tool_name=name, organization_id=self.org_id)
2308
+ tool = self.server.tool_manager.get_tool_by_name(tool_name=name, actor=self.user)
2335
2309
  return tool.id
2336
2310
 
2337
2311
  def load_data(self, connector: DataConnector, source_name: str):
@@ -2572,24 +2546,6 @@ class LocalClient(AbstractClient):
2572
2546
  return_message_object=True,
2573
2547
  )
2574
2548
 
2575
- def list_models(self) -> List[LLMConfig]:
2576
- """
2577
- List available LLM models
2578
-
2579
- Returns:
2580
- models (List[LLMConfig]): List of LLM models
2581
- """
2582
- return self.server.list_models()
2583
-
2584
- def list_embedding_models(self) -> List[EmbeddingConfig]:
2585
- """
2586
- List available embedding models
2587
-
2588
- Returns:
2589
- models (List[EmbeddingConfig]): List of embedding models
2590
- """
2591
- return [self.server.server_embedding_config]
2592
-
2593
2549
  def list_blocks(self, label: Optional[str] = None, templates_only: Optional[bool] = True) -> List[Block]:
2594
2550
  """
2595
2551
  List available blocks
@@ -313,7 +313,6 @@ def create(
313
313
  stream_interface.stream_start()
314
314
  try:
315
315
  # groq uses the openai chat completions API, so this component should be reusable
316
- assert model_settings.groq_api_key is not None, "Groq key is missing"
317
316
  response = openai_chat_completions_request(
318
317
  url=llm_config.model_endpoint,
319
318
  api_key=model_settings.groq_api_key,
letta/orm/base.py CHANGED
@@ -1,9 +1,7 @@
1
1
  from datetime import datetime
2
2
  from typing import Optional
3
- from uuid import UUID
4
3
 
5
- from sqlalchemy import UUID as SQLUUID
6
- from sqlalchemy import Boolean, DateTime, func, text
4
+ from sqlalchemy import Boolean, DateTime, String, func, text
7
5
  from sqlalchemy.orm import (
8
6
  DeclarativeBase,
9
7
  Mapped,
@@ -25,6 +23,13 @@ class CommonSqlalchemyMetaMixins(Base):
25
23
  updated_at: Mapped[Optional[datetime]] = mapped_column(DateTime(timezone=True), server_default=func.now(), server_onupdate=func.now())
26
24
  is_deleted: Mapped[bool] = mapped_column(Boolean, server_default=text("FALSE"))
27
25
 
26
+ def _set_created_and_updated_by_fields(self, actor_id: str) -> None:
27
+ """Populate created_by_id and last_updated_by_id based on actor."""
28
+ if not self.created_by_id:
29
+ self.created_by_id = actor_id
30
+ # Always set the last_updated_by_id when updating
31
+ self.last_updated_by_id = actor_id
32
+
28
33
  @declared_attr
29
34
  def _created_by_id(cls):
30
35
  return cls._user_by_id()
@@ -38,7 +43,7 @@ class CommonSqlalchemyMetaMixins(Base):
38
43
  """a flexible non-constrained record of a user.
39
44
  This way users can get added, deleted etc without history freaking out
40
45
  """
41
- return mapped_column(SQLUUID(), nullable=True)
46
+ return mapped_column(String, nullable=True)
42
47
 
43
48
  @property
44
49
  def last_updated_by_id(self) -> Optional[str]:
@@ -72,4 +77,4 @@ class CommonSqlalchemyMetaMixins(Base):
72
77
  return
73
78
  prefix, id_ = value.split("-", 1)
74
79
  assert prefix == "user", f"{prefix} is not a valid id prefix for a user id"
75
- setattr(self, full_prop, UUID(id_))
80
+ setattr(self, full_prop, id_)
@@ -1,5 +1,5 @@
1
- from typing import TYPE_CHECKING, List, Literal, Optional, Type, Union
2
- from uuid import UUID, uuid4
1
+ from typing import TYPE_CHECKING, List, Literal, Optional, Type
2
+ from uuid import uuid4
3
3
 
4
4
  from humps import depascalize
5
5
  from sqlalchemy import Boolean, String, select
@@ -88,7 +88,7 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
88
88
  def read(
89
89
  cls,
90
90
  db_session: "Session",
91
- identifier: Union[str, UUID],
91
+ identifier: Optional[str] = None,
92
92
  actor: Optional["User"] = None,
93
93
  access: Optional[List[Literal["read", "write", "admin"]]] = ["read"],
94
94
  **kwargs,
@@ -105,19 +105,29 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
105
105
  Raises:
106
106
  NoResultFound: if the object is not found
107
107
  """
108
- del kwargs # arity for more complex reads
109
- identifier = cls.get_uid_from_identifier(identifier)
110
- query = select(cls).where(cls._id == identifier)
111
- # if actor:
112
- # query = cls.apply_access_predicate(query, actor, access)
108
+ # Start the query
109
+ query = select(cls)
110
+
111
+ # If an identifier is provided, add it to the query conditions
112
+ if identifier is not None:
113
+ identifier = cls.get_uid_from_identifier(identifier)
114
+ query = query.where(cls._id == identifier)
115
+
116
+ if kwargs:
117
+ query = query.filter_by(**kwargs)
118
+
119
+ if actor:
120
+ query = cls.apply_access_predicate(query, actor, access)
121
+
113
122
  if hasattr(cls, "is_deleted"):
114
123
  query = query.where(cls.is_deleted == False)
115
124
  if found := db_session.execute(query).scalar():
116
125
  return found
117
126
  raise NoResultFound(f"{cls.__name__} with id {identifier} not found")
118
127
 
119
- def create(self, db_session: "Session") -> Type["SqlalchemyBase"]:
120
- # self._infer_organization(db_session)
128
+ def create(self, db_session: "Session", actor: Optional["User"] = None) -> Type["SqlalchemyBase"]:
129
+ if actor:
130
+ self._set_created_and_updated_by_fields(actor.id)
121
131
 
122
132
  with db_session as session:
123
133
  session.add(self)
@@ -125,11 +135,17 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
125
135
  session.refresh(self)
126
136
  return self
127
137
 
128
- def delete(self, db_session: "Session") -> Type["SqlalchemyBase"]:
138
+ def delete(self, db_session: "Session", actor: Optional["User"] = None) -> Type["SqlalchemyBase"]:
139
+ if actor:
140
+ self._set_created_and_updated_by_fields(actor.id)
141
+
129
142
  self.is_deleted = True
130
143
  return self.update(db_session)
131
144
 
132
- def update(self, db_session: "Session") -> Type["SqlalchemyBase"]:
145
+ def update(self, db_session: "Session", actor: Optional["User"] = None) -> Type["SqlalchemyBase"]:
146
+ if actor:
147
+ self._set_created_and_updated_by_fields(actor.id)
148
+
133
149
  with db_session as session:
134
150
  session.add(self)
135
151
  session.commit()
@@ -137,39 +153,28 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
137
153
  return self
138
154
 
139
155
  @classmethod
140
- def read_or_create(cls, *, db_session: "Session", **kwargs) -> Type["SqlalchemyBase"]:
141
- """get an instance by search criteria or create it if it doesn't exist"""
142
- try:
143
- return cls.read(db_session=db_session, identifier=kwargs.get("id", None))
144
- except NoResultFound:
145
- clean_kwargs = {k: v for k, v in kwargs.items() if k in cls.__table__.columns}
146
- return cls(**clean_kwargs).create(db_session=db_session)
147
-
148
- # TODO: Add back later when access predicates are actually important
149
- # The idea behind this is that you can add a WHERE clause restricting the actions you can take, e.g. R/W
150
- # @classmethod
151
- # def apply_access_predicate(
152
- # cls,
153
- # query: "Select",
154
- # actor: "User",
155
- # access: List[Literal["read", "write", "admin"]],
156
- # ) -> "Select":
157
- # """applies a WHERE clause restricting results to the given actor and access level
158
- # Args:
159
- # query: The initial sqlalchemy select statement
160
- # actor: The user acting on the query. **Note**: this is called 'actor' to identify the
161
- # person or system acting. Users can act on users, making naming very sticky otherwise.
162
- # access:
163
- # what mode of access should the query restrict to? This will be used with granular permissions,
164
- # but because of how it will impact every query we want to be explicitly calling access ahead of time.
165
- # Returns:
166
- # the sqlalchemy select statement restricted to the given access.
167
- # """
168
- # del access # entrypoint for row-level permissions. Defaults to "same org as the actor, all permissions" at the moment
169
- # org_uid = getattr(actor, "_organization_id", getattr(actor.organization, "_id", None))
170
- # if not org_uid:
171
- # raise ValueError("object %s has no organization accessor", actor)
172
- # return query.where(cls._organization_id == org_uid, cls.is_deleted == False)
156
+ def apply_access_predicate(
157
+ cls,
158
+ query: "Select",
159
+ actor: "User",
160
+ access: List[Literal["read", "write", "admin"]],
161
+ ) -> "Select":
162
+ """applies a WHERE clause restricting results to the given actor and access level
163
+ Args:
164
+ query: The initial sqlalchemy select statement
165
+ actor: The user acting on the query. **Note**: this is called 'actor' to identify the
166
+ person or system acting. Users can act on users, making naming very sticky otherwise.
167
+ access:
168
+ what mode of access should the query restrict to? This will be used with granular permissions,
169
+ but because of how it will impact every query we want to be explicitly calling access ahead of time.
170
+ Returns:
171
+ the sqlalchemy select statement restricted to the given access.
172
+ """
173
+ del access # entrypoint for row-level permissions. Defaults to "same org as the actor, all permissions" at the moment
174
+ org_id = getattr(actor, "organization_id", None)
175
+ if not org_id:
176
+ raise ValueError(f"object {actor} has no organization accessor")
177
+ return query.where(cls._organization_id == cls.get_uid_from_identifier(org_id, indifferent=True), cls.is_deleted == False)
173
178
 
174
179
  @property
175
180
  def __pydantic_model__(self) -> Type["BaseModel"]:
@@ -183,21 +188,3 @@ class SqlalchemyBase(CommonSqlalchemyMetaMixins, Base):
183
188
  """Deprecated accessor for to_pydantic"""
184
189
  logger.warning("to_record is deprecated, use to_pydantic instead.")
185
190
  return self.to_pydantic()
186
-
187
- def _infer_organization(self, db_session: "Session") -> None:
188
- """🪄 MAGIC ALERT! 🪄
189
- Because so much of the original API is centered around user scopes,
190
- this allows us to continue with that scope and then infer the org from the creating user.
191
-
192
- IF a created_by_id is set, we will use that to infer the organization and magic set it at create time!
193
- If not do nothing to the object. Mutates in place.
194
- """
195
- if self.created_by_id and hasattr(self, "_organization_id"):
196
- try:
197
- from letta.orm.user import User # to avoid circular import
198
-
199
- created_by = User.read(db_session, self.created_by_id)
200
- except NoResultFound:
201
- logger.warning(f"User {self.created_by_id} not found, unable to infer organization.")
202
- return
203
- self._organization_id = created_by._organization_id
letta/orm/tool.py CHANGED
@@ -5,18 +5,15 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
5
5
 
6
6
  # TODO everything in functions should live in this model
7
7
  from letta.orm.enums import ToolSourceType
8
- from letta.orm.mixins import OrganizationMixin, UserMixin
8
+ from letta.orm.mixins import OrganizationMixin
9
9
  from letta.orm.sqlalchemy_base import SqlalchemyBase
10
10
  from letta.schemas.tool import Tool as PydanticTool
11
11
 
12
12
  if TYPE_CHECKING:
13
- pass
14
-
15
13
  from letta.orm.organization import Organization
16
- from letta.orm.user import User
17
14
 
18
15
 
19
- class Tool(SqlalchemyBase, OrganizationMixin, UserMixin):
16
+ class Tool(SqlalchemyBase, OrganizationMixin):
20
17
  """Represents an available tool that the LLM can invoke.
21
18
 
22
19
  NOTE: polymorphic inheritance makes more sense here as a TODO. We want a superset of tools
@@ -29,10 +26,7 @@ class Tool(SqlalchemyBase, OrganizationMixin, UserMixin):
29
26
 
30
27
  # Add unique constraint on (name, _organization_id)
31
28
  # An organization should not have multiple tools with the same name
32
- __table_args__ = (
33
- UniqueConstraint("name", "_organization_id", name="uix_name_organization"),
34
- UniqueConstraint("name", "_user_id", name="uix_name_user"),
35
- )
29
+ __table_args__ = (UniqueConstraint("name", "_organization_id", name="uix_name_organization"),)
36
30
 
37
31
  name: Mapped[str] = mapped_column(doc="The display name of the tool.")
38
32
  description: Mapped[Optional[str]] = mapped_column(nullable=True, doc="The description of the tool.")
@@ -48,7 +42,4 @@ class Tool(SqlalchemyBase, OrganizationMixin, UserMixin):
48
42
  # This was an intentional decision by Sarah
49
43
 
50
44
  # relationships
51
- # TODO: Possibly add in user in the future
52
- # This will require some more thought and justification to add this in.
53
- user: Mapped["User"] = relationship("User", back_populates="tools", lazy="selectin")
54
45
  organization: Mapped["Organization"] = relationship("Organization", back_populates="tools", lazy="selectin")
letta/orm/user.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import TYPE_CHECKING, List
1
+ from typing import TYPE_CHECKING
2
2
 
3
3
  from sqlalchemy.orm import Mapped, mapped_column, relationship
4
4
 
@@ -8,7 +8,6 @@ from letta.schemas.user import User as PydanticUser
8
8
 
9
9
  if TYPE_CHECKING:
10
10
  from letta.orm.organization import Organization
11
- from letta.orm.tool import Tool
12
11
 
13
12
 
14
13
  class User(SqlalchemyBase, OrganizationMixin):
@@ -21,7 +20,6 @@ class User(SqlalchemyBase, OrganizationMixin):
21
20
 
22
21
  # relationships
23
22
  organization: Mapped["Organization"] = relationship("Organization", back_populates="users")
24
- tools: Mapped[List["Tool"]] = relationship("Tool", back_populates="user", cascade="all, delete-orphan")
25
23
 
26
24
  # TODO: Add this back later potentially
27
25
  # agents: Mapped[List["Agent"]] = relationship(
letta/providers.py CHANGED
@@ -313,7 +313,7 @@ class GroqProvider(OpenAIProvider):
313
313
  continue
314
314
  configs.append(
315
315
  LLMConfig(
316
- model=model["id"], model_endpoint_type="openai", model_endpoint=self.base_url, context_window=model["context_window"]
316
+ model=model["id"], model_endpoint_type="groq", model_endpoint=self.base_url, context_window=model["context_window"]
317
317
  )
318
318
  )
319
319
  return configs
letta/schemas/tool.py CHANGED
@@ -11,7 +11,6 @@ from letta.functions.schema_generator import generate_schema_from_args_schema
11
11
  from letta.schemas.letta_base import LettaBase
12
12
  from letta.schemas.openai.chat_completions import ToolCall
13
13
  from letta.services.organization_manager import OrganizationManager
14
- from letta.services.user_manager import UserManager
15
14
 
16
15
 
17
16
  class BaseTool(LettaBase):
@@ -35,7 +34,6 @@ class Tool(BaseTool):
35
34
  description: Optional[str] = Field(None, description="The description of the tool.")
36
35
  source_type: Optional[str] = Field(None, description="The type of the source code.")
37
36
  module: Optional[str] = Field(None, description="The module of the function.")
38
- user_id: str = Field(..., description="The unique identifier of the user associated with the tool.")
39
37
  organization_id: str = Field(..., description="The unique identifier of the organization associated with the tool.")
40
38
  name: str = Field(..., description="The name of the function.")
41
39
  tags: List[str] = Field(..., description="Metadata tags.")
@@ -44,6 +42,10 @@ class Tool(BaseTool):
44
42
  source_code: str = Field(..., description="The source code of the function.")
45
43
  json_schema: Dict = Field(default_factory=dict, description="The JSON schema of the function.")
46
44
 
45
+ # 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.")
48
+
47
49
  def to_dict(self):
48
50
  """
49
51
  Convert tool into OpenAI representation.
@@ -58,11 +60,6 @@ class Tool(BaseTool):
58
60
 
59
61
 
60
62
  class ToolCreate(LettaBase):
61
- user_id: str = Field(UserManager.DEFAULT_USER_ID, description="The user that this tool belongs to. Defaults to the default user ID.")
62
- organization_id: str = Field(
63
- OrganizationManager.DEFAULT_ORG_ID,
64
- description="The organization that this tool belongs to. Defaults to the default organization ID.",
65
- )
66
63
  name: Optional[str] = Field(None, description="The name of the function (auto-generated from source_code if not provided).")
67
64
  description: Optional[str] = Field(None, description="The description of the tool.")
68
65
  tags: List[str] = Field([], description="Metadata tags.")
@@ -75,9 +72,7 @@ class ToolCreate(LettaBase):
75
72
  terminal: Optional[bool] = Field(None, description="Whether the tool is a terminal tool (allow requesting heartbeats).")
76
73
 
77
74
  @classmethod
78
- def from_composio(
79
- cls, action: "ActionType", user_id: str = UserManager.DEFAULT_USER_ID, organization_id: str = OrganizationManager.DEFAULT_ORG_ID
80
- ) -> "ToolCreate":
75
+ def from_composio(cls, action: "ActionType", organization_id: str = OrganizationManager.DEFAULT_ORG_ID) -> "ToolCreate":
81
76
  """
82
77
  Class method to create an instance of Letta-compatible Composio Tool.
83
78
  Check https://docs.composio.dev/introduction/intro/overview to look at options for from_composio
@@ -106,8 +101,6 @@ class ToolCreate(LettaBase):
106
101
  json_schema = generate_schema_from_args_schema(composio_tool.args_schema, name=wrapper_func_name, description=description)
107
102
 
108
103
  return cls(
109
- user_id=user_id,
110
- organization_id=organization_id,
111
104
  name=wrapper_func_name,
112
105
  description=description,
113
106
  source_type=source_type,
@@ -121,7 +114,6 @@ class ToolCreate(LettaBase):
121
114
  cls,
122
115
  langchain_tool: "LangChainBaseTool",
123
116
  additional_imports_module_attr_map: dict[str, str] = None,
124
- user_id: str = UserManager.DEFAULT_USER_ID,
125
117
  organization_id: str = OrganizationManager.DEFAULT_ORG_ID,
126
118
  ) -> "ToolCreate":
127
119
  """
@@ -142,8 +134,6 @@ class ToolCreate(LettaBase):
142
134
  json_schema = generate_schema_from_args_schema(langchain_tool.args_schema, name=wrapper_func_name, description=description)
143
135
 
144
136
  return cls(
145
- user_id=user_id,
146
- organization_id=organization_id,
147
137
  name=wrapper_func_name,
148
138
  description=description,
149
139
  source_type=source_type,
@@ -157,7 +147,6 @@ class ToolCreate(LettaBase):
157
147
  cls,
158
148
  crewai_tool: "CrewAIBaseTool",
159
149
  additional_imports_module_attr_map: dict[str, str] = None,
160
- user_id: str = UserManager.DEFAULT_USER_ID,
161
150
  organization_id: str = OrganizationManager.DEFAULT_ORG_ID,
162
151
  ) -> "ToolCreate":
163
152
  """
@@ -176,8 +165,6 @@ class ToolCreate(LettaBase):
176
165
  json_schema = generate_schema_from_args_schema(crewai_tool.args_schema, name=wrapper_func_name, description=description)
177
166
 
178
167
  return cls(
179
- user_id=user_id,
180
- organization_id=organization_id,
181
168
  name=wrapper_func_name,
182
169
  description=description,
183
170
  source_type=source_type,
@@ -84,7 +84,7 @@ def create_agent(
84
84
  blocks = agent.memory.get_blocks()
85
85
  agent.memory = BasicBlockMemory(blocks=blocks)
86
86
 
87
- return server.create_agent(agent, user_id=actor.id)
87
+ return server.create_agent(agent, actor=actor)
88
88
 
89
89
 
90
90
  @router.patch("/{agent_id}", response_model=AgentState, operation_id="update_agent")
@@ -96,9 +96,7 @@ def update_agent(
96
96
  ):
97
97
  """Update an exsiting agent"""
98
98
  actor = server.get_user_or_default(user_id=user_id)
99
-
100
- update_agent.id = agent_id
101
- return server.update_agent(update_agent, user_id=actor.id)
99
+ return server.update_agent(update_agent, actor=actor)
102
100
 
103
101
 
104
102
  @router.get("/{agent_id}/tools", response_model=List[Tool], operation_id="get_tools_from_agent")
@@ -26,11 +26,13 @@ def delete_tool(
26
26
  def get_tool(
27
27
  tool_id: str,
28
28
  server: SyncServer = Depends(get_letta_server),
29
+ user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
29
30
  ):
30
31
  """
31
32
  Get a tool by ID
32
33
  """
33
- tool = server.tool_manager.get_tool_by_id(tool_id=tool_id)
34
+ actor = server.get_user_or_default(user_id=user_id)
35
+ tool = server.tool_manager.get_tool_by_id(tool_id=tool_id, actor=actor)
34
36
  if tool is None:
35
37
  # return 404 error
36
38
  raise HTTPException(status_code=404, detail=f"Tool with id {tool_id} not found.")
@@ -49,7 +51,7 @@ def get_tool_id(
49
51
  actor = server.get_user_or_default(user_id=user_id)
50
52
 
51
53
  try:
52
- tool = server.tool_manager.get_tool_by_name_and_org_id(tool_name=tool_name, organization_id=actor.organization_id)
54
+ tool = server.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
53
55
  return tool.id
54
56
  except NoResultFound:
55
57
  raise HTTPException(status_code=404, detail=f"Tool with name {tool_name} and organization id {actor.organization_id} not found.")
@@ -67,7 +69,7 @@ def list_tools(
67
69
  """
68
70
  try:
69
71
  actor = server.get_user_or_default(user_id=user_id)
70
- return server.tool_manager.list_tools_for_org(organization_id=actor.organization_id, cursor=cursor, limit=limit)
72
+ return server.tool_manager.list_tools(actor=actor, cursor=cursor, limit=limit)
71
73
  except Exception as e:
72
74
  # Log or print the full exception here for debugging
73
75
  print(f"Error occurred: {e}")
@@ -85,13 +87,9 @@ def create_tool(
85
87
  """
86
88
  # Derive user and org id from actor
87
89
  actor = server.get_user_or_default(user_id=user_id)
88
- request.organization_id = actor.organization_id
89
- request.user_id = actor.id
90
90
 
91
91
  # Send request to create the tool
92
- return server.tool_manager.create_or_update_tool(
93
- tool_create=request,
94
- )
92
+ return server.tool_manager.create_or_update_tool(tool_create=request, actor=actor)
95
93
 
96
94
 
97
95
  @router.patch("/{tool_id}", response_model=Tool, operation_id="update_tool")
@@ -104,4 +102,5 @@ def update_tool(
104
102
  """
105
103
  Update an existing tool
106
104
  """
107
- return server.tool_manager.update_tool_by_id(tool_id, request)
105
+ actor = server.get_user_or_default(user_id=user_id)
106
+ return server.tool_manager.update_tool_by_id(tool_id, actor.id, request)
letta/server/server.py CHANGED
@@ -37,11 +37,13 @@ from letta.log import get_logger
37
37
  from letta.memory import get_memory_functions
38
38
  from letta.metadata import Base, MetadataStore
39
39
  from letta.o1_agent import O1Agent
40
+ from letta.orm.errors import NoResultFound
40
41
  from letta.prompts import gpt_system
41
42
  from letta.providers import (
42
43
  AnthropicProvider,
43
44
  AzureProvider,
44
45
  GoogleAIProvider,
46
+ GroqProvider,
45
47
  LettaProvider,
46
48
  OllamaProvider,
47
49
  OpenAIProvider,
@@ -73,6 +75,7 @@ from letta.schemas.memory import (
73
75
  RecallMemorySummary,
74
76
  )
75
77
  from letta.schemas.message import Message, MessageCreate, MessageRole, UpdateMessage
78
+ from letta.schemas.organization import Organization
76
79
  from letta.schemas.passage import Passage
77
80
  from letta.schemas.source import Source, SourceCreate, SourceUpdate
78
81
  from letta.schemas.tool import Tool, ToolCreate
@@ -251,12 +254,12 @@ class SyncServer(Server):
251
254
  self.default_org = self.organization_manager.create_default_organization()
252
255
  self.default_user = self.user_manager.create_default_user()
253
256
  self.add_default_blocks(self.default_user.id)
254
- self.tool_manager.add_default_tools(module_name="base", user_id=self.default_user.id, org_id=self.default_org.id)
257
+ self.tool_manager.add_default_tools(module_name="base", actor=self.default_user)
255
258
 
256
259
  # If there is a default org/user
257
260
  # This logic may have to change in the future
258
261
  if settings.load_default_external_tools:
259
- self.add_default_external_tools(user_id=self.default_user.id, org_id=self.default_org.id)
262
+ self.add_default_external_tools(actor=self.default_user)
260
263
 
261
264
  # collect providers (always has Letta as a default)
262
265
  self._enabled_providers: List[Provider] = [LettaProvider()]
@@ -296,6 +299,8 @@ class SyncServer(Server):
296
299
  api_version=model_settings.azure_api_version,
297
300
  )
298
301
  )
302
+ if model_settings.groq_api_key:
303
+ self._enabled_providers.append(GroqProvider(api_key=model_settings.groq_api_key))
299
304
  if model_settings.vllm_api_base:
300
305
  # vLLM exposes both a /chat/completions and a /completions endpoint
301
306
  self._enabled_providers.append(
@@ -345,10 +350,10 @@ class SyncServer(Server):
345
350
  }
346
351
  )
347
352
 
348
- def _load_agent(self, user_id: str, agent_id: str, interface: Union[AgentInterface, None] = None) -> Agent:
353
+ def _load_agent(self, agent_id: str, actor: User, interface: Union[AgentInterface, None] = None) -> Agent:
349
354
  """Loads a saved agent into memory (if it doesn't exist, throw an error)"""
350
- assert isinstance(user_id, str), user_id
351
355
  assert isinstance(agent_id, str), agent_id
356
+ user_id = actor.id
352
357
 
353
358
  # If an interface isn't specified, use the default
354
359
  if interface is None:
@@ -365,7 +370,7 @@ class SyncServer(Server):
365
370
  logger.debug(f"Creating an agent object")
366
371
  tool_objs = []
367
372
  for name in agent_state.tools:
368
- tool_obj = self.tool_manager.get_tool_by_name_and_user_id(tool_name=name, user_id=user_id)
373
+ tool_obj = self.tool_manager.get_tool_by_name(tool_name=name, actor=actor)
369
374
  if not tool_obj:
370
375
  logger.exception(f"Tool {name} does not exist for user {user_id}")
371
376
  raise ValueError(f"Tool {name} does not exist for user {user_id}")
@@ -396,13 +401,14 @@ class SyncServer(Server):
396
401
  if not agent_state:
397
402
  raise ValueError(f"Agent does not exist")
398
403
  user_id = agent_state.user_id
404
+ actor = self.user_manager.get_user_by_id(user_id)
399
405
 
400
406
  logger.debug(f"Checking for agent user_id={user_id} agent_id={agent_id}")
401
407
  # TODO: consider disabling loading cached agents due to potential concurrency issues
402
408
  letta_agent = self._get_agent(user_id=user_id, agent_id=agent_id)
403
409
  if not letta_agent:
404
410
  logger.debug(f"Agent not loaded, loading agent user_id={user_id} agent_id={agent_id}")
405
- letta_agent = self._load_agent(user_id=user_id, agent_id=agent_id)
411
+ letta_agent = self._load_agent(agent_id=agent_id, actor=actor)
406
412
  return letta_agent
407
413
 
408
414
  def _step(
@@ -759,11 +765,12 @@ class SyncServer(Server):
759
765
  def create_agent(
760
766
  self,
761
767
  request: CreateAgent,
762
- user_id: str,
768
+ actor: User,
763
769
  # interface
764
770
  interface: Union[AgentInterface, None] = None,
765
771
  ) -> AgentState:
766
772
  """Create a new agent using a config"""
773
+ user_id = actor.id
767
774
  if self.user_manager.get_user_by_id(user_id=user_id) is None:
768
775
  raise ValueError(f"User user_id={user_id} does not exist")
769
776
 
@@ -801,7 +808,7 @@ class SyncServer(Server):
801
808
  tool_objs = []
802
809
  if request.tools:
803
810
  for tool_name in request.tools:
804
- tool_obj = self.tool_manager.get_tool_by_name_and_user_id(tool_name=tool_name, user_id=user_id)
811
+ tool_obj = self.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
805
812
  tool_objs.append(tool_obj)
806
813
 
807
814
  assert request.memory is not None
@@ -822,9 +829,8 @@ class SyncServer(Server):
822
829
  source_type=source_type,
823
830
  tags=tags,
824
831
  json_schema=json_schema,
825
- user_id=user_id,
826
- organization_id=user.organization_id,
827
- )
832
+ ),
833
+ actor=actor,
828
834
  )
829
835
  tool_objs.append(tool)
830
836
  if not request.tools:
@@ -887,11 +893,14 @@ class SyncServer(Server):
887
893
  def update_agent(
888
894
  self,
889
895
  request: UpdateAgentState,
890
- user_id: str,
896
+ actor: User,
891
897
  ):
892
898
  """Update the agents core memory block, return the new state"""
893
- if self.user_manager.get_user_by_id(user_id=user_id) is None:
894
- raise ValueError(f"User user_id={user_id} does not exist")
899
+ try:
900
+ self.user_manager.get_user_by_id(user_id=actor.id)
901
+ except Exception:
902
+ raise ValueError(f"User user_id={actor.id} does not exist")
903
+
895
904
  if self.ms.get_agent(agent_id=request.id) is None:
896
905
  raise ValueError(f"Agent agent_id={request.id} does not exist")
897
906
 
@@ -902,7 +911,7 @@ class SyncServer(Server):
902
911
  if request.memory:
903
912
  assert isinstance(request.memory, Memory), type(request.memory)
904
913
  new_memory_contents = request.memory.to_flat_dict()
905
- _ = self.update_agent_core_memory(user_id=user_id, agent_id=request.id, new_memory_contents=new_memory_contents)
914
+ _ = self.update_agent_core_memory(user_id=actor.id, agent_id=request.id, new_memory_contents=new_memory_contents)
906
915
 
907
916
  # update the system prompt
908
917
  if request.system:
@@ -922,7 +931,7 @@ class SyncServer(Server):
922
931
  # (1) get tools + make sure they exist
923
932
  tool_objs = []
924
933
  for tool_name in request.tools:
925
- tool_obj = self.tool_manager.get_tool_by_name_and_user_id(tool_name=tool_name, user_id=user_id)
934
+ tool_obj = self.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
926
935
  assert tool_obj, f"Tool {tool_name} does not exist"
927
936
  tool_objs.append(tool_obj)
928
937
 
@@ -968,8 +977,11 @@ class SyncServer(Server):
968
977
  user_id: str,
969
978
  ):
970
979
  """Add tools from an existing agent"""
971
- if self.user_manager.get_user_by_id(user_id=user_id) is None:
980
+ try:
981
+ user = self.user_manager.get_user_by_id(user_id=user_id)
982
+ except NoResultFound:
972
983
  raise ValueError(f"User user_id={user_id} does not exist")
984
+
973
985
  if self.ms.get_agent(agent_id=agent_id) is None:
974
986
  raise ValueError(f"Agent agent_id={agent_id} does not exist")
975
987
 
@@ -978,12 +990,12 @@ class SyncServer(Server):
978
990
 
979
991
  # Get all the tool objects from the request
980
992
  tool_objs = []
981
- tool_obj = self.tool_manager.get_tool_by_id(tool_id=tool_id)
993
+ tool_obj = self.tool_manager.get_tool_by_id(tool_id=tool_id, actor=user)
982
994
  assert tool_obj, f"Tool with id={tool_id} does not exist"
983
995
  tool_objs.append(tool_obj)
984
996
 
985
997
  for tool in letta_agent.tools:
986
- tool_obj = self.tool_manager.get_tool_by_id(tool_id=tool.id)
998
+ tool_obj = self.tool_manager.get_tool_by_id(tool_id=tool.id, actor=user)
987
999
  assert tool_obj, f"Tool with id={tool.id} does not exist"
988
1000
 
989
1001
  # If it's not the already added tool
@@ -1007,8 +1019,11 @@ class SyncServer(Server):
1007
1019
  user_id: str,
1008
1020
  ):
1009
1021
  """Remove tools from an existing agent"""
1010
- if self.user_manager.get_user_by_id(user_id=user_id) is None:
1022
+ try:
1023
+ user = self.user_manager.get_user_by_id(user_id=user_id)
1024
+ except NoResultFound:
1011
1025
  raise ValueError(f"User user_id={user_id} does not exist")
1026
+
1012
1027
  if self.ms.get_agent(agent_id=agent_id) is None:
1013
1028
  raise ValueError(f"Agent agent_id={agent_id} does not exist")
1014
1029
 
@@ -1018,7 +1033,7 @@ class SyncServer(Server):
1018
1033
  # Get all the tool_objs
1019
1034
  tool_objs = []
1020
1035
  for tool in letta_agent.tools:
1021
- tool_obj = self.tool_manager.get_tool_by_id(tool_id=tool.id)
1036
+ tool_obj = self.tool_manager.get_tool_by_id(tool_id=tool.id, actor=user)
1022
1037
  assert tool_obj, f"Tool with id={tool.id} does not exist"
1023
1038
 
1024
1039
  # If it's not the tool we want to remove
@@ -1733,7 +1748,7 @@ class SyncServer(Server):
1733
1748
 
1734
1749
  return sources_with_metadata
1735
1750
 
1736
- def add_default_external_tools(self, user_id: str, org_id: str) -> bool:
1751
+ def add_default_external_tools(self, actor: User) -> bool:
1737
1752
  """Add default langchain tools. Return true if successful, false otherwise."""
1738
1753
  success = True
1739
1754
  tool_creates = ToolCreate.load_default_langchain_tools() + ToolCreate.load_default_crewai_tools()
@@ -1741,7 +1756,7 @@ class SyncServer(Server):
1741
1756
  tool_creates += ToolCreate.load_default_composio_tools()
1742
1757
  for tool_create in tool_creates:
1743
1758
  try:
1744
- self.tool_manager.create_or_update_tool(tool_create)
1759
+ self.tool_manager.create_or_update_tool(tool_create, actor=actor)
1745
1760
  except Exception as e:
1746
1761
  warnings.warn(f"An error occurred while creating tool {tool_create}: {e}")
1747
1762
  warnings.warn(traceback.format_exc())
@@ -1843,6 +1858,16 @@ class SyncServer(Server):
1843
1858
  except ValueError:
1844
1859
  raise HTTPException(status_code=404, detail=f"User with id {user_id} not found")
1845
1860
 
1861
+ def get_organization_or_default(self, org_id: Optional[str]) -> Organization:
1862
+ """Get the organization object for org_id if it exists, otherwise return the default organization object"""
1863
+ if org_id is None:
1864
+ org_id = self.organization_manager.DEFAULT_ORG_ID
1865
+
1866
+ try:
1867
+ return self.organization_manager.get_organization_by_id(org_id=org_id)
1868
+ except ValueError:
1869
+ raise HTTPException(status_code=404, detail=f"Organization with id {org_id} not found")
1870
+
1846
1871
  def list_llm_models(self) -> List[LLMConfig]:
1847
1872
  """List available models"""
1848
1873
 
letta/server/startup.sh CHANGED
@@ -2,7 +2,7 @@
2
2
  echo "Starting MEMGPT server..."
3
3
  if [ "$MEMGPT_ENVIRONMENT" = "DEVELOPMENT" ] ; then
4
4
  echo "Starting in development mode!"
5
- uvicorn letta.server.rest_api.app:app --reload --reload-dir /letta --host 0.0.0.0 --port 8083
5
+ uvicorn letta.server.rest_api.app:app --reload --reload-dir /letta --host 0.0.0.0 --port 8283
6
6
  else
7
- uvicorn letta.server.rest_api.app:app --host 0.0.0.0 --port 8083
7
+ uvicorn letta.server.rest_api.app:app --host 0.0.0.0 --port 8283
8
8
  fi
@@ -9,9 +9,9 @@ from letta.functions.functions import derive_openai_json_schema, load_function_s
9
9
  from letta.orm.errors import NoResultFound
10
10
  from letta.orm.organization import Organization as OrganizationModel
11
11
  from letta.orm.tool import Tool as ToolModel
12
- from letta.orm.user import User as UserModel
13
12
  from letta.schemas.tool import Tool as PydanticTool
14
13
  from letta.schemas.tool import ToolCreate, ToolUpdate
14
+ from letta.schemas.user import User as PydanticUser
15
15
  from letta.utils import enforce_types
16
16
 
17
17
 
@@ -25,7 +25,7 @@ class ToolManager:
25
25
  self.session_maker = db_context
26
26
 
27
27
  @enforce_types
28
- def create_or_update_tool(self, tool_create: ToolCreate) -> PydanticTool:
28
+ def create_or_update_tool(self, tool_create: ToolCreate, actor: PydanticUser) -> PydanticTool:
29
29
  """Create a new tool based on the ToolCreate schema."""
30
30
  # Derive json_schema
31
31
  derived_json_schema = tool_create.json_schema or derive_openai_json_schema(tool_create)
@@ -34,105 +34,72 @@ class ToolManager:
34
34
  try:
35
35
  # NOTE: We use the organization id here
36
36
  # This is important, because even if it's a different user, adding the same tool to the org should not happen
37
- tool = self.get_tool_by_name_and_org_id(tool_name=derived_name, organization_id=tool_create.organization_id)
37
+ tool = self.get_tool_by_name(tool_name=derived_name, actor=actor)
38
38
  # Put to dict and remove fields that should not be reset
39
- update_data = tool_create.model_dump(exclude={"user_id", "organization_id", "module", "terminal"}, exclude_unset=True)
39
+ update_data = tool_create.model_dump(exclude={"module", "terminal"}, exclude_unset=True)
40
40
  # Remove redundant update fields
41
41
  update_data = {key: value for key, value in update_data.items() if getattr(tool, key) != value}
42
42
 
43
43
  # If there's anything to update
44
44
  if update_data:
45
- self.update_tool_by_id(tool.id, ToolUpdate(**update_data))
45
+ self.update_tool_by_id(tool.id, ToolUpdate(**update_data), actor)
46
46
  else:
47
47
  warnings.warn(
48
- f"`create_or_update_tool` was called with user_id={tool_create.user_id}, organization_id={tool_create.organization_id}, name={tool_create.name}, but found existing tool with nothing to update."
48
+ 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."
49
49
  )
50
50
  except NoResultFound:
51
51
  tool_create.json_schema = derived_json_schema
52
52
  tool_create.name = derived_name
53
- tool = self.create_tool(tool_create)
53
+ tool = self.create_tool(tool_create, actor=actor)
54
54
 
55
55
  return tool
56
56
 
57
57
  @enforce_types
58
- def create_tool(self, tool_create: ToolCreate) -> PydanticTool:
58
+ def create_tool(self, tool_create: ToolCreate, actor: PydanticUser) -> PydanticTool:
59
59
  """Create a new tool based on the ToolCreate schema."""
60
60
  # Create the tool
61
61
  with self.session_maker() as session:
62
- # Include all fields except 'terminal' (which is not part of ToolModel) at the moment
62
+ # Include all fields except `terminal` (which is not part of ToolModel) at the moment
63
63
  create_data = tool_create.model_dump(exclude={"terminal"})
64
- tool = ToolModel(**create_data) # Unpack everything directly into ToolModel
65
- tool.create(session)
64
+ tool = ToolModel(**create_data, organization_id=actor.organization_id) # Unpack everything directly into ToolModel
65
+ tool.create(session, actor=actor)
66
66
 
67
67
  return tool.to_pydantic()
68
68
 
69
69
  @enforce_types
70
- def get_tool_by_id(self, tool_id: str) -> PydanticTool:
70
+ def get_tool_by_id(self, tool_id: str, actor: PydanticUser) -> PydanticTool:
71
71
  """Fetch a tool by its ID."""
72
72
  with self.session_maker() as session:
73
- try:
74
- # Retrieve tool by id using the Tool model's read method
75
- tool = ToolModel.read(db_session=session, identifier=tool_id)
76
- # Convert the SQLAlchemy Tool object to PydanticTool
77
- return tool.to_pydantic()
78
- except NoResultFound:
79
- raise ValueError(f"Tool with id {tool_id} not found.")
80
-
81
- @enforce_types
82
- def get_tool_by_name_and_user_id(self, tool_name: str, user_id: str) -> PydanticTool:
83
- """Retrieve a tool by its name and organization_id."""
84
- with self.session_maker() as session:
85
- # Use the list method to apply filters
86
- results = ToolModel.list(db_session=session, name=tool_name, _user_id=UserModel.get_uid_from_identifier(user_id))
87
-
88
- # Ensure only one result is returned (since there is a unique constraint)
89
- if not results:
90
- raise NoResultFound(f"Tool with name {tool_name} and user_id {user_id} not found.")
91
-
92
- if len(results) > 1:
93
- raise RuntimeError(
94
- f"Multiple tools with name {tool_name} and user_id {user_id} were found. This is a serious error, and means that our table does not have uniqueness constraints properly set up. Please reach out to the letta development team if you see this error."
95
- )
96
-
97
- # Return the single result
98
- return results[0]
73
+ # Retrieve tool by id using the Tool model's read method
74
+ tool = ToolModel.read(db_session=session, identifier=tool_id, actor=actor)
75
+ # Convert the SQLAlchemy Tool object to PydanticTool
76
+ return tool.to_pydantic()
99
77
 
100
78
  @enforce_types
101
- def get_tool_by_name_and_org_id(self, tool_name: str, organization_id: str) -> PydanticTool:
102
- """Retrieve a tool by its name and organization_id."""
79
+ def get_tool_by_name(self, tool_name: str, actor: PydanticUser):
80
+ """Retrieve a tool by its name and a user. We derive the organization from the user, and retrieve that tool."""
103
81
  with self.session_maker() as session:
104
- # Use the list method to apply filters
105
- results = ToolModel.list(
106
- db_session=session, name=tool_name, _organization_id=OrganizationModel.get_uid_from_identifier(organization_id)
107
- )
108
-
109
- # Ensure only one result is returned (since there is a unique constraint)
110
- if not results:
111
- raise NoResultFound(f"Tool with name {tool_name} and organization_id {organization_id} not found.")
112
-
113
- if len(results) > 1:
114
- raise RuntimeError(
115
- f"Multiple tools with name {tool_name} and organization_id {organization_id} were found. This is a serious error, and means that our table does not have uniqueness constraints properly set up. Please reach out to the letta development team if you see this error."
116
- )
117
-
118
- # Return the single result
119
- return results[0]
82
+ tool = ToolModel.read(db_session=session, name=tool_name, actor=actor)
83
+ return tool.to_pydantic()
120
84
 
121
85
  @enforce_types
122
- def list_tools_for_org(self, organization_id: str, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticTool]:
86
+ def list_tools(self, actor: PydanticUser, cursor: Optional[str] = None, limit: Optional[int] = 50) -> List[PydanticTool]:
123
87
  """List all tools with optional pagination using cursor and limit."""
124
88
  with self.session_maker() as session:
125
89
  tools = ToolModel.list(
126
- db_session=session, cursor=cursor, limit=limit, _organization_id=OrganizationModel.get_uid_from_identifier(organization_id)
90
+ db_session=session,
91
+ cursor=cursor,
92
+ limit=limit,
93
+ _organization_id=OrganizationModel.get_uid_from_identifier(actor.organization_id),
127
94
  )
128
95
  return [tool.to_pydantic() for tool in tools]
129
96
 
130
97
  @enforce_types
131
- def update_tool_by_id(self, tool_id: str, tool_update: ToolUpdate) -> None:
98
+ def update_tool_by_id(self, tool_id: str, tool_update: ToolUpdate, actor: PydanticUser) -> None:
132
99
  """Update a tool by its ID with the given ToolUpdate object."""
133
100
  with self.session_maker() as session:
134
101
  # Fetch the tool by ID
135
- tool = ToolModel.read(db_session=session, identifier=tool_id)
102
+ tool = ToolModel.read(db_session=session, identifier=tool_id, actor=actor)
136
103
 
137
104
  # Update tool attributes with only the fields that were explicitly set
138
105
  update_data = tool_update.model_dump(exclude_unset=True, exclude_none=True)
@@ -140,20 +107,20 @@ class ToolManager:
140
107
  setattr(tool, key, value)
141
108
 
142
109
  # Save the updated tool to the database
143
- tool.update(db_session=session)
110
+ tool.update(db_session=session, actor=actor)
144
111
 
145
112
  @enforce_types
146
- def delete_tool_by_id(self, tool_id: str) -> None:
113
+ def delete_tool_by_id(self, tool_id: str, actor: PydanticUser) -> None:
147
114
  """Delete a tool by its ID."""
148
115
  with self.session_maker() as session:
149
116
  try:
150
117
  tool = ToolModel.read(db_session=session, identifier=tool_id)
151
- tool.delete(db_session=session)
118
+ tool.delete(db_session=session, actor=actor)
152
119
  except NoResultFound:
153
120
  raise ValueError(f"Tool with id {tool_id} not found.")
154
121
 
155
122
  @enforce_types
156
- def add_default_tools(self, user_id: str, org_id: str, module_name="base"):
123
+ def add_default_tools(self, actor: PydanticUser, module_name="base"):
157
124
  """Add default tools in {module_name}.py"""
158
125
  full_module_name = f"letta.functions.function_sets.{module_name}"
159
126
  try:
@@ -187,7 +154,6 @@ class ToolManager:
187
154
  module=schema["module"],
188
155
  source_code=source_code,
189
156
  json_schema=schema["json_schema"],
190
- organization_id=org_id,
191
- user_id=user_id,
192
157
  ),
158
+ actor=actor,
193
159
  )
@@ -85,11 +85,8 @@ class UserManager:
85
85
  def get_user_by_id(self, user_id: str) -> PydanticUser:
86
86
  """Fetch a user by ID."""
87
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.")
88
+ user = UserModel.read(db_session=session, identifier=user_id)
89
+ return user.to_pydantic()
93
90
 
94
91
  @enforce_types
95
92
  def get_default_user(self) -> PydanticUser:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: letta-nightly
3
- Version: 0.5.1.dev20241028104150
3
+ Version: 0.5.1.dev20241029104141
4
4
  Summary: Create LLM agents with long-term memory and custom tools
5
5
  License: Apache License
6
6
  Author: Letta Team
@@ -9,11 +9,11 @@ letta/agent_store/qdrant.py,sha256=6_33V-FEDpT9LG5zmr6-3y9slw1YFLswxpahiyMkvHA,7
9
9
  letta/agent_store/storage.py,sha256=4gKvMRYBGm9cwyaDOzljxDKgqr4MxGXcC4yGhAdKcAA,6693
10
10
  letta/benchmark/benchmark.py,sha256=ebvnwfp3yezaXOQyGXkYCDYpsmre-b9hvNtnyx4xkG0,3701
11
11
  letta/benchmark/constants.py,sha256=aXc5gdpMGJT327VuxsT5FngbCK2J41PQYeICBO7g_RE,536
12
- letta/cli/cli.py,sha256=145g2WAApbeEfOjVgxBzuv6BbbDr-cZSfT_ekfj945I,16162
12
+ letta/cli/cli.py,sha256=i6wFBaX8-WwZ6nl_T0FFptfozlnYEBM7RcShwBl-qfw,16106
13
13
  letta/cli/cli_config.py,sha256=ynsezKawQ0l95rymlv-AJ3QjbqiyaXZyuzsDfBYwMwg,8537
14
14
  letta/cli/cli_load.py,sha256=x4L8s15GwIW13xrhKYFWHo_y-IVGtoPDHWWKcHDRP10,4587
15
15
  letta/client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
- letta/client/client.py,sha256=lWd5Fv9UkkEn5oDPHjtbGhKNowwXA4sbhS3_OuCBStk,93707
16
+ letta/client/client.py,sha256=3tZfSKez9uCCpcItjtUJ0CMpRwoTSA-CGvX7I4pNBG4,92340
17
17
  letta/client/streaming.py,sha256=bfWlUu7z7EoPfKxBqIarYxGKyrL7Pj79BlliToqcCgI,4592
18
18
  letta/client/utils.py,sha256=OJlAKWrldc4I6M1WpcTWNtPJ4wfxlzlZqWLfCozkFtI,2872
19
19
  letta/config.py,sha256=eK-ip06ELHNYriInkgfidDvJxQ2tD1u49I-VLXB87nE,18929
@@ -40,7 +40,7 @@ letta/llm_api/azure_openai_constants.py,sha256=oXtKrgBFHf744gyt5l1thILXgyi8NDNUr
40
40
  letta/llm_api/cohere.py,sha256=vDRd-SUGp1t_JUIdwC3RkIhwMl0OY7n-tAU9uPORYkY,14826
41
41
  letta/llm_api/google_ai.py,sha256=3xZ074nSOCC22c15yerA5ngWzh0ex4wxeI-6faNbHPE,17708
42
42
  letta/llm_api/helpers.py,sha256=sGCmNA1U_7-AhRFgvT668jdp_xyzSliKQYbTvRR6O7c,9812
43
- letta/llm_api/llm_api_tools.py,sha256=GEBO7Dlt7xtAQud1sVsigKZKPpLOZOt2IWL8LwcNV4o,14869
43
+ letta/llm_api/llm_api_tools.py,sha256=RRczEZ3h_fxIJLXbw1UT5YZwdsr1gZD6HRPVA9qopmE,14787
44
44
  letta/llm_api/mistral.py,sha256=fHdfD9ug-rQIk2qn8tRKay1U6w9maF11ryhKi91FfXM,1593
45
45
  letta/llm_api/openai.py,sha256=0hwAZhpjrmubYy549hvjzhw6zK_aBhSVuIQN0OsTq-w,23705
46
46
  letta/local_llm/README.md,sha256=hFJyw5B0TU2jrh9nb0zGZMgdH-Ei1dSRfhvPQG_NSoU,168
@@ -89,14 +89,14 @@ letta/openai_backcompat/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
89
89
  letta/openai_backcompat/openai_object.py,sha256=Y1ZS1sATP60qxJiOsjOP3NbwSzuzvkNAvb3DeuhM5Uk,13490
90
90
  letta/orm/__all__.py,sha256=2gh2MZTkA3Hw67VWVKK3JIStJOqTeLdpCvYSVYNeEDA,692
91
91
  letta/orm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
- letta/orm/base.py,sha256=9k7mwKDApJNpbrk6oXosQ7amlpeYhDzArdYTZ6BhfpQ,2405
92
+ letta/orm/base.py,sha256=uK56wXg4Hv5jGuBsThSgmlFdl0e2u4VGiTX4RiRqTK4,2671
93
93
  letta/orm/enums.py,sha256=KfHcFt_fR6GUmSlmfsa-TetvmuRxGESNve8MStRYW64,145
94
94
  letta/orm/errors.py,sha256=somsGtotFlb3SDM6tKdZ5TDGwEEP3ppx47ICAvNMnkg,225
95
95
  letta/orm/mixins.py,sha256=hZJFYnmSGklryuAOypvVhMgXyU7pgatGEun-p-2f_Fc,2457
96
96
  letta/orm/organization.py,sha256=ccE0ShFWqWPdtXiCRkZCSeIERbtwU-a7AF99N3S6IRM,1468
97
- letta/orm/sqlalchemy_base.py,sha256=w3x8ShZhjQY59XjDj2nWiELSi7rJDLzTAnGUBKA1_c0,8880
98
- letta/orm/tool.py,sha256=EB8d9Q1n-ktM6hJ0PP7xK7bL-ankrYTS_vdFfTfS7hY,2541
99
- letta/orm/user.py,sha256=kaiqduMhHYcTLPWyzWyNTysNJijkReIhVtgIJ3qPfSY,1286
97
+ letta/orm/sqlalchemy_base.py,sha256=LuV-or0T8kdg2M0H7Y8KEIcaeKD1DkARzA-LgAGR4qU,7721
98
+ letta/orm/tool.py,sha256=mVOgN2TdszQ5SM6cHqY9Rl6405ccB7mXrq_DHisA1WU,2179
99
+ letta/orm/user.py,sha256=69FU1rSXhlRJMaAUPYebxgFPn8UCQRU8TYUkjXLrALQ,1136
100
100
  letta/persistence_manager.py,sha256=LlLgEDpSafCPAiyKmuq0NvVAnfBkZo6TWbGIKYQjQBs,5200
101
101
  letta/personas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
102
102
  letta/personas/examples/anna_pa.txt,sha256=zgiNdSNhy1HQy58cF_6RFPzcg2i37F9v38YuL1CW40A,1849
@@ -120,7 +120,7 @@ letta/prompts/system/memgpt_gpt35_extralong.txt,sha256=FheNhYoIzNz6qnJKhVquZVSMj
120
120
  letta/prompts/system/memgpt_intuitive_knowledge.txt,sha256=sA7c3urYqREVnSBI81nTGImXAekqC0Fxc7RojFqud1g,2966
121
121
  letta/prompts/system/memgpt_modified_chat.txt,sha256=HOaPVurEftD8KsuwsclDgE2afIfklMjxhuSO96q1-6I,4656
122
122
  letta/prompts/system/memgpt_modified_o1.txt,sha256=AxxYVjYLZwpZ6yfifh1SuPtwlJGWTcTVzw53QbkN-Ao,5492
123
- letta/providers.py,sha256=tGnji2OlZSo5fgRaLiFaopqiyhKGOt5akngSjjM5RSI,19637
123
+ letta/providers.py,sha256=nWvTvVsZH1EE2aHAwihJvCIDJpgfeWAOUE_YK1x5Zj4,19635
124
124
  letta/pytest.ini,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
125
125
  letta/schemas/agent.py,sha256=e69lAKJQYtx92w8tM9sdLdv1hDqZ_0V_qiUiQyI-uks,7138
126
126
  letta/schemas/api_key.py,sha256=u07yzzMn-hBAHZIIKbWY16KsgiFjSNR8lAghpMUo3_4,682
@@ -145,7 +145,7 @@ letta/schemas/openai/openai.py,sha256=Hilo5BiLAGabzxCwnwfzK5QrWqwYD8epaEKFa4Pwnd
145
145
  letta/schemas/organization.py,sha256=0MOUbaiDGTXkzTjaALLI2wj4E1bL5V_0jjI6jEgFKlA,635
146
146
  letta/schemas/passage.py,sha256=eYQMxD_XjHAi72jmqcGBU4wM4VZtSU0XK8uhQxxN3Ug,3563
147
147
  letta/schemas/source.py,sha256=hB4Ai6Nj8dFdbxv5_Qaf4uN_cmdGmnzgc-4QnHXcV3o,2562
148
- letta/schemas/tool.py,sha256=shkyJ-qM5QtyrfG15yKFt8qujZfeW1wfzz6n9yVS4Y8,10259
148
+ letta/schemas/tool.py,sha256=itC9jIwrCPeE-w70CbkaJ7l8ueA5C9Heq_bGNmKHUcc,9598
149
149
  letta/schemas/usage.py,sha256=lvn1ooHwLEdv6gwQpw5PBUbcwn_gwdT6HA-fCiix6sY,817
150
150
  letta/schemas/user.py,sha256=toiA53UKB1hpH6k2xHyQNKVeDUoSB3RmLfxSwmKPAz4,1523
151
151
  letta/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -169,19 +169,19 @@ letta/server/rest_api/routers/openai/assistants/threads.py,sha256=WXVGBaBvSNPB7Z
169
169
  letta/server/rest_api/routers/openai/chat_completions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
170
170
  letta/server/rest_api/routers/openai/chat_completions/chat_completions.py,sha256=-uye6cm4SnoQGwxhr1N1FrSXOlnO2Hvbfj6k8JSc45k,4918
171
171
  letta/server/rest_api/routers/v1/__init__.py,sha256=sqlVZa-u9DJwdRsp0_8YUGrac9DHguIB4wETlEDRylA,666
172
- letta/server/rest_api/routers/v1/agents.py,sha256=BY4rQOcwsi_WiWh6DwrO8Vz6Nu2JgMBKSHxiBqlaaYY,25397
172
+ letta/server/rest_api/routers/v1/agents.py,sha256=EoUWyhiWYYxhtwZUg9agyDeCpixLqzsP9YXIk_zX7MM,25355
173
173
  letta/server/rest_api/routers/v1/blocks.py,sha256=0WekE_yBD2U3jYgPxI0DCFjACWavCAlvm_Ybw5SZBnw,2583
174
174
  letta/server/rest_api/routers/v1/health.py,sha256=pKCuVESlVOhGIb4VC4K-H82eZqfghmT6kvj2iOkkKuc,401
175
175
  letta/server/rest_api/routers/v1/jobs.py,sha256=a-j0v-5A0un0pVCOHpfeWnzpOWkVDQO6ti42k_qAlZY,2272
176
176
  letta/server/rest_api/routers/v1/llms.py,sha256=TcyvSx6MEM3je5F4DysL7ligmssL_pFlJaaO4uL95VY,877
177
177
  letta/server/rest_api/routers/v1/organizations.py,sha256=3XlHPUc6ZdMOVZNdarwDM8ZxYisoHunSZQ0ozzX5orU,2037
178
178
  letta/server/rest_api/routers/v1/sources.py,sha256=eY_pk9jRL2Y9yIZdsTjH6EuKsfH1neaTU15MKNL0dvw,8749
179
- letta/server/rest_api/routers/v1/tools.py,sha256=38raONlEbflpspGenk_m8kVdE9p0Ev0SMBvAy_D25Xo,3692
179
+ letta/server/rest_api/routers/v1/tools.py,sha256=aVZcVxL68Oq8N6p3p_MsGrNxmnK3-UWAIHdVztFw_Nk,3794
180
180
  letta/server/rest_api/routers/v1/users.py,sha256=bxQ-YdevjDqmgNDfbSPAG_4KEVvPNBHD_-Lp1MdeMec,3374
181
181
  letta/server/rest_api/static_files.py,sha256=NG8sN4Z5EJ8JVQdj19tkFa9iQ1kBPTab9f_CUxd_u4Q,3143
182
182
  letta/server/rest_api/utils.py,sha256=Fc2ZGKzLaBa2sEtSTVjJ8D5M0xIwsWC0CVAOIJaD3rY,2176
183
- letta/server/server.py,sha256=IqVfEU4qYybWbUTrhRwcNmSwtL8nh4Qu-I3-f9c4bl4,79467
184
- letta/server/startup.sh,sha256=jeGV7B_PS0hS-tT6o6GpACrUbV9WV1NI2L9aLoUDDtc,311
183
+ letta/server/server.py,sha256=HeIucHAaf6CpV0izHTnrMfQwDX-Lbs7j9DHYogZpLAc,80255
184
+ letta/server/startup.sh,sha256=o2dY9wc5Ew1j_10U6dK2FYevAj79udZ-T969j5rG-a0,311
185
185
  letta/server/static_files/assets/index-3ab03d5b.css,sha256=OrA9W4iKJ5h2Wlr7GwdAT4wow0CM8hVit1yOxEL49Qw,54295
186
186
  letta/server/static_files/assets/index-d6b3669a.js,sha256=i1nHReU0RPnj-a5W0nNPV4Y9bQ0FOW0ztjMz8a2AE-Y,1821560
187
187
  letta/server/static_files/favicon.ico,sha256=DezhLdFSbM8o81wCOZcV3riq7tFUOGQD4h6-vr-HuU0,342
@@ -195,15 +195,15 @@ letta/server/ws_api/protocol.py,sha256=M_-gM5iuDBwa1cuN2IGNCG5GxMJwU2d3XW93XALv9
195
195
  letta/server/ws_api/server.py,sha256=C2Kv48PCwl46DQFb0ZP30s86KJLQ6dZk2AhWQEZn9pY,6004
196
196
  letta/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
197
197
  letta/services/organization_manager.py,sha256=wW4wOmKaqhHC7oTGWU38OO-dFtmRI0JVmq_k8gI1b0A,3658
198
- letta/services/tool_manager.py,sha256=7elTmzk0vhceD8ezcywYfLvRaVEi0bxp7HFcw27CQzg,8877
199
- letta/services/user_manager.py,sha256=h6K06LnYIbzw7YoQia5WGmBNSXZg5AuHZTvS54aEmSI,4534
198
+ letta/services/tool_manager.py,sha256=RAY7mNGuFoVfWfxe4mOmmPp53Ghm1Wy1UtLFuUwKPhA,7082
199
+ letta/services/user_manager.py,sha256=5wjmnhLoxsc9FCKD0IPotfV24i_iCqmifOkhIBvshtc,4404
200
200
  letta/settings.py,sha256=yiYNmnYKj_BdTm0cBEIvQKYGU-lCmFntqsyVfRUy3_k,3411
201
201
  letta/streaming_interface.py,sha256=_FPUWy58j50evHcpXyd7zB1wWqeCc71NCFeWh_TBvnw,15736
202
202
  letta/streaming_utils.py,sha256=329fsvj1ZN0r0LpQtmMPZ2vSxkDBIUUwvGHZFkjm2I8,11745
203
203
  letta/system.py,sha256=buKYPqG5n2x41hVmWpu6JUpyd7vTWED9Km2_M7dLrvk,6960
204
204
  letta/utils.py,sha256=SXLEYhyp3gHyIjrxNIKNZZ5ittKo3KOj6zxgC_Trex0,31012
205
- letta_nightly-0.5.1.dev20241028104150.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
206
- letta_nightly-0.5.1.dev20241028104150.dist-info/METADATA,sha256=hHWbNCjBir7WYHKQCe19Ioek8Bu8GMcj_lSQswGMusk,10660
207
- letta_nightly-0.5.1.dev20241028104150.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
208
- letta_nightly-0.5.1.dev20241028104150.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
209
- letta_nightly-0.5.1.dev20241028104150.dist-info/RECORD,,
205
+ letta_nightly-0.5.1.dev20241029104141.dist-info/LICENSE,sha256=mExtuZ_GYJgDEI38GWdiEYZizZS4KkVt2SF1g_GPNhI,10759
206
+ letta_nightly-0.5.1.dev20241029104141.dist-info/METADATA,sha256=_xR7OpWIFvzC-jEsKBRLUsZ1cXbNOYvxH1ZlSB2U9EY,10660
207
+ letta_nightly-0.5.1.dev20241029104141.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
208
+ letta_nightly-0.5.1.dev20241029104141.dist-info/entry_points.txt,sha256=2zdiyGNEZGV5oYBuS-y2nAAgjDgcC9yM_mHJBFSRt5U,40
209
+ letta_nightly-0.5.1.dev20241029104141.dist-info/RECORD,,