letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.12.0.dev20251009104148__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 +1 -1
- 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 +927 -0
- letta/agents/voice_agent.py +2 -6
- letta/constants.py +8 -4
- letta/database_utils.py +161 -0
- 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 +127 -16
- letta/llm_api/bedrock_client.py +4 -2
- letta/llm_api/deepseek_client.py +4 -1
- letta/llm_api/google_vertex_client.py +124 -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 +2 -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/run_metrics.py +82 -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 +564 -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/run_metrics.py +21 -0
- 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 +79 -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 +149 -99
- 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/context_window_calculator/token_counter.py +1 -1
- 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 +69 -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 +364 -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/system.py +5 -1
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +3 -5
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +146 -135
- 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.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/licenses/LICENSE +0 -0
@@ -31,10 +31,10 @@ from letta.otel.metric_registry import MetricRegistry
|
|
31
31
|
from letta.schemas.agent import AgentState, CreateAgent, UpdateAgent
|
32
32
|
from letta.schemas.agent_file import AgentFileSchema
|
33
33
|
from letta.schemas.block import Block, BlockUpdate
|
34
|
-
from letta.schemas.enums import
|
34
|
+
from letta.schemas.enums import AgentType, RunStatus
|
35
35
|
from letta.schemas.file import AgentFileAttachment, PaginatedAgentFiles
|
36
36
|
from letta.schemas.group import Group
|
37
|
-
from letta.schemas.job import
|
37
|
+
from letta.schemas.job import LettaRequestConfig
|
38
38
|
from letta.schemas.letta_message import LettaMessageUnion, LettaMessageUpdateUnion, MessageType
|
39
39
|
from letta.schemas.letta_request import LettaAsyncRequest, LettaRequest, LettaStreamingRequest
|
40
40
|
from letta.schemas.letta_response import LettaResponse
|
@@ -48,7 +48,7 @@ from letta.schemas.memory import (
|
|
48
48
|
)
|
49
49
|
from letta.schemas.message import MessageCreate, MessageSearchRequest, MessageSearchResult
|
50
50
|
from letta.schemas.passage import Passage
|
51
|
-
from letta.schemas.run import Run
|
51
|
+
from letta.schemas.run import Run as PydanticRun, RunUpdate
|
52
52
|
from letta.schemas.source import Source
|
53
53
|
from letta.schemas.tool import Tool
|
54
54
|
from letta.schemas.user import User
|
@@ -56,6 +56,8 @@ from letta.serialize_schemas.pydantic_agent_schema import AgentSchema
|
|
56
56
|
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
57
57
|
from letta.server.rest_api.redis_stream_manager import create_background_stream_processor, redis_sse_stream_generator
|
58
58
|
from letta.server.server import SyncServer
|
59
|
+
from letta.services.lettuce import LettuceClient
|
60
|
+
from letta.services.run_manager import RunManager
|
59
61
|
from letta.settings import settings
|
60
62
|
from letta.utils import safe_create_shielded_task, safe_create_task, truncate_file_visible_content
|
61
63
|
|
@@ -188,28 +190,16 @@ async def export_agent(
|
|
188
190
|
- Legacy format (use_legacy_format=true): Single agent with inline tools/blocks
|
189
191
|
- New format (default): Multi-entity format with separate agents, tools, blocks, files, etc.
|
190
192
|
"""
|
191
|
-
actor = server.user_manager.
|
193
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
192
194
|
|
193
195
|
if use_legacy_format:
|
194
196
|
# Use the legacy serialization method
|
195
|
-
|
196
|
-
|
197
|
-
return agent.model_dump()
|
198
|
-
except NoResultFound:
|
199
|
-
raise HTTPException(status_code=404, detail=f"Agent with id={agent_id} not found for user_id={actor.id}.")
|
197
|
+
agent = await server.agent_manager.serialize(agent_id=agent_id, actor=actor, max_steps=max_steps)
|
198
|
+
return agent.model_dump()
|
200
199
|
else:
|
201
200
|
# Use the new multi-entity export format
|
202
|
-
|
203
|
-
|
204
|
-
return agent_file_schema.model_dump()
|
205
|
-
except AgentNotFoundForExportError:
|
206
|
-
raise HTTPException(status_code=404, detail=f"Agent with id={agent_id} not found for user_id={actor.id}.")
|
207
|
-
except AgentExportIdMappingError as e:
|
208
|
-
raise HTTPException(
|
209
|
-
status_code=500, detail=f"Internal error during export: ID mapping failed for {e.entity_type} ID '{e.db_id}'"
|
210
|
-
)
|
211
|
-
except AgentExportProcessingError as e:
|
212
|
-
raise HTTPException(status_code=500, detail=f"Export processing failed: {str(e.original_error)}")
|
201
|
+
agent_file_schema = await server.agent_serialization_manager.export(agent_ids=[agent_id], actor=actor)
|
202
|
+
return agent_file_schema.model_dump()
|
213
203
|
|
214
204
|
|
215
205
|
class ImportedAgentsResponse(BaseModel):
|
@@ -231,33 +221,19 @@ def import_agent_legacy(
|
|
231
221
|
"""
|
232
222
|
Import an agent using the legacy AgentSchema format.
|
233
223
|
"""
|
234
|
-
|
235
|
-
|
236
|
-
agent_schema = AgentSchema.model_validate(agent_json)
|
237
|
-
|
238
|
-
new_agent = server.agent_manager.deserialize(
|
239
|
-
serialized_agent=agent_schema, # Ensure we're passing a validated AgentSchema
|
240
|
-
actor=actor,
|
241
|
-
append_copy_suffix=append_copy_suffix,
|
242
|
-
override_existing_tools=override_existing_tools,
|
243
|
-
project_id=project_id,
|
244
|
-
strip_messages=strip_messages,
|
245
|
-
env_vars=env_vars,
|
246
|
-
)
|
247
|
-
return [new_agent.id]
|
224
|
+
# Validate the JSON against AgentSchema before passing it to deserialize
|
225
|
+
agent_schema = AgentSchema.model_validate(agent_json)
|
248
226
|
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
traceback.print_exc()
|
260
|
-
raise HTTPException(status_code=500, detail=f"An unexpected error occurred while uploading the agent: {e!s}")
|
227
|
+
new_agent = server.agent_manager.deserialize(
|
228
|
+
serialized_agent=agent_schema, # Ensure we're passing a validated AgentSchema
|
229
|
+
actor=actor,
|
230
|
+
append_copy_suffix=append_copy_suffix,
|
231
|
+
override_existing_tools=override_existing_tools,
|
232
|
+
project_id=project_id,
|
233
|
+
strip_messages=strip_messages,
|
234
|
+
env_vars=env_vars,
|
235
|
+
)
|
236
|
+
return [new_agent.id]
|
261
237
|
|
262
238
|
|
263
239
|
async def _import_agent(
|
@@ -275,46 +251,29 @@ async def _import_agent(
|
|
275
251
|
"""
|
276
252
|
Import an agent using the new AgentFileSchema format.
|
277
253
|
"""
|
278
|
-
|
279
|
-
agent_schema = AgentFileSchema.model_validate(agent_file_json)
|
280
|
-
except ValidationError as e:
|
281
|
-
raise HTTPException(status_code=422, detail=f"Invalid agent file schema: {e!s}")
|
282
|
-
|
283
|
-
try:
|
284
|
-
if override_embedding_handle:
|
285
|
-
embedding_config_override = await server.get_cached_embedding_config_async(actor=actor, handle=override_embedding_handle)
|
286
|
-
else:
|
287
|
-
embedding_config_override = None
|
254
|
+
agent_schema = AgentFileSchema.model_validate(agent_file_json)
|
288
255
|
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
override_existing_tools=override_existing_tools,
|
294
|
-
env_vars=env_vars,
|
295
|
-
override_embedding_config=embedding_config_override,
|
296
|
-
project_id=project_id,
|
297
|
-
)
|
298
|
-
|
299
|
-
if not import_result.success:
|
300
|
-
raise HTTPException(
|
301
|
-
status_code=500, detail=f"Import failed: {import_result.message}. Errors: {', '.join(import_result.errors)}"
|
302
|
-
)
|
303
|
-
|
304
|
-
return import_result.imported_agent_ids
|
256
|
+
if override_embedding_handle:
|
257
|
+
embedding_config_override = await server.get_cached_embedding_config_async(actor=actor, handle=override_embedding_handle)
|
258
|
+
else:
|
259
|
+
embedding_config_override = None
|
305
260
|
|
306
|
-
|
307
|
-
|
261
|
+
import_result = await server.agent_serialization_manager.import_file(
|
262
|
+
schema=agent_schema,
|
263
|
+
actor=actor,
|
264
|
+
append_copy_suffix=append_copy_suffix,
|
265
|
+
override_existing_tools=override_existing_tools,
|
266
|
+
env_vars=env_vars,
|
267
|
+
override_embedding_config=embedding_config_override,
|
268
|
+
project_id=project_id,
|
269
|
+
)
|
308
270
|
|
309
|
-
|
310
|
-
|
271
|
+
if not import_result.success:
|
272
|
+
from letta.errors import AgentFileImportError
|
311
273
|
|
312
|
-
|
313
|
-
raise HTTPException(status_code=503, detail=f"Database connection error. Please try again later: {e!s}")
|
274
|
+
raise AgentFileImportError(f"Import failed: {import_result.message}. Errors: {', '.join(import_result.errors)}")
|
314
275
|
|
315
|
-
|
316
|
-
traceback.print_exc()
|
317
|
-
raise HTTPException(status_code=500, detail=f"An unexpected error occurred while importing agents: {e!s}")
|
276
|
+
return import_result.imported_agent_ids
|
318
277
|
|
319
278
|
|
320
279
|
@router.post("/import", response_model=ImportedAgentsResponse, operation_id="import_agent")
|
@@ -345,7 +304,7 @@ async def import_agent(
|
|
345
304
|
Import a serialized agent file and recreate the agent(s) in the system.
|
346
305
|
Returns the IDs of all imported agents.
|
347
306
|
"""
|
348
|
-
actor = server.user_manager.
|
307
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
349
308
|
|
350
309
|
try:
|
351
310
|
serialized_data = file.file.read()
|
@@ -384,15 +343,9 @@ async def import_agent(
|
|
384
343
|
)
|
385
344
|
else:
|
386
345
|
# This is a legacy AgentSchema
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
actor=actor,
|
391
|
-
append_copy_suffix=append_copy_suffix,
|
392
|
-
override_existing_tools=override_existing_tools,
|
393
|
-
project_id=project_id,
|
394
|
-
strip_messages=strip_messages,
|
395
|
-
env_vars=env_vars,
|
346
|
+
raise HTTPException(
|
347
|
+
status_code=400,
|
348
|
+
detail="Legacy AgentSchema format is deprecated. Please use the new AgentFileSchema format with 'agents' field.",
|
396
349
|
)
|
397
350
|
|
398
351
|
return ImportedAgentsResponse(agent_ids=agent_ids)
|
@@ -408,11 +361,7 @@ async def retrieve_agent_context_window(
|
|
408
361
|
Retrieve the context window of a specific agent.
|
409
362
|
"""
|
410
363
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
411
|
-
|
412
|
-
return await server.agent_manager.get_context_window(agent_id=agent_id, actor=actor)
|
413
|
-
except Exception as e:
|
414
|
-
traceback.print_exc()
|
415
|
-
raise e
|
364
|
+
return await server.agent_manager.get_context_window(agent_id=agent_id, actor=actor)
|
416
365
|
|
417
366
|
|
418
367
|
class CreateAgentRequest(CreateAgent):
|
@@ -436,12 +385,10 @@ async def create_agent(
|
|
436
385
|
"""
|
437
386
|
Create an agent.
|
438
387
|
"""
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
traceback.print_exc()
|
444
|
-
raise HTTPException(status_code=500, detail=str(e))
|
388
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
389
|
+
if headers.experimental_params.letta_v1_agent and agent.agent_type == AgentType.memgpt_v2_agent and not agent.enable_sleeptime:
|
390
|
+
agent.agent_type = AgentType.letta_v1_agent
|
391
|
+
return await server.create_agent_async(agent, actor=actor)
|
445
392
|
|
446
393
|
|
447
394
|
@router.patch("/{agent_id}", response_model=AgentState, operation_id="modify_agent")
|
@@ -461,10 +408,28 @@ async def list_agent_tools(
|
|
461
408
|
agent_id: str,
|
462
409
|
server: "SyncServer" = Depends(get_letta_server),
|
463
410
|
headers: HeaderParams = Depends(get_headers),
|
411
|
+
before: Optional[str] = Query(
|
412
|
+
None, description="Tool ID cursor for pagination. Returns tools that come before this tool ID in the specified sort order"
|
413
|
+
),
|
414
|
+
after: Optional[str] = Query(
|
415
|
+
None, description="Tool ID cursor for pagination. Returns tools that come after this tool ID in the specified sort order"
|
416
|
+
),
|
417
|
+
limit: Optional[int] = Query(10, description="Maximum number of tools to return"),
|
418
|
+
order: Literal["asc", "desc"] = Query(
|
419
|
+
"desc", description="Sort order for tools by creation time. 'asc' for oldest first, 'desc' for newest first"
|
420
|
+
),
|
421
|
+
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
464
422
|
):
|
465
423
|
"""Get tools from an existing agent"""
|
466
424
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
467
|
-
return await server.agent_manager.list_attached_tools_async(
|
425
|
+
return await server.agent_manager.list_attached_tools_async(
|
426
|
+
agent_id=agent_id,
|
427
|
+
actor=actor,
|
428
|
+
before=before,
|
429
|
+
after=after,
|
430
|
+
limit=limit,
|
431
|
+
ascending=(order == "asc"),
|
432
|
+
)
|
468
433
|
|
469
434
|
|
470
435
|
@router.patch("/{agent_id}/tools/attach/{tool_id}", response_model=AgentState, operation_id="attach_tool")
|
@@ -666,12 +631,9 @@ async def open_file(
|
|
666
631
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
667
632
|
|
668
633
|
# Get the agent to access files configuration
|
669
|
-
|
670
|
-
|
671
|
-
|
672
|
-
)
|
673
|
-
except ValueError:
|
674
|
-
raise HTTPException(status_code=404, detail=f"Agent with id={agent_id} not found")
|
634
|
+
per_file_view_window_char_limit, max_files_open = await server.agent_manager.get_agent_files_config_async(
|
635
|
+
agent_id=agent_id, actor=actor
|
636
|
+
)
|
675
637
|
|
676
638
|
# Get file metadata
|
677
639
|
file_metadata = await server.file_manager.get_file_by_id(file_id=file_id, actor=actor, include_content=True)
|
@@ -717,16 +679,13 @@ async def close_file(
|
|
717
679
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
718
680
|
|
719
681
|
# Use update_file_agent_by_id to close the file
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"File id={file_id} successfully closed"})
|
728
|
-
except NoResultFound:
|
729
|
-
raise HTTPException(status_code=404, detail=f"File association for file_id={file_id} and agent_id={agent_id} not found")
|
682
|
+
await server.file_agent_manager.update_file_agent_by_id(
|
683
|
+
agent_id=agent_id,
|
684
|
+
file_id=file_id,
|
685
|
+
actor=actor,
|
686
|
+
is_open=False,
|
687
|
+
)
|
688
|
+
return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"File id={file_id} successfully closed"})
|
730
689
|
|
731
690
|
|
732
691
|
@router.get("/{agent_id}", response_model=AgentState, operation_id="retrieve_agent")
|
@@ -752,10 +711,7 @@ async def retrieve_agent(
|
|
752
711
|
|
753
712
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
754
713
|
|
755
|
-
|
756
|
-
return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, include_relationships=include_relationships, actor=actor)
|
757
|
-
except NoResultFound as e:
|
758
|
-
raise HTTPException(status_code=404, detail=str(e))
|
714
|
+
return await server.agent_manager.get_agent_by_id_async(agent_id=agent_id, include_relationships=include_relationships, actor=actor)
|
759
715
|
|
760
716
|
|
761
717
|
@router.delete("/{agent_id}", response_model=None, operation_id="delete_agent")
|
@@ -768,11 +724,8 @@ async def delete_agent(
|
|
768
724
|
Delete an agent.
|
769
725
|
"""
|
770
726
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
771
|
-
|
772
|
-
|
773
|
-
return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Agent id={agent_id} successfully deleted"})
|
774
|
-
except NoResultFound:
|
775
|
-
raise HTTPException(status_code=404, detail=f"Agent agent_id={agent_id} not found for user_id={actor.id}.")
|
727
|
+
await server.agent_manager.delete_agent_async(agent_id=agent_id, actor=actor)
|
728
|
+
return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Agent id={agent_id} successfully deleted"})
|
776
729
|
|
777
730
|
|
778
731
|
@router.get("/{agent_id}/sources", response_model=list[Source], operation_id="list_agent_sources")
|
@@ -780,12 +733,30 @@ async def list_agent_sources(
|
|
780
733
|
agent_id: str,
|
781
734
|
server: "SyncServer" = Depends(get_letta_server),
|
782
735
|
headers: HeaderParams = Depends(get_headers),
|
736
|
+
before: Optional[str] = Query(
|
737
|
+
None, description="Source ID cursor for pagination. Returns sources that come before this source ID in the specified sort order"
|
738
|
+
),
|
739
|
+
after: Optional[str] = Query(
|
740
|
+
None, description="Source ID cursor for pagination. Returns sources that come after this source ID in the specified sort order"
|
741
|
+
),
|
742
|
+
limit: Optional[int] = Query(100, description="Maximum number of sources to return"),
|
743
|
+
order: Literal["asc", "desc"] = Query(
|
744
|
+
"desc", description="Sort order for sources by creation time. 'asc' for oldest first, 'desc' for newest first"
|
745
|
+
),
|
746
|
+
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
783
747
|
):
|
784
748
|
"""
|
785
749
|
Get the sources associated with an agent.
|
786
750
|
"""
|
787
751
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
788
|
-
return await server.agent_manager.list_attached_sources_async(
|
752
|
+
return await server.agent_manager.list_attached_sources_async(
|
753
|
+
agent_id=agent_id,
|
754
|
+
actor=actor,
|
755
|
+
before=before,
|
756
|
+
after=after,
|
757
|
+
limit=limit,
|
758
|
+
ascending=(order == "asc"),
|
759
|
+
)
|
789
760
|
|
790
761
|
|
791
762
|
@router.get("/{agent_id}/folders", response_model=list[Source], operation_id="list_agent_folders")
|
@@ -793,19 +764,49 @@ async def list_agent_folders(
|
|
793
764
|
agent_id: str,
|
794
765
|
server: "SyncServer" = Depends(get_letta_server),
|
795
766
|
headers: HeaderParams = Depends(get_headers),
|
767
|
+
before: Optional[str] = Query(
|
768
|
+
None, description="Source ID cursor for pagination. Returns sources that come before this source ID in the specified sort order"
|
769
|
+
),
|
770
|
+
after: Optional[str] = Query(
|
771
|
+
None, description="Source ID cursor for pagination. Returns sources that come after this source ID in the specified sort order"
|
772
|
+
),
|
773
|
+
limit: Optional[int] = Query(100, description="Maximum number of sources to return"),
|
774
|
+
order: Literal["asc", "desc"] = Query(
|
775
|
+
"desc", description="Sort order for sources by creation time. 'asc' for oldest first, 'desc' for newest first"
|
776
|
+
),
|
777
|
+
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
796
778
|
):
|
797
779
|
"""
|
798
780
|
Get the folders associated with an agent.
|
799
781
|
"""
|
800
782
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
801
|
-
return await server.agent_manager.list_attached_sources_async(
|
783
|
+
return await server.agent_manager.list_attached_sources_async(
|
784
|
+
agent_id=agent_id,
|
785
|
+
actor=actor,
|
786
|
+
before=before,
|
787
|
+
after=after,
|
788
|
+
limit=limit,
|
789
|
+
ascending=(order == "asc"),
|
790
|
+
)
|
802
791
|
|
803
792
|
|
804
793
|
@router.get("/{agent_id}/files", response_model=PaginatedAgentFiles, operation_id="list_agent_files")
|
805
794
|
async def list_agent_files(
|
806
795
|
agent_id: str,
|
807
|
-
|
808
|
-
|
796
|
+
before: Optional[str] = Query(
|
797
|
+
None, description="File ID cursor for pagination. Returns files that come before this file ID in the specified sort order"
|
798
|
+
),
|
799
|
+
after: Optional[str] = Query(
|
800
|
+
None, description="File ID cursor for pagination. Returns files that come after this file ID in the specified sort order"
|
801
|
+
),
|
802
|
+
limit: Optional[int] = Query(100, description="Maximum number of files to return"),
|
803
|
+
order: Literal["asc", "desc"] = Query(
|
804
|
+
"desc", description="Sort order for files by creation time. 'asc' for oldest first, 'desc' for newest first"
|
805
|
+
),
|
806
|
+
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
807
|
+
cursor: Optional[str] = Query(
|
808
|
+
None, description="Pagination cursor from previous response (deprecated, use before/after)", deprecated=True
|
809
|
+
),
|
809
810
|
is_open: Optional[bool] = Query(None, description="Filter by open status (true for open files, false for closed files)"),
|
810
811
|
server: "SyncServer" = Depends(get_letta_server),
|
811
812
|
headers: HeaderParams = Depends(get_headers),
|
@@ -815,9 +816,18 @@ async def list_agent_files(
|
|
815
816
|
"""
|
816
817
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
817
818
|
|
819
|
+
effective_limit = limit or 20
|
820
|
+
|
818
821
|
# get paginated file-agent relationships for this agent
|
819
822
|
file_agents, next_cursor, has_more = await server.file_agent_manager.list_files_for_agent_paginated(
|
820
|
-
agent_id=agent_id,
|
823
|
+
agent_id=agent_id,
|
824
|
+
actor=actor,
|
825
|
+
cursor=cursor, # keep for backwards compatibility
|
826
|
+
limit=effective_limit,
|
827
|
+
is_open=is_open,
|
828
|
+
before=before,
|
829
|
+
after=after,
|
830
|
+
ascending=(order == "asc"),
|
821
831
|
)
|
822
832
|
|
823
833
|
# enrich with file and source metadata
|
@@ -872,10 +882,7 @@ async def retrieve_block(
|
|
872
882
|
"""
|
873
883
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
874
884
|
|
875
|
-
|
876
|
-
return await server.agent_manager.get_block_with_label_async(agent_id=agent_id, block_label=block_label, actor=actor)
|
877
|
-
except NoResultFound as e:
|
878
|
-
raise HTTPException(status_code=404, detail=str(e))
|
885
|
+
return await server.agent_manager.get_block_with_label_async(agent_id=agent_id, block_label=block_label, actor=actor)
|
879
886
|
|
880
887
|
|
881
888
|
@router.get("/{agent_id}/core-memory/blocks", response_model=list[Block], operation_id="list_core_memory_blocks")
|
@@ -883,16 +890,31 @@ async def list_blocks(
|
|
883
890
|
agent_id: str,
|
884
891
|
server: "SyncServer" = Depends(get_letta_server),
|
885
892
|
headers: HeaderParams = Depends(get_headers),
|
893
|
+
before: Optional[str] = Query(
|
894
|
+
None, description="Block ID cursor for pagination. Returns blocks that come before this block ID in the specified sort order"
|
895
|
+
),
|
896
|
+
after: Optional[str] = Query(
|
897
|
+
None, description="Block ID cursor for pagination. Returns blocks that come after this block ID in the specified sort order"
|
898
|
+
),
|
899
|
+
limit: Optional[int] = Query(100, description="Maximum number of blocks to return"),
|
900
|
+
order: Literal["asc", "desc"] = Query(
|
901
|
+
"desc", description="Sort order for blocks by creation time. 'asc' for oldest first, 'desc' for newest first"
|
902
|
+
),
|
903
|
+
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
886
904
|
):
|
887
905
|
"""
|
888
906
|
Retrieve the core memory blocks of a specific agent.
|
889
907
|
"""
|
890
908
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
909
|
+
|
910
|
+
return await server.agent_manager.list_agent_blocks_async(
|
911
|
+
agent_id=agent_id,
|
912
|
+
actor=actor,
|
913
|
+
before=before,
|
914
|
+
after=after,
|
915
|
+
limit=limit,
|
916
|
+
ascending=(order == "asc"),
|
917
|
+
)
|
896
918
|
|
897
919
|
|
898
920
|
@router.patch("/{agent_id}/core-memory/blocks/{block_label}", response_model=Block, operation_id="modify_core_memory_block")
|
@@ -1015,34 +1037,26 @@ async def search_archival_memory(
|
|
1015
1037
|
"""
|
1016
1038
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
1017
1039
|
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1021
|
-
end_datetime = end_datetime.isoformat() if end_datetime else None
|
1040
|
+
# convert datetime to string in ISO 8601 format
|
1041
|
+
start_datetime = start_datetime.isoformat() if start_datetime else None
|
1042
|
+
end_datetime = end_datetime.isoformat() if end_datetime else None
|
1022
1043
|
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
1027
|
-
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
|
1034
|
-
|
1035
|
-
# Convert to proper response schema
|
1036
|
-
search_results = [ArchivalMemorySearchResult(**result) for result in formatted_results]
|
1044
|
+
# Use the shared agent manager method
|
1045
|
+
formatted_results = await server.agent_manager.search_agent_archival_memory_async(
|
1046
|
+
agent_id=agent_id,
|
1047
|
+
actor=actor,
|
1048
|
+
query=query,
|
1049
|
+
tags=tags,
|
1050
|
+
tag_match_mode=tag_match_mode,
|
1051
|
+
top_k=top_k,
|
1052
|
+
start_datetime=start_datetime,
|
1053
|
+
end_datetime=end_datetime,
|
1054
|
+
)
|
1037
1055
|
|
1038
|
-
|
1056
|
+
# Convert to proper response schema
|
1057
|
+
search_results = [ArchivalMemorySearchResult(**result) for result in formatted_results]
|
1039
1058
|
|
1040
|
-
|
1041
|
-
raise HTTPException(status_code=404, detail=f"Agent with id={agent_id} not found for user_id={actor.id}.")
|
1042
|
-
except ValueError as e:
|
1043
|
-
raise HTTPException(status_code=400, detail=str(e))
|
1044
|
-
except Exception as e:
|
1045
|
-
raise HTTPException(status_code=500, detail=f"Internal server error during archival memory search: {str(e)}")
|
1059
|
+
return ArchivalMemorySearchResponse(results=search_results, count=len(formatted_results))
|
1046
1060
|
|
1047
1061
|
|
1048
1062
|
# TODO(ethan): query or path parameter for memory_id?
|
@@ -1073,9 +1087,17 @@ AgentMessagesResponse = Annotated[
|
|
1073
1087
|
async def list_messages(
|
1074
1088
|
agent_id: str,
|
1075
1089
|
server: "SyncServer" = Depends(get_letta_server),
|
1076
|
-
|
1077
|
-
|
1078
|
-
|
1090
|
+
before: Optional[str] = Query(
|
1091
|
+
None, description="Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order"
|
1092
|
+
),
|
1093
|
+
after: Optional[str] = Query(
|
1094
|
+
None, description="Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order"
|
1095
|
+
),
|
1096
|
+
limit: Optional[int] = Query(100, description="Maximum number of messages to return"),
|
1097
|
+
order: Literal["asc", "desc"] = Query(
|
1098
|
+
"desc", description="Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first"
|
1099
|
+
),
|
1100
|
+
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
1079
1101
|
group_id: str | None = Query(None, description="Group ID to filter messages by."),
|
1080
1102
|
use_assistant_message: bool = Query(True, description="Whether to use assistant messages"),
|
1081
1103
|
assistant_message_tool_name: str = Query(DEFAULT_MESSAGE_TOOL, description="The name of the designated message tool."),
|
@@ -1096,7 +1118,7 @@ async def list_messages(
|
|
1096
1118
|
before=before,
|
1097
1119
|
limit=limit,
|
1098
1120
|
group_id=group_id,
|
1099
|
-
reverse=
|
1121
|
+
reverse=(order == "desc"),
|
1100
1122
|
return_message_object=False,
|
1101
1123
|
use_assistant_message=use_assistant_message,
|
1102
1124
|
assistant_message_tool_name=assistant_message_tool_name,
|
@@ -1107,7 +1129,7 @@ async def list_messages(
|
|
1107
1129
|
|
1108
1130
|
|
1109
1131
|
@router.patch("/{agent_id}/messages/{message_id}", response_model=LettaMessageUnion, operation_id="modify_message")
|
1110
|
-
def modify_message(
|
1132
|
+
async def modify_message(
|
1111
1133
|
agent_id: str,
|
1112
1134
|
message_id: str,
|
1113
1135
|
request: LettaMessageUpdateUnion = Body(...),
|
@@ -1118,8 +1140,10 @@ def modify_message(
|
|
1118
1140
|
Update the details of a message associated with an agent.
|
1119
1141
|
"""
|
1120
1142
|
# TODO: support modifying tool calls/returns
|
1121
|
-
actor = server.user_manager.
|
1122
|
-
return server.message_manager.
|
1143
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
1144
|
+
return await server.message_manager.update_message_by_letta_message_async(
|
1145
|
+
message_id=message_id, letta_message_update=request, actor=actor
|
1146
|
+
)
|
1123
1147
|
|
1124
1148
|
|
1125
1149
|
# noinspection PyInconsistentReturns
|
@@ -1166,32 +1190,26 @@ async def send_message(
|
|
1166
1190
|
|
1167
1191
|
# Create a new run for execution tracking
|
1168
1192
|
if settings.track_agent_run:
|
1169
|
-
|
1170
|
-
run = await
|
1171
|
-
|
1172
|
-
|
1173
|
-
|
1193
|
+
runs_manager = RunManager()
|
1194
|
+
run = await runs_manager.create_run(
|
1195
|
+
pydantic_run=PydanticRun(
|
1196
|
+
agent_id=agent_id,
|
1197
|
+
background=False,
|
1174
1198
|
metadata={
|
1175
|
-
"
|
1176
|
-
"agent_id": agent_id,
|
1199
|
+
"run_type": "send_message",
|
1177
1200
|
},
|
1178
|
-
request_config=LettaRequestConfig(
|
1179
|
-
use_assistant_message=request.use_assistant_message,
|
1180
|
-
assistant_message_tool_name=request.assistant_message_tool_name,
|
1181
|
-
assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
|
1182
|
-
include_return_message_types=request.include_return_message_types,
|
1183
|
-
),
|
1201
|
+
request_config=LettaRequestConfig.from_letta_request(request),
|
1184
1202
|
),
|
1185
1203
|
actor=actor,
|
1186
1204
|
)
|
1187
1205
|
else:
|
1188
1206
|
run = None
|
1189
1207
|
|
1190
|
-
job_update_metadata = None
|
1191
1208
|
# TODO (cliandy): clean this up
|
1192
1209
|
redis_client = await get_redis_client()
|
1193
1210
|
await redis_client.set(f"{REDIS_RUN_ID_PREFIX}:{agent_id}", run.id if run else None)
|
1194
1211
|
|
1212
|
+
run_update_metadata = None
|
1195
1213
|
try:
|
1196
1214
|
result = None
|
1197
1215
|
if agent_eligible and model_compatible:
|
@@ -1217,17 +1235,17 @@ async def send_message(
|
|
1217
1235
|
assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
|
1218
1236
|
include_return_message_types=request.include_return_message_types,
|
1219
1237
|
)
|
1220
|
-
|
1238
|
+
run_status = result.stop_reason.stop_reason.run_status
|
1221
1239
|
return result
|
1222
1240
|
except PendingApprovalError as e:
|
1223
|
-
|
1224
|
-
|
1241
|
+
run_update_metadata = {"error": str(e)}
|
1242
|
+
run_status = RunStatus.failed
|
1225
1243
|
raise HTTPException(
|
1226
1244
|
status_code=409, detail={"code": "PENDING_APPROVAL", "message": str(e), "pending_request_id": e.pending_request_id}
|
1227
1245
|
)
|
1228
1246
|
except Exception as e:
|
1229
|
-
|
1230
|
-
|
1247
|
+
run_update_metadata = {"error": str(e)}
|
1248
|
+
run_status = RunStatus.failed
|
1231
1249
|
raise
|
1232
1250
|
finally:
|
1233
1251
|
if settings.track_agent_run:
|
@@ -1236,12 +1254,14 @@ async def send_message(
|
|
1236
1254
|
else:
|
1237
1255
|
# NOTE: we could also consider this an error?
|
1238
1256
|
stop_reason = None
|
1239
|
-
await server.
|
1240
|
-
|
1241
|
-
|
1257
|
+
await server.run_manager.update_run_by_id_async(
|
1258
|
+
run_id=run.id,
|
1259
|
+
update=RunUpdate(
|
1260
|
+
status=run_status,
|
1261
|
+
metadata=run_update_metadata,
|
1262
|
+
stop_reason=stop_reason,
|
1263
|
+
),
|
1242
1264
|
actor=actor,
|
1243
|
-
metadata=job_update_metadata,
|
1244
|
-
stop_reason=stop_reason,
|
1245
1265
|
)
|
1246
1266
|
|
1247
1267
|
|
@@ -1297,29 +1317,24 @@ async def send_message_streaming(
|
|
1297
1317
|
"deepseek",
|
1298
1318
|
]
|
1299
1319
|
model_compatible_token_streaming = agent.llm_config.model_endpoint_type in ["anthropic", "openai", "bedrock", "deepseek"]
|
1320
|
+
if agent.agent_type == AgentType.letta_v1_agent and agent.llm_config.model_endpoint_type in ["google_ai", "google_vertex"]:
|
1321
|
+
model_compatible_token_streaming = True
|
1300
1322
|
|
1301
|
-
# Create a new
|
1323
|
+
# Create a new run for execution tracking
|
1302
1324
|
if settings.track_agent_run:
|
1303
|
-
|
1304
|
-
run = await
|
1305
|
-
|
1306
|
-
|
1307
|
-
|
1325
|
+
runs_manager = RunManager()
|
1326
|
+
run = await runs_manager.create_run(
|
1327
|
+
pydantic_run=PydanticRun(
|
1328
|
+
agent_id=agent_id,
|
1329
|
+
background=request.background or False,
|
1308
1330
|
metadata={
|
1309
|
-
"
|
1310
|
-
"agent_id": agent_id,
|
1311
|
-
"background": request.background or False,
|
1331
|
+
"run_type": "send_message_streaming",
|
1312
1332
|
},
|
1313
|
-
request_config=LettaRequestConfig(
|
1314
|
-
use_assistant_message=request.use_assistant_message,
|
1315
|
-
assistant_message_tool_name=request.assistant_message_tool_name,
|
1316
|
-
assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
|
1317
|
-
include_return_message_types=request.include_return_message_types,
|
1318
|
-
),
|
1333
|
+
request_config=LettaRequestConfig.from_letta_request(request),
|
1319
1334
|
),
|
1320
1335
|
actor=actor,
|
1321
1336
|
)
|
1322
|
-
|
1337
|
+
run_update_metadata = None
|
1323
1338
|
await redis_client.set(f"{REDIS_RUN_ID_PREFIX}:{agent_id}", run.id if run else None)
|
1324
1339
|
else:
|
1325
1340
|
run = None
|
@@ -1345,6 +1360,16 @@ async def send_message_streaming(
|
|
1345
1360
|
async for chunk in stream:
|
1346
1361
|
yield chunk
|
1347
1362
|
|
1363
|
+
if run:
|
1364
|
+
runs_manager = RunManager()
|
1365
|
+
from letta.schemas.enums import RunStatus
|
1366
|
+
|
1367
|
+
await runs_manager.update_run_by_id_async(
|
1368
|
+
run_id=run.id,
|
1369
|
+
update=RunUpdate(status=RunStatus.completed, stop_reason=agent_loop.stop_reason.stop_reason.value),
|
1370
|
+
actor=actor,
|
1371
|
+
)
|
1372
|
+
|
1348
1373
|
except LLMTimeoutError as e:
|
1349
1374
|
error_data = {
|
1350
1375
|
"error": {"type": "llm_timeout", "message": "The LLM request timed out. Please try again.", "detail": str(e)}
|
@@ -1395,7 +1420,7 @@ async def send_message_streaming(
|
|
1395
1420
|
stream_generator=raw_stream,
|
1396
1421
|
redis_client=redis_client,
|
1397
1422
|
run_id=run.id,
|
1398
|
-
|
1423
|
+
run_manager=server.run_manager,
|
1399
1424
|
actor=actor,
|
1400
1425
|
),
|
1401
1426
|
label=f"background_stream_processor_{run.id}",
|
@@ -1431,24 +1456,24 @@ async def send_message_streaming(
|
|
1431
1456
|
include_return_message_types=request.include_return_message_types,
|
1432
1457
|
)
|
1433
1458
|
if settings.track_agent_run:
|
1434
|
-
|
1459
|
+
run_status = RunStatus.running
|
1435
1460
|
return result
|
1436
1461
|
except PendingApprovalError as e:
|
1437
1462
|
if settings.track_agent_run:
|
1438
|
-
|
1439
|
-
|
1463
|
+
run_update_metadata = {"error": str(e)}
|
1464
|
+
run_status = RunStatus.failed
|
1440
1465
|
raise HTTPException(
|
1441
1466
|
status_code=409, detail={"code": "PENDING_APPROVAL", "message": str(e), "pending_request_id": e.pending_request_id}
|
1442
1467
|
)
|
1443
1468
|
except Exception as e:
|
1444
1469
|
if settings.track_agent_run:
|
1445
|
-
|
1446
|
-
|
1470
|
+
run_update_metadata = {"error": str(e)}
|
1471
|
+
run_status = RunStatus.failed
|
1447
1472
|
raise
|
1448
1473
|
finally:
|
1449
1474
|
if settings.track_agent_run:
|
1450
|
-
await server.
|
1451
|
-
|
1475
|
+
await server.run_manager.update_run_by_id_async(
|
1476
|
+
run_id=run.id, update=RunUpdate(status=run_status, metadata=run_update_metadata), actor=actor
|
1452
1477
|
)
|
1453
1478
|
|
1454
1479
|
|
@@ -1477,21 +1502,25 @@ async def cancel_agent_run(
|
|
1477
1502
|
run_id = await redis_client.get(f"{REDIS_RUN_ID_PREFIX}:{agent_id}")
|
1478
1503
|
if run_id is None:
|
1479
1504
|
logger.warning("Cannot find run associated with agent to cancel in redis, fetching from db.")
|
1480
|
-
|
1505
|
+
run_ids = await server.run_manager.list_runs(
|
1481
1506
|
actor=actor,
|
1482
|
-
statuses=[
|
1483
|
-
job_type=JobType.RUN,
|
1507
|
+
statuses=[RunStatus.created, RunStatus.running],
|
1484
1508
|
ascending=False,
|
1509
|
+
agent_id=agent_id, # NOTE: this will override agent_ids if provided
|
1485
1510
|
)
|
1486
|
-
run_ids = [
|
1511
|
+
run_ids = [run.id for run in run_ids]
|
1487
1512
|
else:
|
1488
1513
|
run_ids = [run_id]
|
1489
1514
|
|
1490
1515
|
results = {}
|
1491
1516
|
for run_id in run_ids:
|
1492
|
-
|
1493
|
-
|
1494
|
-
|
1517
|
+
run = await server.run_manager.get_run_by_id(run_id=run_id, actor=actor)
|
1518
|
+
if run.metadata.get("lettuce"):
|
1519
|
+
lettuce_client = await LettuceClient.create()
|
1520
|
+
await lettuce_client.cancel(run_id)
|
1521
|
+
success = await server.run_manager.update_run_by_id_async(
|
1522
|
+
run_id=run_id,
|
1523
|
+
update=RunUpdate(status=RunStatus.cancelled),
|
1495
1524
|
actor=actor,
|
1496
1525
|
)
|
1497
1526
|
results[run_id] = "cancelled" if success else "failed"
|
@@ -1517,21 +1546,18 @@ async def search_messages(
|
|
1517
1546
|
if agent_count == 0:
|
1518
1547
|
raise HTTPException(status_code=400, detail="No agents found in organization to derive embedding configuration from")
|
1519
1548
|
|
1520
|
-
|
1521
|
-
|
1522
|
-
|
1523
|
-
|
1524
|
-
|
1525
|
-
|
1526
|
-
|
1527
|
-
|
1528
|
-
|
1529
|
-
|
1530
|
-
|
1531
|
-
|
1532
|
-
return results
|
1533
|
-
except ValueError as e:
|
1534
|
-
raise HTTPException(status_code=400, detail=str(e))
|
1549
|
+
results = await server.message_manager.search_messages_org_async(
|
1550
|
+
actor=actor,
|
1551
|
+
query_text=request.query,
|
1552
|
+
search_mode=request.search_mode,
|
1553
|
+
roles=request.roles,
|
1554
|
+
project_id=request.project_id,
|
1555
|
+
template_id=request.template_id,
|
1556
|
+
limit=request.limit,
|
1557
|
+
start_date=request.start_date,
|
1558
|
+
end_date=request.end_date,
|
1559
|
+
)
|
1560
|
+
return results
|
1535
1561
|
|
1536
1562
|
|
1537
1563
|
async def _process_message_background(
|
@@ -1546,7 +1572,7 @@ async def _process_message_background(
|
|
1546
1572
|
max_steps: int = DEFAULT_MAX_STEPS,
|
1547
1573
|
include_return_message_types: list[MessageType] | None = None,
|
1548
1574
|
) -> None:
|
1549
|
-
"""Background task to process the message and update
|
1575
|
+
"""Background task to process the message and update run status."""
|
1550
1576
|
request_start_timestamp_ns = get_utc_timestamp_ns()
|
1551
1577
|
try:
|
1552
1578
|
agent = await server.agent_manager.get_agent_by_id_async(
|
@@ -1583,7 +1609,7 @@ async def _process_message_background(
|
|
1583
1609
|
input_messages=messages,
|
1584
1610
|
stream_steps=False,
|
1585
1611
|
stream_tokens=False,
|
1586
|
-
metadata={"
|
1612
|
+
metadata={"run_id": run_id},
|
1587
1613
|
# Support for AssistantMessage
|
1588
1614
|
use_assistant_message=use_assistant_message,
|
1589
1615
|
assistant_message_tool_name=assistant_message_tool_name,
|
@@ -1591,34 +1617,40 @@ async def _process_message_background(
|
|
1591
1617
|
include_return_message_types=include_return_message_types,
|
1592
1618
|
)
|
1593
1619
|
|
1594
|
-
|
1595
|
-
|
1596
|
-
|
1597
|
-
|
1620
|
+
runs_manager = RunManager()
|
1621
|
+
from letta.schemas.enums import RunStatus
|
1622
|
+
|
1623
|
+
await runs_manager.update_run_by_id_async(
|
1624
|
+
run_id=run_id,
|
1625
|
+
update=RunUpdate(status=RunStatus.completed, stop_reason=result.stop_reason.stop_reason),
|
1626
|
+
actor=actor,
|
1598
1627
|
)
|
1599
|
-
await server.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=actor)
|
1600
1628
|
|
1601
1629
|
except PendingApprovalError as e:
|
1602
|
-
# Update
|
1603
|
-
|
1604
|
-
|
1605
|
-
|
1606
|
-
|
1630
|
+
# Update run status to failed with specific error info
|
1631
|
+
runs_manager = RunManager()
|
1632
|
+
from letta.schemas.enums import RunStatus
|
1633
|
+
|
1634
|
+
await runs_manager.update_run_by_id_async(
|
1635
|
+
run_id=run_id,
|
1636
|
+
update=RunUpdate(status=RunStatus.failed),
|
1637
|
+
actor=actor,
|
1607
1638
|
)
|
1608
|
-
await server.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=actor)
|
1609
1639
|
except Exception as e:
|
1610
|
-
# Update
|
1611
|
-
|
1612
|
-
|
1613
|
-
|
1614
|
-
|
1640
|
+
# Update run status to failed
|
1641
|
+
runs_manager = RunManager()
|
1642
|
+
from letta.schemas.enums import RunStatus
|
1643
|
+
|
1644
|
+
await runs_manager.update_run_by_id_async(
|
1645
|
+
run_id=run_id,
|
1646
|
+
update=RunUpdate(status=RunStatus.failed),
|
1647
|
+
actor=actor,
|
1615
1648
|
)
|
1616
|
-
await server.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=actor)
|
1617
1649
|
|
1618
1650
|
|
1619
1651
|
@router.post(
|
1620
1652
|
"/{agent_id}/messages/async",
|
1621
|
-
response_model=
|
1653
|
+
response_model=PydanticRun,
|
1622
1654
|
operation_id="create_agent_message_async",
|
1623
1655
|
)
|
1624
1656
|
async def send_message_async(
|
@@ -1631,29 +1663,44 @@ async def send_message_async(
|
|
1631
1663
|
Asynchronously process a user message and return a run object.
|
1632
1664
|
The actual processing happens in the background, and the status can be checked using the run ID.
|
1633
1665
|
|
1634
|
-
This is "asynchronous" in the sense that it's a background
|
1635
|
-
This is more like `send_message_job`
|
1666
|
+
This is "asynchronous" in the sense that it's a background run and explicitly must be fetched by the run ID.
|
1636
1667
|
"""
|
1637
1668
|
MetricRegistry().user_message_counter.add(1, get_ctx_attributes())
|
1638
1669
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
1639
|
-
|
1640
|
-
|
1641
|
-
run =
|
1642
|
-
user_id=actor.id,
|
1643
|
-
status=JobStatus.created,
|
1670
|
+
# Create a new run
|
1671
|
+
use_lettuce = headers.experimental_params.message_async
|
1672
|
+
run = PydanticRun(
|
1644
1673
|
callback_url=request.callback_url,
|
1674
|
+
agent_id=agent_id,
|
1675
|
+
background=True, # Async endpoints are always background
|
1645
1676
|
metadata={
|
1646
|
-
"
|
1647
|
-
"
|
1677
|
+
"run_type": "send_message_async",
|
1678
|
+
"lettuce": use_lettuce,
|
1648
1679
|
},
|
1649
|
-
request_config=LettaRequestConfig(
|
1650
|
-
use_assistant_message=request.use_assistant_message,
|
1651
|
-
assistant_message_tool_name=request.assistant_message_tool_name,
|
1652
|
-
assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
|
1653
|
-
include_return_message_types=request.include_return_message_types,
|
1654
|
-
),
|
1680
|
+
request_config=LettaRequestConfig.from_letta_request(request),
|
1655
1681
|
)
|
1656
|
-
run = await server.
|
1682
|
+
run = await server.run_manager.create_run(
|
1683
|
+
pydantic_run=run,
|
1684
|
+
actor=actor,
|
1685
|
+
)
|
1686
|
+
|
1687
|
+
if use_lettuce:
|
1688
|
+
agent_state = await server.agent_manager.get_agent_by_id_async(
|
1689
|
+
agent_id, actor, include_relationships=["memory", "multi_agent_group", "sources", "tool_exec_environment_variables", "tools"]
|
1690
|
+
)
|
1691
|
+
if agent_state.multi_agent_group is None and agent_state.agent_type != AgentType.letta_v1_agent:
|
1692
|
+
lettuce_client = await LettuceClient.create()
|
1693
|
+
run_id_from_lettuce = await lettuce_client.step(
|
1694
|
+
agent_state=agent_state,
|
1695
|
+
actor=actor,
|
1696
|
+
input_messages=request.messages,
|
1697
|
+
max_steps=request.max_steps,
|
1698
|
+
run_id=run.id,
|
1699
|
+
use_assistant_message=request.use_assistant_message,
|
1700
|
+
include_return_message_types=request.include_return_message_types,
|
1701
|
+
)
|
1702
|
+
if run_id_from_lettuce:
|
1703
|
+
return run
|
1657
1704
|
|
1658
1705
|
# Create asyncio task for background processing (shielded to prevent cancellation)
|
1659
1706
|
task = safe_create_shielded_task(
|
@@ -1681,17 +1728,21 @@ async def send_message_async(
|
|
1681
1728
|
# Don't mark as failed since the shielded task is still running
|
1682
1729
|
except Exception as e:
|
1683
1730
|
logger.error(f"Unhandled exception in background task for run {run.id}: {e}")
|
1684
|
-
|
1685
|
-
|
1686
|
-
|
1687
|
-
|
1688
|
-
|
1689
|
-
|
1690
|
-
|
1691
|
-
|
1731
|
+
from letta.services.run_manager import RunManager
|
1732
|
+
|
1733
|
+
async def update_failed_run():
|
1734
|
+
runs_manager = RunManager()
|
1735
|
+
from letta.schemas.enums import RunStatus
|
1736
|
+
|
1737
|
+
await runs_manager.update_run_by_id_async(
|
1738
|
+
run_id=run.id,
|
1739
|
+
update=RunUpdate(status=RunStatus.failed),
|
1692
1740
|
actor=actor,
|
1693
|
-
)
|
1694
|
-
|
1741
|
+
)
|
1742
|
+
|
1743
|
+
safe_create_task(
|
1744
|
+
update_failed_run(),
|
1745
|
+
label=f"update_failed_run_{run.id}",
|
1695
1746
|
)
|
1696
1747
|
|
1697
1748
|
task.add_done_callback(handle_task_completion)
|
@@ -1719,11 +1770,30 @@ async def list_agent_groups(
|
|
1719
1770
|
manager_type: str | None = Query(None, description="Manager type to filter groups by"),
|
1720
1771
|
server: "SyncServer" = Depends(get_letta_server),
|
1721
1772
|
headers: HeaderParams = Depends(get_headers),
|
1773
|
+
before: Optional[str] = Query(
|
1774
|
+
None, description="Group ID cursor for pagination. Returns groups that come before this group ID in the specified sort order"
|
1775
|
+
),
|
1776
|
+
after: Optional[str] = Query(
|
1777
|
+
None, description="Group ID cursor for pagination. Returns groups that come after this group ID in the specified sort order"
|
1778
|
+
),
|
1779
|
+
limit: Optional[int] = Query(100, description="Maximum number of groups to return"),
|
1780
|
+
order: Literal["asc", "desc"] = Query(
|
1781
|
+
"desc", description="Sort order for groups by creation time. 'asc' for oldest first, 'desc' for newest first"
|
1782
|
+
),
|
1783
|
+
order_by: Literal["created_at"] = Query("created_at", description="Field to sort by"),
|
1722
1784
|
):
|
1723
1785
|
"""Lists the groups for an agent"""
|
1724
1786
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
1725
1787
|
logger.info("in list agents with manager_type", manager_type)
|
1726
|
-
return server.agent_manager.
|
1788
|
+
return await server.agent_manager.list_groups_async(
|
1789
|
+
agent_id=agent_id,
|
1790
|
+
manager_type=manager_type,
|
1791
|
+
actor=actor,
|
1792
|
+
before=before,
|
1793
|
+
after=after,
|
1794
|
+
limit=limit,
|
1795
|
+
ascending=(order == "asc"),
|
1796
|
+
)
|
1727
1797
|
|
1728
1798
|
|
1729
1799
|
@router.post(
|
@@ -1745,7 +1815,9 @@ async def preview_raw_payload(
|
|
1745
1815
|
be sent to the LLM provider. Useful for debugging and inspection.
|
1746
1816
|
"""
|
1747
1817
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
1748
|
-
agent = await server.agent_manager.get_agent_by_id_async(
|
1818
|
+
agent = await server.agent_manager.get_agent_by_id_async(
|
1819
|
+
agent_id, actor, include_relationships=["multi_agent_group", "memory", "sources"]
|
1820
|
+
)
|
1749
1821
|
agent_eligible = agent.multi_agent_group is None or agent.multi_agent_group.manager_type in ["sleeptime", "voice_sleeptime"]
|
1750
1822
|
model_compatible = agent.llm_config.model_endpoint_type in [
|
1751
1823
|
"anthropic",
|