letta-nightly 0.11.7.dev20250915104130__py3-none-any.whl → 0.11.7.dev20250917104122__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 +10 -2
- letta/adapters/letta_llm_request_adapter.py +0 -1
- letta/adapters/letta_llm_stream_adapter.py +0 -1
- letta/agent.py +1 -1
- letta/agents/letta_agent.py +1 -4
- letta/agents/letta_agent_v2.py +2 -1
- letta/agents/voice_agent.py +1 -1
- letta/functions/function_sets/multi_agent.py +1 -1
- letta/functions/helpers.py +1 -1
- letta/helpers/converters.py +8 -2
- letta/helpers/crypto_utils.py +144 -0
- letta/llm_api/llm_api_tools.py +0 -1
- letta/llm_api/llm_client_base.py +0 -2
- letta/orm/__init__.py +1 -0
- letta/orm/agent.py +5 -1
- letta/orm/job.py +3 -1
- letta/orm/mcp_oauth.py +6 -0
- letta/orm/mcp_server.py +7 -1
- letta/orm/sqlalchemy_base.py +2 -1
- letta/prompts/gpt_system.py +13 -15
- letta/prompts/system_prompts/__init__.py +27 -0
- letta/prompts/{system/memgpt_chat.txt → system_prompts/memgpt_chat.py} +2 -0
- letta/prompts/{system/memgpt_generate_tool.txt → system_prompts/memgpt_generate_tool.py} +4 -2
- letta/prompts/{system/memgpt_v2_chat.txt → system_prompts/memgpt_v2_chat.py} +2 -0
- letta/prompts/{system/react.txt → system_prompts/react.py} +2 -0
- letta/prompts/{system/sleeptime_doc_ingest.txt → system_prompts/sleeptime_doc_ingest.py} +2 -0
- letta/prompts/{system/sleeptime_v2.txt → system_prompts/sleeptime_v2.py} +2 -0
- letta/prompts/{system/summary_system_prompt.txt → system_prompts/summary_system_prompt.py} +2 -0
- letta/prompts/{system/voice_chat.txt → system_prompts/voice_chat.py} +2 -0
- letta/prompts/{system/voice_sleeptime.txt → system_prompts/voice_sleeptime.py} +2 -0
- letta/prompts/{system/workflow.txt → system_prompts/workflow.py} +2 -0
- letta/schemas/agent.py +10 -7
- letta/schemas/job.py +10 -0
- letta/schemas/mcp.py +146 -6
- letta/schemas/provider_trace.py +0 -2
- letta/schemas/run.py +2 -0
- letta/schemas/secret.py +378 -0
- letta/serialize_schemas/marshmallow_agent.py +4 -0
- letta/server/rest_api/dependencies.py +37 -0
- letta/server/rest_api/routers/openai/chat_completions/chat_completions.py +4 -3
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +115 -107
- letta/server/rest_api/routers/v1/archives.py +113 -0
- letta/server/rest_api/routers/v1/blocks.py +44 -20
- letta/server/rest_api/routers/v1/embeddings.py +3 -3
- letta/server/rest_api/routers/v1/folders.py +107 -47
- letta/server/rest_api/routers/v1/groups.py +52 -32
- letta/server/rest_api/routers/v1/identities.py +110 -21
- letta/server/rest_api/routers/v1/internal_templates.py +28 -13
- letta/server/rest_api/routers/v1/jobs.py +19 -14
- letta/server/rest_api/routers/v1/llms.py +6 -8
- letta/server/rest_api/routers/v1/messages.py +14 -14
- letta/server/rest_api/routers/v1/organizations.py +1 -1
- letta/server/rest_api/routers/v1/providers.py +40 -16
- letta/server/rest_api/routers/v1/runs.py +28 -20
- letta/server/rest_api/routers/v1/sandbox_configs.py +25 -25
- letta/server/rest_api/routers/v1/sources.py +44 -45
- letta/server/rest_api/routers/v1/steps.py +27 -25
- letta/server/rest_api/routers/v1/tags.py +11 -7
- letta/server/rest_api/routers/v1/telemetry.py +11 -6
- letta/server/rest_api/routers/v1/tools.py +78 -80
- letta/server/rest_api/routers/v1/users.py +1 -1
- letta/server/rest_api/routers/v1/voice.py +6 -5
- letta/server/rest_api/utils.py +1 -18
- letta/services/agent_manager.py +17 -9
- letta/services/agent_serialization_manager.py +11 -3
- letta/services/archive_manager.py +73 -0
- letta/services/file_manager.py +6 -0
- letta/services/group_manager.py +2 -1
- letta/services/helpers/agent_manager_helper.py +6 -1
- letta/services/identity_manager.py +67 -0
- letta/services/job_manager.py +18 -2
- letta/services/mcp_manager.py +198 -82
- letta/services/provider_manager.py +14 -1
- letta/services/source_manager.py +11 -1
- letta/services/telemetry_manager.py +2 -0
- letta/services/tool_executor/composio_tool_executor.py +1 -1
- letta/services/tool_manager.py +46 -9
- letta/services/tool_sandbox/base.py +2 -3
- letta/utils.py +4 -2
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/METADATA +5 -2
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/RECORD +85 -94
- letta/prompts/system/memgpt_base.txt +0 -54
- letta/prompts/system/memgpt_chat_compressed.txt +0 -13
- letta/prompts/system/memgpt_chat_fstring.txt +0 -51
- letta/prompts/system/memgpt_convo_only.txt +0 -12
- letta/prompts/system/memgpt_doc.txt +0 -50
- letta/prompts/system/memgpt_gpt35_extralong.txt +0 -53
- letta/prompts/system/memgpt_intuitive_knowledge.txt +0 -31
- letta/prompts/system/memgpt_memory_only.txt +0 -29
- letta/prompts/system/memgpt_modified_chat.txt +0 -23
- letta/prompts/system/memgpt_modified_o1.txt +0 -31
- letta/prompts/system/memgpt_offline_memory.txt +0 -23
- letta/prompts/system/memgpt_offline_memory_chat.txt +0 -35
- letta/prompts/system/memgpt_sleeptime_chat.txt +0 -52
- letta/prompts/system/sleeptime.txt +0 -37
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20250915104130.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/licenses/LICENSE +0 -0
letta/services/agent_manager.py
CHANGED
@@ -455,7 +455,8 @@ class AgentManager:
|
|
455
455
|
[{"agent_id": aid, "identity_id": iid} for iid in identity_ids],
|
456
456
|
)
|
457
457
|
|
458
|
-
|
458
|
+
agent_secrets = agent_create.secrets or agent_create.tool_exec_environment_variables
|
459
|
+
if agent_secrets:
|
459
460
|
env_rows = [
|
460
461
|
{
|
461
462
|
"agent_id": aid,
|
@@ -463,7 +464,7 @@ class AgentManager:
|
|
463
464
|
"value": val,
|
464
465
|
"organization_id": actor.organization_id,
|
465
466
|
}
|
466
|
-
for key, val in
|
467
|
+
for key, val in agent_secrets.items()
|
467
468
|
]
|
468
469
|
session.execute(insert(AgentEnvironmentVariable).values(env_rows))
|
469
470
|
|
@@ -674,7 +675,8 @@ class AgentManager:
|
|
674
675
|
)
|
675
676
|
|
676
677
|
env_rows = []
|
677
|
-
|
678
|
+
agent_secrets = agent_create.secrets or agent_create.tool_exec_environment_variables
|
679
|
+
if agent_secrets:
|
678
680
|
env_rows = [
|
679
681
|
{
|
680
682
|
"agent_id": aid,
|
@@ -682,7 +684,7 @@ class AgentManager:
|
|
682
684
|
"value": val,
|
683
685
|
"organization_id": actor.organization_id,
|
684
686
|
}
|
685
|
-
for key, val in
|
687
|
+
for key, val in agent_secrets.items()
|
686
688
|
]
|
687
689
|
result = await session.execute(insert(AgentEnvironmentVariable).values(env_rows).returning(AgentEnvironmentVariable.id))
|
688
690
|
env_rows = [{**row, "id": env_var_id} for row, env_var_id in zip(env_rows, result.scalars().all())]
|
@@ -701,8 +703,9 @@ class AgentManager:
|
|
701
703
|
|
702
704
|
result = await new_agent.to_pydantic_async(include_relationships=include_relationships)
|
703
705
|
|
704
|
-
if
|
706
|
+
if agent_secrets and env_rows:
|
705
707
|
result.tool_exec_environment_variables = [AgentEnvironmentVariable(**row) for row in env_rows]
|
708
|
+
result.secrets = [AgentEnvironmentVariable(**row) for row in env_rows]
|
706
709
|
|
707
710
|
# initial message sequence (skip if _init_with_no_messages is True)
|
708
711
|
if not _init_with_no_messages:
|
@@ -894,7 +897,8 @@ class AgentManager:
|
|
894
897
|
)
|
895
898
|
session.expire(agent, ["tags"])
|
896
899
|
|
897
|
-
|
900
|
+
agent_secrets = agent_update.secrets or agent_update.tool_exec_environment_variables
|
901
|
+
if agent_secrets is not None:
|
898
902
|
session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
|
899
903
|
env_rows = [
|
900
904
|
{
|
@@ -903,7 +907,7 @@ class AgentManager:
|
|
903
907
|
"value": v,
|
904
908
|
"organization_id": agent.organization_id,
|
905
909
|
}
|
906
|
-
for k, v in
|
910
|
+
for k, v in agent_secrets.items()
|
907
911
|
]
|
908
912
|
if env_rows:
|
909
913
|
self._bulk_insert_pivot(session, AgentEnvironmentVariable.__table__, env_rows)
|
@@ -1019,7 +1023,8 @@ class AgentManager:
|
|
1019
1023
|
)
|
1020
1024
|
session.expire(agent, ["tags"])
|
1021
1025
|
|
1022
|
-
|
1026
|
+
agent_secrets = agent_update.secrets or agent_update.tool_exec_environment_variables
|
1027
|
+
if agent_secrets is not None:
|
1023
1028
|
await session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
|
1024
1029
|
env_rows = [
|
1025
1030
|
{
|
@@ -1028,7 +1033,7 @@ class AgentManager:
|
|
1028
1033
|
"value": v,
|
1029
1034
|
"organization_id": agent.organization_id,
|
1030
1035
|
}
|
1031
|
-
for k, v in
|
1036
|
+
for k, v in agent_secrets.items()
|
1032
1037
|
]
|
1033
1038
|
if env_rows:
|
1034
1039
|
await self._bulk_insert_pivot_async(session, AgentEnvironmentVariable.__table__, env_rows)
|
@@ -1544,6 +1549,8 @@ class AgentManager:
|
|
1544
1549
|
if env_vars:
|
1545
1550
|
for var in agent.tool_exec_environment_variables:
|
1546
1551
|
var.value = env_vars.get(var.key, "")
|
1552
|
+
for var in agent.secrets:
|
1553
|
+
var.value = env_vars.get(var.key, "")
|
1547
1554
|
|
1548
1555
|
agent = agent.create(session, actor=actor)
|
1549
1556
|
|
@@ -1627,6 +1634,7 @@ class AgentManager:
|
|
1627
1634
|
# Remove stale variables
|
1628
1635
|
stale_keys = set(existing_vars) - set(env_vars)
|
1629
1636
|
agent.tool_exec_environment_variables = [var for var in updated_vars if var.key not in stale_keys]
|
1637
|
+
agent.secrets = [var for var in updated_vars if var.key not in stale_keys]
|
1630
1638
|
|
1631
1639
|
# Update the agent in the database
|
1632
1640
|
agent.update(session, actor=actor)
|
@@ -209,8 +209,10 @@ class AgentSerializationManager:
|
|
209
209
|
agent_schema.id = agent_file_id
|
210
210
|
|
211
211
|
# wipe the values of tool_exec_environment_variables (they contain secrets)
|
212
|
-
|
213
|
-
|
212
|
+
agent_secrets = agent_schema.secrets or agent_schema.tool_exec_environment_variables
|
213
|
+
if agent_secrets:
|
214
|
+
agent_schema.tool_exec_environment_variables = {key: "" for key in agent_secrets}
|
215
|
+
agent_schema.secrets = {key: "" for key in agent_secrets}
|
214
216
|
|
215
217
|
if agent_schema.messages:
|
216
218
|
for message in agent_schema.messages:
|
@@ -655,10 +657,16 @@ class AgentSerializationManager:
|
|
655
657
|
if agent_data.get("source_ids"):
|
656
658
|
agent_data["source_ids"] = [file_to_db_ids[file_id] for file_id in agent_data["source_ids"]]
|
657
659
|
|
658
|
-
if env_vars and agent_data.get("
|
660
|
+
if env_vars and agent_data.get("secrets"):
|
659
661
|
# update environment variable values from the provided env_vars dict
|
662
|
+
for key in agent_data["secrets"]:
|
663
|
+
agent_data["secrets"][key] = env_vars.get(key, "")
|
664
|
+
agent_data["tool_exec_environment_variables"][key] = env_vars.get(key, "")
|
665
|
+
elif env_vars and agent_data.get("tool_exec_environment_variables"):
|
666
|
+
# also handle tool_exec_environment_variables for backwards compatibility
|
660
667
|
for key in agent_data["tool_exec_environment_variables"]:
|
661
668
|
agent_data["tool_exec_environment_variables"][key] = env_vars.get(key, "")
|
669
|
+
agent_data["secrets"][key] = env_vars.get(key, "")
|
662
670
|
|
663
671
|
# Override project_id if provided
|
664
672
|
if project_id:
|
@@ -87,6 +87,79 @@ class ArchiveManager:
|
|
87
87
|
)
|
88
88
|
return archive.to_pydantic()
|
89
89
|
|
90
|
+
@enforce_types
|
91
|
+
@trace_method
|
92
|
+
async def update_archive_async(
|
93
|
+
self,
|
94
|
+
archive_id: str,
|
95
|
+
name: Optional[str] = None,
|
96
|
+
description: Optional[str] = None,
|
97
|
+
actor: PydanticUser = None,
|
98
|
+
) -> PydanticArchive:
|
99
|
+
"""Update archive name and/or description."""
|
100
|
+
async with db_registry.async_session() as session:
|
101
|
+
archive = await ArchiveModel.read_async(
|
102
|
+
db_session=session,
|
103
|
+
identifier=archive_id,
|
104
|
+
actor=actor,
|
105
|
+
check_is_deleted=True,
|
106
|
+
)
|
107
|
+
|
108
|
+
if name is not None:
|
109
|
+
archive.name = name
|
110
|
+
if description is not None:
|
111
|
+
archive.description = description
|
112
|
+
|
113
|
+
await archive.update_async(session, actor=actor)
|
114
|
+
return archive.to_pydantic()
|
115
|
+
|
116
|
+
@enforce_types
|
117
|
+
@trace_method
|
118
|
+
async def list_archives_async(
|
119
|
+
self,
|
120
|
+
*,
|
121
|
+
actor: PydanticUser,
|
122
|
+
before: Optional[str] = None,
|
123
|
+
after: Optional[str] = None,
|
124
|
+
limit: Optional[int] = 50,
|
125
|
+
ascending: bool = False,
|
126
|
+
name: Optional[str] = None,
|
127
|
+
agent_id: Optional[str] = None,
|
128
|
+
) -> List[PydanticArchive]:
|
129
|
+
"""List archives with pagination and optional filters.
|
130
|
+
|
131
|
+
Filters:
|
132
|
+
- name: exact match on name
|
133
|
+
- agent_id: only archives attached to given agent
|
134
|
+
"""
|
135
|
+
filter_kwargs = {}
|
136
|
+
if name is not None:
|
137
|
+
filter_kwargs["name"] = name
|
138
|
+
|
139
|
+
join_model = None
|
140
|
+
join_conditions = None
|
141
|
+
if agent_id is not None:
|
142
|
+
join_model = ArchivesAgents
|
143
|
+
join_conditions = [
|
144
|
+
ArchivesAgents.archive_id == ArchiveModel.id,
|
145
|
+
ArchivesAgents.agent_id == agent_id,
|
146
|
+
]
|
147
|
+
|
148
|
+
async with db_registry.async_session() as session:
|
149
|
+
archives = await ArchiveModel.list_async(
|
150
|
+
db_session=session,
|
151
|
+
before=before,
|
152
|
+
after=after,
|
153
|
+
limit=limit,
|
154
|
+
ascending=ascending,
|
155
|
+
actor=actor,
|
156
|
+
check_is_deleted=True,
|
157
|
+
join_model=join_model,
|
158
|
+
join_conditions=join_conditions,
|
159
|
+
**filter_kwargs,
|
160
|
+
)
|
161
|
+
return [a.to_pydantic() for a in archives]
|
162
|
+
|
90
163
|
@enforce_types
|
91
164
|
@trace_method
|
92
165
|
def attach_agent_to_archive(
|
letta/services/file_manager.py
CHANGED
@@ -404,8 +404,10 @@ class FileManager:
|
|
404
404
|
self,
|
405
405
|
source_id: str,
|
406
406
|
actor: PydanticUser,
|
407
|
+
before: Optional[str] = None,
|
407
408
|
after: Optional[str] = None,
|
408
409
|
limit: Optional[int] = 50,
|
410
|
+
ascending: Optional[bool] = True,
|
409
411
|
include_content: bool = False,
|
410
412
|
strip_directory_prefix: bool = False,
|
411
413
|
check_status_updates: bool = False,
|
@@ -415,8 +417,10 @@ class FileManager:
|
|
415
417
|
Args:
|
416
418
|
source_id: Source to list files from
|
417
419
|
actor: User performing the request
|
420
|
+
before: Before filter
|
418
421
|
after: Pagination cursor
|
419
422
|
limit: Maximum number of files to return
|
423
|
+
ascending: Sort by ascending or descending order
|
420
424
|
include_content: Whether to include file content
|
421
425
|
strip_directory_prefix: Whether to strip directory prefix from filenames
|
422
426
|
check_status_updates: Whether to check and update status for timeout and embedding completion
|
@@ -429,8 +433,10 @@ class FileManager:
|
|
429
433
|
|
430
434
|
files = await FileMetadataModel.list_async(
|
431
435
|
db_session=session,
|
436
|
+
before=before,
|
432
437
|
after=after,
|
433
438
|
limit=limit,
|
439
|
+
ascending=ascending,
|
434
440
|
organization_id=actor.organization_id,
|
435
441
|
source_id=source_id,
|
436
442
|
query_options=options,
|
letta/services/group_manager.py
CHANGED
@@ -29,6 +29,7 @@ class GroupManager:
|
|
29
29
|
before: Optional[str] = None,
|
30
30
|
after: Optional[str] = None,
|
31
31
|
limit: Optional[int] = 50,
|
32
|
+
ascending: bool = True,
|
32
33
|
show_hidden_groups: Optional[bool] = None,
|
33
34
|
) -> list[PydanticGroup]:
|
34
35
|
async with db_registry.async_session() as session:
|
@@ -50,7 +51,7 @@ class GroupManager:
|
|
50
51
|
query = query.where((GroupModel.hidden.is_(None)) | (GroupModel.hidden == False))
|
51
52
|
|
52
53
|
# Apply pagination
|
53
|
-
query = await _apply_group_pagination_async(query, before, after, session, ascending=
|
54
|
+
query = await _apply_group_pagination_async(query, before, after, session, ascending=ascending)
|
54
55
|
|
55
56
|
if limit:
|
56
57
|
query = query.limit(limit)
|
@@ -29,7 +29,6 @@ from letta.orm.errors import NoResultFound
|
|
29
29
|
from letta.orm.identity import Identity
|
30
30
|
from letta.orm.passage import ArchivalPassage, SourcePassage
|
31
31
|
from letta.orm.sources_agents import SourcesAgents
|
32
|
-
from letta.orm.sqlite_functions import adapt_array
|
33
32
|
from letta.otel.tracing import trace_method
|
34
33
|
from letta.prompts import gpt_system
|
35
34
|
from letta.prompts.prompt_generator import PromptGenerator
|
@@ -921,6 +920,8 @@ async def build_passage_query(
|
|
921
920
|
main_query = main_query.order_by(combined_query.c.embedding.cosine_distance(embedded_text).asc())
|
922
921
|
else:
|
923
922
|
# SQLite with custom vector type
|
923
|
+
from letta.orm.sqlite_functions import adapt_array
|
924
|
+
|
924
925
|
query_embedding_binary = adapt_array(embedded_text)
|
925
926
|
main_query = main_query.order_by(
|
926
927
|
func.cosine_distance(combined_query.c.embedding, query_embedding_binary).asc(),
|
@@ -1054,6 +1055,8 @@ async def build_source_passage_query(
|
|
1054
1055
|
query = query.order_by(SourcePassage.embedding.cosine_distance(embedded_text).asc())
|
1055
1056
|
else:
|
1056
1057
|
# SQLite with custom vector type
|
1058
|
+
from letta.orm.sqlite_functions import adapt_array
|
1059
|
+
|
1057
1060
|
query_embedding_binary = adapt_array(embedded_text)
|
1058
1061
|
query = query.order_by(
|
1059
1062
|
func.cosine_distance(SourcePassage.embedding, query_embedding_binary).asc(),
|
@@ -1151,6 +1154,8 @@ async def build_agent_passage_query(
|
|
1151
1154
|
query = query.order_by(ArchivalPassage.embedding.cosine_distance(embedded_text).asc())
|
1152
1155
|
else:
|
1153
1156
|
# SQLite with custom vector type
|
1157
|
+
from letta.orm.sqlite_functions import adapt_array
|
1158
|
+
|
1154
1159
|
query_embedding_binary = adapt_array(embedded_text)
|
1155
1160
|
query = query.order_by(
|
1156
1161
|
func.cosine_distance(ArchivalPassage.embedding, query_embedding_binary).asc(),
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import asyncio
|
1
2
|
from typing import List, Optional
|
2
3
|
|
3
4
|
from fastapi import HTTPException
|
@@ -9,6 +10,8 @@ from letta.orm.block import Block as BlockModel
|
|
9
10
|
from letta.orm.errors import UniqueConstraintViolationError
|
10
11
|
from letta.orm.identity import Identity as IdentityModel
|
11
12
|
from letta.otel.tracing import trace_method
|
13
|
+
from letta.schemas.agent import AgentState
|
14
|
+
from letta.schemas.block import Block
|
12
15
|
from letta.schemas.identity import (
|
13
16
|
Identity as PydanticIdentity,
|
14
17
|
IdentityCreate,
|
@@ -35,6 +38,7 @@ class IdentityManager:
|
|
35
38
|
before: Optional[str] = None,
|
36
39
|
after: Optional[str] = None,
|
37
40
|
limit: Optional[int] = 50,
|
41
|
+
ascending: bool = False,
|
38
42
|
actor: PydanticUser = None,
|
39
43
|
) -> list[PydanticIdentity]:
|
40
44
|
async with db_registry.async_session() as session:
|
@@ -51,6 +55,7 @@ class IdentityManager:
|
|
51
55
|
before=before,
|
52
56
|
after=after,
|
53
57
|
limit=limit,
|
58
|
+
ascending=ascending,
|
54
59
|
**filters,
|
55
60
|
)
|
56
61
|
return [identity.to_pydantic() for identity in identities]
|
@@ -272,3 +277,65 @@ class IdentityManager:
|
|
272
277
|
current_ids = {item.id for item in current_relationship}
|
273
278
|
new_items = [item for item in found_items if item.id not in current_ids]
|
274
279
|
current_relationship.extend(new_items)
|
280
|
+
|
281
|
+
@enforce_types
|
282
|
+
@trace_method
|
283
|
+
async def list_agents_for_identity_async(
|
284
|
+
self,
|
285
|
+
identity_id: str,
|
286
|
+
before: Optional[str] = None,
|
287
|
+
after: Optional[str] = None,
|
288
|
+
limit: Optional[int] = 50,
|
289
|
+
ascending: bool = False,
|
290
|
+
actor: PydanticUser = None,
|
291
|
+
) -> List[AgentState]:
|
292
|
+
"""
|
293
|
+
Get all agents associated with the specified identity.
|
294
|
+
"""
|
295
|
+
async with db_registry.async_session() as session:
|
296
|
+
# First verify the identity exists and belongs to the user
|
297
|
+
identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor)
|
298
|
+
if identity is None:
|
299
|
+
raise HTTPException(status_code=404, detail=f"Identity with id={identity_id} not found")
|
300
|
+
|
301
|
+
# Get agents associated with this identity with pagination
|
302
|
+
agents = await AgentModel.list_async(
|
303
|
+
db_session=session,
|
304
|
+
before=before,
|
305
|
+
after=after,
|
306
|
+
limit=limit,
|
307
|
+
ascending=ascending,
|
308
|
+
identity_id=identity.id,
|
309
|
+
)
|
310
|
+
return await asyncio.gather(*[agent.to_pydantic_async() for agent in agents])
|
311
|
+
|
312
|
+
@enforce_types
|
313
|
+
@trace_method
|
314
|
+
async def list_blocks_for_identity_async(
|
315
|
+
self,
|
316
|
+
identity_id: str,
|
317
|
+
before: Optional[str] = None,
|
318
|
+
after: Optional[str] = None,
|
319
|
+
limit: Optional[int] = 50,
|
320
|
+
ascending: bool = False,
|
321
|
+
actor: PydanticUser = None,
|
322
|
+
) -> List[Block]:
|
323
|
+
"""
|
324
|
+
Get all blocks associated with the specified identity.
|
325
|
+
"""
|
326
|
+
async with db_registry.async_session() as session:
|
327
|
+
# First verify the identity exists and belongs to the user
|
328
|
+
identity = await IdentityModel.read_async(db_session=session, identifier=identity_id, actor=actor)
|
329
|
+
if identity is None:
|
330
|
+
raise HTTPException(status_code=404, detail=f"Identity with id={identity_id} not found")
|
331
|
+
|
332
|
+
# Get blocks associated with this identity with pagination
|
333
|
+
blocks = await BlockModel.list_async(
|
334
|
+
db_session=session,
|
335
|
+
before=before,
|
336
|
+
after=after,
|
337
|
+
limit=limit,
|
338
|
+
ascending=ascending,
|
339
|
+
identity_id=identity.id,
|
340
|
+
)
|
341
|
+
return [block.to_pydantic() for block in blocks]
|
letta/services/job_manager.py
CHANGED
@@ -18,6 +18,7 @@ from letta.otel.tracing import log_event, trace_method
|
|
18
18
|
from letta.schemas.enums import JobStatus, JobType, MessageRole
|
19
19
|
from letta.schemas.job import BatchJob as PydanticBatchJob, Job as PydanticJob, JobUpdate, LettaRequestConfig
|
20
20
|
from letta.schemas.letta_message import LettaMessage
|
21
|
+
from letta.schemas.letta_stop_reason import StopReasonType
|
21
22
|
from letta.schemas.message import Message as PydanticMessage
|
22
23
|
from letta.schemas.run import Run as PydanticRun
|
23
24
|
from letta.schemas.step import Step as PydanticStep
|
@@ -207,7 +208,12 @@ class JobManager:
|
|
207
208
|
@enforce_types
|
208
209
|
@trace_method
|
209
210
|
async def safe_update_job_status_async(
|
210
|
-
self,
|
211
|
+
self,
|
212
|
+
job_id: str,
|
213
|
+
new_status: JobStatus,
|
214
|
+
actor: PydanticUser,
|
215
|
+
stop_reason: Optional[StopReasonType] = None,
|
216
|
+
metadata: Optional[dict] = None,
|
211
217
|
) -> bool:
|
212
218
|
"""
|
213
219
|
Safely update job status with state transition guards.
|
@@ -217,7 +223,7 @@ class JobManager:
|
|
217
223
|
True if update was successful, False if update was skipped due to invalid transition
|
218
224
|
"""
|
219
225
|
try:
|
220
|
-
job_update_builder = partial(JobUpdate, status=new_status)
|
226
|
+
job_update_builder = partial(JobUpdate, status=new_status, stop_reason=stop_reason)
|
221
227
|
|
222
228
|
# If metadata is provided, merge it with existing metadata
|
223
229
|
if metadata:
|
@@ -268,6 +274,7 @@ class JobManager:
|
|
268
274
|
statuses: Optional[List[JobStatus]] = None,
|
269
275
|
job_type: JobType = JobType.JOB,
|
270
276
|
ascending: bool = True,
|
277
|
+
stop_reason: Optional[StopReasonType] = None,
|
271
278
|
) -> List[PydanticJob]:
|
272
279
|
"""List all jobs with optional pagination and status filter."""
|
273
280
|
with db_registry.session() as session:
|
@@ -277,6 +284,10 @@ class JobManager:
|
|
277
284
|
if statuses:
|
278
285
|
filter_kwargs["status"] = statuses
|
279
286
|
|
287
|
+
# Add stop_reason filter if provided
|
288
|
+
if stop_reason is not None:
|
289
|
+
filter_kwargs["stop_reason"] = stop_reason
|
290
|
+
|
280
291
|
jobs = JobModel.list(
|
281
292
|
db_session=session,
|
282
293
|
before=before,
|
@@ -299,6 +310,7 @@ class JobManager:
|
|
299
310
|
job_type: JobType = JobType.JOB,
|
300
311
|
ascending: bool = True,
|
301
312
|
source_id: Optional[str] = None,
|
313
|
+
stop_reason: Optional[StopReasonType] = None,
|
302
314
|
) -> List[PydanticJob]:
|
303
315
|
"""List all jobs with optional pagination and status filter."""
|
304
316
|
from sqlalchemy import and_, or_, select
|
@@ -317,6 +329,10 @@ class JobManager:
|
|
317
329
|
column = column.op("->>")("source_id")
|
318
330
|
query = query.where(column == source_id)
|
319
331
|
|
332
|
+
# add stop_reason filter if provided
|
333
|
+
if stop_reason is not None:
|
334
|
+
query = query.where(JobModel.stop_reason == stop_reason)
|
335
|
+
|
320
336
|
# handle cursor-based pagination
|
321
337
|
if before or after:
|
322
338
|
# get cursor objects
|