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
letta/services/job_manager.py
CHANGED
@@ -10,7 +10,6 @@ from letta.helpers.datetime_helpers import get_utc_time
|
|
10
10
|
from letta.log import get_logger
|
11
11
|
from letta.orm.errors import NoResultFound
|
12
12
|
from letta.orm.job import Job as JobModel
|
13
|
-
from letta.orm.job_messages import JobMessage
|
14
13
|
from letta.orm.message import Message as MessageModel
|
15
14
|
from letta.orm.sqlalchemy_base import AccessType
|
16
15
|
from letta.orm.step import Step, Step as StepModel
|
@@ -25,6 +24,7 @@ from letta.schemas.step import Step as PydanticStep
|
|
25
24
|
from letta.schemas.usage import LettaUsageStatistics
|
26
25
|
from letta.schemas.user import User as PydanticUser
|
27
26
|
from letta.server.db import db_registry
|
27
|
+
from letta.services.helpers.agent_manager_helper import validate_agent_exists_async
|
28
28
|
from letta.utils import enforce_types
|
29
29
|
|
30
30
|
logger = get_logger(__name__)
|
@@ -33,21 +33,6 @@ logger = get_logger(__name__)
|
|
33
33
|
class JobManager:
|
34
34
|
"""Manager class to handle business logic related to Jobs."""
|
35
35
|
|
36
|
-
@enforce_types
|
37
|
-
@trace_method
|
38
|
-
def create_job(
|
39
|
-
self, pydantic_job: Union[PydanticJob, PydanticRun, PydanticBatchJob], actor: PydanticUser
|
40
|
-
) -> Union[PydanticJob, PydanticRun, PydanticBatchJob]:
|
41
|
-
"""Create a new job based on the JobCreate schema."""
|
42
|
-
with db_registry.session() as session:
|
43
|
-
# Associate the job with the user
|
44
|
-
pydantic_job.user_id = actor.id
|
45
|
-
job_data = pydantic_job.model_dump(to_orm=True)
|
46
|
-
job = JobModel(**job_data)
|
47
|
-
job.organization_id = actor.organization_id
|
48
|
-
job.create(session, actor=actor) # Save job in the database
|
49
|
-
return job.to_pydantic()
|
50
|
-
|
51
36
|
@enforce_types
|
52
37
|
@trace_method
|
53
38
|
async def create_job_async(
|
@@ -57,76 +42,31 @@ class JobManager:
|
|
57
42
|
async with db_registry.async_session() as session:
|
58
43
|
# Associate the job with the user
|
59
44
|
pydantic_job.user_id = actor.id
|
45
|
+
|
46
|
+
# Get agent_id if present
|
47
|
+
agent_id = getattr(pydantic_job, "agent_id", None)
|
48
|
+
|
49
|
+
# Verify agent exists before creating the job
|
50
|
+
if agent_id:
|
51
|
+
await validate_agent_exists_async(session, agent_id, actor)
|
52
|
+
|
60
53
|
job_data = pydantic_job.model_dump(to_orm=True)
|
54
|
+
# Remove agent_id from job_data as it's not a field in the Job ORM model
|
55
|
+
job_data.pop("agent_id", None)
|
61
56
|
job = JobModel(**job_data)
|
62
57
|
job.organization_id = actor.organization_id
|
63
58
|
job = await job.create_async(session, actor=actor, no_commit=True, no_refresh=True) # Save job in the database
|
64
|
-
result = job.to_pydantic()
|
65
|
-
await session.commit()
|
66
|
-
return result
|
67
59
|
|
68
|
-
|
69
|
-
@trace_method
|
70
|
-
def update_job_by_id(self, job_id: str, job_update: JobUpdate, actor: PydanticUser) -> PydanticJob:
|
71
|
-
"""Update a job by its ID with the given JobUpdate object."""
|
72
|
-
# First check if we need to dispatch a callback
|
73
|
-
needs_callback = False
|
74
|
-
callback_url = None
|
75
|
-
with db_registry.session() as session:
|
76
|
-
job = self._verify_job_access(session=session, job_id=job_id, actor=actor, access=["write"])
|
77
|
-
not_completed_before = not bool(job.completed_at)
|
78
|
-
|
79
|
-
# Check if we'll need to dispatch callback
|
80
|
-
if job_update.status in {JobStatus.completed, JobStatus.failed} and not_completed_before and job.callback_url:
|
81
|
-
needs_callback = True
|
82
|
-
callback_url = job.callback_url
|
83
|
-
|
84
|
-
# Update the job first to get the final metadata
|
85
|
-
with db_registry.session() as session:
|
86
|
-
job = self._verify_job_access(session=session, job_id=job_id, actor=actor, access=["write"])
|
87
|
-
not_completed_before = not bool(job.completed_at)
|
88
|
-
|
89
|
-
# Update job attributes with only the fields that were explicitly set
|
90
|
-
update_data = job_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
|
91
|
-
|
92
|
-
# Automatically update the completion timestamp if status is set to 'completed'
|
93
|
-
for key, value in update_data.items():
|
94
|
-
# Ensure completed_at is timezone-naive for database compatibility
|
95
|
-
if key == "completed_at" and value is not None and hasattr(value, "replace"):
|
96
|
-
value = value.replace(tzinfo=None)
|
97
|
-
setattr(job, key, value)
|
98
|
-
|
99
|
-
if job_update.status in {JobStatus.completed, JobStatus.failed} and not_completed_before:
|
100
|
-
job.completed_at = get_utc_time().replace(tzinfo=None)
|
101
|
-
|
102
|
-
# Save the updated job to the database first
|
103
|
-
job = job.update(db_session=session, actor=actor)
|
60
|
+
await session.commit()
|
104
61
|
|
105
|
-
#
|
106
|
-
|
107
|
-
result = job.to_pydantic()
|
62
|
+
# Convert to pydantic first, then add agent_id if needed
|
63
|
+
result = super(JobModel, job).to_pydantic()
|
108
64
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
"job_id": job_id,
|
113
|
-
"callback_url": callback_url,
|
114
|
-
"status": job_update.status,
|
115
|
-
"completed_at": get_utc_time().replace(tzinfo=None),
|
116
|
-
"metadata": final_metadata,
|
117
|
-
}
|
118
|
-
callback_result = self._dispatch_callback_sync(callback_info)
|
119
|
-
|
120
|
-
# Update callback status in a separate transaction
|
121
|
-
with db_registry.session() as session:
|
122
|
-
job = self._verify_job_access(session=session, job_id=job_id, actor=actor, access=["write"])
|
123
|
-
job.callback_sent_at = callback_result["callback_sent_at"]
|
124
|
-
job.callback_status_code = callback_result.get("callback_status_code")
|
125
|
-
job.callback_error = callback_result.get("callback_error")
|
126
|
-
job.update(db_session=session, actor=actor)
|
127
|
-
result = job.to_pydantic()
|
65
|
+
# Add back the agent_id field to the result if it was present
|
66
|
+
if agent_id and isinstance(pydantic_job, PydanticRun):
|
67
|
+
result.agent_id = agent_id
|
128
68
|
|
129
|
-
|
69
|
+
return result
|
130
70
|
|
131
71
|
@enforce_types
|
132
72
|
@trace_method
|
@@ -245,15 +185,6 @@ class JobManager:
|
|
245
185
|
logger.error(f"Failed to safely update job status for job {job_id}: {e}")
|
246
186
|
return False
|
247
187
|
|
248
|
-
@enforce_types
|
249
|
-
@trace_method
|
250
|
-
def get_job_by_id(self, job_id: str, actor: PydanticUser) -> PydanticJob:
|
251
|
-
"""Fetch a job by its ID."""
|
252
|
-
with db_registry.session() as session:
|
253
|
-
# Retrieve job by ID using the Job model's read method
|
254
|
-
job = JobModel.read(db_session=session, identifier=job_id, actor=actor, access_type=AccessType.USER)
|
255
|
-
return job.to_pydantic()
|
256
|
-
|
257
188
|
@enforce_types
|
258
189
|
@trace_method
|
259
190
|
async def get_job_by_id_async(self, job_id: str, actor: PydanticUser) -> PydanticJob:
|
@@ -263,41 +194,6 @@ class JobManager:
|
|
263
194
|
job = await JobModel.read_async(db_session=session, identifier=job_id, actor=actor, access_type=AccessType.USER)
|
264
195
|
return job.to_pydantic()
|
265
196
|
|
266
|
-
@enforce_types
|
267
|
-
@trace_method
|
268
|
-
def list_jobs(
|
269
|
-
self,
|
270
|
-
actor: PydanticUser,
|
271
|
-
before: Optional[str] = None,
|
272
|
-
after: Optional[str] = None,
|
273
|
-
limit: Optional[int] = 50,
|
274
|
-
statuses: Optional[List[JobStatus]] = None,
|
275
|
-
job_type: JobType = JobType.JOB,
|
276
|
-
ascending: bool = True,
|
277
|
-
stop_reason: Optional[StopReasonType] = None,
|
278
|
-
) -> List[PydanticJob]:
|
279
|
-
"""List all jobs with optional pagination and status filter."""
|
280
|
-
with db_registry.session() as session:
|
281
|
-
filter_kwargs = {"user_id": actor.id, "job_type": job_type}
|
282
|
-
|
283
|
-
# Add status filter if provided
|
284
|
-
if statuses:
|
285
|
-
filter_kwargs["status"] = statuses
|
286
|
-
|
287
|
-
# Add stop_reason filter if provided
|
288
|
-
if stop_reason is not None:
|
289
|
-
filter_kwargs["stop_reason"] = stop_reason
|
290
|
-
|
291
|
-
jobs = JobModel.list(
|
292
|
-
db_session=session,
|
293
|
-
before=before,
|
294
|
-
after=after,
|
295
|
-
limit=limit,
|
296
|
-
ascending=ascending,
|
297
|
-
**filter_kwargs,
|
298
|
-
)
|
299
|
-
return [job.to_pydantic() for job in jobs]
|
300
|
-
|
301
197
|
@enforce_types
|
302
198
|
@trace_method
|
303
199
|
async def list_jobs_async(
|
@@ -311,6 +207,9 @@ class JobManager:
|
|
311
207
|
ascending: bool = True,
|
312
208
|
source_id: Optional[str] = None,
|
313
209
|
stop_reason: Optional[StopReasonType] = None,
|
210
|
+
# agent_id: Optional[str] = None,
|
211
|
+
agent_ids: Optional[List[str]] = None,
|
212
|
+
background: Optional[bool] = None,
|
314
213
|
) -> List[PydanticJob]:
|
315
214
|
"""List all jobs with optional pagination and status filter."""
|
316
215
|
from sqlalchemy import and_, or_, select
|
@@ -323,16 +222,20 @@ class JobManager:
|
|
323
222
|
if statuses:
|
324
223
|
query = query.where(JobModel.status.in_(statuses))
|
325
224
|
|
225
|
+
# add stop_reason filter if provided
|
226
|
+
if stop_reason is not None:
|
227
|
+
query = query.where(JobModel.stop_reason == stop_reason)
|
228
|
+
|
229
|
+
# add background filter if provided
|
230
|
+
if background is not None:
|
231
|
+
query = query.where(JobModel.background == background)
|
232
|
+
|
326
233
|
# add source_id filter if provided
|
327
234
|
if source_id:
|
328
235
|
column = getattr(JobModel, "metadata_")
|
329
236
|
column = column.op("->>")("source_id")
|
330
237
|
query = query.where(column == source_id)
|
331
238
|
|
332
|
-
# add stop_reason filter if provided
|
333
|
-
if stop_reason is not None:
|
334
|
-
query = query.where(JobModel.stop_reason == stop_reason)
|
335
|
-
|
336
239
|
# handle cursor-based pagination
|
337
240
|
if before or after:
|
338
241
|
# get cursor objects
|
@@ -396,15 +299,6 @@ class JobManager:
|
|
396
299
|
|
397
300
|
return [job.to_pydantic() for job in jobs]
|
398
301
|
|
399
|
-
@enforce_types
|
400
|
-
@trace_method
|
401
|
-
def delete_job_by_id(self, job_id: str, actor: PydanticUser) -> PydanticJob:
|
402
|
-
"""Delete a job by its ID."""
|
403
|
-
with db_registry.session() as session:
|
404
|
-
job = self._verify_job_access(session=session, job_id=job_id, actor=actor)
|
405
|
-
job.hard_delete(db_session=session, actor=actor)
|
406
|
-
return job.to_pydantic()
|
407
|
-
|
408
302
|
@enforce_types
|
409
303
|
@trace_method
|
410
304
|
async def delete_job_by_id_async(self, job_id: str, actor: PydanticUser) -> PydanticJob:
|
@@ -416,234 +310,7 @@ class JobManager:
|
|
416
310
|
|
417
311
|
@enforce_types
|
418
312
|
@trace_method
|
419
|
-
def
|
420
|
-
self,
|
421
|
-
job_id: str,
|
422
|
-
actor: PydanticUser,
|
423
|
-
before: Optional[str] = None,
|
424
|
-
after: Optional[str] = None,
|
425
|
-
limit: Optional[int] = 100,
|
426
|
-
role: Optional[MessageRole] = None,
|
427
|
-
ascending: bool = True,
|
428
|
-
) -> List[PydanticMessage]:
|
429
|
-
"""
|
430
|
-
Get all messages associated with a job.
|
431
|
-
|
432
|
-
Args:
|
433
|
-
job_id: The ID of the job to get messages for
|
434
|
-
actor: The user making the request
|
435
|
-
before: Cursor for pagination
|
436
|
-
after: Cursor for pagination
|
437
|
-
limit: Maximum number of messages to return
|
438
|
-
role: Optional filter for message role
|
439
|
-
ascending: Optional flag to sort in ascending order
|
440
|
-
|
441
|
-
Returns:
|
442
|
-
List of messages associated with the job
|
443
|
-
|
444
|
-
Raises:
|
445
|
-
NoResultFound: If the job does not exist or user does not have access
|
446
|
-
"""
|
447
|
-
with db_registry.session() as session:
|
448
|
-
# Build filters
|
449
|
-
filters = {}
|
450
|
-
if role is not None:
|
451
|
-
filters["role"] = role
|
452
|
-
|
453
|
-
# Get messages
|
454
|
-
messages = MessageModel.list(
|
455
|
-
db_session=session,
|
456
|
-
before=before,
|
457
|
-
after=after,
|
458
|
-
ascending=ascending,
|
459
|
-
limit=limit,
|
460
|
-
actor=actor,
|
461
|
-
join_model=JobMessage,
|
462
|
-
join_conditions=[MessageModel.id == JobMessage.message_id, JobMessage.job_id == job_id],
|
463
|
-
**filters,
|
464
|
-
)
|
465
|
-
|
466
|
-
return [message.to_pydantic() for message in messages]
|
467
|
-
|
468
|
-
@enforce_types
|
469
|
-
@trace_method
|
470
|
-
def get_job_steps(
|
471
|
-
self,
|
472
|
-
job_id: str,
|
473
|
-
actor: PydanticUser,
|
474
|
-
before: Optional[str] = None,
|
475
|
-
after: Optional[str] = None,
|
476
|
-
limit: Optional[int] = 100,
|
477
|
-
ascending: bool = True,
|
478
|
-
) -> List[PydanticStep]:
|
479
|
-
"""
|
480
|
-
Get all steps associated with a job.
|
481
|
-
|
482
|
-
Args:
|
483
|
-
job_id: The ID of the job to get steps for
|
484
|
-
actor: The user making the request
|
485
|
-
before: Cursor for pagination
|
486
|
-
after: Cursor for pagination
|
487
|
-
limit: Maximum number of steps to return
|
488
|
-
ascending: Optional flag to sort in ascending order
|
489
|
-
|
490
|
-
Returns:
|
491
|
-
List of steps associated with the job
|
492
|
-
|
493
|
-
Raises:
|
494
|
-
NoResultFound: If the job does not exist or user does not have access
|
495
|
-
"""
|
496
|
-
with db_registry.session() as session:
|
497
|
-
# Build filters
|
498
|
-
filters = {}
|
499
|
-
filters["job_id"] = job_id
|
500
|
-
|
501
|
-
# Get steps
|
502
|
-
steps = StepModel.list(
|
503
|
-
db_session=session,
|
504
|
-
before=before,
|
505
|
-
after=after,
|
506
|
-
ascending=ascending,
|
507
|
-
limit=limit,
|
508
|
-
actor=actor,
|
509
|
-
**filters,
|
510
|
-
)
|
511
|
-
|
512
|
-
return [step.to_pydantic() for step in steps]
|
513
|
-
|
514
|
-
@enforce_types
|
515
|
-
@trace_method
|
516
|
-
def add_message_to_job(self, job_id: str, message_id: str, actor: PydanticUser) -> None:
|
517
|
-
"""
|
518
|
-
Associate a message with a job by creating a JobMessage record.
|
519
|
-
Each message can only be associated with one job.
|
520
|
-
|
521
|
-
Args:
|
522
|
-
job_id: The ID of the job
|
523
|
-
message_id: The ID of the message to associate
|
524
|
-
actor: The user making the request
|
525
|
-
|
526
|
-
Raises:
|
527
|
-
NoResultFound: If the job does not exist or user does not have access
|
528
|
-
"""
|
529
|
-
with db_registry.session() as session:
|
530
|
-
# First verify job exists and user has access
|
531
|
-
self._verify_job_access(session, job_id, actor, access=["write"])
|
532
|
-
|
533
|
-
# Create new JobMessage association
|
534
|
-
job_message = JobMessage(job_id=job_id, message_id=message_id)
|
535
|
-
session.add(job_message)
|
536
|
-
session.commit()
|
537
|
-
|
538
|
-
@enforce_types
|
539
|
-
@trace_method
|
540
|
-
async def add_messages_to_job_async(self, job_id: str, message_ids: List[str], actor: PydanticUser) -> None:
|
541
|
-
"""
|
542
|
-
Associate a message with a job by creating a JobMessage record.
|
543
|
-
Each message can only be associated with one job.
|
544
|
-
|
545
|
-
Args:
|
546
|
-
job_id: The ID of the job
|
547
|
-
message_id: The ID of the message to associate
|
548
|
-
actor: The user making the request
|
549
|
-
|
550
|
-
Raises:
|
551
|
-
NoResultFound: If the job does not exist or user does not have access
|
552
|
-
"""
|
553
|
-
if not message_ids:
|
554
|
-
return
|
555
|
-
|
556
|
-
async with db_registry.async_session() as session:
|
557
|
-
# First verify job exists and user has access
|
558
|
-
await self._verify_job_access_async(session, job_id, actor, access=["write"])
|
559
|
-
|
560
|
-
# Create new JobMessage associations
|
561
|
-
job_messages = [JobMessage(job_id=job_id, message_id=message_id) for message_id in message_ids]
|
562
|
-
session.add_all(job_messages)
|
563
|
-
await session.commit()
|
564
|
-
|
565
|
-
@enforce_types
|
566
|
-
@trace_method
|
567
|
-
def get_job_usage(self, job_id: str, actor: PydanticUser) -> LettaUsageStatistics:
|
568
|
-
"""
|
569
|
-
Get usage statistics for a job.
|
570
|
-
|
571
|
-
Args:
|
572
|
-
job_id: The ID of the job
|
573
|
-
actor: The user making the request
|
574
|
-
|
575
|
-
Returns:
|
576
|
-
Usage statistics for the job
|
577
|
-
|
578
|
-
Raises:
|
579
|
-
NoResultFound: If the job does not exist or user does not have access
|
580
|
-
"""
|
581
|
-
with db_registry.session() as session:
|
582
|
-
# First verify job exists and user has access
|
583
|
-
self._verify_job_access(session, job_id, actor)
|
584
|
-
|
585
|
-
# Get the latest usage statistics for the job
|
586
|
-
latest_stats = session.query(Step).filter(Step.job_id == job_id).order_by(Step.created_at.desc()).all()
|
587
|
-
|
588
|
-
if not latest_stats:
|
589
|
-
return LettaUsageStatistics(
|
590
|
-
completion_tokens=0,
|
591
|
-
prompt_tokens=0,
|
592
|
-
total_tokens=0,
|
593
|
-
step_count=0,
|
594
|
-
)
|
595
|
-
|
596
|
-
return LettaUsageStatistics(
|
597
|
-
completion_tokens=reduce(add, (step.completion_tokens or 0 for step in latest_stats), 0),
|
598
|
-
prompt_tokens=reduce(add, (step.prompt_tokens or 0 for step in latest_stats), 0),
|
599
|
-
total_tokens=reduce(add, (step.total_tokens or 0 for step in latest_stats), 0),
|
600
|
-
step_count=len(latest_stats),
|
601
|
-
)
|
602
|
-
|
603
|
-
@enforce_types
|
604
|
-
@trace_method
|
605
|
-
def add_job_usage(
|
606
|
-
self,
|
607
|
-
job_id: str,
|
608
|
-
usage: LettaUsageStatistics,
|
609
|
-
step_id: Optional[str] = None,
|
610
|
-
actor: PydanticUser = None,
|
611
|
-
) -> None:
|
612
|
-
"""
|
613
|
-
Add usage statistics for a job.
|
614
|
-
|
615
|
-
Args:
|
616
|
-
job_id: The ID of the job
|
617
|
-
usage: Usage statistics for the job
|
618
|
-
step_id: Optional ID of the specific step within the job
|
619
|
-
actor: The user making the request
|
620
|
-
|
621
|
-
Raises:
|
622
|
-
NoResultFound: If the job does not exist or user does not have access
|
623
|
-
"""
|
624
|
-
with db_registry.session() as session:
|
625
|
-
# First verify job exists and user has access
|
626
|
-
self._verify_job_access(session, job_id, actor, access=["write"])
|
627
|
-
|
628
|
-
# Manually log step with usage data
|
629
|
-
# TODO(@caren): log step under the hood and remove this
|
630
|
-
usage_stats = Step(
|
631
|
-
job_id=job_id,
|
632
|
-
completion_tokens=usage.completion_tokens,
|
633
|
-
prompt_tokens=usage.prompt_tokens,
|
634
|
-
total_tokens=usage.total_tokens,
|
635
|
-
step_count=usage.step_count,
|
636
|
-
step_id=step_id,
|
637
|
-
)
|
638
|
-
if actor:
|
639
|
-
usage_stats._set_created_and_updated_by_fields(actor.id)
|
640
|
-
|
641
|
-
session.add(usage_stats)
|
642
|
-
session.commit()
|
643
|
-
|
644
|
-
@enforce_types
|
645
|
-
@trace_method
|
646
|
-
def get_run_messages(
|
313
|
+
async def get_run_messages(
|
647
314
|
self,
|
648
315
|
run_id: str,
|
649
316
|
actor: PydanticUser,
|
@@ -672,7 +339,7 @@ class JobManager:
|
|
672
339
|
Raises:
|
673
340
|
NoResultFound: If the job does not exist or user does not have access
|
674
341
|
"""
|
675
|
-
messages = self.get_job_messages(
|
342
|
+
messages = await self.get_job_messages(
|
676
343
|
job_id=run_id,
|
677
344
|
actor=actor,
|
678
345
|
before=before,
|
@@ -682,7 +349,7 @@ class JobManager:
|
|
682
349
|
ascending=ascending,
|
683
350
|
)
|
684
351
|
|
685
|
-
request_config = self._get_run_request_config(run_id)
|
352
|
+
request_config = await self._get_run_request_config(run_id)
|
686
353
|
print("request_config", request_config)
|
687
354
|
|
688
355
|
messages = PydanticMessage.to_letta_messages_from_list(
|
@@ -700,7 +367,7 @@ class JobManager:
|
|
700
367
|
|
701
368
|
@enforce_types
|
702
369
|
@trace_method
|
703
|
-
def get_step_messages(
|
370
|
+
async def get_step_messages(
|
704
371
|
self,
|
705
372
|
run_id: str,
|
706
373
|
actor: PydanticUser,
|
@@ -729,7 +396,7 @@ class JobManager:
|
|
729
396
|
Raises:
|
730
397
|
NoResultFound: If the job does not exist or user does not have access
|
731
398
|
"""
|
732
|
-
messages = self.get_job_messages(
|
399
|
+
messages = await self.get_job_messages(
|
733
400
|
job_id=run_id,
|
734
401
|
actor=actor,
|
735
402
|
before=before,
|
@@ -739,7 +406,7 @@ class JobManager:
|
|
739
406
|
ascending=ascending,
|
740
407
|
)
|
741
408
|
|
742
|
-
request_config = self._get_run_request_config(run_id)
|
409
|
+
request_config = await self._get_run_request_config(run_id)
|
743
410
|
|
744
411
|
messages = PydanticMessage.to_letta_messages_from_list(
|
745
412
|
messages=messages,
|
@@ -750,34 +417,6 @@ class JobManager:
|
|
750
417
|
|
751
418
|
return messages
|
752
419
|
|
753
|
-
def _verify_job_access(
|
754
|
-
self,
|
755
|
-
session: Session,
|
756
|
-
job_id: str,
|
757
|
-
actor: PydanticUser,
|
758
|
-
access: List[Literal["read", "write", "admin"]] = ["read"],
|
759
|
-
) -> JobModel:
|
760
|
-
"""
|
761
|
-
Verify that a job exists and the user has the required access.
|
762
|
-
|
763
|
-
Args:
|
764
|
-
session: The database session
|
765
|
-
job_id: The ID of the job to verify
|
766
|
-
actor: The user making the request
|
767
|
-
|
768
|
-
Returns:
|
769
|
-
The job if it exists and the user has access
|
770
|
-
|
771
|
-
Raises:
|
772
|
-
NoResultFound: If the job does not exist or user does not have access
|
773
|
-
"""
|
774
|
-
job_query = select(JobModel).where(JobModel.id == job_id)
|
775
|
-
job_query = JobModel.apply_access_predicate(job_query, actor, access, AccessType.USER)
|
776
|
-
job = session.execute(job_query).scalar_one_or_none()
|
777
|
-
if not job:
|
778
|
-
raise NoResultFound(f"Job with id {job_id} does not exist or user does not have access")
|
779
|
-
return job
|
780
|
-
|
781
420
|
async def _verify_job_access_async(
|
782
421
|
self,
|
783
422
|
session: Session,
|
@@ -807,21 +446,6 @@ class JobManager:
|
|
807
446
|
raise NoResultFound(f"Job with id {job_id} does not exist or user does not have access")
|
808
447
|
return job
|
809
448
|
|
810
|
-
def _get_run_request_config(self, run_id: str) -> LettaRequestConfig:
|
811
|
-
"""
|
812
|
-
Get the request config for a job.
|
813
|
-
|
814
|
-
Args:
|
815
|
-
job_id: The ID of the job to get messages for
|
816
|
-
|
817
|
-
Returns:
|
818
|
-
The request config for the job
|
819
|
-
"""
|
820
|
-
with db_registry.session() as session:
|
821
|
-
job = session.query(JobModel).filter(JobModel.id == run_id).first()
|
822
|
-
request_config = job.request_config or LettaRequestConfig()
|
823
|
-
return request_config
|
824
|
-
|
825
449
|
@enforce_types
|
826
450
|
async def record_ttft(self, job_id: str, ttft_ns: int, actor: PydanticUser) -> None:
|
827
451
|
"""Record time to first token for a run"""
|
@@ -902,3 +526,64 @@ class JobManager:
|
|
902
526
|
# Continue silently - callback failures should not affect job completion
|
903
527
|
finally:
|
904
528
|
return result
|
529
|
+
|
530
|
+
@enforce_types
|
531
|
+
@trace_method
|
532
|
+
async def get_job_steps(
|
533
|
+
self,
|
534
|
+
job_id: str,
|
535
|
+
actor: PydanticUser,
|
536
|
+
before: Optional[str] = None,
|
537
|
+
after: Optional[str] = None,
|
538
|
+
limit: Optional[int] = 100,
|
539
|
+
ascending: bool = True,
|
540
|
+
) -> List[PydanticStep]:
|
541
|
+
"""
|
542
|
+
Get all steps associated with a job.
|
543
|
+
|
544
|
+
Args:
|
545
|
+
job_id: The ID of the job to get steps for
|
546
|
+
actor: The user making the request
|
547
|
+
before: Cursor for pagination
|
548
|
+
after: Cursor for pagination
|
549
|
+
limit: Maximum number of steps to return
|
550
|
+
ascending: Optional flag to sort in ascending order
|
551
|
+
|
552
|
+
Returns:
|
553
|
+
List of steps associated with the job
|
554
|
+
|
555
|
+
Raises:
|
556
|
+
NoResultFound: If the job does not exist or user does not have access
|
557
|
+
"""
|
558
|
+
async with db_registry.async_session() as session:
|
559
|
+
# Build filters
|
560
|
+
filters = {}
|
561
|
+
filters["job_id"] = job_id
|
562
|
+
|
563
|
+
# Get steps
|
564
|
+
steps = StepModel.list_async(
|
565
|
+
db_session=session,
|
566
|
+
before=before,
|
567
|
+
after=after,
|
568
|
+
ascending=ascending,
|
569
|
+
limit=limit,
|
570
|
+
actor=actor,
|
571
|
+
**filters,
|
572
|
+
)
|
573
|
+
|
574
|
+
return [step.to_pydantic() for step in steps]
|
575
|
+
|
576
|
+
async def _get_run_request_config(self, run_id: str) -> LettaRequestConfig:
|
577
|
+
"""
|
578
|
+
Get the request config for a job.
|
579
|
+
|
580
|
+
Args:
|
581
|
+
job_id: The ID of the job to get messages for
|
582
|
+
|
583
|
+
Returns:
|
584
|
+
The request config for the job
|
585
|
+
"""
|
586
|
+
async with db_registry.async_session() as session:
|
587
|
+
job = await JobModel.read_async(db_session=session, identifier=run_id)
|
588
|
+
request_config = job.request_config or LettaRequestConfig()
|
589
|
+
return request_config
|