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.
- letta/__init__.py +2 -2
- letta/agent.py +54 -45
- letta/chat_only_agent.py +6 -8
- letta/cli/cli.py +2 -10
- letta/client/client.py +121 -138
- letta/config.py +0 -161
- letta/main.py +3 -8
- letta/memory.py +3 -14
- letta/o1_agent.py +1 -5
- letta/offline_memory_agent.py +2 -6
- letta/orm/__init__.py +2 -0
- letta/orm/agent.py +109 -0
- letta/orm/agents_tags.py +10 -18
- letta/orm/block.py +29 -4
- letta/orm/blocks_agents.py +5 -11
- letta/orm/custom_columns.py +152 -0
- letta/orm/message.py +3 -38
- letta/orm/organization.py +2 -7
- letta/orm/passage.py +10 -32
- letta/orm/source.py +5 -25
- letta/orm/sources_agents.py +13 -0
- letta/orm/sqlalchemy_base.py +54 -30
- letta/orm/tool.py +1 -19
- letta/orm/tools_agents.py +7 -24
- letta/orm/user.py +3 -4
- letta/schemas/agent.py +48 -65
- letta/schemas/memory.py +2 -1
- letta/schemas/sandbox_config.py +12 -1
- letta/server/rest_api/app.py +0 -5
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +1 -1
- letta/server/rest_api/routers/v1/agents.py +99 -78
- letta/server/rest_api/routers/v1/blocks.py +22 -25
- letta/server/rest_api/routers/v1/jobs.py +4 -4
- letta/server/rest_api/routers/v1/sandbox_configs.py +10 -10
- letta/server/rest_api/routers/v1/sources.py +12 -12
- letta/server/rest_api/routers/v1/tools.py +35 -15
- letta/server/rest_api/routers/v1/users.py +0 -46
- letta/server/server.py +172 -718
- letta/server/ws_api/server.py +0 -5
- letta/services/agent_manager.py +405 -0
- letta/services/block_manager.py +13 -21
- letta/services/helpers/agent_manager_helper.py +90 -0
- letta/services/organization_manager.py +0 -1
- letta/services/passage_manager.py +62 -62
- letta/services/sandbox_config_manager.py +3 -3
- letta/services/source_manager.py +22 -1
- letta/services/tool_execution_sandbox.py +4 -4
- letta/services/user_manager.py +11 -6
- letta/utils.py +2 -2
- {letta_nightly-0.6.3.dev20241213104231.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/METADATA +1 -1
- {letta_nightly-0.6.3.dev20241213104231.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/RECORD +54 -58
- letta/metadata.py +0 -407
- letta/schemas/agents_tags.py +0 -33
- letta/schemas/api_key.py +0 -21
- letta/schemas/blocks_agents.py +0 -32
- letta/schemas/tools_agents.py +0 -32
- letta/server/rest_api/routers/openai/assistants/threads.py +0 -338
- letta/services/agents_tags_manager.py +0 -64
- letta/services/blocks_agents_manager.py +0 -106
- letta/services/tools_agents_manager.py +0 -94
- {letta_nightly-0.6.3.dev20241213104231.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/LICENSE +0 -0
- {letta_nightly-0.6.3.dev20241213104231.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/WHEEL +0 -0
- {letta_nightly-0.6.3.dev20241213104231.dist-info → letta_nightly-0.6.4.dev20241214104034.dist-info}/entry_points.txt +0 -0
letta/server/ws_api/server.py
CHANGED
|
@@ -19,11 +19,6 @@ class WebSocketServer:
|
|
|
19
19
|
self.server = SyncServer(default_interface=self.interface)
|
|
20
20
|
|
|
21
21
|
def shutdown_server(self):
|
|
22
|
-
try:
|
|
23
|
-
self.server.save_agents()
|
|
24
|
-
print(f"Saved agents")
|
|
25
|
-
except Exception as e:
|
|
26
|
-
print(f"Saving agents failed with: {e}")
|
|
27
22
|
try:
|
|
28
23
|
self.interface.close()
|
|
29
24
|
print(f"Closed the WS interface")
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
from typing import Dict, List, Optional
|
|
2
|
+
|
|
3
|
+
from letta.constants import BASE_MEMORY_TOOLS, BASE_TOOLS
|
|
4
|
+
from letta.orm import Agent as AgentModel
|
|
5
|
+
from letta.orm import Block as BlockModel
|
|
6
|
+
from letta.orm import Source as SourceModel
|
|
7
|
+
from letta.orm import Tool as ToolModel
|
|
8
|
+
from letta.orm.errors import NoResultFound
|
|
9
|
+
from letta.schemas.agent import AgentState as PydanticAgentState
|
|
10
|
+
from letta.schemas.agent import AgentType, CreateAgent, UpdateAgent
|
|
11
|
+
from letta.schemas.block import Block as PydanticBlock
|
|
12
|
+
from letta.schemas.embedding_config import EmbeddingConfig
|
|
13
|
+
from letta.schemas.llm_config import LLMConfig
|
|
14
|
+
from letta.schemas.source import Source as PydanticSource
|
|
15
|
+
from letta.schemas.tool_rule import ToolRule as PydanticToolRule
|
|
16
|
+
from letta.schemas.user import User as PydanticUser
|
|
17
|
+
from letta.services.block_manager import BlockManager
|
|
18
|
+
from letta.services.helpers.agent_manager_helper import (
|
|
19
|
+
_process_relationship,
|
|
20
|
+
_process_tags,
|
|
21
|
+
derive_system_message,
|
|
22
|
+
)
|
|
23
|
+
from letta.services.passage_manager import PassageManager
|
|
24
|
+
from letta.services.source_manager import SourceManager
|
|
25
|
+
from letta.services.tool_manager import ToolManager
|
|
26
|
+
from letta.utils import enforce_types
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Agent Manager Class
|
|
30
|
+
class AgentManager:
|
|
31
|
+
"""Manager class to handle business logic related to Agents."""
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
from letta.server.server import db_context
|
|
35
|
+
|
|
36
|
+
self.session_maker = db_context
|
|
37
|
+
self.block_manager = BlockManager()
|
|
38
|
+
self.tool_manager = ToolManager()
|
|
39
|
+
self.source_manager = SourceManager()
|
|
40
|
+
|
|
41
|
+
# ======================================================================================================================
|
|
42
|
+
# Basic CRUD operations
|
|
43
|
+
# ======================================================================================================================
|
|
44
|
+
@enforce_types
|
|
45
|
+
def create_agent(
|
|
46
|
+
self,
|
|
47
|
+
agent_create: CreateAgent,
|
|
48
|
+
actor: PydanticUser,
|
|
49
|
+
) -> PydanticAgentState:
|
|
50
|
+
system = derive_system_message(agent_type=agent_create.agent_type, system=agent_create.system)
|
|
51
|
+
|
|
52
|
+
# create blocks (note: cannot be linked into the agent_id is created)
|
|
53
|
+
block_ids = list(agent_create.block_ids or []) # Create a local copy to avoid modifying the original
|
|
54
|
+
for create_block in agent_create.memory_blocks:
|
|
55
|
+
block = self.block_manager.create_or_update_block(PydanticBlock(**create_block.model_dump()), actor=actor)
|
|
56
|
+
block_ids.append(block.id)
|
|
57
|
+
|
|
58
|
+
# TODO: Remove this block once we deprecate the legacy `tools` field
|
|
59
|
+
# create passed in `tools`
|
|
60
|
+
tool_names = []
|
|
61
|
+
if agent_create.include_base_tools:
|
|
62
|
+
tool_names.extend(BASE_TOOLS + BASE_MEMORY_TOOLS)
|
|
63
|
+
if agent_create.tools:
|
|
64
|
+
tool_names.extend(agent_create.tools)
|
|
65
|
+
|
|
66
|
+
tool_ids = agent_create.tool_ids or []
|
|
67
|
+
for tool_name in tool_names:
|
|
68
|
+
tool = self.tool_manager.get_tool_by_name(tool_name=tool_name, actor=actor)
|
|
69
|
+
if tool:
|
|
70
|
+
tool_ids.append(tool.id)
|
|
71
|
+
# Remove duplicates
|
|
72
|
+
tool_ids = list(set(tool_ids))
|
|
73
|
+
|
|
74
|
+
return self._create_agent(
|
|
75
|
+
name=agent_create.name,
|
|
76
|
+
system=system,
|
|
77
|
+
agent_type=agent_create.agent_type,
|
|
78
|
+
llm_config=agent_create.llm_config,
|
|
79
|
+
embedding_config=agent_create.embedding_config,
|
|
80
|
+
block_ids=block_ids,
|
|
81
|
+
tool_ids=tool_ids,
|
|
82
|
+
source_ids=agent_create.source_ids or [],
|
|
83
|
+
tags=agent_create.tags or [],
|
|
84
|
+
description=agent_create.description,
|
|
85
|
+
metadata_=agent_create.metadata_,
|
|
86
|
+
tool_rules=agent_create.tool_rules,
|
|
87
|
+
actor=actor,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@enforce_types
|
|
91
|
+
def _create_agent(
|
|
92
|
+
self,
|
|
93
|
+
actor: PydanticUser,
|
|
94
|
+
name: str,
|
|
95
|
+
system: str,
|
|
96
|
+
agent_type: AgentType,
|
|
97
|
+
llm_config: LLMConfig,
|
|
98
|
+
embedding_config: EmbeddingConfig,
|
|
99
|
+
block_ids: List[str],
|
|
100
|
+
tool_ids: List[str],
|
|
101
|
+
source_ids: List[str],
|
|
102
|
+
tags: List[str],
|
|
103
|
+
description: Optional[str] = None,
|
|
104
|
+
metadata_: Optional[Dict] = None,
|
|
105
|
+
tool_rules: Optional[List[PydanticToolRule]] = None,
|
|
106
|
+
) -> PydanticAgentState:
|
|
107
|
+
"""Create a new agent."""
|
|
108
|
+
with self.session_maker() as session:
|
|
109
|
+
# Prepare the agent data
|
|
110
|
+
data = {
|
|
111
|
+
"name": name,
|
|
112
|
+
"system": system,
|
|
113
|
+
"agent_type": agent_type,
|
|
114
|
+
"llm_config": llm_config,
|
|
115
|
+
"embedding_config": embedding_config,
|
|
116
|
+
"organization_id": actor.organization_id,
|
|
117
|
+
"description": description,
|
|
118
|
+
"metadata_": metadata_,
|
|
119
|
+
"tool_rules": tool_rules,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Create the new agent using SqlalchemyBase.create
|
|
123
|
+
new_agent = AgentModel(**data)
|
|
124
|
+
_process_relationship(session, new_agent, "tools", ToolModel, tool_ids, replace=True)
|
|
125
|
+
_process_relationship(session, new_agent, "sources", SourceModel, source_ids, replace=True)
|
|
126
|
+
_process_relationship(session, new_agent, "core_memory", BlockModel, block_ids, replace=True)
|
|
127
|
+
_process_tags(new_agent, tags, replace=True)
|
|
128
|
+
new_agent.create(session, actor=actor)
|
|
129
|
+
|
|
130
|
+
# Convert to PydanticAgentState and return
|
|
131
|
+
return new_agent.to_pydantic()
|
|
132
|
+
|
|
133
|
+
@enforce_types
|
|
134
|
+
def update_agent(self, agent_id: str, agent_update: UpdateAgent, actor: PydanticUser) -> PydanticAgentState:
|
|
135
|
+
"""
|
|
136
|
+
Update an existing agent.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
agent_id: The ID of the agent to update.
|
|
140
|
+
agent_update: UpdateAgent object containing the updated fields.
|
|
141
|
+
actor: User performing the action.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
PydanticAgentState: The updated agent as a Pydantic model.
|
|
145
|
+
"""
|
|
146
|
+
with self.session_maker() as session:
|
|
147
|
+
# Retrieve the existing agent
|
|
148
|
+
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
|
149
|
+
|
|
150
|
+
# Update scalar fields directly
|
|
151
|
+
scalar_fields = {"name", "system", "llm_config", "embedding_config", "message_ids", "tool_rules", "description", "metadata_"}
|
|
152
|
+
for field in scalar_fields:
|
|
153
|
+
value = getattr(agent_update, field, None)
|
|
154
|
+
if value is not None:
|
|
155
|
+
setattr(agent, field, value)
|
|
156
|
+
|
|
157
|
+
# Update relationships using _process_relationship and _process_tags
|
|
158
|
+
if agent_update.tool_ids is not None:
|
|
159
|
+
_process_relationship(session, agent, "tools", ToolModel, agent_update.tool_ids, replace=True)
|
|
160
|
+
if agent_update.source_ids is not None:
|
|
161
|
+
_process_relationship(session, agent, "sources", SourceModel, agent_update.source_ids, replace=True)
|
|
162
|
+
if agent_update.block_ids is not None:
|
|
163
|
+
_process_relationship(session, agent, "core_memory", BlockModel, agent_update.block_ids, replace=True)
|
|
164
|
+
if agent_update.tags is not None:
|
|
165
|
+
_process_tags(agent, agent_update.tags, replace=True)
|
|
166
|
+
|
|
167
|
+
# Commit and refresh the agent
|
|
168
|
+
agent.update(session, actor=actor)
|
|
169
|
+
|
|
170
|
+
# Convert to PydanticAgentState and return
|
|
171
|
+
return agent.to_pydantic()
|
|
172
|
+
|
|
173
|
+
@enforce_types
|
|
174
|
+
def list_agents(
|
|
175
|
+
self,
|
|
176
|
+
actor: PydanticUser,
|
|
177
|
+
tags: Optional[List[str]] = None,
|
|
178
|
+
match_all_tags: bool = False,
|
|
179
|
+
cursor: Optional[str] = None,
|
|
180
|
+
limit: Optional[int] = 50,
|
|
181
|
+
**kwargs,
|
|
182
|
+
) -> List[PydanticAgentState]:
|
|
183
|
+
"""
|
|
184
|
+
List agents that have the specified tags.
|
|
185
|
+
"""
|
|
186
|
+
with self.session_maker() as session:
|
|
187
|
+
agents = AgentModel.list(
|
|
188
|
+
db_session=session,
|
|
189
|
+
tags=tags,
|
|
190
|
+
match_all_tags=match_all_tags,
|
|
191
|
+
cursor=cursor,
|
|
192
|
+
limit=limit,
|
|
193
|
+
organization_id=actor.organization_id if actor else None,
|
|
194
|
+
**kwargs,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return [agent.to_pydantic() for agent in agents]
|
|
198
|
+
|
|
199
|
+
@enforce_types
|
|
200
|
+
def get_agent_by_id(self, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
|
|
201
|
+
"""Fetch an agent by its ID."""
|
|
202
|
+
with self.session_maker() as session:
|
|
203
|
+
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
|
204
|
+
return agent.to_pydantic()
|
|
205
|
+
|
|
206
|
+
@enforce_types
|
|
207
|
+
def get_agent_by_name(self, agent_name: str, actor: PydanticUser) -> PydanticAgentState:
|
|
208
|
+
"""Fetch an agent by its ID."""
|
|
209
|
+
with self.session_maker() as session:
|
|
210
|
+
agent = AgentModel.read(db_session=session, name=agent_name, actor=actor)
|
|
211
|
+
return agent.to_pydantic()
|
|
212
|
+
|
|
213
|
+
@enforce_types
|
|
214
|
+
def delete_agent(self, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
|
|
215
|
+
"""
|
|
216
|
+
Deletes an agent and its associated relationships.
|
|
217
|
+
Ensures proper permission checks and cascades where applicable.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
agent_id: ID of the agent to be deleted.
|
|
221
|
+
actor: User performing the action.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
PydanticAgentState: The deleted agent state
|
|
225
|
+
"""
|
|
226
|
+
with self.session_maker() as session:
|
|
227
|
+
# Retrieve the agent
|
|
228
|
+
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
|
229
|
+
|
|
230
|
+
# TODO: @mindy delete this piece when we have a proper passages/sources implementation
|
|
231
|
+
# TODO: This is done very hacky on purpose
|
|
232
|
+
# TODO: 1000 limit is also wack
|
|
233
|
+
passage_manager = PassageManager()
|
|
234
|
+
passage_manager.delete_passages(actor=actor, agent_id=agent_id, limit=1000)
|
|
235
|
+
|
|
236
|
+
agent_state = agent.to_pydantic()
|
|
237
|
+
agent.hard_delete(session)
|
|
238
|
+
return agent_state
|
|
239
|
+
|
|
240
|
+
# ======================================================================================================================
|
|
241
|
+
# Source Management
|
|
242
|
+
# ======================================================================================================================
|
|
243
|
+
@enforce_types
|
|
244
|
+
def attach_source(self, agent_id: str, source_id: str, actor: PydanticUser) -> None:
|
|
245
|
+
"""
|
|
246
|
+
Attaches a source to an agent.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
agent_id: ID of the agent to attach the source to
|
|
250
|
+
source_id: ID of the source to attach
|
|
251
|
+
actor: User performing the action
|
|
252
|
+
|
|
253
|
+
Raises:
|
|
254
|
+
ValueError: If either agent or source doesn't exist
|
|
255
|
+
IntegrityError: If the source is already attached to the agent
|
|
256
|
+
"""
|
|
257
|
+
with self.session_maker() as session:
|
|
258
|
+
# Verify both agent and source exist and user has permission to access them
|
|
259
|
+
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
|
260
|
+
|
|
261
|
+
# The _process_relationship helper already handles duplicate checking via unique constraint
|
|
262
|
+
_process_relationship(
|
|
263
|
+
session=session,
|
|
264
|
+
agent=agent,
|
|
265
|
+
relationship_name="sources",
|
|
266
|
+
model_class=SourceModel,
|
|
267
|
+
item_ids=[source_id],
|
|
268
|
+
allow_partial=False,
|
|
269
|
+
replace=False, # Extend existing sources rather than replace
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# Commit the changes
|
|
273
|
+
agent.update(session, actor=actor)
|
|
274
|
+
|
|
275
|
+
@enforce_types
|
|
276
|
+
def list_attached_sources(self, agent_id: str, actor: PydanticUser) -> List[PydanticSource]:
|
|
277
|
+
"""
|
|
278
|
+
Lists all sources attached to an agent.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
agent_id: ID of the agent to list sources for
|
|
282
|
+
actor: User performing the action
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
List[str]: List of source IDs attached to the agent
|
|
286
|
+
"""
|
|
287
|
+
with self.session_maker() as session:
|
|
288
|
+
# Verify agent exists and user has permission to access it
|
|
289
|
+
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
|
290
|
+
|
|
291
|
+
# Use the lazy-loaded relationship to get sources
|
|
292
|
+
return [source.to_pydantic() for source in agent.sources]
|
|
293
|
+
|
|
294
|
+
@enforce_types
|
|
295
|
+
def detach_source(self, agent_id: str, source_id: str, actor: PydanticUser) -> None:
|
|
296
|
+
"""
|
|
297
|
+
Detaches a source from an agent.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
agent_id: ID of the agent to detach the source from
|
|
301
|
+
source_id: ID of the source to detach
|
|
302
|
+
actor: User performing the action
|
|
303
|
+
"""
|
|
304
|
+
with self.session_maker() as session:
|
|
305
|
+
# Verify agent exists and user has permission to access it
|
|
306
|
+
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
|
307
|
+
|
|
308
|
+
# Remove the source from the relationship
|
|
309
|
+
agent.sources = [s for s in agent.sources if s.id != source_id]
|
|
310
|
+
|
|
311
|
+
# Commit the changes
|
|
312
|
+
agent.update(session, actor=actor)
|
|
313
|
+
|
|
314
|
+
# ======================================================================================================================
|
|
315
|
+
# Block management
|
|
316
|
+
# ======================================================================================================================
|
|
317
|
+
@enforce_types
|
|
318
|
+
def get_block_with_label(
|
|
319
|
+
self,
|
|
320
|
+
agent_id: str,
|
|
321
|
+
block_label: str,
|
|
322
|
+
actor: PydanticUser,
|
|
323
|
+
) -> PydanticBlock:
|
|
324
|
+
"""Gets a block attached to an agent by its label."""
|
|
325
|
+
with self.session_maker() as session:
|
|
326
|
+
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
|
327
|
+
for block in agent.core_memory:
|
|
328
|
+
if block.label == block_label:
|
|
329
|
+
return block.to_pydantic()
|
|
330
|
+
raise NoResultFound(f"No block with label '{block_label}' found for agent '{agent_id}'")
|
|
331
|
+
|
|
332
|
+
@enforce_types
|
|
333
|
+
def update_block_with_label(
|
|
334
|
+
self,
|
|
335
|
+
agent_id: str,
|
|
336
|
+
block_label: str,
|
|
337
|
+
new_block_id: str,
|
|
338
|
+
actor: PydanticUser,
|
|
339
|
+
) -> PydanticAgentState:
|
|
340
|
+
"""Updates which block is assigned to a specific label for an agent."""
|
|
341
|
+
with self.session_maker() as session:
|
|
342
|
+
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
|
343
|
+
new_block = BlockModel.read(db_session=session, identifier=new_block_id, actor=actor)
|
|
344
|
+
|
|
345
|
+
if new_block.label != block_label:
|
|
346
|
+
raise ValueError(f"New block label '{new_block.label}' doesn't match required label '{block_label}'")
|
|
347
|
+
|
|
348
|
+
# Remove old block with this label if it exists
|
|
349
|
+
agent.core_memory = [b for b in agent.core_memory if b.label != block_label]
|
|
350
|
+
|
|
351
|
+
# Add new block
|
|
352
|
+
agent.core_memory.append(new_block)
|
|
353
|
+
agent.update(session, actor=actor)
|
|
354
|
+
return agent.to_pydantic()
|
|
355
|
+
|
|
356
|
+
@enforce_types
|
|
357
|
+
def attach_block(self, agent_id: str, block_id: str, actor: PydanticUser) -> PydanticAgentState:
|
|
358
|
+
"""Attaches a block to an agent."""
|
|
359
|
+
with self.session_maker() as session:
|
|
360
|
+
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
|
361
|
+
block = BlockModel.read(db_session=session, identifier=block_id, actor=actor)
|
|
362
|
+
|
|
363
|
+
agent.core_memory.append(block)
|
|
364
|
+
agent.update(session, actor=actor)
|
|
365
|
+
return agent.to_pydantic()
|
|
366
|
+
|
|
367
|
+
@enforce_types
|
|
368
|
+
def detach_block(
|
|
369
|
+
self,
|
|
370
|
+
agent_id: str,
|
|
371
|
+
block_id: str,
|
|
372
|
+
actor: PydanticUser,
|
|
373
|
+
) -> PydanticAgentState:
|
|
374
|
+
"""Detaches a block from an agent."""
|
|
375
|
+
with self.session_maker() as session:
|
|
376
|
+
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
|
377
|
+
original_length = len(agent.core_memory)
|
|
378
|
+
|
|
379
|
+
agent.core_memory = [b for b in agent.core_memory if b.id != block_id]
|
|
380
|
+
|
|
381
|
+
if len(agent.core_memory) == original_length:
|
|
382
|
+
raise NoResultFound(f"No block with id '{block_id}' found for agent '{agent_id}' with actor id: '{actor.id}'")
|
|
383
|
+
|
|
384
|
+
agent.update(session, actor=actor)
|
|
385
|
+
return agent.to_pydantic()
|
|
386
|
+
|
|
387
|
+
@enforce_types
|
|
388
|
+
def detach_block_with_label(
|
|
389
|
+
self,
|
|
390
|
+
agent_id: str,
|
|
391
|
+
block_label: str,
|
|
392
|
+
actor: PydanticUser,
|
|
393
|
+
) -> PydanticAgentState:
|
|
394
|
+
"""Detaches a block with the specified label from an agent."""
|
|
395
|
+
with self.session_maker() as session:
|
|
396
|
+
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
|
397
|
+
original_length = len(agent.core_memory)
|
|
398
|
+
|
|
399
|
+
agent.core_memory = [b for b in agent.core_memory if b.label != block_label]
|
|
400
|
+
|
|
401
|
+
if len(agent.core_memory) == original_length:
|
|
402
|
+
raise NoResultFound(f"No block with label '{block_label}' found for agent '{agent_id}' with actor id: '{actor.id}'")
|
|
403
|
+
|
|
404
|
+
agent.update(session, actor=actor)
|
|
405
|
+
return agent.to_pydantic()
|
letta/services/block_manager.py
CHANGED
|
@@ -7,7 +7,6 @@ from letta.schemas.block import Block
|
|
|
7
7
|
from letta.schemas.block import Block as PydanticBlock
|
|
8
8
|
from letta.schemas.block import BlockUpdate, Human, Persona
|
|
9
9
|
from letta.schemas.user import User as PydanticUser
|
|
10
|
-
from letta.services.blocks_agents_manager import BlocksAgentsManager
|
|
11
10
|
from letta.utils import enforce_types, list_human_files, list_persona_files
|
|
12
11
|
|
|
13
12
|
|
|
@@ -37,33 +36,17 @@ class BlockManager:
|
|
|
37
36
|
@enforce_types
|
|
38
37
|
def update_block(self, block_id: str, block_update: BlockUpdate, actor: PydanticUser) -> PydanticBlock:
|
|
39
38
|
"""Update a block by its ID with the given BlockUpdate object."""
|
|
40
|
-
#
|
|
41
|
-
blocks_agents_manager = BlocksAgentsManager()
|
|
42
|
-
agent_ids = []
|
|
43
|
-
if block_update.label:
|
|
44
|
-
agent_ids = blocks_agents_manager.list_agent_ids_with_block(block_id=block_id)
|
|
45
|
-
for agent_id in agent_ids:
|
|
46
|
-
blocks_agents_manager.remove_block_with_id_from_agent(agent_id=agent_id, block_id=block_id)
|
|
39
|
+
# Safety check for block
|
|
47
40
|
|
|
48
41
|
with self.session_maker() as session:
|
|
49
|
-
# Update block
|
|
50
42
|
block = BlockModel.read(db_session=session, identifier=block_id, actor=actor)
|
|
51
43
|
update_data = block_update.model_dump(exclude_unset=True, exclude_none=True)
|
|
44
|
+
|
|
52
45
|
for key, value in update_data.items():
|
|
53
46
|
setattr(block, key, value)
|
|
54
|
-
try:
|
|
55
|
-
block.to_pydantic()
|
|
56
|
-
except Exception as e:
|
|
57
|
-
# invalid pydantic model
|
|
58
|
-
raise ValueError(f"Failed to create pydantic model: {e}")
|
|
59
|
-
block.update(db_session=session, actor=actor)
|
|
60
|
-
|
|
61
|
-
# TODO: REMOVE THIS ONCE AGENT IS ON ORM -> Update blocks_agents
|
|
62
|
-
if block_update.label:
|
|
63
|
-
for agent_id in agent_ids:
|
|
64
|
-
blocks_agents_manager.add_block_to_agent(agent_id=agent_id, block_id=block_id, block_label=block_update.label)
|
|
65
47
|
|
|
66
|
-
|
|
48
|
+
block.update(db_session=session, actor=actor)
|
|
49
|
+
return block.to_pydantic()
|
|
67
50
|
|
|
68
51
|
@enforce_types
|
|
69
52
|
def delete_block(self, block_id: str, actor: PydanticUser) -> PydanticBlock:
|
|
@@ -111,6 +94,15 @@ class BlockManager:
|
|
|
111
94
|
except NoResultFound:
|
|
112
95
|
return None
|
|
113
96
|
|
|
97
|
+
@enforce_types
|
|
98
|
+
def get_all_blocks_by_ids(self, block_ids: List[str], actor: Optional[PydanticUser] = None) -> List[PydanticBlock]:
|
|
99
|
+
# TODO: We can do this much more efficiently by listing, instead of executing individual queries per block_id
|
|
100
|
+
blocks = []
|
|
101
|
+
for block_id in block_ids:
|
|
102
|
+
block = self.get_block_by_id(block_id, actor=actor)
|
|
103
|
+
blocks.append(block)
|
|
104
|
+
return blocks
|
|
105
|
+
|
|
114
106
|
@enforce_types
|
|
115
107
|
def add_default_blocks(self, actor: PydanticUser):
|
|
116
108
|
for persona_file in list_persona_files():
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
|
|
3
|
+
from letta.orm.agent import Agent as AgentModel
|
|
4
|
+
from letta.orm.agents_tags import AgentsTags
|
|
5
|
+
from letta.orm.errors import NoResultFound
|
|
6
|
+
from letta.prompts import gpt_system
|
|
7
|
+
from letta.schemas.agent import AgentType
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Static methods
|
|
11
|
+
def _process_relationship(
|
|
12
|
+
session, agent: AgentModel, relationship_name: str, model_class, item_ids: List[str], allow_partial=False, replace=True
|
|
13
|
+
):
|
|
14
|
+
"""
|
|
15
|
+
Generalized function to handle relationships like tools, sources, and blocks using item IDs.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
session: The database session.
|
|
19
|
+
agent: The AgentModel instance.
|
|
20
|
+
relationship_name: The name of the relationship attribute (e.g., 'tools', 'sources').
|
|
21
|
+
model_class: The ORM class corresponding to the related items.
|
|
22
|
+
item_ids: List of IDs to set or update.
|
|
23
|
+
allow_partial: If True, allows missing items without raising errors.
|
|
24
|
+
replace: If True, replaces the entire relationship; otherwise, extends it.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: If `allow_partial` is False and some IDs are missing.
|
|
28
|
+
"""
|
|
29
|
+
current_relationship = getattr(agent, relationship_name, [])
|
|
30
|
+
if not item_ids:
|
|
31
|
+
if replace:
|
|
32
|
+
setattr(agent, relationship_name, [])
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
# Retrieve models for the provided IDs
|
|
36
|
+
found_items = session.query(model_class).filter(model_class.id.in_(item_ids)).all()
|
|
37
|
+
|
|
38
|
+
# Validate all items are found if allow_partial is False
|
|
39
|
+
if not allow_partial and len(found_items) != len(item_ids):
|
|
40
|
+
missing = set(item_ids) - {item.id for item in found_items}
|
|
41
|
+
raise NoResultFound(f"Items not found in {relationship_name}: {missing}")
|
|
42
|
+
|
|
43
|
+
if replace:
|
|
44
|
+
# Replace the relationship
|
|
45
|
+
setattr(agent, relationship_name, found_items)
|
|
46
|
+
else:
|
|
47
|
+
# Extend the relationship (only add new items)
|
|
48
|
+
current_ids = {item.id for item in current_relationship}
|
|
49
|
+
new_items = [item for item in found_items if item.id not in current_ids]
|
|
50
|
+
current_relationship.extend(new_items)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _process_tags(agent: AgentModel, tags: List[str], replace=True):
|
|
54
|
+
"""
|
|
55
|
+
Handles tags for an agent.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
agent: The AgentModel instance.
|
|
59
|
+
tags: List of tags to set or update.
|
|
60
|
+
replace: If True, replaces all tags; otherwise, extends them.
|
|
61
|
+
"""
|
|
62
|
+
if not tags:
|
|
63
|
+
if replace:
|
|
64
|
+
agent.tags = []
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# Ensure tags are unique and prepare for replacement/extension
|
|
68
|
+
new_tags = {AgentsTags(agent_id=agent.id, tag=tag) for tag in set(tags)}
|
|
69
|
+
if replace:
|
|
70
|
+
agent.tags = list(new_tags)
|
|
71
|
+
else:
|
|
72
|
+
existing_tags = {t.tag for t in agent.tags}
|
|
73
|
+
agent.tags.extend([tag for tag in new_tags if tag.tag not in existing_tags])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def derive_system_message(agent_type: AgentType, system: Optional[str] = None):
|
|
77
|
+
if system is None:
|
|
78
|
+
# TODO: don't hardcode
|
|
79
|
+
if agent_type == AgentType.memgpt_agent:
|
|
80
|
+
system = gpt_system.get_system_text("memgpt_chat")
|
|
81
|
+
elif agent_type == AgentType.o1_agent:
|
|
82
|
+
system = gpt_system.get_system_text("memgpt_modified_o1")
|
|
83
|
+
elif agent_type == AgentType.offline_memory_agent:
|
|
84
|
+
system = gpt_system.get_system_text("memgpt_offline_memory")
|
|
85
|
+
elif agent_type == AgentType.chat_only_agent:
|
|
86
|
+
system = gpt_system.get_system_text("memgpt_convo_only")
|
|
87
|
+
else:
|
|
88
|
+
raise ValueError(f"Invalid agent type: {agent_type}")
|
|
89
|
+
|
|
90
|
+
return system
|
|
@@ -13,7 +13,6 @@ class OrganizationManager:
|
|
|
13
13
|
DEFAULT_ORG_NAME = "default_org"
|
|
14
14
|
|
|
15
15
|
def __init__(self):
|
|
16
|
-
# This is probably horrible but we reuse this technique from metadata.py
|
|
17
16
|
# TODO: Please refactor this out
|
|
18
17
|
# I am currently working on a ORM refactor and would like to make a more minimal set of changes
|
|
19
18
|
# - Matt
|