letta-nightly 0.11.7.dev20250916104104__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/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/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/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +9 -4
- letta/server/rest_api/routers/v1/archives.py +113 -0
- letta/server/rest_api/routers/v1/jobs.py +7 -2
- letta/server/rest_api/routers/v1/runs.py +9 -1
- letta/server/rest_api/routers/v1/tools.py +7 -26
- letta/services/agent_manager.py +17 -9
- letta/services/agent_serialization_manager.py +11 -3
- letta/services/archive_manager.py +73 -0
- letta/services/helpers/agent_manager_helper.py +6 -1
- letta/services/job_manager.py +18 -2
- letta/services/mcp_manager.py +198 -82
- letta/services/telemetry_manager.py +2 -0
- letta/services/tool_executor/composio_tool_executor.py +1 -1
- letta/services/tool_sandbox/base.py +2 -3
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/METADATA +5 -2
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/RECORD +44 -41
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250917104122.dist-info}/licenses/LICENSE +0 -0
@@ -38,6 +38,7 @@ from letta.schemas.job import JobStatus, JobUpdate, LettaRequestConfig
|
|
38
38
|
from letta.schemas.letta_message import LettaMessageUnion, LettaMessageUpdateUnion, MessageType
|
39
39
|
from letta.schemas.letta_request import LettaAsyncRequest, LettaRequest, LettaStreamingRequest
|
40
40
|
from letta.schemas.letta_response import LettaResponse
|
41
|
+
from letta.schemas.letta_stop_reason import StopReasonType
|
41
42
|
from letta.schemas.memory import (
|
42
43
|
ArchivalMemorySearchResponse,
|
43
44
|
ArchivalMemorySearchResult,
|
@@ -1192,6 +1193,7 @@ async def send_message(
|
|
1192
1193
|
await redis_client.set(f"{REDIS_RUN_ID_PREFIX}:{agent_id}", run.id if run else None)
|
1193
1194
|
|
1194
1195
|
try:
|
1196
|
+
result = None
|
1195
1197
|
if agent_eligible and model_compatible:
|
1196
1198
|
agent_loop = AgentLoop.load(agent_state=agent, actor=actor)
|
1197
1199
|
result = await agent_loop.step(
|
@@ -1229,11 +1231,17 @@ async def send_message(
|
|
1229
1231
|
raise
|
1230
1232
|
finally:
|
1231
1233
|
if settings.track_agent_run:
|
1234
|
+
if result:
|
1235
|
+
stop_reason = result.stop_reason.stop_reason
|
1236
|
+
else:
|
1237
|
+
# NOTE: we could also consider this an error?
|
1238
|
+
stop_reason = None
|
1232
1239
|
await server.job_manager.safe_update_job_status_async(
|
1233
1240
|
job_id=run.id,
|
1234
1241
|
new_status=job_status,
|
1235
1242
|
actor=actor,
|
1236
1243
|
metadata=job_update_metadata,
|
1244
|
+
stop_reason=stop_reason,
|
1237
1245
|
)
|
1238
1246
|
|
1239
1247
|
|
@@ -1440,10 +1448,7 @@ async def send_message_streaming(
|
|
1440
1448
|
finally:
|
1441
1449
|
if settings.track_agent_run:
|
1442
1450
|
await server.job_manager.safe_update_job_status_async(
|
1443
|
-
job_id=run.id,
|
1444
|
-
new_status=job_status,
|
1445
|
-
actor=actor,
|
1446
|
-
metadata=job_update_metadata,
|
1451
|
+
job_id=run.id, new_status=job_status, actor=actor, metadata=job_update_metadata
|
1447
1452
|
)
|
1448
1453
|
|
1449
1454
|
|
@@ -0,0 +1,113 @@
|
|
1
|
+
from typing import List, Literal, Optional
|
2
|
+
|
3
|
+
from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
4
|
+
from pydantic import BaseModel
|
5
|
+
|
6
|
+
from letta.orm.errors import NoResultFound
|
7
|
+
from letta.schemas.archive import Archive as PydanticArchive
|
8
|
+
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
9
|
+
from letta.server.server import SyncServer
|
10
|
+
|
11
|
+
router = APIRouter(prefix="/archives", tags=["archives"])
|
12
|
+
|
13
|
+
|
14
|
+
class ArchiveCreateRequest(BaseModel):
|
15
|
+
"""Request model for creating an archive.
|
16
|
+
|
17
|
+
Intentionally excludes vector_db_provider. These are derived internally (vector DB provider from env).
|
18
|
+
"""
|
19
|
+
|
20
|
+
name: str
|
21
|
+
description: Optional[str] = None
|
22
|
+
|
23
|
+
|
24
|
+
class ArchiveUpdateRequest(BaseModel):
|
25
|
+
"""Request model for updating an archive (partial).
|
26
|
+
|
27
|
+
Supports updating only name and description.
|
28
|
+
"""
|
29
|
+
|
30
|
+
name: Optional[str] = None
|
31
|
+
description: Optional[str] = None
|
32
|
+
|
33
|
+
|
34
|
+
@router.post("/", response_model=PydanticArchive, operation_id="create_archive")
|
35
|
+
async def create_archive(
|
36
|
+
archive: ArchiveCreateRequest = Body(...),
|
37
|
+
server: "SyncServer" = Depends(get_letta_server),
|
38
|
+
headers: HeaderParams = Depends(get_headers),
|
39
|
+
):
|
40
|
+
"""
|
41
|
+
Create a new archive.
|
42
|
+
"""
|
43
|
+
try:
|
44
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
45
|
+
return await server.archive_manager.create_archive_async(
|
46
|
+
name=archive.name,
|
47
|
+
description=archive.description,
|
48
|
+
actor=actor,
|
49
|
+
)
|
50
|
+
except Exception as e:
|
51
|
+
raise HTTPException(status_code=500, detail=str(e))
|
52
|
+
|
53
|
+
|
54
|
+
@router.get("/", response_model=List[PydanticArchive], operation_id="list_archives")
|
55
|
+
async def list_archives(
|
56
|
+
before: Optional[str] = Query(
|
57
|
+
None,
|
58
|
+
description="Archive ID cursor for pagination. Returns archives that come before this archive ID in the specified sort order",
|
59
|
+
),
|
60
|
+
after: Optional[str] = Query(
|
61
|
+
None,
|
62
|
+
description="Archive ID cursor for pagination. Returns archives that come after this archive ID in the specified sort order",
|
63
|
+
),
|
64
|
+
limit: Optional[int] = Query(50, description="Maximum number of archives to return"),
|
65
|
+
order: Literal["asc", "desc"] = Query(
|
66
|
+
"desc", description="Sort order for archives by creation time. 'asc' for oldest first, 'desc' for newest first"
|
67
|
+
),
|
68
|
+
name: Optional[str] = Query(None, description="Filter by archive name (exact match)"),
|
69
|
+
agent_id: Optional[str] = Query(None, description="Only archives attached to this agent ID"),
|
70
|
+
server: "SyncServer" = Depends(get_letta_server),
|
71
|
+
headers: HeaderParams = Depends(get_headers),
|
72
|
+
):
|
73
|
+
"""
|
74
|
+
Get a list of all archives for the current organization with optional filters and pagination.
|
75
|
+
"""
|
76
|
+
try:
|
77
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
78
|
+
archives = await server.archive_manager.list_archives_async(
|
79
|
+
actor=actor,
|
80
|
+
before=before,
|
81
|
+
after=after,
|
82
|
+
limit=limit,
|
83
|
+
ascending=(order == "asc"),
|
84
|
+
name=name,
|
85
|
+
agent_id=agent_id,
|
86
|
+
)
|
87
|
+
return archives
|
88
|
+
except Exception as e:
|
89
|
+
raise HTTPException(status_code=500, detail=str(e))
|
90
|
+
|
91
|
+
|
92
|
+
@router.patch("/{archive_id}", response_model=PydanticArchive, operation_id="modify_archive")
|
93
|
+
async def modify_archive(
|
94
|
+
archive_id: str,
|
95
|
+
archive: ArchiveUpdateRequest = Body(...),
|
96
|
+
server: "SyncServer" = Depends(get_letta_server),
|
97
|
+
headers: HeaderParams = Depends(get_headers),
|
98
|
+
):
|
99
|
+
"""
|
100
|
+
Update an existing archive's name and/or description.
|
101
|
+
"""
|
102
|
+
try:
|
103
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
104
|
+
return await server.archive_manager.update_archive_async(
|
105
|
+
archive_id=archive_id,
|
106
|
+
name=archive.name,
|
107
|
+
description=archive.description,
|
108
|
+
actor=actor,
|
109
|
+
)
|
110
|
+
except NoResultFound as e:
|
111
|
+
raise HTTPException(status_code=404, detail=str(e))
|
112
|
+
except Exception as e:
|
113
|
+
raise HTTPException(status_code=500, detail=str(e))
|
@@ -19,18 +19,23 @@ async def list_jobs(
|
|
19
19
|
before: Optional[str] = Query(None, description="Cursor for pagination"),
|
20
20
|
after: Optional[str] = Query(None, description="Cursor for pagination"),
|
21
21
|
limit: Optional[int] = Query(50, description="Limit for pagination"),
|
22
|
+
active: bool = Query(False, description="Filter for active jobs."),
|
22
23
|
ascending: bool = Query(True, description="Whether to sort jobs oldest to newest (True, default) or newest to oldest (False)"),
|
23
24
|
headers: HeaderParams = Depends(get_headers),
|
24
25
|
):
|
25
26
|
"""
|
26
27
|
List all jobs.
|
27
|
-
TODO (cliandy): implementation for pagination
|
28
28
|
"""
|
29
29
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
30
30
|
|
31
|
+
statuses = None
|
32
|
+
if active:
|
33
|
+
statuses = [JobStatus.created, JobStatus.running]
|
34
|
+
|
31
35
|
# TODO: add filtering by status
|
32
36
|
return await server.job_manager.list_jobs_async(
|
33
37
|
actor=actor,
|
38
|
+
statuses=statuses,
|
34
39
|
source_id=source_id,
|
35
40
|
before=before,
|
36
41
|
after=after,
|
@@ -39,7 +44,7 @@ async def list_jobs(
|
|
39
44
|
)
|
40
45
|
|
41
46
|
|
42
|
-
@router.get("/active", response_model=List[Job], operation_id="list_active_jobs")
|
47
|
+
@router.get("/active", response_model=List[Job], operation_id="list_active_jobs", deprecated=True)
|
43
48
|
async def list_active_jobs(
|
44
49
|
server: "SyncServer" = Depends(get_letta_server),
|
45
50
|
headers: HeaderParams = Depends(get_headers),
|
@@ -10,6 +10,7 @@ from letta.orm.errors import NoResultFound
|
|
10
10
|
from letta.schemas.enums import JobStatus, JobType
|
11
11
|
from letta.schemas.letta_message import LettaMessageUnion
|
12
12
|
from letta.schemas.letta_request import RetrieveStreamRequest
|
13
|
+
from letta.schemas.letta_stop_reason import StopReasonType
|
13
14
|
from letta.schemas.openai.chat_completion_response import UsageStatistics
|
14
15
|
from letta.schemas.run import Run
|
15
16
|
from letta.schemas.step import Step
|
@@ -31,9 +32,11 @@ def list_runs(
|
|
31
32
|
server: "SyncServer" = Depends(get_letta_server),
|
32
33
|
agent_ids: Optional[List[str]] = Query(None, description="The unique identifier of the agent associated with the run."),
|
33
34
|
background: Optional[bool] = Query(None, description="If True, filters for runs that were created in background mode."),
|
35
|
+
stop_reason: Optional[StopReasonType] = Query(None, description="Filter runs by stop reason."),
|
34
36
|
after: Optional[str] = Query(None, description="Cursor for pagination"),
|
35
37
|
before: Optional[str] = Query(None, description="Cursor for pagination"),
|
36
38
|
limit: Optional[int] = Query(50, description="Maximum number of runs to return"),
|
39
|
+
active: bool = Query(False, description="Filter for active runs."),
|
37
40
|
ascending: bool = Query(
|
38
41
|
False,
|
39
42
|
description="Whether to sort agents oldest to newest (True) or newest to oldest (False, default)",
|
@@ -44,16 +47,21 @@ def list_runs(
|
|
44
47
|
List all runs.
|
45
48
|
"""
|
46
49
|
actor = server.user_manager.get_user_or_default(user_id=headers.actor_id)
|
50
|
+
statuses = None
|
51
|
+
if active:
|
52
|
+
statuses = [JobStatus.created, JobStatus.running]
|
47
53
|
|
48
54
|
runs = [
|
49
55
|
Run.from_job(job)
|
50
56
|
for job in server.job_manager.list_jobs(
|
51
57
|
actor=actor,
|
58
|
+
statuses=statuses,
|
52
59
|
job_type=JobType.RUN,
|
53
60
|
limit=limit,
|
54
61
|
before=before,
|
55
62
|
after=after,
|
56
63
|
ascending=False,
|
64
|
+
stop_reason=stop_reason,
|
57
65
|
)
|
58
66
|
]
|
59
67
|
if agent_ids:
|
@@ -63,7 +71,7 @@ def list_runs(
|
|
63
71
|
return runs
|
64
72
|
|
65
73
|
|
66
|
-
@router.get("/active", response_model=List[Run], operation_id="list_active_runs")
|
74
|
+
@router.get("/active", response_model=List[Run], operation_id="list_active_runs", deprecated=True)
|
67
75
|
def list_active_runs(
|
68
76
|
server: "SyncServer" = Depends(get_letta_server),
|
69
77
|
agent_ids: Optional[List[str]] = Query(None, description="The unique identifier of the agent associated with the run."),
|
@@ -728,35 +728,16 @@ async def add_mcp_server_to_config(
|
|
728
728
|
return await server.add_mcp_server_to_config(server_config=request, allow_upsert=True)
|
729
729
|
else:
|
730
730
|
# log to DB
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
736
|
-
if tool_settings.mcp_disable_stdio: # protected server
|
737
|
-
raise HTTPException(
|
738
|
-
status_code=400,
|
739
|
-
detail="stdio is not supported in the current environment, please use a self-hosted Letta server in order to add a stdio MCP server",
|
740
|
-
)
|
741
|
-
elif isinstance(request, SSEServerConfig):
|
742
|
-
mapped_request = MCPServer(
|
743
|
-
server_name=request.server_name,
|
744
|
-
server_type=request.type,
|
745
|
-
server_url=request.server_url,
|
746
|
-
token=request.resolve_token(),
|
747
|
-
custom_headers=request.custom_headers,
|
748
|
-
)
|
749
|
-
elif isinstance(request, StreamableHTTPServerConfig):
|
750
|
-
mapped_request = MCPServer(
|
751
|
-
server_name=request.server_name,
|
752
|
-
server_type=request.type,
|
753
|
-
server_url=request.server_url,
|
754
|
-
token=request.resolve_token(),
|
755
|
-
custom_headers=request.custom_headers,
|
731
|
+
# Check if stdio servers are disabled
|
732
|
+
if isinstance(request, StdioServerConfig) and tool_settings.mcp_disable_stdio:
|
733
|
+
raise HTTPException(
|
734
|
+
status_code=400,
|
735
|
+
detail="stdio is not supported in the current environment, please use a self-hosted Letta server in order to add a stdio MCP server",
|
756
736
|
)
|
757
737
|
|
758
738
|
# Create MCP server and optimistically sync tools
|
759
|
-
|
739
|
+
# The mcp_manager will handle encryption of sensitive fields
|
740
|
+
await server.mcp_manager.create_mcp_server_from_config_with_tools(request, actor=actor)
|
760
741
|
|
761
742
|
# TODO: don't do this in the future (just return MCPServer)
|
762
743
|
all_servers = await server.mcp_manager.list_mcp_servers(actor=actor)
|
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(
|
@@ -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(),
|
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
|