letta-nightly 0.5.1.dev20241028104150__py3-none-any.whl → 0.5.1.dev20241030104135__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")
@@ -361,7 +359,7 @@ def update_message(
361
359
  return server.update_agent_message(agent_id=agent_id, request=request)
362
360
 
363
361
 
364
- @router.post("/{agent_id}/messages", response_model=LettaResponse, operation_id="create_agent_message")
362
+ @router.post("/{agent_id}/messages", response_model=None, operation_id="create_agent_message")
365
363
  async def send_message(
366
364
  agent_id: str,
367
365
  server: SyncServer = Depends(get_letta_server),
@@ -375,16 +373,10 @@ async def send_message(
375
373
  """
376
374
  actor = server.get_user_or_default(user_id=user_id)
377
375
 
378
- # TODO(charles): support sending multiple messages
379
- assert len(request.messages) == 1, f"Multiple messages not supported: {request.messages}"
380
- request.messages[0]
381
-
382
376
  return await send_message_to_agent(
383
377
  server=server,
384
378
  agent_id=agent_id,
385
379
  user_id=actor.id,
386
- # role=message.role,
387
- # message=message.text,
388
380
  messages=request.messages,
389
381
  stream_steps=request.stream_steps,
390
382
  stream_tokens=request.stream_tokens,
@@ -465,8 +457,12 @@ async def send_message_to_agent(
465
457
  # Offload the synchronous message_func to a separate thread
466
458
  streaming_interface.stream_start()
467
459
  task = asyncio.create_task(
468
- # asyncio.to_thread(message_func, user_id=user_id, agent_id=agent_id, message=message, timestamp=timestamp)
469
- asyncio.to_thread(server.send_messages, user_id=user_id, agent_id=agent_id, messages=messages)
460
+ asyncio.to_thread(
461
+ server.send_messages,
462
+ user_id=user_id,
463
+ agent_id=agent_id,
464
+ messages=messages,
465
+ )
470
466
  )
471
467
 
472
468
  if stream_steps:
@@ -476,7 +472,11 @@ async def send_message_to_agent(
476
472
 
477
473
  # return a stream
478
474
  return StreamingResponse(
479
- sse_async_generator(streaming_interface.get_generator(), finish_message=include_final_message),
475
+ sse_async_generator(
476
+ streaming_interface.get_generator(),
477
+ usage_task=task,
478
+ finish_message=include_final_message,
479
+ ),
480
480
  media_type="text/event-stream",
481
481
  )
482
482