letta-nightly 0.7.15.dev20250514104255__py3-none-any.whl → 0.7.16.dev20250515205957__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.
- letta/__init__.py +1 -1
- letta/agent.py +12 -0
- letta/agents/helpers.py +48 -5
- letta/agents/letta_agent.py +46 -18
- letta/agents/letta_agent_batch.py +44 -26
- letta/agents/voice_sleeptime_agent.py +6 -4
- letta/client/client.py +16 -1
- letta/constants.py +3 -0
- letta/functions/async_composio_toolset.py +1 -1
- letta/groups/sleeptime_multi_agent.py +1 -0
- letta/interfaces/anthropic_streaming_interface.py +40 -6
- letta/jobs/llm_batch_job_polling.py +6 -2
- letta/orm/agent.py +102 -1
- letta/orm/block.py +3 -0
- letta/orm/sqlalchemy_base.py +365 -133
- letta/schemas/agent.py +10 -2
- letta/schemas/block.py +3 -0
- letta/schemas/memory.py +7 -2
- letta/server/rest_api/routers/v1/agents.py +13 -13
- letta/server/rest_api/routers/v1/messages.py +6 -6
- letta/server/rest_api/routers/v1/tools.py +3 -3
- letta/server/server.py +74 -0
- letta/services/agent_manager.py +421 -7
- letta/services/block_manager.py +12 -8
- letta/services/helpers/agent_manager_helper.py +19 -0
- letta/services/job_manager.py +99 -0
- letta/services/llm_batch_manager.py +28 -27
- letta/services/message_manager.py +51 -19
- letta/services/tool_executor/tool_executor.py +19 -1
- letta/services/tool_manager.py +13 -3
- letta/types/__init__.py +0 -0
- {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/METADATA +3 -3
- {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/RECORD +36 -35
- {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.15.dev20250514104255.dist-info → letta_nightly-0.7.16.dev20250515205957.dist-info}/entry_points.txt +0 -0
letta/services/agent_manager.py
CHANGED
@@ -62,6 +62,7 @@ from letta.services.helpers.agent_manager_helper import (
|
|
62
62
|
_apply_filters,
|
63
63
|
_apply_identity_filters,
|
64
64
|
_apply_pagination,
|
65
|
+
_apply_pagination_async,
|
65
66
|
_apply_tag_filter,
|
66
67
|
_process_relationship,
|
67
68
|
check_supports_structured_output,
|
@@ -122,7 +123,35 @@ class AgentManager:
|
|
122
123
|
return name_to_id, id_to_name
|
123
124
|
|
124
125
|
@staticmethod
|
125
|
-
|
126
|
+
async def _resolve_tools_async(session, names: Set[str], ids: Set[str], org_id: str) -> Tuple[Dict[str, str], Dict[str, str]]:
|
127
|
+
"""
|
128
|
+
Bulk‑fetch all ToolModel rows matching either name ∈ names or id ∈ ids
|
129
|
+
(and scoped to this organization), and return two maps:
|
130
|
+
name_to_id, id_to_name.
|
131
|
+
Raises if any requested name or id was not found.
|
132
|
+
"""
|
133
|
+
stmt = select(ToolModel.id, ToolModel.name).where(
|
134
|
+
ToolModel.organization_id == org_id,
|
135
|
+
or_(
|
136
|
+
ToolModel.name.in_(names),
|
137
|
+
ToolModel.id.in_(ids),
|
138
|
+
),
|
139
|
+
)
|
140
|
+
result = await session.execute(stmt)
|
141
|
+
rows = result.fetchall() # Use fetchall()
|
142
|
+
name_to_id = {row[1]: row[0] for row in rows} # row[1] is name, row[0] is id
|
143
|
+
id_to_name = {row[0]: row[1] for row in rows} # row[0] is id, row[1] is name
|
144
|
+
|
145
|
+
missing_names = names - set(name_to_id.keys())
|
146
|
+
missing_ids = ids - set(id_to_name.keys())
|
147
|
+
if missing_names:
|
148
|
+
raise ValueError(f"Tools not found by name: {missing_names}")
|
149
|
+
if missing_ids:
|
150
|
+
raise ValueError(f"Tools not found by id: {missing_ids}")
|
151
|
+
|
152
|
+
return name_to_id, id_to_name
|
153
|
+
|
154
|
+
@staticmethod
|
126
155
|
def _bulk_insert_pivot(session, table, rows: list[dict]):
|
127
156
|
if not rows:
|
128
157
|
return
|
@@ -146,7 +175,29 @@ class AgentManager:
|
|
146
175
|
session.execute(stmt)
|
147
176
|
|
148
177
|
@staticmethod
|
149
|
-
|
178
|
+
async def _bulk_insert_pivot_async(session, table, rows: list[dict]):
|
179
|
+
if not rows:
|
180
|
+
return
|
181
|
+
|
182
|
+
dialect = session.bind.dialect.name
|
183
|
+
if dialect == "postgresql":
|
184
|
+
stmt = pg_insert(table).values(rows).on_conflict_do_nothing()
|
185
|
+
elif dialect == "sqlite":
|
186
|
+
stmt = sa.insert(table).values(rows).prefix_with("OR IGNORE")
|
187
|
+
else:
|
188
|
+
# fallback: filter out exact-duplicate dicts in Python
|
189
|
+
seen = set()
|
190
|
+
filtered = []
|
191
|
+
for row in rows:
|
192
|
+
key = tuple(sorted(row.items()))
|
193
|
+
if key not in seen:
|
194
|
+
seen.add(key)
|
195
|
+
filtered.append(row)
|
196
|
+
stmt = sa.insert(table).values(filtered)
|
197
|
+
|
198
|
+
await session.execute(stmt)
|
199
|
+
|
200
|
+
@staticmethod
|
150
201
|
def _replace_pivot_rows(session, table, agent_id: str, rows: list[dict]):
|
151
202
|
"""
|
152
203
|
Replace all pivot rows for an agent with *exactly* the provided list.
|
@@ -157,6 +208,17 @@ class AgentManager:
|
|
157
208
|
if rows:
|
158
209
|
AgentManager._bulk_insert_pivot(session, table, rows)
|
159
210
|
|
211
|
+
@staticmethod
|
212
|
+
async def _replace_pivot_rows_async(session, table, agent_id: str, rows: list[dict]):
|
213
|
+
"""
|
214
|
+
Replace all pivot rows for an agent with *exactly* the provided list.
|
215
|
+
Uses two bulk statements (DELETE + INSERT ... ON CONFLICT DO NOTHING).
|
216
|
+
"""
|
217
|
+
# delete all existing rows for this agent
|
218
|
+
await session.execute(delete(table).where(table.c.agent_id == agent_id))
|
219
|
+
if rows:
|
220
|
+
await AgentManager._bulk_insert_pivot_async(session, table, rows)
|
221
|
+
|
160
222
|
# ======================================================================================================================
|
161
223
|
# Basic CRUD operations
|
162
224
|
# ======================================================================================================================
|
@@ -252,6 +314,7 @@ class AgentManager:
|
|
252
314
|
session.flush()
|
253
315
|
aid = new_agent.id
|
254
316
|
|
317
|
+
# Note: These methods may need async versions if they perform database operations
|
255
318
|
self._bulk_insert_pivot(
|
256
319
|
session,
|
257
320
|
ToolsAgents.__table__,
|
@@ -259,10 +322,8 @@ class AgentManager:
|
|
259
322
|
)
|
260
323
|
|
261
324
|
if block_ids:
|
262
|
-
|
263
|
-
|
264
|
-
for bid, lbl in session.execute(select(BlockModel.id, BlockModel.label).where(BlockModel.id.in_(block_ids))).all()
|
265
|
-
]
|
325
|
+
result = session.execute(select(BlockModel.id, BlockModel.label).where(BlockModel.id.in_(block_ids)))
|
326
|
+
rows = [{"agent_id": aid, "block_id": bid, "block_label": lbl} for bid, lbl in result.all()]
|
266
327
|
self._bulk_insert_pivot(session, BlocksAgents.__table__, rows)
|
267
328
|
|
268
329
|
self._bulk_insert_pivot(
|
@@ -303,9 +364,162 @@ class AgentManager:
|
|
303
364
|
|
304
365
|
session.refresh(new_agent)
|
305
366
|
|
367
|
+
# Using the synchronous version since we don't have an async version yet
|
368
|
+
# If you implement an async version of create_many_messages, you can switch to that
|
306
369
|
self.message_manager.create_many_messages(pydantic_msgs=init_messages, actor=actor)
|
307
370
|
return new_agent.to_pydantic()
|
308
371
|
|
372
|
+
@trace_method
|
373
|
+
async def create_agent_async(
|
374
|
+
self, agent_create: CreateAgent, actor: PydanticUser, _test_only_force_id: Optional[str] = None
|
375
|
+
) -> PydanticAgentState:
|
376
|
+
# validate required configs
|
377
|
+
if not agent_create.llm_config or not agent_create.embedding_config:
|
378
|
+
raise ValueError("llm_config and embedding_config are required")
|
379
|
+
|
380
|
+
# blocks
|
381
|
+
block_ids = list(agent_create.block_ids or [])
|
382
|
+
if agent_create.memory_blocks:
|
383
|
+
pydantic_blocks = [PydanticBlock(**b.model_dump(to_orm=True)) for b in agent_create.memory_blocks]
|
384
|
+
created_blocks = self.block_manager.batch_create_blocks(
|
385
|
+
pydantic_blocks,
|
386
|
+
actor=actor,
|
387
|
+
)
|
388
|
+
block_ids.extend([blk.id for blk in created_blocks])
|
389
|
+
|
390
|
+
# tools
|
391
|
+
tool_names = set(agent_create.tools or [])
|
392
|
+
if agent_create.include_base_tools:
|
393
|
+
if agent_create.agent_type == AgentType.voice_sleeptime_agent:
|
394
|
+
tool_names |= set(BASE_VOICE_SLEEPTIME_TOOLS)
|
395
|
+
elif agent_create.agent_type == AgentType.voice_convo_agent:
|
396
|
+
tool_names |= set(BASE_VOICE_SLEEPTIME_CHAT_TOOLS)
|
397
|
+
elif agent_create.agent_type == AgentType.sleeptime_agent:
|
398
|
+
tool_names |= set(BASE_SLEEPTIME_TOOLS)
|
399
|
+
elif agent_create.enable_sleeptime:
|
400
|
+
tool_names |= set(BASE_SLEEPTIME_CHAT_TOOLS)
|
401
|
+
else:
|
402
|
+
tool_names |= set(BASE_TOOLS + BASE_MEMORY_TOOLS)
|
403
|
+
if agent_create.include_multi_agent_tools:
|
404
|
+
tool_names |= set(MULTI_AGENT_TOOLS)
|
405
|
+
|
406
|
+
supplied_ids = set(agent_create.tool_ids or [])
|
407
|
+
|
408
|
+
source_ids = agent_create.source_ids or []
|
409
|
+
identity_ids = agent_create.identity_ids or []
|
410
|
+
tag_values = agent_create.tags or []
|
411
|
+
|
412
|
+
async with db_registry.async_session() as session:
|
413
|
+
async with session.begin():
|
414
|
+
# Note: This will need to be modified if _resolve_tools needs an async version
|
415
|
+
name_to_id, id_to_name = await self._resolve_tools_async(
|
416
|
+
session,
|
417
|
+
tool_names,
|
418
|
+
supplied_ids,
|
419
|
+
actor.organization_id,
|
420
|
+
)
|
421
|
+
|
422
|
+
tool_ids = set(name_to_id.values()) | set(id_to_name.keys())
|
423
|
+
tool_names = set(name_to_id.keys()) # now canonical
|
424
|
+
|
425
|
+
tool_rules = list(agent_create.tool_rules or [])
|
426
|
+
if agent_create.include_base_tool_rules:
|
427
|
+
for tn in tool_names:
|
428
|
+
if tn in {"send_message", "send_message_to_agent_async", "memory_finish_edits"}:
|
429
|
+
tool_rules.append(TerminalToolRule(tool_name=tn))
|
430
|
+
elif tn in (BASE_TOOLS + BASE_MEMORY_TOOLS + BASE_SLEEPTIME_TOOLS):
|
431
|
+
tool_rules.append(ContinueToolRule(tool_name=tn))
|
432
|
+
|
433
|
+
if tool_rules:
|
434
|
+
check_supports_structured_output(model=agent_create.llm_config.model, tool_rules=tool_rules)
|
435
|
+
|
436
|
+
new_agent = AgentModel(
|
437
|
+
name=agent_create.name,
|
438
|
+
system=derive_system_message(
|
439
|
+
agent_type=agent_create.agent_type,
|
440
|
+
enable_sleeptime=agent_create.enable_sleeptime,
|
441
|
+
system=agent_create.system,
|
442
|
+
),
|
443
|
+
agent_type=agent_create.agent_type,
|
444
|
+
llm_config=agent_create.llm_config,
|
445
|
+
embedding_config=agent_create.embedding_config,
|
446
|
+
organization_id=actor.organization_id,
|
447
|
+
description=agent_create.description,
|
448
|
+
metadata_=agent_create.metadata,
|
449
|
+
tool_rules=tool_rules,
|
450
|
+
project_id=agent_create.project_id,
|
451
|
+
template_id=agent_create.template_id,
|
452
|
+
base_template_id=agent_create.base_template_id,
|
453
|
+
message_buffer_autoclear=agent_create.message_buffer_autoclear,
|
454
|
+
enable_sleeptime=agent_create.enable_sleeptime,
|
455
|
+
response_format=agent_create.response_format,
|
456
|
+
created_by_id=actor.id,
|
457
|
+
last_updated_by_id=actor.id,
|
458
|
+
)
|
459
|
+
|
460
|
+
if _test_only_force_id:
|
461
|
+
new_agent.id = _test_only_force_id
|
462
|
+
|
463
|
+
session.add(new_agent)
|
464
|
+
await session.flush()
|
465
|
+
aid = new_agent.id
|
466
|
+
|
467
|
+
# Note: These methods may need async versions if they perform database operations
|
468
|
+
await self._bulk_insert_pivot_async(
|
469
|
+
session,
|
470
|
+
ToolsAgents.__table__,
|
471
|
+
[{"agent_id": aid, "tool_id": tid} for tid in tool_ids],
|
472
|
+
)
|
473
|
+
|
474
|
+
if block_ids:
|
475
|
+
result = await session.execute(select(BlockModel.id, BlockModel.label).where(BlockModel.id.in_(block_ids)))
|
476
|
+
rows = [{"agent_id": aid, "block_id": bid, "block_label": lbl} for bid, lbl in result.all()]
|
477
|
+
await self._bulk_insert_pivot_async(session, BlocksAgents.__table__, rows)
|
478
|
+
|
479
|
+
await self._bulk_insert_pivot_async(
|
480
|
+
session,
|
481
|
+
SourcesAgents.__table__,
|
482
|
+
[{"agent_id": aid, "source_id": sid} for sid in source_ids],
|
483
|
+
)
|
484
|
+
await self._bulk_insert_pivot_async(
|
485
|
+
session,
|
486
|
+
AgentsTags.__table__,
|
487
|
+
[{"agent_id": aid, "tag": tag} for tag in tag_values],
|
488
|
+
)
|
489
|
+
await self._bulk_insert_pivot_async(
|
490
|
+
session,
|
491
|
+
IdentitiesAgents.__table__,
|
492
|
+
[{"agent_id": aid, "identity_id": iid} for iid in identity_ids],
|
493
|
+
)
|
494
|
+
|
495
|
+
if agent_create.tool_exec_environment_variables:
|
496
|
+
env_rows = [
|
497
|
+
{
|
498
|
+
"agent_id": aid,
|
499
|
+
"key": key,
|
500
|
+
"value": val,
|
501
|
+
"organization_id": actor.organization_id,
|
502
|
+
}
|
503
|
+
for key, val in agent_create.tool_exec_environment_variables.items()
|
504
|
+
]
|
505
|
+
await session.execute(insert(AgentEnvironmentVariable).values(env_rows))
|
506
|
+
|
507
|
+
# initial message sequence
|
508
|
+
agent_state = await new_agent.to_pydantic_async(include_relationships={"memory"})
|
509
|
+
init_messages = self._generate_initial_message_sequence(
|
510
|
+
actor,
|
511
|
+
agent_state=agent_state,
|
512
|
+
supplied_initial_message_sequence=agent_create.initial_message_sequence,
|
513
|
+
)
|
514
|
+
new_agent.message_ids = [msg.id for msg in init_messages]
|
515
|
+
|
516
|
+
await session.refresh(new_agent)
|
517
|
+
|
518
|
+
# Using the synchronous version since we don't have an async version yet
|
519
|
+
# If you implement an async version of create_many_messages, you can switch to that
|
520
|
+
await self.message_manager.create_many_messages_async(pydantic_msgs=init_messages, actor=actor)
|
521
|
+
return await new_agent.to_pydantic_async()
|
522
|
+
|
309
523
|
@enforce_types
|
310
524
|
def _generate_initial_message_sequence(
|
311
525
|
self, actor: PydanticUser, agent_state: PydanticAgentState, supplied_initial_message_sequence: Optional[List[MessageCreate]] = None
|
@@ -459,6 +673,123 @@ class AgentManager:
|
|
459
673
|
|
460
674
|
return agent.to_pydantic()
|
461
675
|
|
676
|
+
@enforce_types
|
677
|
+
async def update_agent_async(
|
678
|
+
self,
|
679
|
+
agent_id: str,
|
680
|
+
agent_update: UpdateAgent,
|
681
|
+
actor: PydanticUser,
|
682
|
+
) -> PydanticAgentState:
|
683
|
+
|
684
|
+
new_tools = set(agent_update.tool_ids or [])
|
685
|
+
new_sources = set(agent_update.source_ids or [])
|
686
|
+
new_blocks = set(agent_update.block_ids or [])
|
687
|
+
new_idents = set(agent_update.identity_ids or [])
|
688
|
+
new_tags = set(agent_update.tags or [])
|
689
|
+
|
690
|
+
async with db_registry.async_session() as session, session.begin():
|
691
|
+
|
692
|
+
agent: AgentModel = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
|
693
|
+
agent.updated_at = datetime.now(timezone.utc)
|
694
|
+
agent.last_updated_by_id = actor.id
|
695
|
+
|
696
|
+
scalar_updates = {
|
697
|
+
"name": agent_update.name,
|
698
|
+
"system": agent_update.system,
|
699
|
+
"llm_config": agent_update.llm_config,
|
700
|
+
"embedding_config": agent_update.embedding_config,
|
701
|
+
"message_ids": agent_update.message_ids,
|
702
|
+
"tool_rules": agent_update.tool_rules,
|
703
|
+
"description": agent_update.description,
|
704
|
+
"project_id": agent_update.project_id,
|
705
|
+
"template_id": agent_update.template_id,
|
706
|
+
"base_template_id": agent_update.base_template_id,
|
707
|
+
"message_buffer_autoclear": agent_update.message_buffer_autoclear,
|
708
|
+
"enable_sleeptime": agent_update.enable_sleeptime,
|
709
|
+
"response_format": agent_update.response_format,
|
710
|
+
}
|
711
|
+
for col, val in scalar_updates.items():
|
712
|
+
if val is not None:
|
713
|
+
setattr(agent, col, val)
|
714
|
+
|
715
|
+
if agent_update.metadata is not None:
|
716
|
+
agent.metadata_ = agent_update.metadata
|
717
|
+
|
718
|
+
aid = agent.id
|
719
|
+
|
720
|
+
if agent_update.tool_ids is not None:
|
721
|
+
await self._replace_pivot_rows_async(
|
722
|
+
session,
|
723
|
+
ToolsAgents.__table__,
|
724
|
+
aid,
|
725
|
+
[{"agent_id": aid, "tool_id": tid} for tid in new_tools],
|
726
|
+
)
|
727
|
+
session.expire(agent, ["tools"])
|
728
|
+
|
729
|
+
if agent_update.source_ids is not None:
|
730
|
+
await self._replace_pivot_rows_async(
|
731
|
+
session,
|
732
|
+
SourcesAgents.__table__,
|
733
|
+
aid,
|
734
|
+
[{"agent_id": aid, "source_id": sid} for sid in new_sources],
|
735
|
+
)
|
736
|
+
session.expire(agent, ["sources"])
|
737
|
+
|
738
|
+
if agent_update.block_ids is not None:
|
739
|
+
rows = []
|
740
|
+
if new_blocks:
|
741
|
+
result = await session.execute(select(BlockModel.id, BlockModel.label).where(BlockModel.id.in_(new_blocks)))
|
742
|
+
label_map = {bid: lbl for bid, lbl in result.all()}
|
743
|
+
rows = [{"agent_id": aid, "block_id": bid, "block_label": label_map[bid]} for bid in new_blocks]
|
744
|
+
|
745
|
+
await self._replace_pivot_rows_async(session, BlocksAgents.__table__, aid, rows)
|
746
|
+
session.expire(agent, ["core_memory"])
|
747
|
+
|
748
|
+
if agent_update.identity_ids is not None:
|
749
|
+
await self._replace_pivot_rows_async(
|
750
|
+
session,
|
751
|
+
IdentitiesAgents.__table__,
|
752
|
+
aid,
|
753
|
+
[{"agent_id": aid, "identity_id": iid} for iid in new_idents],
|
754
|
+
)
|
755
|
+
session.expire(agent, ["identities"])
|
756
|
+
|
757
|
+
if agent_update.tags is not None:
|
758
|
+
await self._replace_pivot_rows_async(
|
759
|
+
session,
|
760
|
+
AgentsTags.__table__,
|
761
|
+
aid,
|
762
|
+
[{"agent_id": aid, "tag": tag} for tag in new_tags],
|
763
|
+
)
|
764
|
+
session.expire(agent, ["tags"])
|
765
|
+
|
766
|
+
if agent_update.tool_exec_environment_variables is not None:
|
767
|
+
await session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
|
768
|
+
env_rows = [
|
769
|
+
{
|
770
|
+
"agent_id": aid,
|
771
|
+
"key": k,
|
772
|
+
"value": v,
|
773
|
+
"organization_id": agent.organization_id,
|
774
|
+
}
|
775
|
+
for k, v in agent_update.tool_exec_environment_variables.items()
|
776
|
+
]
|
777
|
+
if env_rows:
|
778
|
+
await self._bulk_insert_pivot_async(session, AgentEnvironmentVariable.__table__, env_rows)
|
779
|
+
session.expire(agent, ["tool_exec_environment_variables"])
|
780
|
+
|
781
|
+
if agent_update.enable_sleeptime and agent_update.system is None:
|
782
|
+
agent.system = derive_system_message(
|
783
|
+
agent_type=agent.agent_type,
|
784
|
+
enable_sleeptime=agent_update.enable_sleeptime,
|
785
|
+
system=agent.system,
|
786
|
+
)
|
787
|
+
|
788
|
+
await session.flush()
|
789
|
+
await session.refresh(agent)
|
790
|
+
|
791
|
+
return await agent.to_pydantic_async()
|
792
|
+
|
462
793
|
# TODO: Make this general and think about how to roll this into sqlalchemybase
|
463
794
|
def list_agents(
|
464
795
|
self,
|
@@ -514,9 +845,73 @@ class AgentManager:
|
|
514
845
|
if limit:
|
515
846
|
query = query.limit(limit)
|
516
847
|
|
517
|
-
|
848
|
+
result = session.execute(query)
|
849
|
+
agents = result.scalars().all()
|
518
850
|
return [agent.to_pydantic(include_relationships=include_relationships) for agent in agents]
|
519
851
|
|
852
|
+
async def list_agents_async(
|
853
|
+
self,
|
854
|
+
actor: PydanticUser,
|
855
|
+
name: Optional[str] = None,
|
856
|
+
tags: Optional[List[str]] = None,
|
857
|
+
match_all_tags: bool = False,
|
858
|
+
before: Optional[str] = None,
|
859
|
+
after: Optional[str] = None,
|
860
|
+
limit: Optional[int] = 50,
|
861
|
+
query_text: Optional[str] = None,
|
862
|
+
project_id: Optional[str] = None,
|
863
|
+
template_id: Optional[str] = None,
|
864
|
+
base_template_id: Optional[str] = None,
|
865
|
+
identity_id: Optional[str] = None,
|
866
|
+
identifier_keys: Optional[List[str]] = None,
|
867
|
+
include_relationships: Optional[List[str]] = None,
|
868
|
+
ascending: bool = True,
|
869
|
+
) -> List[PydanticAgentState]:
|
870
|
+
"""
|
871
|
+
Retrieves agents with optimized filtering and optional field selection.
|
872
|
+
|
873
|
+
Args:
|
874
|
+
actor: The User requesting the list
|
875
|
+
name (Optional[str]): Filter by agent name.
|
876
|
+
tags (Optional[List[str]]): Filter agents by tags.
|
877
|
+
match_all_tags (bool): If True, only return agents that match ALL given tags.
|
878
|
+
before (Optional[str]): Cursor for pagination.
|
879
|
+
after (Optional[str]): Cursor for pagination.
|
880
|
+
limit (Optional[int]): Maximum number of agents to return.
|
881
|
+
query_text (Optional[str]): Search agents by name.
|
882
|
+
project_id (Optional[str]): Filter by project ID.
|
883
|
+
template_id (Optional[str]): Filter by template ID.
|
884
|
+
base_template_id (Optional[str]): Filter by base template ID.
|
885
|
+
identity_id (Optional[str]): Filter by identifier ID.
|
886
|
+
identifier_keys (Optional[List[str]]): Search agents by identifier keys.
|
887
|
+
include_relationships (Optional[List[str]]): List of fields to load for performance optimization.
|
888
|
+
ascending
|
889
|
+
|
890
|
+
Returns:
|
891
|
+
List[PydanticAgentState]: The filtered list of matching agents.
|
892
|
+
"""
|
893
|
+
async with db_registry.async_session() as session:
|
894
|
+
query = select(AgentModel).distinct(AgentModel.created_at, AgentModel.id)
|
895
|
+
query = AgentModel.apply_access_predicate(query, actor, ["read"], AccessType.ORGANIZATION)
|
896
|
+
|
897
|
+
# Apply filters
|
898
|
+
query = _apply_filters(query, name, query_text, project_id, template_id, base_template_id)
|
899
|
+
query = _apply_identity_filters(query, identity_id, identifier_keys)
|
900
|
+
query = _apply_tag_filter(query, tags, match_all_tags)
|
901
|
+
query = await _apply_pagination_async(query, before, after, session, ascending=ascending)
|
902
|
+
|
903
|
+
if limit:
|
904
|
+
query = query.limit(limit)
|
905
|
+
|
906
|
+
result = await session.execute(query)
|
907
|
+
agents = result.scalars().all()
|
908
|
+
pydantic_agents = []
|
909
|
+
for agent in agents:
|
910
|
+
pydantic_agent = await agent.to_pydantic_async(include_relationships=include_relationships)
|
911
|
+
pydantic_agents.append(pydantic_agent)
|
912
|
+
|
913
|
+
return pydantic_agents
|
914
|
+
|
520
915
|
@enforce_types
|
521
916
|
def list_agents_matching_tags(
|
522
917
|
self,
|
@@ -577,6 +972,20 @@ class AgentManager:
|
|
577
972
|
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
578
973
|
return agent.to_pydantic()
|
579
974
|
|
975
|
+
@enforce_types
|
976
|
+
async def get_agent_by_id_async(self, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
|
977
|
+
"""Fetch an agent by its ID."""
|
978
|
+
async with db_registry.async_session() as session:
|
979
|
+
agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
|
980
|
+
return agent.to_pydantic()
|
981
|
+
|
982
|
+
@enforce_types
|
983
|
+
async def get_agents_by_ids_async(self, agent_ids: list[str], actor: PydanticUser) -> list[PydanticAgentState]:
|
984
|
+
"""Fetch a list of agents by their IDs."""
|
985
|
+
async with db_registry.async_session() as session:
|
986
|
+
agents = await AgentModel.read_multiple_async(db_session=session, identifiers=agent_ids, actor=actor)
|
987
|
+
return [await agent.to_pydantic_async() for agent in agents]
|
988
|
+
|
580
989
|
@enforce_types
|
581
990
|
def get_agent_by_name(self, agent_name: str, actor: PydanticUser) -> PydanticAgentState:
|
582
991
|
"""Fetch an agent by its ID."""
|
@@ -784,6 +1193,11 @@ class AgentManager:
|
|
784
1193
|
message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
|
785
1194
|
return self.message_manager.get_messages_by_ids(message_ids=message_ids, actor=actor)
|
786
1195
|
|
1196
|
+
@enforce_types
|
1197
|
+
async def get_in_context_messages_async(self, agent_id: str, actor: PydanticUser) -> List[PydanticMessage]:
|
1198
|
+
message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
|
1199
|
+
return await self.message_manager.get_messages_by_ids_async(message_ids=message_ids, actor=actor)
|
1200
|
+
|
787
1201
|
@enforce_types
|
788
1202
|
def get_system_message(self, agent_id: str, actor: PydanticUser) -> PydanticMessage:
|
789
1203
|
message_ids = self.get_agent_by_id(agent_id=agent_id, actor=actor).message_ids
|
letta/services/block_manager.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
from typing import Dict, List, Optional
|
2
2
|
|
3
|
+
from sqlalchemy import select
|
3
4
|
from sqlalchemy.orm import Session
|
4
5
|
|
5
6
|
from letta.log import get_logger
|
@@ -454,7 +455,7 @@ class BlockManager:
|
|
454
455
|
return block.to_pydantic()
|
455
456
|
|
456
457
|
@enforce_types
|
457
|
-
def
|
458
|
+
async def bulk_update_block_values_async(
|
458
459
|
self, updates: Dict[str, str], actor: PydanticUser, return_hydrated: bool = False
|
459
460
|
) -> Optional[List[PydanticBlock]]:
|
460
461
|
"""
|
@@ -469,12 +470,13 @@ class BlockManager:
|
|
469
470
|
the updated Block objects as Pydantic schemas
|
470
471
|
|
471
472
|
Raises:
|
472
|
-
NoResultFound if any block_id doesn
|
473
|
-
ValueError if any new value exceeds its block
|
473
|
+
NoResultFound if any block_id doesn't exist or isn't visible to this actor
|
474
|
+
ValueError if any new value exceeds its block's limit
|
474
475
|
"""
|
475
|
-
with db_registry.
|
476
|
-
|
477
|
-
|
476
|
+
async with db_registry.async_session() as session:
|
477
|
+
query = select(BlockModel).where(BlockModel.id.in_(updates.keys()), BlockModel.organization_id == actor.organization_id)
|
478
|
+
result = await session.execute(query)
|
479
|
+
blocks = result.scalars().all()
|
478
480
|
|
479
481
|
found_ids = {b.id for b in blocks}
|
480
482
|
missing = set(updates.keys()) - found_ids
|
@@ -488,8 +490,10 @@ class BlockManager:
|
|
488
490
|
new_val = new_val[: block.limit]
|
489
491
|
block.value = new_val
|
490
492
|
|
491
|
-
session.commit()
|
493
|
+
await session.commit()
|
492
494
|
|
493
495
|
if return_hydrated:
|
494
|
-
|
496
|
+
# TODO: implement for async
|
497
|
+
pass
|
498
|
+
|
495
499
|
return None
|
@@ -402,6 +402,25 @@ def _apply_pagination(query, before: Optional[str], after: Optional[str], sessio
|
|
402
402
|
return query
|
403
403
|
|
404
404
|
|
405
|
+
async def _apply_pagination_async(query, before: Optional[str], after: Optional[str], session, ascending: bool = True) -> any:
|
406
|
+
if after:
|
407
|
+
result = (await session.execute(select(AgentModel.created_at, AgentModel.id).where(AgentModel.id == after))).first()
|
408
|
+
if result:
|
409
|
+
after_created_at, after_id = result
|
410
|
+
query = query.where(_cursor_filter(AgentModel.created_at, AgentModel.id, after_created_at, after_id, forward=ascending))
|
411
|
+
|
412
|
+
if before:
|
413
|
+
result = (await session.execute(select(AgentModel.created_at, AgentModel.id).where(AgentModel.id == before))).first()
|
414
|
+
if result:
|
415
|
+
before_created_at, before_id = result
|
416
|
+
query = query.where(_cursor_filter(AgentModel.created_at, AgentModel.id, before_created_at, before_id, forward=not ascending))
|
417
|
+
|
418
|
+
# Apply ordering
|
419
|
+
order_fn = asc if ascending else desc
|
420
|
+
query = query.order_by(order_fn(AgentModel.created_at), order_fn(AgentModel.id))
|
421
|
+
return query
|
422
|
+
|
423
|
+
|
405
424
|
def _apply_tag_filter(query, tags: Optional[List[str]], match_all_tags: bool):
|
406
425
|
"""
|
407
426
|
Apply tag-based filtering to the agent query.
|