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