letta-nightly 0.8.15.dev20250720104313__py3-none-any.whl → 0.8.16.dev20250721070720__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 +27 -11
- letta/agents/helpers.py +1 -1
- letta/agents/letta_agent.py +518 -322
- letta/agents/letta_agent_batch.py +1 -2
- letta/agents/voice_agent.py +15 -17
- letta/client/client.py +3 -3
- letta/constants.py +5 -0
- letta/embeddings.py +0 -2
- letta/errors.py +8 -0
- letta/functions/function_sets/base.py +3 -3
- letta/functions/helpers.py +2 -3
- letta/groups/sleeptime_multi_agent.py +0 -1
- letta/helpers/composio_helpers.py +2 -2
- letta/helpers/converters.py +1 -1
- letta/helpers/pinecone_utils.py +8 -0
- letta/helpers/tool_rule_solver.py +13 -18
- letta/llm_api/aws_bedrock.py +16 -2
- letta/llm_api/cohere.py +1 -1
- letta/llm_api/openai_client.py +1 -1
- letta/local_llm/grammars/gbnf_grammar_generator.py +1 -1
- letta/local_llm/llm_chat_completion_wrappers/zephyr.py +14 -14
- letta/local_llm/utils.py +1 -2
- letta/orm/agent.py +3 -3
- letta/orm/block.py +4 -4
- letta/orm/files_agents.py +0 -1
- letta/orm/identity.py +2 -0
- letta/orm/mcp_server.py +0 -2
- letta/orm/message.py +140 -14
- letta/orm/organization.py +5 -5
- letta/orm/passage.py +4 -4
- letta/orm/source.py +1 -1
- letta/orm/sqlalchemy_base.py +61 -39
- letta/orm/step.py +2 -0
- letta/otel/db_pool_monitoring.py +308 -0
- letta/otel/metric_registry.py +94 -1
- letta/otel/sqlalchemy_instrumentation.py +548 -0
- letta/otel/sqlalchemy_instrumentation_integration.py +124 -0
- letta/otel/tracing.py +37 -1
- letta/schemas/agent.py +0 -3
- letta/schemas/agent_file.py +283 -0
- letta/schemas/block.py +0 -3
- letta/schemas/file.py +28 -26
- letta/schemas/letta_message.py +15 -4
- letta/schemas/memory.py +1 -1
- letta/schemas/message.py +31 -26
- letta/schemas/openai/chat_completion_response.py +0 -1
- letta/schemas/providers.py +20 -0
- letta/schemas/source.py +11 -13
- letta/schemas/step.py +12 -0
- letta/schemas/tool.py +0 -4
- letta/serialize_schemas/marshmallow_agent.py +14 -1
- letta/serialize_schemas/marshmallow_block.py +23 -1
- letta/serialize_schemas/marshmallow_message.py +1 -3
- letta/serialize_schemas/marshmallow_tool.py +23 -1
- letta/server/db.py +110 -6
- letta/server/rest_api/app.py +85 -73
- letta/server/rest_api/routers/v1/agents.py +68 -53
- letta/server/rest_api/routers/v1/blocks.py +2 -2
- letta/server/rest_api/routers/v1/jobs.py +3 -0
- letta/server/rest_api/routers/v1/organizations.py +2 -2
- letta/server/rest_api/routers/v1/sources.py +18 -2
- letta/server/rest_api/routers/v1/tools.py +11 -12
- letta/server/rest_api/routers/v1/users.py +1 -1
- letta/server/rest_api/streaming_response.py +13 -5
- letta/server/rest_api/utils.py +8 -25
- letta/server/server.py +11 -4
- letta/server/ws_api/server.py +2 -2
- letta/services/agent_file_manager.py +616 -0
- letta/services/agent_manager.py +133 -46
- letta/services/block_manager.py +38 -17
- letta/services/file_manager.py +106 -21
- letta/services/file_processor/file_processor.py +93 -0
- letta/services/files_agents_manager.py +28 -0
- letta/services/group_manager.py +4 -5
- letta/services/helpers/agent_manager_helper.py +57 -9
- letta/services/identity_manager.py +22 -0
- letta/services/job_manager.py +210 -91
- letta/services/llm_batch_manager.py +9 -6
- letta/services/mcp/stdio_client.py +1 -2
- letta/services/mcp_manager.py +0 -1
- letta/services/message_manager.py +49 -26
- letta/services/passage_manager.py +0 -1
- letta/services/provider_manager.py +1 -1
- letta/services/source_manager.py +114 -5
- letta/services/step_manager.py +36 -4
- letta/services/telemetry_manager.py +9 -2
- letta/services/tool_executor/builtin_tool_executor.py +5 -1
- letta/services/tool_executor/core_tool_executor.py +3 -3
- letta/services/tool_manager.py +95 -20
- letta/services/user_manager.py +4 -12
- letta/settings.py +23 -6
- letta/system.py +1 -1
- letta/utils.py +26 -2
- {letta_nightly-0.8.15.dev20250720104313.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/METADATA +3 -2
- {letta_nightly-0.8.15.dev20250720104313.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/RECORD +99 -94
- {letta_nightly-0.8.15.dev20250720104313.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.15.dev20250720104313.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.15.dev20250720104313.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/entry_points.txt +0 -0
letta/services/agent_manager.py
CHANGED
@@ -28,7 +28,7 @@ from letta.orm import AgentPassage, AgentsTags
|
|
28
28
|
from letta.orm import Block as BlockModel
|
29
29
|
from letta.orm import BlocksAgents
|
30
30
|
from letta.orm import Group as GroupModel
|
31
|
-
from letta.orm import IdentitiesAgents
|
31
|
+
from letta.orm import GroupsAgents, IdentitiesAgents
|
32
32
|
from letta.orm import Source as SourceModel
|
33
33
|
from letta.orm import SourcePassage, SourcesAgents
|
34
34
|
from letta.orm import Tool as ToolModel
|
@@ -71,6 +71,7 @@ from letta.services.helpers.agent_manager_helper import (
|
|
71
71
|
_apply_identity_filters,
|
72
72
|
_apply_pagination,
|
73
73
|
_apply_pagination_async,
|
74
|
+
_apply_relationship_filters,
|
74
75
|
_apply_tag_filter,
|
75
76
|
_process_relationship,
|
76
77
|
_process_relationship_async,
|
@@ -84,12 +85,14 @@ from letta.services.helpers.agent_manager_helper import (
|
|
84
85
|
derive_system_message,
|
85
86
|
initialize_message_sequence,
|
86
87
|
package_initial_message_sequence,
|
88
|
+
validate_agent_exists_async,
|
87
89
|
)
|
88
90
|
from letta.services.identity_manager import IdentityManager
|
89
91
|
from letta.services.message_manager import MessageManager
|
90
92
|
from letta.services.passage_manager import PassageManager
|
91
93
|
from letta.services.source_manager import SourceManager
|
92
94
|
from letta.services.tool_manager import ToolManager
|
95
|
+
from letta.settings import DatabaseChoice, settings
|
93
96
|
from letta.utils import enforce_types, united_diff
|
94
97
|
|
95
98
|
logger = get_logger(__name__)
|
@@ -448,7 +451,11 @@ class AgentManager:
|
|
448
451
|
|
449
452
|
@trace_method
|
450
453
|
async def create_agent_async(
|
451
|
-
self,
|
454
|
+
self,
|
455
|
+
agent_create: CreateAgent,
|
456
|
+
actor: PydanticUser,
|
457
|
+
_test_only_force_id: Optional[str] = None,
|
458
|
+
_init_with_no_messages: bool = False,
|
452
459
|
) -> PydanticAgentState:
|
453
460
|
# validate required configs
|
454
461
|
if not agent_create.llm_config or not agent_create.embedding_config:
|
@@ -616,20 +623,36 @@ class AgentManager:
|
|
616
623
|
]
|
617
624
|
await session.execute(insert(AgentEnvironmentVariable).values(env_rows))
|
618
625
|
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
626
|
+
include_relationships = []
|
627
|
+
if tool_ids:
|
628
|
+
include_relationships.append("tools")
|
629
|
+
if source_ids:
|
630
|
+
include_relationships.append("sources")
|
631
|
+
if block_ids:
|
632
|
+
include_relationships.append("memory")
|
633
|
+
if identity_ids:
|
634
|
+
include_relationships.append("identity_ids")
|
635
|
+
if tag_values:
|
636
|
+
include_relationships.append("tags")
|
637
|
+
|
638
|
+
result = await new_agent.to_pydantic_async(include_relationships=include_relationships)
|
639
|
+
|
640
|
+
# initial message sequence (skip if _init_with_no_messages is True)
|
641
|
+
if not _init_with_no_messages:
|
642
|
+
init_messages = self._generate_initial_message_sequence(
|
643
|
+
actor,
|
644
|
+
agent_state=result,
|
645
|
+
supplied_initial_message_sequence=agent_create.initial_message_sequence,
|
646
|
+
)
|
647
|
+
result.message_ids = [msg.id for msg in init_messages]
|
648
|
+
new_agent.message_ids = [msg.id for msg in init_messages]
|
649
|
+
await new_agent.update_async(session, no_refresh=True)
|
650
|
+
else:
|
651
|
+
init_messages = []
|
631
652
|
|
632
|
-
|
653
|
+
# Only create messages if we initialized with messages
|
654
|
+
if not _init_with_no_messages:
|
655
|
+
await self.message_manager.create_many_messages_async(pydantic_msgs=init_messages, actor=actor)
|
633
656
|
return result
|
634
657
|
|
635
658
|
@enforce_types
|
@@ -920,6 +943,30 @@ class AgentManager:
|
|
920
943
|
|
921
944
|
return await agent.to_pydantic_async()
|
922
945
|
|
946
|
+
@enforce_types
|
947
|
+
@trace_method
|
948
|
+
async def update_message_ids_async(
|
949
|
+
self,
|
950
|
+
agent_id: str,
|
951
|
+
message_ids: List[str],
|
952
|
+
actor: PydanticUser,
|
953
|
+
) -> None:
|
954
|
+
async with db_registry.async_session() as session:
|
955
|
+
query = select(AgentModel)
|
956
|
+
query = AgentModel.apply_access_predicate(query, actor, ["read"], AccessType.ORGANIZATION)
|
957
|
+
query = query.where(AgentModel.id == agent_id)
|
958
|
+
query = _apply_relationship_filters(query, include_relationships=[])
|
959
|
+
|
960
|
+
result = await session.execute(query)
|
961
|
+
agent = result.scalar_one_or_none()
|
962
|
+
|
963
|
+
agent.updated_at = datetime.now(timezone.utc)
|
964
|
+
agent.last_updated_by_id = actor.id
|
965
|
+
agent.message_ids = message_ids
|
966
|
+
|
967
|
+
await agent.update_async(db_session=session, actor=actor, no_commit=True, no_refresh=True)
|
968
|
+
await session.commit()
|
969
|
+
|
923
970
|
# TODO: Make this general and think about how to roll this into sqlalchemybase
|
924
971
|
@trace_method
|
925
972
|
def list_agents(
|
@@ -1019,7 +1066,8 @@ class AgentManager:
|
|
1019
1066
|
identity_id (Optional[str]): Filter by identifier ID.
|
1020
1067
|
identifier_keys (Optional[List[str]]): Search agents by identifier keys.
|
1021
1068
|
include_relationships (Optional[List[str]]): List of fields to load for performance optimization.
|
1022
|
-
ascending
|
1069
|
+
ascending (bool): Sort agents in ascending order.
|
1070
|
+
sort_by (Optional[str]): Sort agents by this field.
|
1023
1071
|
|
1024
1072
|
Returns:
|
1025
1073
|
List[PydanticAgentState]: The filtered list of matching agents.
|
@@ -1032,11 +1080,11 @@ class AgentManager:
|
|
1032
1080
|
query = _apply_filters(query, name, query_text, project_id, template_id, base_template_id)
|
1033
1081
|
query = _apply_identity_filters(query, identity_id, identifier_keys)
|
1034
1082
|
query = _apply_tag_filter(query, tags, match_all_tags)
|
1083
|
+
query = _apply_relationship_filters(query, include_relationships)
|
1035
1084
|
query = await _apply_pagination_async(query, before, after, session, ascending=ascending, sort_by=sort_by)
|
1036
1085
|
|
1037
1086
|
if limit:
|
1038
1087
|
query = query.limit(limit)
|
1039
|
-
|
1040
1088
|
result = await session.execute(query)
|
1041
1089
|
agents = result.scalars().all()
|
1042
1090
|
return await asyncio.gather(*[agent.to_pydantic_async(include_relationships=include_relationships) for agent in agents])
|
@@ -1168,10 +1216,23 @@ class AgentManager:
|
|
1168
1216
|
include_relationships: Optional[List[str]] = None,
|
1169
1217
|
) -> PydanticAgentState:
|
1170
1218
|
"""Fetch an agent by its ID."""
|
1171
|
-
|
1172
1219
|
async with db_registry.async_session() as session:
|
1173
|
-
|
1174
|
-
|
1220
|
+
try:
|
1221
|
+
query = select(AgentModel)
|
1222
|
+
query = AgentModel.apply_access_predicate(query, actor, ["read"], AccessType.ORGANIZATION)
|
1223
|
+
query = query.where(AgentModel.id == agent_id)
|
1224
|
+
query = _apply_relationship_filters(query, include_relationships)
|
1225
|
+
|
1226
|
+
result = await session.execute(query)
|
1227
|
+
agent = result.scalar_one_or_none()
|
1228
|
+
|
1229
|
+
if agent is None:
|
1230
|
+
raise NoResultFound(f"Agent with ID {agent_id} not found")
|
1231
|
+
|
1232
|
+
return await agent.to_pydantic_async(include_relationships=include_relationships)
|
1233
|
+
except Exception as e:
|
1234
|
+
logger.error(f"Error fetching agent {agent_id}: {str(e)}")
|
1235
|
+
raise
|
1175
1236
|
|
1176
1237
|
@enforce_types
|
1177
1238
|
@trace_method
|
@@ -1183,12 +1244,23 @@ class AgentManager:
|
|
1183
1244
|
) -> list[PydanticAgentState]:
|
1184
1245
|
"""Fetch a list of agents by their IDs."""
|
1185
1246
|
async with db_registry.async_session() as session:
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1247
|
+
try:
|
1248
|
+
query = select(AgentModel)
|
1249
|
+
query = AgentModel.apply_access_predicate(query, actor, ["read"], AccessType.ORGANIZATION)
|
1250
|
+
query = query.where(AgentModel.id.in_(agent_ids))
|
1251
|
+
query = _apply_relationship_filters(query, include_relationships)
|
1252
|
+
|
1253
|
+
result = await session.execute(query)
|
1254
|
+
agents = result.scalars().all()
|
1255
|
+
|
1256
|
+
if not agents:
|
1257
|
+
logger.warning(f"No agents found with IDs: {agent_ids}")
|
1258
|
+
return []
|
1259
|
+
|
1260
|
+
return await asyncio.gather(*[agent.to_pydantic_async(include_relationships=include_relationships) for agent in agents])
|
1261
|
+
except Exception as e:
|
1262
|
+
logger.error(f"Error fetching agents with IDs {agent_ids}: {str(e)}")
|
1263
|
+
raise
|
1192
1264
|
|
1193
1265
|
@enforce_types
|
1194
1266
|
@trace_method
|
@@ -1334,6 +1406,9 @@ class AgentManager:
|
|
1334
1406
|
schema = MarshmallowAgentSchema(session=session, actor=actor)
|
1335
1407
|
agent = schema.load(serialized_agent_dict, session=session)
|
1336
1408
|
|
1409
|
+
agent.organization_id = actor.organization_id
|
1410
|
+
for block in agent.core_memory:
|
1411
|
+
block.organization_id = actor.organization_id
|
1337
1412
|
if append_copy_suffix:
|
1338
1413
|
agent.name += "_copy"
|
1339
1414
|
if project_id:
|
@@ -1435,10 +1510,17 @@ class AgentManager:
|
|
1435
1510
|
@trace_method
|
1436
1511
|
def list_groups(self, agent_id: str, actor: PydanticUser, manager_type: Optional[str] = None) -> List[PydanticGroup]:
|
1437
1512
|
with db_registry.session() as session:
|
1438
|
-
|
1513
|
+
query = (
|
1514
|
+
select(GroupModel)
|
1515
|
+
.join(GroupsAgents, GroupModel.id == GroupsAgents.group_id)
|
1516
|
+
.where(GroupsAgents.agent_id == agent_id, GroupModel.organization_id == actor.organization_id)
|
1517
|
+
)
|
1518
|
+
|
1439
1519
|
if manager_type:
|
1440
|
-
|
1441
|
-
|
1520
|
+
query = query.where(GroupModel.manager_type == manager_type)
|
1521
|
+
|
1522
|
+
result = session.execute(query)
|
1523
|
+
return [group.to_pydantic() for group in result.scalars()]
|
1442
1524
|
|
1443
1525
|
# ======================================================================================================================
|
1444
1526
|
# In Context Messages Management
|
@@ -1559,15 +1641,9 @@ class AgentManager:
|
|
1559
1641
|
|
1560
1642
|
Updates to the memory header should *not* trigger a rebuild, since that will simply flood recall storage with excess messages
|
1561
1643
|
"""
|
1562
|
-
|
1563
|
-
|
1564
|
-
|
1565
|
-
|
1566
|
-
num_messages, num_archival_memories, agent_state = await asyncio.gather(
|
1567
|
-
num_messages_task,
|
1568
|
-
num_archival_memories_task,
|
1569
|
-
agent_state_task,
|
1570
|
-
)
|
1644
|
+
num_messages = await self.message_manager.size_async(actor=actor, agent_id=agent_id)
|
1645
|
+
num_archival_memories = await self.passage_manager.agent_passage_size_async(actor=actor, agent_id=agent_id)
|
1646
|
+
agent_state = await self.get_agent_by_id_async(agent_id=agent_id, include_relationships=["memory", "sources", "tools"], actor=actor)
|
1571
1647
|
|
1572
1648
|
if not tool_rules_solver:
|
1573
1649
|
tool_rules_solver = ToolRulesSolver(agent_state.tool_rules)
|
@@ -1719,7 +1795,7 @@ class AgentManager:
|
|
1719
1795
|
logger.error(
|
1720
1796
|
f"Agent {agent_id} has no message_ids. Agent details: "
|
1721
1797
|
f"name={agent.name}, created_at={agent.created_at}, "
|
1722
|
-
f"message_ids={agent.message_ids}, organization_id={
|
1798
|
+
f"message_ids={agent.message_ids}, organization_id={actor.organization_id}"
|
1723
1799
|
)
|
1724
1800
|
raise ValueError(f"Agent {agent_id} has no message_ids - cannot preserve system message")
|
1725
1801
|
|
@@ -1784,9 +1860,10 @@ class AgentManager:
|
|
1784
1860
|
)
|
1785
1861
|
|
1786
1862
|
# refresh memory from DB (using block ids)
|
1787
|
-
blocks = await
|
1788
|
-
|
1863
|
+
blocks = await self.block_manager.get_all_blocks_by_ids_async(
|
1864
|
+
block_ids=[b.id for b in agent_state.memory.get_blocks()], actor=actor
|
1789
1865
|
)
|
1866
|
+
|
1790
1867
|
agent_state.memory = Memory(
|
1791
1868
|
blocks=blocks,
|
1792
1869
|
file_blocks=agent_state.memory.file_blocks,
|
@@ -1907,7 +1984,7 @@ class AgentManager:
|
|
1907
1984
|
"""
|
1908
1985
|
async with db_registry.async_session() as session:
|
1909
1986
|
# Validate agent exists and user has access
|
1910
|
-
await
|
1987
|
+
await validate_agent_exists_async(session, agent_id, actor)
|
1911
1988
|
|
1912
1989
|
# Use raw SQL to efficiently fetch sources - much faster than lazy loading
|
1913
1990
|
# Fast query without relationship loading
|
@@ -1943,7 +2020,7 @@ class AgentManager:
|
|
1943
2020
|
"""
|
1944
2021
|
async with db_registry.async_session() as session:
|
1945
2022
|
# Validate agent exists and user has access
|
1946
|
-
await
|
2023
|
+
await validate_agent_exists_async(session, agent_id, actor)
|
1947
2024
|
|
1948
2025
|
# Check if the source is actually attached to this agent using junction table
|
1949
2026
|
attachment_check_query = select(SourcesAgents).where(SourcesAgents.agent_id == agent_id, SourcesAgents.source_id == source_id)
|
@@ -2710,7 +2787,7 @@ class AgentManager:
|
|
2710
2787
|
"""
|
2711
2788
|
async with db_registry.async_session() as session:
|
2712
2789
|
# lightweight check for agent access
|
2713
|
-
await
|
2790
|
+
await validate_agent_exists_async(session, agent_id, actor)
|
2714
2791
|
|
2715
2792
|
# direct query for tools via join - much more performant
|
2716
2793
|
query = (
|
@@ -2752,7 +2829,12 @@ class AgentManager:
|
|
2752
2829
|
)
|
2753
2830
|
|
2754
2831
|
if query_text:
|
2755
|
-
|
2832
|
+
if settings.database_engine is DatabaseChoice.POSTGRES:
|
2833
|
+
# PostgreSQL: Use ILIKE for case-insensitive search
|
2834
|
+
query = query.filter(AgentsTags.tag.ilike(f"%{query_text}%"))
|
2835
|
+
else:
|
2836
|
+
# SQLite: Use LIKE with LOWER for case-insensitive search
|
2837
|
+
query = query.filter(func.lower(AgentsTags.tag).like(func.lower(f"%{query_text}%")))
|
2756
2838
|
|
2757
2839
|
if after:
|
2758
2840
|
query = query.filter(AgentsTags.tag > after)
|
@@ -2788,7 +2870,12 @@ class AgentManager:
|
|
2788
2870
|
)
|
2789
2871
|
|
2790
2872
|
if query_text:
|
2791
|
-
|
2873
|
+
if settings.database_engine is DatabaseChoice.POSTGRES:
|
2874
|
+
# PostgreSQL: Use ILIKE for case-insensitive search
|
2875
|
+
query = query.where(AgentsTags.tag.ilike(f"%{query_text}%"))
|
2876
|
+
else:
|
2877
|
+
# SQLite: Use LIKE with LOWER for case-insensitive search
|
2878
|
+
query = query.where(func.lower(AgentsTags.tag).like(func.lower(f"%{query_text}%")))
|
2792
2879
|
|
2793
2880
|
if after:
|
2794
2881
|
query = query.where(AgentsTags.tag > after)
|
letta/services/block_manager.py
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
import asyncio
|
2
2
|
from typing import Dict, List, Optional
|
3
3
|
|
4
|
-
from sqlalchemy import select
|
4
|
+
from sqlalchemy import delete, select
|
5
5
|
from sqlalchemy.orm import Session
|
6
6
|
|
7
7
|
from letta.log import get_logger
|
8
|
+
from letta.orm.agent import Agent as AgentModel
|
8
9
|
from letta.orm.block import Block as BlockModel
|
9
10
|
from letta.orm.block_history import BlockHistory
|
11
|
+
from letta.orm.blocks_agents import BlocksAgents
|
10
12
|
from letta.orm.errors import NoResultFound
|
11
13
|
from letta.otel.tracing import trace_method
|
12
14
|
from letta.schemas.agent import AgentState as PydanticAgentState
|
@@ -50,8 +52,10 @@ class BlockManager:
|
|
50
52
|
async with db_registry.async_session() as session:
|
51
53
|
data = block.model_dump(to_orm=True, exclude_none=True)
|
52
54
|
block = BlockModel(**data, organization_id=actor.organization_id)
|
53
|
-
await block.create_async(session, actor=actor)
|
54
|
-
|
55
|
+
await block.create_async(session, actor=actor, no_commit=True, no_refresh=True)
|
56
|
+
pydantic_block = block.to_pydantic()
|
57
|
+
await session.commit()
|
58
|
+
return pydantic_block
|
55
59
|
|
56
60
|
@enforce_types
|
57
61
|
@trace_method
|
@@ -95,11 +99,12 @@ class BlockManager:
|
|
95
99
|
block_models = [
|
96
100
|
BlockModel(**block.model_dump(to_orm=True, exclude_none=True), organization_id=actor.organization_id) for block in blocks
|
97
101
|
]
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
102
|
+
created_models = await BlockModel.batch_create_async(
|
103
|
+
items=block_models, db_session=session, actor=actor, no_commit=True, no_refresh=True
|
104
|
+
)
|
105
|
+
result = [m.to_pydantic() for m in created_models]
|
106
|
+
await session.commit()
|
107
|
+
return result
|
103
108
|
|
104
109
|
@enforce_types
|
105
110
|
@trace_method
|
@@ -130,26 +135,36 @@ class BlockManager:
|
|
130
135
|
for key, value in update_data.items():
|
131
136
|
setattr(block, key, value)
|
132
137
|
|
133
|
-
await block.update_async(db_session=session, actor=actor)
|
134
|
-
|
138
|
+
await block.update_async(db_session=session, actor=actor, no_commit=True, no_refresh=True)
|
139
|
+
pydantic_block = block.to_pydantic()
|
140
|
+
await session.commit()
|
141
|
+
return pydantic_block
|
135
142
|
|
136
143
|
@enforce_types
|
137
144
|
@trace_method
|
138
|
-
def delete_block(self, block_id: str, actor: PydanticUser) ->
|
145
|
+
def delete_block(self, block_id: str, actor: PydanticUser) -> None:
|
139
146
|
"""Delete a block by its ID."""
|
140
147
|
with db_registry.session() as session:
|
148
|
+
# First, delete all references in blocks_agents table
|
149
|
+
session.execute(delete(BlocksAgents).where(BlocksAgents.block_id == block_id))
|
150
|
+
session.flush()
|
151
|
+
|
152
|
+
# Then delete the block itself
|
141
153
|
block = BlockModel.read(db_session=session, identifier=block_id)
|
142
154
|
block.hard_delete(db_session=session, actor=actor)
|
143
|
-
return block.to_pydantic()
|
144
155
|
|
145
156
|
@enforce_types
|
146
157
|
@trace_method
|
147
|
-
async def delete_block_async(self, block_id: str, actor: PydanticUser) ->
|
158
|
+
async def delete_block_async(self, block_id: str, actor: PydanticUser) -> None:
|
148
159
|
"""Delete a block by its ID."""
|
149
160
|
async with db_registry.async_session() as session:
|
161
|
+
# First, delete all references in blocks_agents table
|
162
|
+
await session.execute(delete(BlocksAgents).where(BlocksAgents.block_id == block_id))
|
163
|
+
await session.flush()
|
164
|
+
|
165
|
+
# Then delete the block itself
|
150
166
|
block = await BlockModel.read_async(db_session=session, identifier=block_id, actor=actor)
|
151
167
|
await block.hard_delete_async(db_session=session, actor=actor)
|
152
|
-
return block.to_pydantic()
|
153
168
|
|
154
169
|
@enforce_types
|
155
170
|
@trace_method
|
@@ -296,8 +311,14 @@ class BlockManager:
|
|
296
311
|
Retrieve all agents associated with a given block.
|
297
312
|
"""
|
298
313
|
async with db_registry.async_session() as session:
|
299
|
-
|
300
|
-
|
314
|
+
query = (
|
315
|
+
select(AgentModel)
|
316
|
+
.where(AgentModel.id.in_(select(BlocksAgents.agent_id).where(BlocksAgents.block_id == block_id)))
|
317
|
+
.where(AgentModel.organization_id == actor.organization_id)
|
318
|
+
)
|
319
|
+
|
320
|
+
result = await session.execute(query)
|
321
|
+
agents_orm = result.scalars().all()
|
301
322
|
agents = await asyncio.gather(*[agent.to_pydantic_async(include_relationships=include_relationships) for agent in agents_orm])
|
302
323
|
return agents
|
303
324
|
|
@@ -531,7 +552,7 @@ class BlockManager:
|
|
531
552
|
for block in blocks:
|
532
553
|
new_val = updates[block.id]
|
533
554
|
if len(new_val) > block.limit:
|
534
|
-
logger.warning(f"Value length ({len(new_val)}) exceeds limit
|
555
|
+
logger.warning(f"Value length ({len(new_val)}) exceeds limit ({block.limit}) for block {block.id!r}, truncating...")
|
535
556
|
new_val = new_val[: block.limit]
|
536
557
|
block.value = new_val
|
537
558
|
|
letta/services/file_manager.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import asyncio
|
1
2
|
import os
|
2
3
|
from datetime import datetime
|
3
4
|
from typing import List, Optional
|
@@ -357,7 +358,9 @@ class FileManager:
|
|
357
358
|
|
358
359
|
@enforce_types
|
359
360
|
@trace_method
|
360
|
-
async def get_organization_sources_metadata(
|
361
|
+
async def get_organization_sources_metadata(
|
362
|
+
self, actor: PydanticUser, include_detailed_per_source_metadata: bool = False
|
363
|
+
) -> OrganizationSourcesStats:
|
361
364
|
"""
|
362
365
|
Get aggregated metadata for all sources in an organization with optimized queries.
|
363
366
|
|
@@ -365,7 +368,7 @@ class FileManager:
|
|
365
368
|
- Total number of sources
|
366
369
|
- Total number of files across all sources
|
367
370
|
- Total size of all files
|
368
|
-
- Per-source breakdown with file details
|
371
|
+
- Per-source breakdown with file details (if include_detailed_per_source_metadata is True)
|
369
372
|
"""
|
370
373
|
async with db_registry.async_session() as session:
|
371
374
|
# Import here to avoid circular imports
|
@@ -395,31 +398,113 @@ class FileManager:
|
|
395
398
|
for row in source_aggregations:
|
396
399
|
source_id, source_name, file_count, total_size = row
|
397
400
|
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
401
|
+
if include_detailed_per_source_metadata:
|
402
|
+
# Get individual file details for this source
|
403
|
+
files_query = (
|
404
|
+
select(FileMetadataModel.id, FileMetadataModel.file_name, FileMetadataModel.file_size)
|
405
|
+
.where(
|
406
|
+
FileMetadataModel.source_id == source_id,
|
407
|
+
FileMetadataModel.organization_id == actor.organization_id,
|
408
|
+
FileMetadataModel.is_deleted == False,
|
409
|
+
)
|
410
|
+
.order_by(FileMetadataModel.file_name)
|
405
411
|
)
|
406
|
-
.order_by(FileMetadataModel.file_name)
|
407
|
-
)
|
408
412
|
|
409
|
-
|
410
|
-
|
413
|
+
files_result = await session.execute(files_query)
|
414
|
+
files_rows = files_result.fetchall()
|
411
415
|
|
412
|
-
|
413
|
-
|
416
|
+
# Build file stats
|
417
|
+
files = [FileStats(file_id=file_row[0], file_name=file_row[1], file_size=file_row[2]) for file_row in files_rows]
|
414
418
|
|
415
|
-
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
+
# Build source metadata
|
420
|
+
source_metadata = SourceStats(
|
421
|
+
source_id=source_id, source_name=source_name, file_count=file_count, total_size=total_size, files=files
|
422
|
+
)
|
423
|
+
|
424
|
+
metadata.sources.append(source_metadata)
|
419
425
|
|
420
|
-
metadata.sources.append(source_metadata)
|
421
426
|
metadata.total_files += file_count
|
422
427
|
metadata.total_size += total_size
|
423
428
|
|
424
|
-
metadata.total_sources = len(
|
429
|
+
metadata.total_sources = len(source_aggregations)
|
425
430
|
return metadata
|
431
|
+
|
432
|
+
@enforce_types
|
433
|
+
@trace_method
|
434
|
+
async def get_files_by_ids_async(
|
435
|
+
self, file_ids: List[str], actor: PydanticUser, *, include_content: bool = False
|
436
|
+
) -> List[PydanticFileMetadata]:
|
437
|
+
"""
|
438
|
+
Get multiple files by their IDs in a single query.
|
439
|
+
|
440
|
+
Args:
|
441
|
+
file_ids: List of file IDs to retrieve
|
442
|
+
actor: User performing the action
|
443
|
+
include_content: Whether to include file content in the response
|
444
|
+
|
445
|
+
Returns:
|
446
|
+
List[PydanticFileMetadata]: List of files (may be fewer than requested if some don't exist)
|
447
|
+
"""
|
448
|
+
if not file_ids:
|
449
|
+
return []
|
450
|
+
|
451
|
+
async with db_registry.async_session() as session:
|
452
|
+
query = select(FileMetadataModel).where(
|
453
|
+
FileMetadataModel.id.in_(file_ids),
|
454
|
+
FileMetadataModel.organization_id == actor.organization_id,
|
455
|
+
FileMetadataModel.is_deleted == False,
|
456
|
+
)
|
457
|
+
|
458
|
+
# Eagerly load content if requested
|
459
|
+
if include_content:
|
460
|
+
query = query.options(selectinload(FileMetadataModel.content))
|
461
|
+
|
462
|
+
result = await session.execute(query)
|
463
|
+
files_orm = result.scalars().all()
|
464
|
+
|
465
|
+
return await asyncio.gather(*[file.to_pydantic_async(include_content=include_content) for file in files_orm])
|
466
|
+
|
467
|
+
@enforce_types
|
468
|
+
@trace_method
|
469
|
+
async def get_files_for_agents_async(
|
470
|
+
self, agent_ids: List[str], actor: PydanticUser, *, include_content: bool = False
|
471
|
+
) -> List[PydanticFileMetadata]:
|
472
|
+
"""
|
473
|
+
Get all files associated with the given agents via file-agent relationships.
|
474
|
+
|
475
|
+
Args:
|
476
|
+
agent_ids: List of agent IDs to find files for
|
477
|
+
actor: User performing the action
|
478
|
+
include_content: Whether to include file content in the response
|
479
|
+
|
480
|
+
Returns:
|
481
|
+
List[PydanticFileMetadata]: List of unique files associated with these agents
|
482
|
+
"""
|
483
|
+
if not agent_ids:
|
484
|
+
return []
|
485
|
+
|
486
|
+
async with db_registry.async_session() as session:
|
487
|
+
# We need to import FileAgent here to avoid circular imports
|
488
|
+
from letta.orm.file_agent import FileAgent as FileAgentModel
|
489
|
+
|
490
|
+
# Join through file-agent relationships
|
491
|
+
query = (
|
492
|
+
select(FileMetadataModel)
|
493
|
+
.join(FileAgentModel, FileMetadataModel.id == FileAgentModel.file_id)
|
494
|
+
.where(
|
495
|
+
FileAgentModel.agent_id.in_(agent_ids),
|
496
|
+
FileMetadataModel.organization_id == actor.organization_id,
|
497
|
+
FileMetadataModel.is_deleted == False,
|
498
|
+
FileAgentModel.is_deleted == False,
|
499
|
+
)
|
500
|
+
.distinct() # Ensure we don't get duplicate files
|
501
|
+
)
|
502
|
+
|
503
|
+
# Eagerly load content if requested
|
504
|
+
if include_content:
|
505
|
+
query = query.options(selectinload(FileMetadataModel.content))
|
506
|
+
|
507
|
+
result = await session.execute(query)
|
508
|
+
files_orm = result.scalars().all()
|
509
|
+
|
510
|
+
return await asyncio.gather(*[file.to_pydantic_async(include_content=include_content) for file in files_orm])
|