letta-nightly 0.11.7.dev20251006104136__py3-none-any.whl → 0.11.7.dev20251008104128__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/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/letta_llm_request_adapter.py +0 -1
- letta/adapters/letta_llm_stream_adapter.py +7 -2
- letta/adapters/simple_llm_request_adapter.py +88 -0
- letta/adapters/simple_llm_stream_adapter.py +192 -0
- letta/agents/agent_loop.py +6 -0
- letta/agents/ephemeral_summary_agent.py +2 -1
- letta/agents/helpers.py +142 -6
- letta/agents/letta_agent.py +13 -33
- letta/agents/letta_agent_batch.py +2 -4
- letta/agents/letta_agent_v2.py +87 -77
- letta/agents/letta_agent_v3.py +899 -0
- letta/agents/voice_agent.py +2 -6
- letta/constants.py +8 -4
- letta/errors.py +40 -0
- letta/functions/function_sets/base.py +84 -4
- letta/functions/function_sets/multi_agent.py +0 -3
- letta/functions/schema_generator.py +113 -71
- letta/groups/dynamic_multi_agent.py +3 -2
- letta/groups/helpers.py +1 -2
- letta/groups/round_robin_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/groups/sleeptime_multi_agent_v3.py +17 -17
- letta/groups/supervisor_multi_agent.py +84 -80
- letta/helpers/converters.py +3 -0
- letta/helpers/message_helper.py +4 -0
- letta/helpers/tool_rule_solver.py +92 -5
- letta/interfaces/anthropic_streaming_interface.py +409 -0
- letta/interfaces/gemini_streaming_interface.py +296 -0
- letta/interfaces/openai_streaming_interface.py +752 -1
- letta/llm_api/anthropic_client.py +126 -16
- letta/llm_api/bedrock_client.py +4 -2
- letta/llm_api/deepseek_client.py +4 -1
- letta/llm_api/google_vertex_client.py +123 -42
- letta/llm_api/groq_client.py +4 -1
- letta/llm_api/llm_api_tools.py +11 -4
- letta/llm_api/llm_client_base.py +6 -2
- letta/llm_api/openai.py +32 -2
- letta/llm_api/openai_client.py +423 -18
- letta/llm_api/xai_client.py +4 -1
- letta/main.py +9 -5
- letta/memory.py +1 -0
- letta/orm/__init__.py +1 -1
- letta/orm/agent.py +10 -0
- letta/orm/block.py +7 -16
- letta/orm/blocks_agents.py +8 -2
- letta/orm/files_agents.py +2 -0
- letta/orm/job.py +7 -5
- letta/orm/mcp_oauth.py +1 -0
- letta/orm/message.py +21 -6
- letta/orm/organization.py +2 -0
- letta/orm/provider.py +6 -2
- letta/orm/run.py +71 -0
- letta/orm/sandbox_config.py +7 -1
- letta/orm/sqlalchemy_base.py +0 -306
- letta/orm/step.py +6 -5
- letta/orm/step_metrics.py +5 -5
- letta/otel/tracing.py +28 -3
- letta/plugins/defaults.py +4 -4
- letta/prompts/system_prompts/__init__.py +2 -0
- letta/prompts/system_prompts/letta_v1.py +25 -0
- letta/schemas/agent.py +3 -2
- letta/schemas/agent_file.py +9 -3
- letta/schemas/block.py +23 -10
- letta/schemas/enums.py +21 -2
- letta/schemas/job.py +17 -4
- letta/schemas/letta_message_content.py +71 -2
- letta/schemas/letta_stop_reason.py +5 -5
- letta/schemas/llm_config.py +53 -3
- letta/schemas/memory.py +1 -1
- letta/schemas/message.py +504 -117
- letta/schemas/openai/responses_request.py +64 -0
- letta/schemas/providers/__init__.py +2 -0
- letta/schemas/providers/anthropic.py +16 -0
- letta/schemas/providers/ollama.py +115 -33
- letta/schemas/providers/openrouter.py +52 -0
- letta/schemas/providers/vllm.py +2 -1
- letta/schemas/run.py +48 -42
- letta/schemas/step.py +2 -2
- letta/schemas/step_metrics.py +1 -1
- letta/schemas/tool.py +15 -107
- letta/schemas/tool_rule.py +88 -5
- letta/serialize_schemas/marshmallow_agent.py +1 -0
- letta/server/db.py +86 -408
- letta/server/rest_api/app.py +61 -10
- letta/server/rest_api/dependencies.py +14 -0
- letta/server/rest_api/redis_stream_manager.py +19 -8
- letta/server/rest_api/routers/v1/agents.py +364 -292
- letta/server/rest_api/routers/v1/blocks.py +14 -20
- letta/server/rest_api/routers/v1/identities.py +45 -110
- letta/server/rest_api/routers/v1/internal_templates.py +21 -0
- letta/server/rest_api/routers/v1/jobs.py +23 -6
- letta/server/rest_api/routers/v1/messages.py +1 -1
- letta/server/rest_api/routers/v1/runs.py +126 -85
- letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
- letta/server/rest_api/routers/v1/tools.py +281 -594
- letta/server/rest_api/routers/v1/voice.py +1 -1
- letta/server/rest_api/streaming_response.py +29 -29
- letta/server/rest_api/utils.py +122 -64
- letta/server/server.py +160 -887
- letta/services/agent_manager.py +236 -919
- letta/services/agent_serialization_manager.py +16 -0
- letta/services/archive_manager.py +0 -100
- letta/services/block_manager.py +211 -168
- letta/services/file_manager.py +1 -1
- letta/services/files_agents_manager.py +24 -33
- letta/services/group_manager.py +0 -142
- letta/services/helpers/agent_manager_helper.py +7 -2
- letta/services/helpers/run_manager_helper.py +85 -0
- letta/services/job_manager.py +96 -411
- letta/services/lettuce/__init__.py +6 -0
- letta/services/lettuce/lettuce_client_base.py +86 -0
- letta/services/mcp_manager.py +38 -6
- letta/services/message_manager.py +165 -362
- letta/services/organization_manager.py +0 -36
- letta/services/passage_manager.py +0 -345
- letta/services/provider_manager.py +0 -80
- letta/services/run_manager.py +301 -0
- letta/services/sandbox_config_manager.py +0 -234
- letta/services/step_manager.py +62 -39
- letta/services/summarizer/summarizer.py +9 -7
- letta/services/telemetry_manager.py +0 -16
- letta/services/tool_executor/builtin_tool_executor.py +35 -0
- letta/services/tool_executor/core_tool_executor.py +397 -2
- letta/services/tool_executor/files_tool_executor.py +3 -3
- letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
- letta/services/tool_executor/tool_execution_manager.py +6 -8
- letta/services/tool_executor/tool_executor_base.py +3 -3
- letta/services/tool_manager.py +85 -339
- letta/services/tool_sandbox/base.py +24 -13
- letta/services/tool_sandbox/e2b_sandbox.py +16 -1
- letta/services/tool_schema_generator.py +123 -0
- letta/services/user_manager.py +0 -99
- letta/settings.py +20 -4
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/METADATA +3 -5
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/RECORD +140 -132
- letta/agents/temporal/activities/__init__.py +0 -4
- letta/agents/temporal/activities/example_activity.py +0 -7
- letta/agents/temporal/activities/prepare_messages.py +0 -10
- letta/agents/temporal/temporal_agent_workflow.py +0 -56
- letta/agents/temporal/types.py +0 -25
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251006104136.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,301 @@
|
|
1
|
+
from datetime import datetime
|
2
|
+
from pickletools import pyunicode
|
3
|
+
from typing import List, Literal, Optional
|
4
|
+
|
5
|
+
from httpx import AsyncClient
|
6
|
+
from sqlalchemy import select
|
7
|
+
from sqlalchemy.orm import Session
|
8
|
+
|
9
|
+
from letta.helpers.datetime_helpers import get_utc_time
|
10
|
+
from letta.log import get_logger
|
11
|
+
from letta.orm.errors import NoResultFound
|
12
|
+
from letta.orm.message import Message as MessageModel
|
13
|
+
from letta.orm.run import Run as RunModel
|
14
|
+
from letta.orm.sqlalchemy_base import AccessType
|
15
|
+
from letta.orm.step import Step as StepModel
|
16
|
+
from letta.otel.tracing import log_event, trace_method
|
17
|
+
from letta.schemas.enums import AgentType, MessageRole, RunStatus
|
18
|
+
from letta.schemas.job import LettaRequestConfig
|
19
|
+
from letta.schemas.letta_message import LettaMessage, LettaMessageUnion
|
20
|
+
from letta.schemas.letta_response import LettaResponse
|
21
|
+
from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
|
22
|
+
from letta.schemas.message import Message as PydanticMessage
|
23
|
+
from letta.schemas.run import Run as PydanticRun, RunUpdate
|
24
|
+
from letta.schemas.step import Step as PydanticStep
|
25
|
+
from letta.schemas.usage import LettaUsageStatistics
|
26
|
+
from letta.schemas.user import User as PydanticUser
|
27
|
+
from letta.server.db import db_registry
|
28
|
+
from letta.services.agent_manager import AgentManager
|
29
|
+
from letta.services.helpers.agent_manager_helper import validate_agent_exists_async
|
30
|
+
from letta.services.message_manager import MessageManager
|
31
|
+
from letta.services.step_manager import StepManager
|
32
|
+
from letta.utils import enforce_types
|
33
|
+
|
34
|
+
logger = get_logger(__name__)
|
35
|
+
|
36
|
+
|
37
|
+
class RunManager:
|
38
|
+
"""Manager class to handle business logic related to Runs."""
|
39
|
+
|
40
|
+
def __init__(self):
|
41
|
+
"""Initialize the RunManager."""
|
42
|
+
self.step_manager = StepManager()
|
43
|
+
self.message_manager = MessageManager()
|
44
|
+
self.agent_manager = AgentManager()
|
45
|
+
|
46
|
+
@enforce_types
|
47
|
+
async def create_run(self, pydantic_run: PydanticRun, actor: PydanticUser) -> PydanticRun:
|
48
|
+
"""Create a new run."""
|
49
|
+
async with db_registry.async_session() as session:
|
50
|
+
# Get agent_id from the pydantic object
|
51
|
+
agent_id = pydantic_run.agent_id
|
52
|
+
|
53
|
+
# Verify agent exists before creating the run
|
54
|
+
await validate_agent_exists_async(session, agent_id, actor)
|
55
|
+
organization_id = actor.organization_id
|
56
|
+
|
57
|
+
run_data = pydantic_run.model_dump(exclude_none=True)
|
58
|
+
# Handle metadata field mapping (Pydantic uses 'metadata', ORM uses 'metadata_')
|
59
|
+
if "metadata" in run_data:
|
60
|
+
run_data["metadata_"] = run_data.pop("metadata")
|
61
|
+
|
62
|
+
run = RunModel(**run_data)
|
63
|
+
run.organization_id = organization_id
|
64
|
+
run = await run.create_async(session, actor=actor, no_commit=True, no_refresh=True)
|
65
|
+
await session.commit()
|
66
|
+
|
67
|
+
return run.to_pydantic()
|
68
|
+
|
69
|
+
@enforce_types
|
70
|
+
async def get_run_by_id(self, run_id: str, actor: PydanticUser) -> PydanticRun:
|
71
|
+
"""Get a run by its ID."""
|
72
|
+
async with db_registry.async_session() as session:
|
73
|
+
run = await RunModel.read_async(db_session=session, identifier=run_id, actor=actor, access_type=AccessType.ORGANIZATION)
|
74
|
+
if not run:
|
75
|
+
raise NoResultFound(f"Run with id {run_id} not found")
|
76
|
+
return run.to_pydantic()
|
77
|
+
|
78
|
+
@enforce_types
|
79
|
+
async def list_runs(
|
80
|
+
self,
|
81
|
+
actor: PydanticUser,
|
82
|
+
agent_id: Optional[str] = None,
|
83
|
+
agent_ids: Optional[List[str]] = None,
|
84
|
+
statuses: Optional[List[RunStatus]] = None,
|
85
|
+
limit: Optional[int] = 50,
|
86
|
+
before: Optional[str] = None,
|
87
|
+
after: Optional[str] = None,
|
88
|
+
ascending: bool = False,
|
89
|
+
stop_reason: Optional[str] = None,
|
90
|
+
background: Optional[bool] = None,
|
91
|
+
) -> List[PydanticRun]:
|
92
|
+
"""List runs with filtering options."""
|
93
|
+
async with db_registry.async_session() as session:
|
94
|
+
from sqlalchemy import select
|
95
|
+
|
96
|
+
query = select(RunModel).filter(RunModel.organization_id == actor.organization_id)
|
97
|
+
|
98
|
+
# Handle agent filtering
|
99
|
+
if agent_id:
|
100
|
+
agent_ids = [agent_id]
|
101
|
+
if agent_ids:
|
102
|
+
query = query.filter(RunModel.agent_id.in_(agent_ids))
|
103
|
+
|
104
|
+
# Filter by status
|
105
|
+
if statuses:
|
106
|
+
query = query.filter(RunModel.status.in_(statuses))
|
107
|
+
|
108
|
+
# Filter by stop reason
|
109
|
+
if stop_reason:
|
110
|
+
query = query.filter(RunModel.stop_reason == stop_reason)
|
111
|
+
|
112
|
+
# Filter by background
|
113
|
+
if background is not None:
|
114
|
+
query = query.filter(RunModel.background == background)
|
115
|
+
|
116
|
+
# Apply pagination
|
117
|
+
from letta.services.helpers.run_manager_helper import _apply_pagination_async
|
118
|
+
|
119
|
+
query = await _apply_pagination_async(query, before, after, session, ascending=ascending)
|
120
|
+
|
121
|
+
# Apply limit
|
122
|
+
if limit:
|
123
|
+
query = query.limit(limit)
|
124
|
+
|
125
|
+
result = await session.execute(query)
|
126
|
+
runs = result.scalars().all()
|
127
|
+
return [run.to_pydantic() for run in runs]
|
128
|
+
|
129
|
+
@enforce_types
|
130
|
+
async def delete_run(self, run_id: str, actor: PydanticUser) -> PydanticRun:
|
131
|
+
"""Delete a run by its ID."""
|
132
|
+
async with db_registry.async_session() as session:
|
133
|
+
run = await RunModel.read_async(db_session=session, identifier=run_id, actor=actor, access_type=AccessType.ORGANIZATION)
|
134
|
+
if not run:
|
135
|
+
raise NoResultFound(f"Run with id {run_id} not found")
|
136
|
+
|
137
|
+
pydantic_run = run.to_pydantic()
|
138
|
+
await run.hard_delete_async(db_session=session, actor=actor)
|
139
|
+
|
140
|
+
return pydantic_run
|
141
|
+
|
142
|
+
@enforce_types
|
143
|
+
async def update_run_by_id_async(
|
144
|
+
self, run_id: str, update: RunUpdate, actor: PydanticUser, refresh_result_messages: bool = True
|
145
|
+
) -> PydanticRun:
|
146
|
+
"""Update a run using a RunUpdate object."""
|
147
|
+
|
148
|
+
async with db_registry.async_session() as session:
|
149
|
+
run = await RunModel.read_async(db_session=session, identifier=run_id, actor=actor)
|
150
|
+
|
151
|
+
# Check if this is a terminal update and whether we should dispatch a callback
|
152
|
+
needs_callback = False
|
153
|
+
callback_url = None
|
154
|
+
not_completed_before = not bool(run.completed_at)
|
155
|
+
is_terminal_update = update.status in {RunStatus.completed, RunStatus.failed}
|
156
|
+
if is_terminal_update and not_completed_before and run.callback_url:
|
157
|
+
needs_callback = True
|
158
|
+
callback_url = run.callback_url
|
159
|
+
|
160
|
+
# Housekeeping only when the run is actually completing
|
161
|
+
if not_completed_before and is_terminal_update:
|
162
|
+
if not update.stop_reason:
|
163
|
+
logger.warning(f"Run {run_id} completed without a stop reason")
|
164
|
+
if not update.completed_at:
|
165
|
+
logger.warning(f"Run {run_id} completed without a completed_at timestamp")
|
166
|
+
update.completed_at = get_utc_time().replace(tzinfo=None)
|
167
|
+
|
168
|
+
# Update job attributes with only the fields that were explicitly set
|
169
|
+
update_data = update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
|
170
|
+
|
171
|
+
# Automatically update the completion timestamp if status is set to 'completed'
|
172
|
+
for key, value in update_data.items():
|
173
|
+
# Ensure completed_at is timezone-naive for database compatibility
|
174
|
+
if key == "completed_at" and value is not None and hasattr(value, "replace"):
|
175
|
+
value = value.replace(tzinfo=None)
|
176
|
+
setattr(run, key, value)
|
177
|
+
|
178
|
+
await run.update_async(db_session=session, actor=actor, no_commit=True, no_refresh=True)
|
179
|
+
final_metadata = run.metadata_
|
180
|
+
pydantic_run = run.to_pydantic()
|
181
|
+
await session.commit()
|
182
|
+
|
183
|
+
# Dispatch callback outside of database session if needed
|
184
|
+
if needs_callback:
|
185
|
+
if refresh_result_messages:
|
186
|
+
result = LettaResponse(
|
187
|
+
messages=await self.get_run_messages(run_id=run_id, actor=actor),
|
188
|
+
stop_reason=LettaStopReason(stop_reason=pydantic_run.stop_reason),
|
189
|
+
usage=await self.get_run_usage(run_id=run_id, actor=actor),
|
190
|
+
)
|
191
|
+
final_metadata["result"] = result.model_dump()
|
192
|
+
callback_info = {
|
193
|
+
"run_id": run_id,
|
194
|
+
"callback_url": callback_url,
|
195
|
+
"status": update.status,
|
196
|
+
"completed_at": get_utc_time().replace(tzinfo=None),
|
197
|
+
"metadata": final_metadata,
|
198
|
+
}
|
199
|
+
callback_result = await self._dispatch_callback_async(callback_info)
|
200
|
+
|
201
|
+
# Update callback status in a separate transaction
|
202
|
+
async with db_registry.async_session() as session:
|
203
|
+
run = await RunModel.read_async(db_session=session, identifier=run_id, actor=actor)
|
204
|
+
run.callback_sent_at = callback_result["callback_sent_at"]
|
205
|
+
run.callback_status_code = callback_result.get("callback_status_code")
|
206
|
+
run.callback_error = callback_result.get("callback_error")
|
207
|
+
pydantic_run = run.to_pydantic()
|
208
|
+
await run.update_async(db_session=session, actor=actor, no_commit=True, no_refresh=True)
|
209
|
+
await session.commit()
|
210
|
+
|
211
|
+
return pydantic_run
|
212
|
+
|
213
|
+
@trace_method
|
214
|
+
async def _dispatch_callback_async(self, callback_info: dict) -> dict:
|
215
|
+
"""
|
216
|
+
POST a standard JSON payload to callback_url and return callback status asynchronously.
|
217
|
+
"""
|
218
|
+
payload = {
|
219
|
+
"run_id": callback_info["run_id"],
|
220
|
+
"status": callback_info["status"],
|
221
|
+
"completed_at": callback_info["completed_at"].isoformat() if callback_info["completed_at"] else None,
|
222
|
+
"metadata": callback_info["metadata"],
|
223
|
+
}
|
224
|
+
|
225
|
+
callback_sent_at = get_utc_time().replace(tzinfo=None)
|
226
|
+
result = {"callback_sent_at": callback_sent_at}
|
227
|
+
|
228
|
+
try:
|
229
|
+
async with AsyncClient() as client:
|
230
|
+
log_event("POST callback dispatched", payload)
|
231
|
+
resp = await client.post(callback_info["callback_url"], json=payload, timeout=5.0)
|
232
|
+
log_event("POST callback finished")
|
233
|
+
result["callback_status_code"] = resp.status_code
|
234
|
+
except Exception as e:
|
235
|
+
error_message = f"Failed to dispatch callback for run {callback_info['run_id']} to {callback_info['callback_url']}: {e!s}"
|
236
|
+
logger.error(error_message)
|
237
|
+
result["callback_error"] = error_message
|
238
|
+
# Continue silently - callback failures should not affect run completion
|
239
|
+
finally:
|
240
|
+
return result
|
241
|
+
|
242
|
+
@enforce_types
|
243
|
+
async def get_run_usage(self, run_id: str, actor: PydanticUser) -> LettaUsageStatistics:
|
244
|
+
"""Get usage statistics for a run."""
|
245
|
+
async with db_registry.async_session() as session:
|
246
|
+
run = await RunModel.read_async(db_session=session, identifier=run_id, actor=actor, access_type=AccessType.ORGANIZATION)
|
247
|
+
if not run:
|
248
|
+
raise NoResultFound(f"Run with id {run_id} not found")
|
249
|
+
|
250
|
+
steps = await self.step_manager.list_steps_async(run_id=run_id, actor=actor)
|
251
|
+
total_usage = LettaUsageStatistics()
|
252
|
+
for step in steps:
|
253
|
+
total_usage.prompt_tokens += step.prompt_tokens
|
254
|
+
total_usage.completion_tokens += step.completion_tokens
|
255
|
+
total_usage.total_tokens += step.total_tokens
|
256
|
+
total_usage.step_count += 1
|
257
|
+
return total_usage
|
258
|
+
|
259
|
+
@enforce_types
|
260
|
+
async def get_run_messages(
|
261
|
+
self,
|
262
|
+
run_id: str,
|
263
|
+
actor: PydanticUser,
|
264
|
+
limit: Optional[int] = 100,
|
265
|
+
before: Optional[str] = None,
|
266
|
+
after: Optional[str] = None,
|
267
|
+
order: Literal["asc", "desc"] = "asc",
|
268
|
+
) -> List[LettaMessage]:
|
269
|
+
"""Get the result of a run."""
|
270
|
+
run = await self.get_run_by_id(run_id=run_id, actor=actor)
|
271
|
+
request_config = run.request_config
|
272
|
+
agent = await self.agent_manager.get_agent_by_id_async(agent_id=run.agent_id, actor=actor, include_relationships=[])
|
273
|
+
text_is_assistant_message = agent.agent_type == AgentType.letta_v1_agent
|
274
|
+
|
275
|
+
messages = await self.message_manager.list_messages(
|
276
|
+
actor=actor,
|
277
|
+
run_id=run_id,
|
278
|
+
limit=limit,
|
279
|
+
before=before,
|
280
|
+
after=after,
|
281
|
+
ascending=(order == "asc"),
|
282
|
+
)
|
283
|
+
letta_messages = PydanticMessage.to_letta_messages_from_list(
|
284
|
+
messages, reverse=(order != "asc"), text_is_assistant_message=text_is_assistant_message
|
285
|
+
)
|
286
|
+
|
287
|
+
if request_config and request_config.include_return_message_types:
|
288
|
+
include_return_message_types_set = set(request_config.include_return_message_types)
|
289
|
+
letta_messages = [msg for msg in letta_messages if msg.message_type in include_return_message_types_set]
|
290
|
+
|
291
|
+
return letta_messages
|
292
|
+
|
293
|
+
@enforce_types
|
294
|
+
async def get_run_request_config(self, run_id: str, actor: PydanticUser) -> Optional[LettaRequestConfig]:
|
295
|
+
"""Get the letta request config from a run."""
|
296
|
+
async with db_registry.async_session() as session:
|
297
|
+
run = await RunModel.read_async(db_session=session, identifier=run_id, actor=actor, access_type=AccessType.ORGANIZATION)
|
298
|
+
if not run:
|
299
|
+
raise NoResultFound(f"Run with id {run_id} not found")
|
300
|
+
pydantic_run = run.to_pydantic()
|
301
|
+
return pydantic_run.request_config
|
@@ -45,40 +45,6 @@ class SandboxConfigManager:
|
|
45
45
|
sandbox_config = self.create_or_update_sandbox_config(SandboxConfigCreate(config=default_config), actor=actor)
|
46
46
|
return sandbox_config
|
47
47
|
|
48
|
-
@enforce_types
|
49
|
-
@trace_method
|
50
|
-
def create_or_update_sandbox_config(self, sandbox_config_create: SandboxConfigCreate, actor: PydanticUser) -> PydanticSandboxConfig:
|
51
|
-
"""Create or update a sandbox configuration based on the PydanticSandboxConfig schema."""
|
52
|
-
config = sandbox_config_create.config
|
53
|
-
sandbox_type = config.type
|
54
|
-
sandbox_config = PydanticSandboxConfig(
|
55
|
-
type=sandbox_type, config=config.model_dump(exclude_none=True), organization_id=actor.organization_id
|
56
|
-
)
|
57
|
-
|
58
|
-
# Attempt to retrieve the existing sandbox configuration by type within the organization
|
59
|
-
db_sandbox = self.get_sandbox_config_by_type(sandbox_config.type, actor=actor)
|
60
|
-
if db_sandbox:
|
61
|
-
# Prepare the update data, excluding fields that should not be reset
|
62
|
-
update_data = sandbox_config.model_dump(exclude_unset=True, exclude_none=True)
|
63
|
-
update_data = {key: value for key, value in update_data.items() if getattr(db_sandbox, key) != value}
|
64
|
-
|
65
|
-
# If there are changes, update the sandbox configuration
|
66
|
-
if update_data:
|
67
|
-
db_sandbox = self.update_sandbox_config(db_sandbox.id, SandboxConfigUpdate(**update_data), actor)
|
68
|
-
else:
|
69
|
-
printd(
|
70
|
-
f"`create_or_update_sandbox_config` was called with user_id={actor.id}, organization_id={actor.organization_id}, "
|
71
|
-
f"type={sandbox_config.type}, but found existing configuration with nothing to update."
|
72
|
-
)
|
73
|
-
|
74
|
-
return db_sandbox
|
75
|
-
else:
|
76
|
-
# If the sandbox configuration doesn't exist, create a new one
|
77
|
-
with db_registry.session() as session:
|
78
|
-
db_sandbox = SandboxConfigModel(**sandbox_config.model_dump(exclude_none=True))
|
79
|
-
db_sandbox.create(session, actor=actor)
|
80
|
-
return db_sandbox.to_pydantic()
|
81
|
-
|
82
48
|
@enforce_types
|
83
49
|
@trace_method
|
84
50
|
async def get_or_create_default_sandbox_config_async(self, sandbox_type: SandboxType, actor: PydanticUser) -> PydanticSandboxConfig:
|
@@ -133,34 +99,6 @@ class SandboxConfigManager:
|
|
133
99
|
await db_sandbox.create_async(session, actor=actor)
|
134
100
|
return db_sandbox.to_pydantic()
|
135
101
|
|
136
|
-
@enforce_types
|
137
|
-
@trace_method
|
138
|
-
def update_sandbox_config(
|
139
|
-
self, sandbox_config_id: str, sandbox_update: SandboxConfigUpdate, actor: PydanticUser
|
140
|
-
) -> PydanticSandboxConfig:
|
141
|
-
"""Update an existing sandbox configuration."""
|
142
|
-
with db_registry.session() as session:
|
143
|
-
sandbox = SandboxConfigModel.read(db_session=session, identifier=sandbox_config_id, actor=actor)
|
144
|
-
# We need to check that the sandbox_update provided is the same type as the original sandbox
|
145
|
-
if sandbox.type != sandbox_update.config.type:
|
146
|
-
raise ValueError(
|
147
|
-
f"Mismatched type for sandbox config update: tried to update sandbox_config of type {sandbox.type} with config of type {sandbox_update.config.type}"
|
148
|
-
)
|
149
|
-
|
150
|
-
update_data = sandbox_update.model_dump(exclude_unset=True, exclude_none=True)
|
151
|
-
update_data = {key: value for key, value in update_data.items() if getattr(sandbox, key) != value}
|
152
|
-
|
153
|
-
if update_data:
|
154
|
-
for key, value in update_data.items():
|
155
|
-
setattr(sandbox, key, value)
|
156
|
-
sandbox.update(db_session=session, actor=actor)
|
157
|
-
else:
|
158
|
-
printd(
|
159
|
-
f"`update_sandbox_config` called with user_id={actor.id}, organization_id={actor.organization_id}, "
|
160
|
-
f"name={sandbox.type}, but nothing to update."
|
161
|
-
)
|
162
|
-
return sandbox.to_pydantic()
|
163
|
-
|
164
102
|
@enforce_types
|
165
103
|
@trace_method
|
166
104
|
async def update_sandbox_config_async(
|
@@ -189,15 +127,6 @@ class SandboxConfigManager:
|
|
189
127
|
)
|
190
128
|
return sandbox.to_pydantic()
|
191
129
|
|
192
|
-
@enforce_types
|
193
|
-
@trace_method
|
194
|
-
def delete_sandbox_config(self, sandbox_config_id: str, actor: PydanticUser) -> PydanticSandboxConfig:
|
195
|
-
"""Delete a sandbox configuration by its ID."""
|
196
|
-
with db_registry.session() as session:
|
197
|
-
sandbox = SandboxConfigModel.read(db_session=session, identifier=sandbox_config_id, actor=actor)
|
198
|
-
sandbox.hard_delete(db_session=session, actor=actor)
|
199
|
-
return sandbox.to_pydantic()
|
200
|
-
|
201
130
|
@enforce_types
|
202
131
|
@trace_method
|
203
132
|
async def delete_sandbox_config_async(self, sandbox_config_id: str, actor: PydanticUser) -> PydanticSandboxConfig:
|
@@ -207,24 +136,6 @@ class SandboxConfigManager:
|
|
207
136
|
await sandbox.hard_delete_async(db_session=session, actor=actor)
|
208
137
|
return sandbox.to_pydantic()
|
209
138
|
|
210
|
-
@enforce_types
|
211
|
-
@trace_method
|
212
|
-
def list_sandbox_configs(
|
213
|
-
self,
|
214
|
-
actor: PydanticUser,
|
215
|
-
after: Optional[str] = None,
|
216
|
-
limit: Optional[int] = 50,
|
217
|
-
sandbox_type: Optional[SandboxType] = None,
|
218
|
-
) -> List[PydanticSandboxConfig]:
|
219
|
-
"""List all sandbox configurations with optional pagination."""
|
220
|
-
kwargs = {"organization_id": actor.organization_id}
|
221
|
-
if sandbox_type:
|
222
|
-
kwargs.update({"type": sandbox_type})
|
223
|
-
|
224
|
-
with db_registry.session() as session:
|
225
|
-
sandboxes = SandboxConfigModel.list(db_session=session, after=after, limit=limit, **kwargs)
|
226
|
-
return [sandbox.to_pydantic() for sandbox in sandboxes]
|
227
|
-
|
228
139
|
@enforce_types
|
229
140
|
@trace_method
|
230
141
|
async def list_sandbox_configs_async(
|
@@ -243,35 +154,6 @@ class SandboxConfigManager:
|
|
243
154
|
sandboxes = await SandboxConfigModel.list_async(db_session=session, after=after, limit=limit, **kwargs)
|
244
155
|
return [sandbox.to_pydantic() for sandbox in sandboxes]
|
245
156
|
|
246
|
-
@enforce_types
|
247
|
-
@trace_method
|
248
|
-
def get_sandbox_config_by_id(self, sandbox_config_id: str, actor: Optional[PydanticUser] = None) -> Optional[PydanticSandboxConfig]:
|
249
|
-
"""Retrieve a sandbox configuration by its ID."""
|
250
|
-
with db_registry.session() as session:
|
251
|
-
try:
|
252
|
-
sandbox = SandboxConfigModel.read(db_session=session, identifier=sandbox_config_id, actor=actor)
|
253
|
-
return sandbox.to_pydantic()
|
254
|
-
except NoResultFound:
|
255
|
-
return None
|
256
|
-
|
257
|
-
@enforce_types
|
258
|
-
@trace_method
|
259
|
-
def get_sandbox_config_by_type(self, type: SandboxType, actor: Optional[PydanticUser] = None) -> Optional[PydanticSandboxConfig]:
|
260
|
-
"""Retrieve a sandbox config by its type."""
|
261
|
-
with db_registry.session() as session:
|
262
|
-
try:
|
263
|
-
sandboxes = SandboxConfigModel.list(
|
264
|
-
db_session=session,
|
265
|
-
type=type,
|
266
|
-
organization_id=actor.organization_id,
|
267
|
-
limit=1,
|
268
|
-
)
|
269
|
-
if sandboxes:
|
270
|
-
return sandboxes[0].to_pydantic()
|
271
|
-
return None
|
272
|
-
except NoResultFound:
|
273
|
-
return None
|
274
|
-
|
275
157
|
@enforce_types
|
276
158
|
@trace_method
|
277
159
|
async def get_sandbox_config_by_type_async(
|
@@ -292,34 +174,6 @@ class SandboxConfigManager:
|
|
292
174
|
except NoResultFound:
|
293
175
|
return None
|
294
176
|
|
295
|
-
@enforce_types
|
296
|
-
@trace_method
|
297
|
-
def create_sandbox_env_var(
|
298
|
-
self, env_var_create: SandboxEnvironmentVariableCreate, sandbox_config_id: str, actor: PydanticUser
|
299
|
-
) -> PydanticEnvVar:
|
300
|
-
"""Create a new sandbox environment variable."""
|
301
|
-
env_var = PydanticEnvVar(**env_var_create.model_dump(), sandbox_config_id=sandbox_config_id, organization_id=actor.organization_id)
|
302
|
-
|
303
|
-
db_env_var = self.get_sandbox_env_var_by_key_and_sandbox_config_id(env_var.key, env_var.sandbox_config_id, actor=actor)
|
304
|
-
if db_env_var:
|
305
|
-
update_data = env_var.model_dump(exclude_unset=True, exclude_none=True)
|
306
|
-
update_data = {key: value for key, value in update_data.items() if getattr(db_env_var, key) != value}
|
307
|
-
# If there are changes, update the environment variable
|
308
|
-
if update_data:
|
309
|
-
db_env_var = self.update_sandbox_env_var(db_env_var.id, SandboxEnvironmentVariableUpdate(**update_data), actor)
|
310
|
-
else:
|
311
|
-
printd(
|
312
|
-
f"`create_or_update_sandbox_env_var` was called with user_id={actor.id}, organization_id={actor.organization_id}, "
|
313
|
-
f"key={env_var.key}, but found existing variable with nothing to update."
|
314
|
-
)
|
315
|
-
|
316
|
-
return db_env_var
|
317
|
-
else:
|
318
|
-
with db_registry.session() as session:
|
319
|
-
env_var = SandboxEnvVarModel(**env_var.model_dump(to_orm=True, exclude_none=True))
|
320
|
-
env_var.create(session, actor=actor)
|
321
|
-
return env_var.to_pydantic()
|
322
|
-
|
323
177
|
@enforce_types
|
324
178
|
@trace_method
|
325
179
|
async def create_sandbox_env_var_async(
|
@@ -348,28 +202,6 @@ class SandboxConfigManager:
|
|
348
202
|
await env_var.create_async(session, actor=actor)
|
349
203
|
return env_var.to_pydantic()
|
350
204
|
|
351
|
-
@enforce_types
|
352
|
-
@trace_method
|
353
|
-
def update_sandbox_env_var(
|
354
|
-
self, env_var_id: str, env_var_update: SandboxEnvironmentVariableUpdate, actor: PydanticUser
|
355
|
-
) -> PydanticEnvVar:
|
356
|
-
"""Update an existing sandbox environment variable."""
|
357
|
-
with db_registry.session() as session:
|
358
|
-
env_var = SandboxEnvVarModel.read(db_session=session, identifier=env_var_id, actor=actor)
|
359
|
-
update_data = env_var_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
|
360
|
-
update_data = {key: value for key, value in update_data.items() if getattr(env_var, key) != value}
|
361
|
-
|
362
|
-
if update_data:
|
363
|
-
for key, value in update_data.items():
|
364
|
-
setattr(env_var, key, value)
|
365
|
-
env_var.update(db_session=session, actor=actor)
|
366
|
-
else:
|
367
|
-
printd(
|
368
|
-
f"`update_sandbox_env_var` called with user_id={actor.id}, organization_id={actor.organization_id}, "
|
369
|
-
f"key={env_var.key}, but nothing to update."
|
370
|
-
)
|
371
|
-
return env_var.to_pydantic()
|
372
|
-
|
373
205
|
@enforce_types
|
374
206
|
@trace_method
|
375
207
|
async def update_sandbox_env_var_async(
|
@@ -392,15 +224,6 @@ class SandboxConfigManager:
|
|
392
224
|
)
|
393
225
|
return env_var.to_pydantic()
|
394
226
|
|
395
|
-
@enforce_types
|
396
|
-
@trace_method
|
397
|
-
def delete_sandbox_env_var(self, env_var_id: str, actor: PydanticUser) -> PydanticEnvVar:
|
398
|
-
"""Delete a sandbox environment variable by its ID."""
|
399
|
-
with db_registry.session() as session:
|
400
|
-
env_var = SandboxEnvVarModel.read(db_session=session, identifier=env_var_id, actor=actor)
|
401
|
-
env_var.hard_delete(db_session=session, actor=actor)
|
402
|
-
return env_var.to_pydantic()
|
403
|
-
|
404
227
|
@enforce_types
|
405
228
|
@trace_method
|
406
229
|
async def delete_sandbox_env_var_async(self, env_var_id: str, actor: PydanticUser) -> PydanticEnvVar:
|
@@ -410,26 +233,6 @@ class SandboxConfigManager:
|
|
410
233
|
await env_var.hard_delete_async(db_session=session, actor=actor)
|
411
234
|
return env_var.to_pydantic()
|
412
235
|
|
413
|
-
@enforce_types
|
414
|
-
@trace_method
|
415
|
-
def list_sandbox_env_vars(
|
416
|
-
self,
|
417
|
-
sandbox_config_id: str,
|
418
|
-
actor: PydanticUser,
|
419
|
-
after: Optional[str] = None,
|
420
|
-
limit: Optional[int] = 50,
|
421
|
-
) -> List[PydanticEnvVar]:
|
422
|
-
"""List all sandbox environment variables with optional pagination."""
|
423
|
-
with db_registry.session() as session:
|
424
|
-
env_vars = SandboxEnvVarModel.list(
|
425
|
-
db_session=session,
|
426
|
-
after=after,
|
427
|
-
limit=limit,
|
428
|
-
organization_id=actor.organization_id,
|
429
|
-
sandbox_config_id=sandbox_config_id,
|
430
|
-
)
|
431
|
-
return [env_var.to_pydantic() for env_var in env_vars]
|
432
|
-
|
433
236
|
@enforce_types
|
434
237
|
@trace_method
|
435
238
|
async def list_sandbox_env_vars_async(
|
@@ -450,22 +253,6 @@ class SandboxConfigManager:
|
|
450
253
|
)
|
451
254
|
return [env_var.to_pydantic() for env_var in env_vars]
|
452
255
|
|
453
|
-
@enforce_types
|
454
|
-
@trace_method
|
455
|
-
def list_sandbox_env_vars_by_key(
|
456
|
-
self, key: str, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50
|
457
|
-
) -> List[PydanticEnvVar]:
|
458
|
-
"""List all sandbox environment variables with optional pagination."""
|
459
|
-
with db_registry.session() as session:
|
460
|
-
env_vars = SandboxEnvVarModel.list(
|
461
|
-
db_session=session,
|
462
|
-
after=after,
|
463
|
-
limit=limit,
|
464
|
-
organization_id=actor.organization_id,
|
465
|
-
key=key,
|
466
|
-
)
|
467
|
-
return [env_var.to_pydantic() for env_var in env_vars]
|
468
|
-
|
469
256
|
@enforce_types
|
470
257
|
@trace_method
|
471
258
|
async def list_sandbox_env_vars_by_key_async(
|
@@ -501,27 +288,6 @@ class SandboxConfigManager:
|
|
501
288
|
env_vars = await self.list_sandbox_env_vars_async(sandbox_config_id, actor, after, limit)
|
502
289
|
return {env_var.key: env_var.value for env_var in env_vars}
|
503
290
|
|
504
|
-
@enforce_types
|
505
|
-
@trace_method
|
506
|
-
def get_sandbox_env_var_by_key_and_sandbox_config_id(
|
507
|
-
self, key: str, sandbox_config_id: str, actor: Optional[PydanticUser] = None
|
508
|
-
) -> Optional[PydanticEnvVar]:
|
509
|
-
"""Retrieve a sandbox environment variable by its key and sandbox_config_id."""
|
510
|
-
with db_registry.session() as session:
|
511
|
-
try:
|
512
|
-
env_var = SandboxEnvVarModel.list(
|
513
|
-
db_session=session,
|
514
|
-
key=key,
|
515
|
-
sandbox_config_id=sandbox_config_id,
|
516
|
-
organization_id=actor.organization_id,
|
517
|
-
limit=1,
|
518
|
-
)
|
519
|
-
if env_var:
|
520
|
-
return env_var[0].to_pydantic()
|
521
|
-
return None
|
522
|
-
except NoResultFound:
|
523
|
-
return None
|
524
|
-
|
525
291
|
@enforce_types
|
526
292
|
@trace_method
|
527
293
|
async def get_sandbox_env_var_by_key_and_sandbox_config_id_async(
|