letta-nightly 0.6.3.dev20241213104231__py3-none-any.whl → 0.6.4.dev20241214104034__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of letta-nightly might be problematic. Click here for more details.

Files changed (63) hide show
  1. letta/__init__.py +2 -2
  2. letta/agent.py +54 -45
  3. letta/chat_only_agent.py +6 -8
  4. letta/cli/cli.py +2 -10
  5. letta/client/client.py +121 -138
  6. letta/config.py +0 -161
  7. letta/main.py +3 -8
  8. letta/memory.py +3 -14
  9. letta/o1_agent.py +1 -5
  10. letta/offline_memory_agent.py +2 -6
  11. letta/orm/__init__.py +2 -0
  12. letta/orm/agent.py +109 -0
  13. letta/orm/agents_tags.py +10 -18
  14. letta/orm/block.py +29 -4
  15. letta/orm/blocks_agents.py +5 -11
  16. letta/orm/custom_columns.py +152 -0
  17. letta/orm/message.py +3 -38
  18. letta/orm/organization.py +2 -7
  19. letta/orm/passage.py +10 -32
  20. letta/orm/source.py +5 -25
  21. letta/orm/sources_agents.py +13 -0
  22. letta/orm/sqlalchemy_base.py +54 -30
  23. letta/orm/tool.py +1 -19
  24. letta/orm/tools_agents.py +7 -24
  25. letta/orm/user.py +3 -4
  26. letta/schemas/agent.py +48 -65
  27. letta/schemas/memory.py +2 -1
  28. letta/schemas/sandbox_config.py +12 -1
  29. letta/server/rest_api/app.py +0 -5
  30. letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +1 -1
  31. letta/server/rest_api/routers/v1/agents.py +99 -78
  32. letta/server/rest_api/routers/v1/blocks.py +22 -25
  33. letta/server/rest_api/routers/v1/jobs.py +4 -4
  34. letta/server/rest_api/routers/v1/sandbox_configs.py +10 -10
  35. letta/server/rest_api/routers/v1/sources.py +12 -12
  36. letta/server/rest_api/routers/v1/tools.py +35 -15
  37. letta/server/rest_api/routers/v1/users.py +0 -46
  38. letta/server/server.py +172 -718
  39. letta/server/ws_api/server.py +0 -5
  40. letta/services/agent_manager.py +405 -0
  41. letta/services/block_manager.py +13 -21
  42. letta/services/helpers/agent_manager_helper.py +90 -0
  43. letta/services/organization_manager.py +0 -1
  44. letta/services/passage_manager.py +62 -62
  45. letta/services/sandbox_config_manager.py +3 -3
  46. letta/services/source_manager.py +22 -1
  47. letta/services/tool_execution_sandbox.py +4 -4
  48. letta/services/user_manager.py +11 -6
  49. letta/utils.py +2 -2
  50. {letta_nightly-0.6.3.dev20241213104231.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/METADATA +1 -1
  51. {letta_nightly-0.6.3.dev20241213104231.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/RECORD +54 -58
  52. letta/metadata.py +0 -407
  53. letta/schemas/agents_tags.py +0 -33
  54. letta/schemas/api_key.py +0 -21
  55. letta/schemas/blocks_agents.py +0 -32
  56. letta/schemas/tools_agents.py +0 -32
  57. letta/server/rest_api/routers/openai/assistants/threads.py +0 -338
  58. letta/services/agents_tags_manager.py +0 -64
  59. letta/services/blocks_agents_manager.py +0 -106
  60. letta/services/tools_agents_manager.py +0 -94
  61. {letta_nightly-0.6.3.dev20241213104231.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/LICENSE +0 -0
  62. {letta_nightly-0.6.3.dev20241213104231.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/WHEEL +0 -0
  63. {letta_nightly-0.6.3.dev20241213104231.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/entry_points.txt +0 -0
@@ -1,338 +0,0 @@
1
- import uuid
2
- from typing import TYPE_CHECKING, List, Optional
3
-
4
- from fastapi import APIRouter, Body, Depends, Header, HTTPException, Path, Query
5
-
6
- from letta.constants import DEFAULT_PRESET
7
- from letta.schemas.agent import CreateAgent
8
- from letta.schemas.enums import MessageRole
9
- from letta.schemas.message import Message
10
- from letta.schemas.openai.openai import (
11
- MessageFile,
12
- OpenAIMessage,
13
- OpenAIRun,
14
- OpenAIRunStep,
15
- OpenAIThread,
16
- Text,
17
- )
18
- from letta.server.rest_api.routers.openai.assistants.schemas import (
19
- CreateMessageRequest,
20
- CreateRunRequest,
21
- CreateThreadRequest,
22
- CreateThreadRunRequest,
23
- DeleteThreadResponse,
24
- ListMessagesResponse,
25
- ModifyMessageRequest,
26
- ModifyRunRequest,
27
- ModifyThreadRequest,
28
- OpenAIThread,
29
- SubmitToolOutputsToRunRequest,
30
- )
31
- from letta.server.rest_api.utils import get_letta_server
32
- from letta.server.server import SyncServer
33
-
34
- if TYPE_CHECKING:
35
- from letta.utils import get_utc_time
36
-
37
-
38
- # TODO: implement mechanism for creating/authenticating users associated with a bearer token
39
- router = APIRouter(prefix="/v1/threads", tags=["threads"])
40
-
41
-
42
- @router.post("/", response_model=OpenAIThread)
43
- def create_thread(
44
- request: CreateThreadRequest = Body(...),
45
- server: SyncServer = Depends(get_letta_server),
46
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
47
- ):
48
- # TODO: use requests.description and requests.metadata fields
49
- # TODO: handle requests.file_ids and requests.tools
50
- # TODO: eventually allow request to override embedding/llm model
51
- actor = server.get_user_or_default(user_id=user_id)
52
-
53
- print("Create thread/agent", request)
54
- # create a letta agent
55
- agent_state = server.create_agent(
56
- request=CreateAgent(),
57
- user_id=actor.id,
58
- )
59
- # TODO: insert messages into recall memory
60
- return OpenAIThread(
61
- id=str(agent_state.id),
62
- created_at=int(agent_state.created_at.timestamp()),
63
- metadata={}, # TODO add metadata?
64
- )
65
-
66
-
67
- @router.get("/{thread_id}", response_model=OpenAIThread)
68
- def retrieve_thread(
69
- thread_id: str = Path(..., description="The unique identifier of the thread."),
70
- server: SyncServer = Depends(get_letta_server),
71
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
72
- ):
73
- actor = server.get_user_or_default(user_id=user_id)
74
- agent = server.get_agent(user_id=actor.id, agent_id=thread_id)
75
- assert agent is not None
76
- return OpenAIThread(
77
- id=str(agent.id),
78
- created_at=int(agent.created_at.timestamp()),
79
- metadata={}, # TODO add metadata?
80
- )
81
-
82
-
83
- @router.get("/{thread_id}", response_model=OpenAIThread)
84
- def modify_thread(
85
- thread_id: str = Path(..., description="The unique identifier of the thread."),
86
- request: ModifyThreadRequest = Body(...),
87
- ):
88
- # TODO: add agent metadata so this can be modified
89
- raise HTTPException(status_code=404, detail="Not yet implemented (coming soon)")
90
-
91
-
92
- @router.delete("/{thread_id}", response_model=DeleteThreadResponse)
93
- def delete_thread(
94
- thread_id: str = Path(..., description="The unique identifier of the thread."),
95
- ):
96
- # TODO: delete agent
97
- raise HTTPException(status_code=404, detail="Not yet implemented (coming soon)")
98
-
99
-
100
- @router.post("/{thread_id}/messages", tags=["messages"], response_model=OpenAIMessage)
101
- def create_message(
102
- thread_id: str = Path(..., description="The unique identifier of the thread."),
103
- request: CreateMessageRequest = Body(...),
104
- server: SyncServer = Depends(get_letta_server),
105
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
106
- ):
107
- actor = server.get_user_or_default(user_id=user_id)
108
- agent_id = thread_id
109
- # create message object
110
- message = Message(
111
- user_id=actor.id,
112
- agent_id=agent_id,
113
- role=MessageRole(request.role),
114
- text=request.content,
115
- model=None,
116
- tool_calls=None,
117
- tool_call_id=None,
118
- name=None,
119
- )
120
- agent = server.load_agent(agent_id=agent_id)
121
- # add message to agent
122
- agent._append_to_messages([message])
123
-
124
- openai_message = OpenAIMessage(
125
- id=str(message.id),
126
- created_at=int(message.created_at.timestamp()),
127
- content=[Text(text=(message.text if message.text else ""))],
128
- role=message.role,
129
- thread_id=str(message.agent_id),
130
- assistant_id=DEFAULT_PRESET, # TODO: update this
131
- # TODO(sarah) fill in?
132
- run_id=None,
133
- file_ids=None,
134
- metadata=None,
135
- # file_ids=message.file_ids,
136
- # metadata=message.metadata,
137
- )
138
- return openai_message
139
-
140
-
141
- @router.get("/{thread_id}/messages", tags=["messages"], response_model=ListMessagesResponse)
142
- def list_messages(
143
- thread_id: str = Path(..., description="The unique identifier of the thread."),
144
- limit: int = Query(1000, description="How many messages to retrieve."),
145
- order: str = Query("asc", description="Order of messages to retrieve (either 'asc' or 'desc')."),
146
- after: str = Query(None, description="A cursor for use in pagination. `after` is an object ID that defines your place in the list."),
147
- before: str = Query(None, description="A cursor for use in pagination. `after` is an object ID that defines your place in the list."),
148
- server: SyncServer = Depends(get_letta_server),
149
- user_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
150
- ):
151
- actor = server.get_user_or_default(user_id)
152
- after_uuid = after if before else None
153
- before_uuid = before if before else None
154
- agent_id = thread_id
155
- reverse = True if (order == "desc") else False
156
- json_messages = server.get_agent_recall_cursor(
157
- user_id=actor.id,
158
- agent_id=agent_id,
159
- limit=limit,
160
- after=after_uuid,
161
- before=before_uuid,
162
- order_by="created_at",
163
- reverse=reverse,
164
- )
165
- assert isinstance(json_messages, List)
166
- assert all([isinstance(message, Message) for message in json_messages])
167
- assert isinstance(json_messages[0], Message)
168
- print(json_messages[0].text)
169
- # convert to openai style messages
170
- openai_messages = []
171
- for message in json_messages:
172
- assert isinstance(message, Message)
173
- openai_messages.append(
174
- OpenAIMessage(
175
- id=str(message.id),
176
- created_at=int(message.created_at.timestamp()),
177
- content=[Text(text=(message.text if message.text else ""))],
178
- role=str(message.role),
179
- thread_id=str(message.agent_id),
180
- assistant_id=DEFAULT_PRESET, # TODO: update this
181
- # TODO(sarah) fill in?
182
- run_id=None,
183
- file_ids=None,
184
- metadata=None,
185
- # file_ids=message.file_ids,
186
- # metadata=message.metadata,
187
- )
188
- )
189
- print("MESSAGES", openai_messages)
190
- # TODO: cast back to message objects
191
- return ListMessagesResponse(messages=openai_messages)
192
-
193
-
194
- @router.get("/{thread_id}/messages/{message_id}", tags=["messages"], response_model=OpenAIMessage)
195
- def retrieve_message(
196
- thread_id: str = Path(..., description="The unique identifier of the thread."),
197
- message_id: str = Path(..., description="The unique identifier of the message."),
198
- server: SyncServer = Depends(get_letta_server),
199
- ):
200
- agent_id = thread_id
201
- message = server.get_agent_message(agent_id=agent_id, message_id=message_id)
202
- assert message is not None
203
- return OpenAIMessage(
204
- id=message_id,
205
- created_at=int(message.created_at.timestamp()),
206
- content=[Text(text=(message.text if message.text else ""))],
207
- role=message.role,
208
- thread_id=str(message.agent_id),
209
- assistant_id=DEFAULT_PRESET, # TODO: update this
210
- # TODO(sarah) fill in?
211
- run_id=None,
212
- file_ids=None,
213
- metadata=None,
214
- # file_ids=message.file_ids,
215
- # metadata=message.metadata,
216
- )
217
-
218
-
219
- @router.get("/{thread_id}/messages/{message_id}/files/{file_id}", tags=["messages"], response_model=MessageFile)
220
- def retrieve_message_file(
221
- thread_id: str = Path(..., description="The unique identifier of the thread."),
222
- message_id: str = Path(..., description="The unique identifier of the message."),
223
- file_id: str = Path(..., description="The unique identifier of the file."),
224
- ):
225
- # TODO: implement?
226
- raise HTTPException(status_code=404, detail="Not yet implemented (coming soon)")
227
-
228
-
229
- @router.post("/{thread_id}/messages/{message_id}", tags=["messages"], response_model=OpenAIMessage)
230
- def modify_message(
231
- thread_id: str = Path(..., description="The unique identifier of the thread."),
232
- message_id: str = Path(..., description="The unique identifier of the message."),
233
- request: ModifyMessageRequest = Body(...),
234
- ):
235
- # TODO: add metada field to message so this can be modified
236
- raise HTTPException(status_code=404, detail="Not yet implemented (coming soon)")
237
-
238
-
239
- @router.post("/{thread_id}/runs", tags=["runs"], response_model=OpenAIRun)
240
- def create_run(
241
- thread_id: str = Path(..., description="The unique identifier of the thread."),
242
- request: CreateRunRequest = Body(...),
243
- server: SyncServer = Depends(get_letta_server),
244
- ):
245
-
246
- # TODO: add request.instructions as a message?
247
- agent_id = thread_id
248
- # TODO: override preset of agent with request.assistant_id
249
- agent = server.load_agent(agent_id=agent_id)
250
- agent.inner_step(messages=[]) # already has messages added
251
- run_id = str(uuid.uuid4())
252
- create_time = int(get_utc_time().timestamp())
253
- return OpenAIRun(
254
- id=run_id,
255
- created_at=create_time,
256
- thread_id=str(agent_id),
257
- assistant_id=DEFAULT_PRESET, # TODO: update this
258
- status="completed", # TODO: eventaully allow offline execution
259
- expires_at=create_time,
260
- model=agent.agent_state.llm_config.model,
261
- instructions=request.instructions,
262
- )
263
-
264
-
265
- @router.post("/runs", tags=["runs"], response_model=OpenAIRun)
266
- def create_thread_and_run(
267
- request: CreateThreadRunRequest = Body(...),
268
- ):
269
- # TODO: add a bunch of messages and execute
270
- raise HTTPException(status_code=404, detail="Not yet implemented (coming soon)")
271
-
272
-
273
- @router.get("/{thread_id}/runs", tags=["runs"], response_model=List[OpenAIRun])
274
- def list_runs(
275
- thread_id: str = Path(..., description="The unique identifier of the thread."),
276
- limit: int = Query(1000, description="How many runs to retrieve."),
277
- order: str = Query("asc", description="Order of runs to retrieve (either 'asc' or 'desc')."),
278
- after: str = Query(None, description="A cursor for use in pagination. `after` is an object ID that defines your place in the list."),
279
- before: str = Query(None, description="A cursor for use in pagination. `after` is an object ID that defines your place in the list."),
280
- ):
281
- # TODO: store run information in a DB so it can be returned here
282
- raise HTTPException(status_code=404, detail="Not yet implemented (coming soon)")
283
-
284
-
285
- @router.get("/{thread_id}/runs/{run_id}/steps", tags=["runs"], response_model=List[OpenAIRunStep])
286
- def list_run_steps(
287
- thread_id: str = Path(..., description="The unique identifier of the thread."),
288
- run_id: str = Path(..., description="The unique identifier of the run."),
289
- limit: int = Query(1000, description="How many run steps to retrieve."),
290
- order: str = Query("asc", description="Order of run steps to retrieve (either 'asc' or 'desc')."),
291
- after: str = Query(None, description="A cursor for use in pagination. `after` is an object ID that defines your place in the list."),
292
- before: str = Query(None, description="A cursor for use in pagination. `after` is an object ID that defines your place in the list."),
293
- ):
294
- # TODO: store run information in a DB so it can be returned here
295
- raise HTTPException(status_code=404, detail="Not yet implemented (coming soon)")
296
-
297
-
298
- @router.get("/{thread_id}/runs/{run_id}", tags=["runs"], response_model=OpenAIRun)
299
- def retrieve_run(
300
- thread_id: str = Path(..., description="The unique identifier of the thread."),
301
- run_id: str = Path(..., description="The unique identifier of the run."),
302
- ):
303
- raise HTTPException(status_code=404, detail="Not yet implemented (coming soon)")
304
-
305
-
306
- @router.get("/{thread_id}/runs/{run_id}/steps/{step_id}", tags=["runs"], response_model=OpenAIRunStep)
307
- def retrieve_run_step(
308
- thread_id: str = Path(..., description="The unique identifier of the thread."),
309
- run_id: str = Path(..., description="The unique identifier of the run."),
310
- step_id: str = Path(..., description="The unique identifier of the run step."),
311
- ):
312
- raise HTTPException(status_code=404, detail="Not yet implemented (coming soon)")
313
-
314
-
315
- @router.post("/{thread_id}/runs/{run_id}", tags=["runs"], response_model=OpenAIRun)
316
- def modify_run(
317
- thread_id: str = Path(..., description="The unique identifier of the thread."),
318
- run_id: str = Path(..., description="The unique identifier of the run."),
319
- request: ModifyRunRequest = Body(...),
320
- ):
321
- raise HTTPException(status_code=404, detail="Not yet implemented (coming soon)")
322
-
323
-
324
- @router.post("/{thread_id}/runs/{run_id}/submit_tool_outputs", tags=["runs"], response_model=OpenAIRun)
325
- def submit_tool_outputs_to_run(
326
- thread_id: str = Path(..., description="The unique identifier of the thread."),
327
- run_id: str = Path(..., description="The unique identifier of the run."),
328
- request: SubmitToolOutputsToRunRequest = Body(...),
329
- ):
330
- raise HTTPException(status_code=404, detail="Not yet implemented (coming soon)")
331
-
332
-
333
- @router.post("/{thread_id}/runs/{run_id}/cancel", tags=["runs"], response_model=OpenAIRun)
334
- def cancel_run(
335
- thread_id: str = Path(..., description="The unique identifier of the thread."),
336
- run_id: str = Path(..., description="The unique identifier of the run."),
337
- ):
338
- raise HTTPException(status_code=404, detail="Not yet implemented (coming soon)")
@@ -1,64 +0,0 @@
1
- from typing import List
2
-
3
- from letta.orm.agents_tags import AgentsTags as AgentsTagsModel
4
- from letta.orm.errors import NoResultFound
5
- from letta.schemas.agents_tags import AgentsTags as PydanticAgentsTags
6
- from letta.schemas.user import User as PydanticUser
7
- from letta.utils import enforce_types
8
-
9
-
10
- class AgentsTagsManager:
11
- """Manager class to handle business logic related to Tags."""
12
-
13
- def __init__(self):
14
- from letta.server.server import db_context
15
-
16
- self.session_maker = db_context
17
-
18
- @enforce_types
19
- def add_tag_to_agent(self, agent_id: str, tag: str, actor: PydanticUser) -> PydanticAgentsTags:
20
- """Add a tag to an agent."""
21
- with self.session_maker() as session:
22
- # Check if the tag already exists for this agent
23
- try:
24
- agents_tags_model = AgentsTagsModel.read(db_session=session, agent_id=agent_id, tag=tag, actor=actor)
25
- return agents_tags_model.to_pydantic()
26
- except NoResultFound:
27
- agents_tags = PydanticAgentsTags(agent_id=agent_id, tag=tag).model_dump(exclude_none=True)
28
- new_tag = AgentsTagsModel(**agents_tags, organization_id=actor.organization_id)
29
- new_tag.create(session, actor=actor)
30
- return new_tag.to_pydantic()
31
-
32
- @enforce_types
33
- def delete_all_tags_from_agent(self, agent_id: str, actor: PydanticUser):
34
- """Delete a tag from an agent. This is a permanent hard delete."""
35
- tags = self.get_tags_for_agent(agent_id=agent_id, actor=actor)
36
- for tag in tags:
37
- self.delete_tag_from_agent(agent_id=agent_id, tag=tag, actor=actor)
38
-
39
- @enforce_types
40
- def delete_tag_from_agent(self, agent_id: str, tag: str, actor: PydanticUser):
41
- """Delete a tag from an agent."""
42
- with self.session_maker() as session:
43
- try:
44
- # Retrieve and delete the tag association
45
- tag_association = AgentsTagsModel.read(db_session=session, agent_id=agent_id, tag=tag, actor=actor)
46
- tag_association.hard_delete(session, actor=actor)
47
- except NoResultFound:
48
- raise ValueError(f"Tag '{tag}' not found for agent '{agent_id}'.")
49
-
50
- @enforce_types
51
- def get_agents_by_tag(self, tag: str, actor: PydanticUser) -> List[str]:
52
- """Retrieve all agent IDs associated with a specific tag."""
53
- with self.session_maker() as session:
54
- # Query for all agents with the given tag
55
- agents_with_tag = AgentsTagsModel.list(db_session=session, tag=tag, organization_id=actor.organization_id)
56
- return [record.agent_id for record in agents_with_tag]
57
-
58
- @enforce_types
59
- def get_tags_for_agent(self, agent_id: str, actor: PydanticUser) -> List[str]:
60
- """Retrieve all tags associated with a specific agent."""
61
- with self.session_maker() as session:
62
- # Query for all tags associated with the given agent
63
- tags_for_agent = AgentsTagsModel.list(db_session=session, agent_id=agent_id, organization_id=actor.organization_id)
64
- return [record.tag for record in tags_for_agent]
@@ -1,106 +0,0 @@
1
- import warnings
2
- from typing import List
3
-
4
- from letta.orm.blocks_agents import BlocksAgents as BlocksAgentsModel
5
- from letta.orm.errors import NoResultFound
6
- from letta.schemas.blocks_agents import BlocksAgents as PydanticBlocksAgents
7
- from letta.utils import enforce_types
8
-
9
-
10
- # TODO: DELETE THIS ASAP
11
- # TODO: So we have a patch where we manually specify CRUD operations
12
- # TODO: This is because Agent is NOT migrated to the ORM yet
13
- # TODO: Once we migrate Agent to the ORM, we should deprecate any agents relationship table managers
14
- class BlocksAgentsManager:
15
- """Manager class to handle business logic related to Blocks and Agents."""
16
-
17
- def __init__(self):
18
- from letta.server.server import db_context
19
-
20
- self.session_maker = db_context
21
-
22
- @enforce_types
23
- def add_block_to_agent(self, agent_id: str, block_id: str, block_label: str) -> PydanticBlocksAgents:
24
- """Add a block to an agent. If the label already exists on that agent, this will error."""
25
- with self.session_maker() as session:
26
- try:
27
- # Check if the block-label combination already exists for this agent
28
- blocks_agents_record = BlocksAgentsModel.read(db_session=session, agent_id=agent_id, block_label=block_label)
29
- warnings.warn(f"Block label '{block_label}' already exists for agent '{agent_id}'.")
30
- except NoResultFound:
31
- blocks_agents_record = PydanticBlocksAgents(agent_id=agent_id, block_id=block_id, block_label=block_label)
32
- blocks_agents_record = BlocksAgentsModel(**blocks_agents_record.model_dump(exclude_none=True))
33
- blocks_agents_record.create(session)
34
-
35
- return blocks_agents_record.to_pydantic()
36
-
37
- @enforce_types
38
- def remove_block_with_label_from_agent(self, agent_id: str, block_label: str) -> PydanticBlocksAgents:
39
- """Remove a block with a label from an agent."""
40
- with self.session_maker() as session:
41
- try:
42
- # Find and delete the block-label association for the agent
43
- blocks_agents_record = BlocksAgentsModel.read(db_session=session, agent_id=agent_id, block_label=block_label)
44
- blocks_agents_record.hard_delete(session)
45
- return blocks_agents_record.to_pydantic()
46
- except NoResultFound:
47
- raise ValueError(f"Block label '{block_label}' not found for agent '{agent_id}'.")
48
-
49
- @enforce_types
50
- def remove_block_with_id_from_agent(self, agent_id: str, block_id: str) -> PydanticBlocksAgents:
51
- """Remove a block with a label from an agent."""
52
- with self.session_maker() as session:
53
- try:
54
- # Find and delete the block-label association for the agent
55
- blocks_agents_record = BlocksAgentsModel.read(db_session=session, agent_id=agent_id, block_id=block_id)
56
- blocks_agents_record.hard_delete(session)
57
- return blocks_agents_record.to_pydantic()
58
- except NoResultFound:
59
- raise ValueError(f"Block id '{block_id}' not found for agent '{agent_id}'.")
60
-
61
- @enforce_types
62
- def update_block_id_for_agent(self, agent_id: str, block_label: str, new_block_id: str) -> PydanticBlocksAgents:
63
- """Update the block ID for a specific block label for an agent."""
64
- with self.session_maker() as session:
65
- try:
66
- blocks_agents_record = BlocksAgentsModel.read(db_session=session, agent_id=agent_id, block_label=block_label)
67
- blocks_agents_record.block_id = new_block_id
68
- return blocks_agents_record.to_pydantic()
69
- except NoResultFound:
70
- raise ValueError(f"Block label '{block_label}' not found for agent '{agent_id}'.")
71
-
72
- @enforce_types
73
- def list_block_ids_for_agent(self, agent_id: str) -> List[str]:
74
- """List all block ids associated with a specific agent."""
75
- with self.session_maker() as session:
76
- blocks_agents_record = BlocksAgentsModel.list(db_session=session, agent_id=agent_id)
77
- return [record.block_id for record in blocks_agents_record]
78
-
79
- @enforce_types
80
- def list_block_labels_for_agent(self, agent_id: str) -> List[str]:
81
- """List all block labels associated with a specific agent."""
82
- with self.session_maker() as session:
83
- blocks_agents_record = BlocksAgentsModel.list(db_session=session, agent_id=agent_id)
84
- return [record.block_label for record in blocks_agents_record]
85
-
86
- @enforce_types
87
- def list_agent_ids_with_block(self, block_id: str) -> List[str]:
88
- """List all agents associated with a specific block."""
89
- with self.session_maker() as session:
90
- blocks_agents_record = BlocksAgentsModel.list(db_session=session, block_id=block_id)
91
- return [record.agent_id for record in blocks_agents_record]
92
-
93
- @enforce_types
94
- def get_block_id_for_label(self, agent_id: str, block_label: str) -> str:
95
- """Get the block ID for a specific block label for an agent."""
96
- with self.session_maker() as session:
97
- try:
98
- blocks_agents_record = BlocksAgentsModel.read(db_session=session, agent_id=agent_id, block_label=block_label)
99
- return blocks_agents_record.block_id
100
- except NoResultFound:
101
- raise ValueError(f"Block label '{block_label}' not found for agent '{agent_id}'.")
102
-
103
- @enforce_types
104
- def remove_all_agent_blocks(self, agent_id: str):
105
- for block_id in self.list_block_ids_for_agent(agent_id):
106
- self.remove_block_with_id_from_agent(agent_id, block_id)
@@ -1,94 +0,0 @@
1
- import warnings
2
- from typing import List, Optional
3
-
4
- from sqlalchemy import select
5
- from sqlalchemy.exc import IntegrityError
6
- from sqlalchemy.orm import Session
7
-
8
- from letta.orm.errors import NoResultFound
9
- from letta.orm.organization import Organization
10
- from letta.orm.tool import Tool
11
- from letta.orm.tools_agents import ToolsAgents as ToolsAgentsModel
12
- from letta.schemas.tools_agents import ToolsAgents as PydanticToolsAgents
13
-
14
- class ToolsAgentsManager:
15
- """Manages the relationship between tools and agents."""
16
-
17
- def __init__(self):
18
- from letta.server.server import db_context
19
- self.session_maker = db_context
20
-
21
- def add_tool_to_agent(self, agent_id: str, tool_id: str, tool_name: str) -> PydanticToolsAgents:
22
- """Add a tool to an agent.
23
-
24
- When a tool is added to an agent, it will be added to all agents in the same organization.
25
- """
26
- with self.session_maker() as session:
27
- try:
28
- # Check if the tool-agent combination already exists for this agent
29
- tools_agents_record = ToolsAgentsModel.read(db_session=session, agent_id=agent_id, tool_name=tool_name)
30
- warnings.warn(f"Tool name '{tool_name}' already exists for agent '{agent_id}'.")
31
- except NoResultFound:
32
- tools_agents_record = PydanticToolsAgents(agent_id=agent_id, tool_id=tool_id, tool_name=tool_name)
33
- tools_agents_record = ToolsAgentsModel(**tools_agents_record.model_dump(exclude_none=True))
34
- tools_agents_record.create(session)
35
-
36
- return tools_agents_record.to_pydantic()
37
-
38
- def remove_tool_with_name_from_agent(self, agent_id: str, tool_name: str) -> None:
39
- """Remove a tool from an agent by its name.
40
-
41
- When a tool is removed from an agent, it will be removed from all agents in the same organization.
42
- """
43
- with self.session_maker() as session:
44
- try:
45
- # Find and delete the tool-agent association for the agent
46
- tools_agents_record = ToolsAgentsModel.read(db_session=session, agent_id=agent_id, tool_name=tool_name)
47
- tools_agents_record.hard_delete(session)
48
- return tools_agents_record.to_pydantic()
49
- except NoResultFound:
50
- raise ValueError(f"Tool name '{tool_name}' not found for agent '{agent_id}'.")
51
-
52
- def remove_tool_with_id_from_agent(self, agent_id: str, tool_id: str) -> PydanticToolsAgents:
53
- """Remove a tool with an ID from an agent."""
54
- with self.session_maker() as session:
55
- try:
56
- tools_agents_record = ToolsAgentsModel.read(db_session=session, agent_id=agent_id, tool_id=tool_id)
57
- tools_agents_record.hard_delete(session)
58
- return tools_agents_record.to_pydantic()
59
- except NoResultFound:
60
- raise ValueError(f"Tool ID '{tool_id}' not found for agent '{agent_id}'.")
61
-
62
- def list_tool_ids_for_agent(self, agent_id: str) -> List[str]:
63
- """List all tool IDs associated with a specific agent."""
64
- with self.session_maker() as session:
65
- tools_agents_record = ToolsAgentsModel.list(db_session=session, agent_id=agent_id)
66
- return [record.tool_id for record in tools_agents_record]
67
-
68
- def list_tool_names_for_agent(self, agent_id: str) -> List[str]:
69
- """List all tool names associated with a specific agent."""
70
- with self.session_maker() as session:
71
- tools_agents_record = ToolsAgentsModel.list(db_session=session, agent_id=agent_id)
72
- return [record.tool_name for record in tools_agents_record]
73
-
74
- def list_agent_ids_with_tool(self, tool_id: str) -> List[str]:
75
- """List all agents associated with a specific tool."""
76
- with self.session_maker() as session:
77
- tools_agents_record = ToolsAgentsModel.list(db_session=session, tool_id=tool_id)
78
- return [record.agent_id for record in tools_agents_record]
79
-
80
- def get_tool_id_for_name(self, agent_id: str, tool_name: str) -> str:
81
- """Get the tool ID for a specific tool name for an agent."""
82
- with self.session_maker() as session:
83
- try:
84
- tools_agents_record = ToolsAgentsModel.read(db_session=session, agent_id=agent_id, tool_name=tool_name)
85
- return tools_agents_record.tool_id
86
- except NoResultFound:
87
- raise ValueError(f"Tool name '{tool_name}' not found for agent '{agent_id}'.")
88
-
89
- def remove_all_agent_tools(self, agent_id: str) -> None:
90
- """Remove all tools associated with an agent."""
91
- with self.session_maker() as session:
92
- tools_agents_records = ToolsAgentsModel.list(db_session=session, agent_id=agent_id)
93
- for record in tools_agents_records:
94
- record.hard_delete(session)