letta-nightly 0.11.7.dev20250916104104__py3-none-any.whl → 0.11.7.dev20250918104055__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 +4 -4
- letta/agents/agent_loop.py +2 -1
- letta/agents/base_agent.py +1 -1
- letta/agents/letta_agent.py +1 -4
- letta/agents/letta_agent_v2.py +5 -4
- letta/agents/temporal/activities/__init__.py +4 -0
- letta/agents/temporal/activities/example_activity.py +7 -0
- letta/agents/temporal/activities/prepare_messages.py +10 -0
- letta/agents/temporal/temporal_agent_workflow.py +56 -0
- letta/agents/temporal/types.py +25 -0
- letta/agents/voice_agent.py +3 -3
- 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 +9 -4
- 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/prompt_generator.py +4 -4
- letta/schemas/agent.py +14 -200
- letta/schemas/enums.py +15 -0
- letta/schemas/job.py +10 -0
- letta/schemas/mcp.py +146 -6
- letta/schemas/memory.py +216 -103
- letta/schemas/provider_trace.py +0 -2
- letta/schemas/run.py +2 -0
- letta/schemas/secret.py +378 -0
- letta/schemas/step.py +5 -1
- letta/schemas/tool_rule.py +34 -44
- 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/steps.py +29 -0
- letta/server/rest_api/routers/v1/tools.py +7 -26
- letta/server/server.py +2 -2
- letta/services/agent_manager.py +21 -15
- letta/services/agent_serialization_manager.py +11 -3
- letta/services/archive_manager.py +73 -0
- letta/services/helpers/agent_manager_helper.py +10 -5
- letta/services/job_manager.py +18 -2
- letta/services/mcp_manager.py +198 -82
- letta/services/step_manager.py +26 -0
- letta/services/summarizer/summarizer.py +25 -3
- letta/services/telemetry_manager.py +2 -0
- letta/services/tool_executor/composio_tool_executor.py +1 -1
- letta/services/tool_executor/sandbox_tool_executor.py +2 -2
- letta/services/tool_sandbox/base.py +135 -9
- letta/settings.py +2 -2
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/METADATA +6 -3
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/RECORD +62 -55
- letta/templates/template_helper.py +0 -53
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20250916104104.dist-info → letta_nightly-0.11.7.dev20250918104055.dist-info}/licenses/LICENSE +0 -0
@@ -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."),
|
@@ -5,6 +5,8 @@ from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query
|
|
5
5
|
from pydantic import BaseModel, Field
|
6
6
|
|
7
7
|
from letta.orm.errors import NoResultFound
|
8
|
+
from letta.schemas.letta_message import LettaMessageUnion
|
9
|
+
from letta.schemas.message import Message
|
8
10
|
from letta.schemas.provider_trace import ProviderTrace
|
9
11
|
from letta.schemas.step import Step
|
10
12
|
from letta.schemas.step_metrics import StepMetrics
|
@@ -138,6 +140,33 @@ async def modify_feedback_for_step(
|
|
138
140
|
raise HTTPException(status_code=404, detail="Step not found")
|
139
141
|
|
140
142
|
|
143
|
+
@router.get("/{step_id}/messages", response_model=List[LettaMessageUnion], operation_id="list_messages_for_step")
|
144
|
+
async def list_messages_for_step(
|
145
|
+
step_id: str,
|
146
|
+
headers: HeaderParams = Depends(get_headers),
|
147
|
+
server: SyncServer = Depends(get_letta_server),
|
148
|
+
before: Optional[str] = Query(
|
149
|
+
None, description="Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order"
|
150
|
+
),
|
151
|
+
after: Optional[str] = Query(
|
152
|
+
None, description="Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order"
|
153
|
+
),
|
154
|
+
limit: Optional[int] = Query(100, description="Maximum number of messages to return"),
|
155
|
+
order: Literal["asc", "desc"] = Query(
|
156
|
+
"asc", description="Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first"
|
157
|
+
),
|
158
|
+
order_by: Literal["created_at"] = Query("created_at", description="Sort by field"),
|
159
|
+
):
|
160
|
+
"""
|
161
|
+
List messages for a given step.
|
162
|
+
"""
|
163
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
164
|
+
messages = await server.step_manager.list_step_messages_async(
|
165
|
+
step_id=step_id, actor=actor, before=before, after=after, limit=limit, ascending=(order == "asc")
|
166
|
+
)
|
167
|
+
return Message.to_letta_messages_from_list(messages)
|
168
|
+
|
169
|
+
|
141
170
|
@router.patch("/{step_id}/transaction/{transaction_id}", response_model=Step, operation_id="update_step_transaction_id")
|
142
171
|
async def update_step_transaction_id(
|
143
172
|
step_id: str,
|
@@ -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/server/server.py
CHANGED
@@ -38,12 +38,12 @@ from letta.log import get_logger
|
|
38
38
|
from letta.orm.errors import NoResultFound
|
39
39
|
from letta.otel.tracing import log_event, trace_method
|
40
40
|
from letta.prompts.gpt_system import get_system_text
|
41
|
-
from letta.schemas.agent import AgentState,
|
41
|
+
from letta.schemas.agent import AgentState, CreateAgent, UpdateAgent
|
42
42
|
from letta.schemas.block import Block, BlockUpdate, CreateBlock
|
43
43
|
from letta.schemas.embedding_config import EmbeddingConfig
|
44
44
|
|
45
45
|
# openai schemas
|
46
|
-
from letta.schemas.enums import JobStatus, MessageStreamStatus, ProviderCategory, ProviderType, SandboxType, ToolSourceType
|
46
|
+
from letta.schemas.enums import AgentType, JobStatus, MessageStreamStatus, ProviderCategory, ProviderType, SandboxType, ToolSourceType
|
47
47
|
from letta.schemas.environment_variables import SandboxEnvironmentVariableCreate
|
48
48
|
from letta.schemas.group import GroupCreate, ManagerType, SleeptimeManager, VoiceSleeptimeManager
|
49
49
|
from letta.schemas.job import Job, JobUpdate
|
letta/services/agent_manager.py
CHANGED
@@ -50,15 +50,13 @@ from letta.otel.tracing import trace_method
|
|
50
50
|
from letta.prompts.prompt_generator import PromptGenerator
|
51
51
|
from letta.schemas.agent import (
|
52
52
|
AgentState as PydanticAgentState,
|
53
|
-
AgentType,
|
54
53
|
CreateAgent,
|
55
54
|
InternalTemplateAgentCreate,
|
56
55
|
UpdateAgent,
|
57
|
-
get_prompt_template_for_agent_type,
|
58
56
|
)
|
59
57
|
from letta.schemas.block import DEFAULT_BLOCKS, Block as PydanticBlock, BlockUpdate
|
60
58
|
from letta.schemas.embedding_config import EmbeddingConfig
|
61
|
-
from letta.schemas.enums import ProviderType, TagMatchMode, ToolType, VectorDBProvider
|
59
|
+
from letta.schemas.enums import AgentType, ProviderType, TagMatchMode, ToolType, VectorDBProvider
|
62
60
|
from letta.schemas.file import FileMetadata as PydanticFileMetadata
|
63
61
|
from letta.schemas.group import Group as PydanticGroup, ManagerType
|
64
62
|
from letta.schemas.llm_config import LLMConfig
|
@@ -455,7 +453,8 @@ class AgentManager:
|
|
455
453
|
[{"agent_id": aid, "identity_id": iid} for iid in identity_ids],
|
456
454
|
)
|
457
455
|
|
458
|
-
|
456
|
+
agent_secrets = agent_create.secrets or agent_create.tool_exec_environment_variables
|
457
|
+
if agent_secrets:
|
459
458
|
env_rows = [
|
460
459
|
{
|
461
460
|
"agent_id": aid,
|
@@ -463,7 +462,7 @@ class AgentManager:
|
|
463
462
|
"value": val,
|
464
463
|
"organization_id": actor.organization_id,
|
465
464
|
}
|
466
|
-
for key, val in
|
465
|
+
for key, val in agent_secrets.items()
|
467
466
|
]
|
468
467
|
session.execute(insert(AgentEnvironmentVariable).values(env_rows))
|
469
468
|
|
@@ -674,7 +673,8 @@ class AgentManager:
|
|
674
673
|
)
|
675
674
|
|
676
675
|
env_rows = []
|
677
|
-
|
676
|
+
agent_secrets = agent_create.secrets or agent_create.tool_exec_environment_variables
|
677
|
+
if agent_secrets:
|
678
678
|
env_rows = [
|
679
679
|
{
|
680
680
|
"agent_id": aid,
|
@@ -682,7 +682,7 @@ class AgentManager:
|
|
682
682
|
"value": val,
|
683
683
|
"organization_id": actor.organization_id,
|
684
684
|
}
|
685
|
-
for key, val in
|
685
|
+
for key, val in agent_secrets.items()
|
686
686
|
]
|
687
687
|
result = await session.execute(insert(AgentEnvironmentVariable).values(env_rows).returning(AgentEnvironmentVariable.id))
|
688
688
|
env_rows = [{**row, "id": env_var_id} for row, env_var_id in zip(env_rows, result.scalars().all())]
|
@@ -701,8 +701,9 @@ class AgentManager:
|
|
701
701
|
|
702
702
|
result = await new_agent.to_pydantic_async(include_relationships=include_relationships)
|
703
703
|
|
704
|
-
if
|
704
|
+
if agent_secrets and env_rows:
|
705
705
|
result.tool_exec_environment_variables = [AgentEnvironmentVariable(**row) for row in env_rows]
|
706
|
+
result.secrets = [AgentEnvironmentVariable(**row) for row in env_rows]
|
706
707
|
|
707
708
|
# initial message sequence (skip if _init_with_no_messages is True)
|
708
709
|
if not _init_with_no_messages:
|
@@ -894,7 +895,8 @@ class AgentManager:
|
|
894
895
|
)
|
895
896
|
session.expire(agent, ["tags"])
|
896
897
|
|
897
|
-
|
898
|
+
agent_secrets = agent_update.secrets or agent_update.tool_exec_environment_variables
|
899
|
+
if agent_secrets is not None:
|
898
900
|
session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
|
899
901
|
env_rows = [
|
900
902
|
{
|
@@ -903,7 +905,7 @@ class AgentManager:
|
|
903
905
|
"value": v,
|
904
906
|
"organization_id": agent.organization_id,
|
905
907
|
}
|
906
|
-
for k, v in
|
908
|
+
for k, v in agent_secrets.items()
|
907
909
|
]
|
908
910
|
if env_rows:
|
909
911
|
self._bulk_insert_pivot(session, AgentEnvironmentVariable.__table__, env_rows)
|
@@ -1019,7 +1021,8 @@ class AgentManager:
|
|
1019
1021
|
)
|
1020
1022
|
session.expire(agent, ["tags"])
|
1021
1023
|
|
1022
|
-
|
1024
|
+
agent_secrets = agent_update.secrets or agent_update.tool_exec_environment_variables
|
1025
|
+
if agent_secrets is not None:
|
1023
1026
|
await session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
|
1024
1027
|
env_rows = [
|
1025
1028
|
{
|
@@ -1028,7 +1031,7 @@ class AgentManager:
|
|
1028
1031
|
"value": v,
|
1029
1032
|
"organization_id": agent.organization_id,
|
1030
1033
|
}
|
1031
|
-
for k, v in
|
1034
|
+
for k, v in agent_secrets.items()
|
1032
1035
|
]
|
1033
1036
|
if env_rows:
|
1034
1037
|
await self._bulk_insert_pivot_async(session, AgentEnvironmentVariable.__table__, env_rows)
|
@@ -1544,6 +1547,8 @@ class AgentManager:
|
|
1544
1547
|
if env_vars:
|
1545
1548
|
for var in agent.tool_exec_environment_variables:
|
1546
1549
|
var.value = env_vars.get(var.key, "")
|
1550
|
+
for var in agent.secrets:
|
1551
|
+
var.value = env_vars.get(var.key, "")
|
1547
1552
|
|
1548
1553
|
agent = agent.create(session, actor=actor)
|
1549
1554
|
|
@@ -1627,6 +1632,7 @@ class AgentManager:
|
|
1627
1632
|
# Remove stale variables
|
1628
1633
|
stale_keys = set(existing_vars) - set(env_vars)
|
1629
1634
|
agent.tool_exec_environment_variables = [var for var in updated_vars if var.key not in stale_keys]
|
1635
|
+
agent.secrets = [var for var in updated_vars if var.key not in stale_keys]
|
1630
1636
|
|
1631
1637
|
# Update the agent in the database
|
1632
1638
|
agent.update(session, actor=actor)
|
@@ -1786,7 +1792,7 @@ class AgentManager:
|
|
1786
1792
|
|
1787
1793
|
# note: we only update the system prompt if the core memory is changed
|
1788
1794
|
# this means that the archival/recall memory statistics may be someout out of date
|
1789
|
-
curr_memory_str =
|
1795
|
+
curr_memory_str = agent_state.memory.compile(
|
1790
1796
|
sources=agent_state.sources,
|
1791
1797
|
tool_usage_rules=tool_rules_solver.compile_tool_rule_prompts(),
|
1792
1798
|
max_files_open=agent_state.max_files_open,
|
@@ -1976,7 +1982,7 @@ class AgentManager:
|
|
1976
1982
|
agent_state = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor, include_relationships=["memory", "sources"])
|
1977
1983
|
system_message = await self.message_manager.get_message_by_id_async(message_id=agent_state.message_ids[0], actor=actor)
|
1978
1984
|
temp_tool_rules_solver = ToolRulesSolver(agent_state.tool_rules)
|
1979
|
-
new_memory_str =
|
1985
|
+
new_memory_str = new_memory.compile(
|
1980
1986
|
sources=agent_state.sources,
|
1981
1987
|
tool_usage_rules=temp_tool_rules_solver.compile_tool_rule_prompts(),
|
1982
1988
|
max_files_open=agent_state.max_files_open,
|
@@ -2000,7 +2006,7 @@ class AgentManager:
|
|
2000
2006
|
agent_state.memory = Memory(
|
2001
2007
|
blocks=blocks,
|
2002
2008
|
file_blocks=agent_state.memory.file_blocks,
|
2003
|
-
|
2009
|
+
agent_type=agent_state.agent_type,
|
2004
2010
|
)
|
2005
2011
|
|
2006
2012
|
# NOTE: don't do this since re-buildin the memory is handled at the start of the step
|
@@ -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,13 +29,12 @@ 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
|
36
|
-
from letta.schemas.agent import AgentState
|
35
|
+
from letta.schemas.agent import AgentState
|
37
36
|
from letta.schemas.embedding_config import EmbeddingConfig
|
38
|
-
from letta.schemas.enums import MessageRole
|
37
|
+
from letta.schemas.enums import AgentType, MessageRole
|
39
38
|
from letta.schemas.letta_message_content import TextContent
|
40
39
|
from letta.schemas.memory import Memory
|
41
40
|
from letta.schemas.message import Message, MessageCreate
|
@@ -246,7 +245,7 @@ def compile_system_message(
|
|
246
245
|
timezone: str,
|
247
246
|
user_defined_variables: Optional[dict] = None,
|
248
247
|
append_icm_if_missing: bool = True,
|
249
|
-
template_format: Literal["f-string", "mustache"
|
248
|
+
template_format: Literal["f-string", "mustache"] = "f-string",
|
250
249
|
previous_message_count: int = 0,
|
251
250
|
archival_memory_size: int | None = 0,
|
252
251
|
tool_rules_solver: Optional[ToolRulesSolver] = None,
|
@@ -312,7 +311,7 @@ def compile_system_message(
|
|
312
311
|
raise ValueError(f"Failed to format system prompt - {str(e)}. System prompt value:\n{system_prompt}")
|
313
312
|
|
314
313
|
else:
|
315
|
-
# TODO support for mustache
|
314
|
+
# TODO support for mustache
|
316
315
|
raise NotImplementedError(template_format)
|
317
316
|
|
318
317
|
return formatted_prompt
|
@@ -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(),
|