letta-nightly 0.11.7.dev20250908104137__py3-none-any.whl → 0.11.7.dev20250910104051__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 +81 -0
- letta/adapters/letta_llm_request_adapter.py +111 -0
- letta/adapters/letta_llm_stream_adapter.py +169 -0
- letta/agents/base_agent.py +4 -1
- letta/agents/base_agent_v2.py +68 -0
- letta/agents/helpers.py +3 -5
- letta/agents/letta_agent.py +23 -12
- letta/agents/letta_agent_v2.py +1220 -0
- letta/agents/voice_agent.py +2 -1
- letta/constants.py +1 -1
- letta/errors.py +12 -0
- letta/functions/function_sets/base.py +53 -12
- letta/functions/schema_generator.py +1 -1
- letta/groups/sleeptime_multi_agent_v3.py +231 -0
- letta/helpers/tool_rule_solver.py +4 -0
- letta/helpers/tpuf_client.py +607 -34
- letta/interfaces/anthropic_streaming_interface.py +64 -24
- letta/interfaces/openai_streaming_interface.py +80 -37
- letta/llm_api/openai_client.py +45 -4
- letta/orm/block.py +1 -0
- letta/orm/group.py +1 -0
- letta/orm/source.py +8 -1
- letta/orm/step_metrics.py +10 -0
- letta/schemas/block.py +4 -0
- letta/schemas/enums.py +1 -0
- letta/schemas/group.py +8 -0
- letta/schemas/letta_message.py +1 -1
- letta/schemas/letta_request.py +2 -2
- letta/schemas/mcp.py +9 -1
- letta/schemas/message.py +23 -0
- letta/schemas/providers/ollama.py +1 -1
- letta/schemas/providers.py +1 -2
- letta/schemas/source.py +6 -0
- letta/schemas/step_metrics.py +2 -0
- letta/server/rest_api/routers/v1/__init__.py +2 -0
- letta/server/rest_api/routers/v1/agents.py +100 -5
- letta/server/rest_api/routers/v1/blocks.py +6 -0
- letta/server/rest_api/routers/v1/folders.py +23 -5
- letta/server/rest_api/routers/v1/groups.py +6 -0
- letta/server/rest_api/routers/v1/internal_templates.py +218 -12
- letta/server/rest_api/routers/v1/messages.py +14 -19
- letta/server/rest_api/routers/v1/runs.py +43 -28
- letta/server/rest_api/routers/v1/sources.py +23 -5
- letta/server/rest_api/routers/v1/tools.py +42 -0
- letta/server/rest_api/streaming_response.py +9 -1
- letta/server/server.py +2 -1
- letta/services/agent_manager.py +39 -59
- letta/services/agent_serialization_manager.py +22 -8
- letta/services/archive_manager.py +60 -9
- letta/services/block_manager.py +5 -0
- letta/services/file_processor/embedder/base_embedder.py +5 -0
- letta/services/file_processor/embedder/openai_embedder.py +4 -0
- letta/services/file_processor/embedder/pinecone_embedder.py +5 -1
- letta/services/file_processor/embedder/turbopuffer_embedder.py +71 -0
- letta/services/file_processor/file_processor.py +9 -7
- letta/services/group_manager.py +74 -11
- letta/services/mcp_manager.py +132 -26
- letta/services/message_manager.py +229 -125
- letta/services/passage_manager.py +2 -1
- letta/services/source_manager.py +23 -1
- letta/services/summarizer/summarizer.py +2 -0
- letta/services/tool_executor/core_tool_executor.py +2 -120
- letta/services/tool_executor/files_tool_executor.py +133 -8
- letta/settings.py +6 -0
- letta/utils.py +34 -1
- {letta_nightly-0.11.7.dev20250908104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/METADATA +2 -2
- {letta_nightly-0.11.7.dev20250908104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/RECORD +70 -63
- {letta_nightly-0.11.7.dev20250908104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20250908104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20250908104137.dist-info → letta_nightly-0.11.7.dev20250910104051.dist-info}/licenses/LICENSE +0 -0
@@ -15,7 +15,13 @@ from starlette.responses import Response, StreamingResponse
|
|
15
15
|
from letta.agents.letta_agent import LettaAgent
|
16
16
|
from letta.constants import AGENT_ID_PATTERN, DEFAULT_MAX_STEPS, DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, REDIS_RUN_ID_PREFIX
|
17
17
|
from letta.data_sources.redis_client import NoopAsyncRedisClient, get_redis_client
|
18
|
-
from letta.errors import
|
18
|
+
from letta.errors import (
|
19
|
+
AgentExportIdMappingError,
|
20
|
+
AgentExportProcessingError,
|
21
|
+
AgentFileImportError,
|
22
|
+
AgentNotFoundForExportError,
|
23
|
+
PendingApprovalError,
|
24
|
+
)
|
19
25
|
from letta.groups.sleeptime_multi_agent_v2 import SleeptimeMultiAgentV2
|
20
26
|
from letta.helpers.datetime_helpers import get_utc_timestamp_ns
|
21
27
|
from letta.log import get_logger
|
@@ -39,7 +45,7 @@ from letta.schemas.memory import (
|
|
39
45
|
CreateArchivalMemory,
|
40
46
|
Memory,
|
41
47
|
)
|
42
|
-
from letta.schemas.message import MessageCreate
|
48
|
+
from letta.schemas.message import MessageCreate, MessageSearchRequest, MessageSearchResult
|
43
49
|
from letta.schemas.passage import Passage
|
44
50
|
from letta.schemas.run import Run
|
45
51
|
from letta.schemas.source import Source
|
@@ -1013,7 +1019,7 @@ async def search_archival_memory(
|
|
1013
1019
|
end_datetime = end_datetime.isoformat() if end_datetime else None
|
1014
1020
|
|
1015
1021
|
# Use the shared agent manager method
|
1016
|
-
formatted_results
|
1022
|
+
formatted_results = await server.agent_manager.search_agent_archival_memory_async(
|
1017
1023
|
agent_id=agent_id,
|
1018
1024
|
actor=actor,
|
1019
1025
|
query=query,
|
@@ -1027,7 +1033,7 @@ async def search_archival_memory(
|
|
1027
1033
|
# Convert to proper response schema
|
1028
1034
|
search_results = [ArchivalMemorySearchResult(**result) for result in formatted_results]
|
1029
1035
|
|
1030
|
-
return ArchivalMemorySearchResponse(results=search_results, count=
|
1036
|
+
return ArchivalMemorySearchResponse(results=search_results, count=len(formatted_results))
|
1031
1037
|
|
1032
1038
|
except NoResultFound as e:
|
1033
1039
|
raise HTTPException(status_code=404, detail=f"Agent with id={agent_id} not found for user_id={actor.id}.")
|
@@ -1239,6 +1245,12 @@ async def send_message(
|
|
1239
1245
|
)
|
1240
1246
|
job_status = result.stop_reason.stop_reason.run_status
|
1241
1247
|
return result
|
1248
|
+
except PendingApprovalError as e:
|
1249
|
+
job_update_metadata = {"error": str(e)}
|
1250
|
+
job_status = JobStatus.failed
|
1251
|
+
raise HTTPException(
|
1252
|
+
status_code=409, detail={"code": "PENDING_APPROVAL", "message": str(e), "pending_request_id": e.pending_request_id}
|
1253
|
+
)
|
1242
1254
|
except Exception as e:
|
1243
1255
|
job_update_metadata = {"error": str(e)}
|
1244
1256
|
job_status = JobStatus.failed
|
@@ -1437,6 +1449,13 @@ async def send_message_streaming(
|
|
1437
1449
|
if settings.track_agent_run:
|
1438
1450
|
job_status = JobStatus.running
|
1439
1451
|
return result
|
1452
|
+
except PendingApprovalError as e:
|
1453
|
+
if settings.track_agent_run:
|
1454
|
+
job_update_metadata = {"error": str(e)}
|
1455
|
+
job_status = JobStatus.failed
|
1456
|
+
raise HTTPException(
|
1457
|
+
status_code=409, detail={"code": "PENDING_APPROVAL", "message": str(e), "pending_request_id": e.pending_request_id}
|
1458
|
+
)
|
1440
1459
|
except Exception as e:
|
1441
1460
|
if settings.track_agent_run:
|
1442
1461
|
job_update_metadata = {"error": str(e)}
|
@@ -1498,6 +1517,42 @@ async def cancel_agent_run(
|
|
1498
1517
|
return results
|
1499
1518
|
|
1500
1519
|
|
1520
|
+
@router.post("/messages/search", response_model=List[MessageSearchResult], operation_id="search_messages")
|
1521
|
+
async def search_messages(
|
1522
|
+
request: MessageSearchRequest = Body(...),
|
1523
|
+
server: SyncServer = Depends(get_letta_server),
|
1524
|
+
actor_id: str | None = Header(None, alias="user_id"),
|
1525
|
+
):
|
1526
|
+
"""
|
1527
|
+
Search messages across the entire organization with optional project and template filtering. Returns messages with FTS/vector ranks and total RRF score.
|
1528
|
+
|
1529
|
+
This is a cloud-only feature.
|
1530
|
+
"""
|
1531
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
1532
|
+
|
1533
|
+
# get embedding config from the default agent if needed
|
1534
|
+
# check if any agents exist in the org
|
1535
|
+
agent_count = await server.agent_manager.size_async(actor=actor)
|
1536
|
+
if agent_count == 0:
|
1537
|
+
raise HTTPException(status_code=400, detail="No agents found in organization to derive embedding configuration from")
|
1538
|
+
|
1539
|
+
try:
|
1540
|
+
results = await server.message_manager.search_messages_org_async(
|
1541
|
+
actor=actor,
|
1542
|
+
query_text=request.query,
|
1543
|
+
search_mode=request.search_mode,
|
1544
|
+
roles=request.roles,
|
1545
|
+
project_id=request.project_id,
|
1546
|
+
template_id=request.template_id,
|
1547
|
+
limit=request.limit,
|
1548
|
+
start_date=request.start_date,
|
1549
|
+
end_date=request.end_date,
|
1550
|
+
)
|
1551
|
+
return results
|
1552
|
+
except ValueError as e:
|
1553
|
+
raise HTTPException(status_code=400, detail=str(e))
|
1554
|
+
|
1555
|
+
|
1501
1556
|
async def _process_message_background(
|
1502
1557
|
run_id: str,
|
1503
1558
|
server: SyncServer,
|
@@ -1590,6 +1645,14 @@ async def _process_message_background(
|
|
1590
1645
|
)
|
1591
1646
|
await server.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=actor)
|
1592
1647
|
|
1648
|
+
except PendingApprovalError as e:
|
1649
|
+
# Update job status to failed with specific error info
|
1650
|
+
job_update = JobUpdate(
|
1651
|
+
status=JobStatus.failed,
|
1652
|
+
completed_at=datetime.now(timezone.utc),
|
1653
|
+
metadata={"error": str(e), "error_code": "PENDING_APPROVAL", "pending_request_id": e.pending_request_id},
|
1654
|
+
)
|
1655
|
+
await server.job_manager.update_job_by_id_async(job_id=run_id, job_update=job_update, actor=actor)
|
1593
1656
|
except Exception as e:
|
1594
1657
|
# Update job status to failed
|
1595
1658
|
job_update = JobUpdate(
|
@@ -1640,7 +1703,7 @@ async def send_message_async(
|
|
1640
1703
|
run = await server.job_manager.create_job_async(pydantic_job=run, actor=actor)
|
1641
1704
|
|
1642
1705
|
# Create asyncio task for background processing
|
1643
|
-
asyncio.create_task(
|
1706
|
+
task = asyncio.create_task(
|
1644
1707
|
_process_message_background(
|
1645
1708
|
run_id=run.id,
|
1646
1709
|
server=server,
|
@@ -1655,6 +1718,38 @@ async def send_message_async(
|
|
1655
1718
|
)
|
1656
1719
|
)
|
1657
1720
|
|
1721
|
+
def handle_task_completion(t):
|
1722
|
+
try:
|
1723
|
+
t.result()
|
1724
|
+
except asyncio.CancelledError:
|
1725
|
+
logger.error(f"Background task for run {run.id} was cancelled")
|
1726
|
+
asyncio.create_task(
|
1727
|
+
server.job_manager.update_job_by_id_async(
|
1728
|
+
job_id=run.id,
|
1729
|
+
job_update=JobUpdate(
|
1730
|
+
status=JobStatus.failed,
|
1731
|
+
completed_at=datetime.now(timezone.utc),
|
1732
|
+
metadata={"error": "Task was cancelled"},
|
1733
|
+
),
|
1734
|
+
actor=actor,
|
1735
|
+
)
|
1736
|
+
)
|
1737
|
+
except Exception as e:
|
1738
|
+
logger.error(f"Unhandled exception in background task for run {run.id}: {e}")
|
1739
|
+
asyncio.create_task(
|
1740
|
+
server.job_manager.update_job_by_id_async(
|
1741
|
+
job_id=run.id,
|
1742
|
+
job_update=JobUpdate(
|
1743
|
+
status=JobStatus.failed,
|
1744
|
+
completed_at=datetime.now(timezone.utc),
|
1745
|
+
metadata={"error": str(e)},
|
1746
|
+
),
|
1747
|
+
actor=actor,
|
1748
|
+
)
|
1749
|
+
)
|
1750
|
+
|
1751
|
+
task.add_done_callback(handle_task_completion)
|
1752
|
+
|
1658
1753
|
return run
|
1659
1754
|
|
1660
1755
|
|
@@ -68,6 +68,11 @@ async def list_blocks(
|
|
68
68
|
"If provided, returns blocks that have exactly this number of connected agents."
|
69
69
|
),
|
70
70
|
),
|
71
|
+
show_hidden_blocks: bool | None = Query(
|
72
|
+
False,
|
73
|
+
include_in_schema=False,
|
74
|
+
description="If set to True, include blocks marked as hidden in the results.",
|
75
|
+
),
|
71
76
|
server: SyncServer = Depends(get_letta_server),
|
72
77
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
73
78
|
):
|
@@ -89,6 +94,7 @@ async def list_blocks(
|
|
89
94
|
connected_to_agents_count_eq=connected_to_agents_count_eq,
|
90
95
|
limit=limit,
|
91
96
|
after=after,
|
97
|
+
show_hidden_blocks=show_hidden_blocks,
|
92
98
|
)
|
93
99
|
|
94
100
|
|
@@ -15,6 +15,7 @@ from letta.helpers.pinecone_utils import (
|
|
15
15
|
delete_source_records_from_pinecone_index,
|
16
16
|
should_use_pinecone,
|
17
17
|
)
|
18
|
+
from letta.helpers.tpuf_client import should_use_tpuf
|
18
19
|
from letta.log import get_logger
|
19
20
|
from letta.otel.tracing import trace_method
|
20
21
|
from letta.schemas.agent import AgentState
|
@@ -191,7 +192,13 @@ async def delete_folder(
|
|
191
192
|
files = await server.file_manager.list_files(folder_id, actor)
|
192
193
|
file_ids = [f.id for f in files]
|
193
194
|
|
194
|
-
if
|
195
|
+
if should_use_tpuf():
|
196
|
+
logger.info(f"Deleting folder {folder_id} from Turbopuffer")
|
197
|
+
from letta.helpers.tpuf_client import TurbopufferClient
|
198
|
+
|
199
|
+
tpuf_client = TurbopufferClient()
|
200
|
+
await tpuf_client.delete_source_passages(source_id=folder_id, organization_id=actor.organization_id)
|
201
|
+
elif should_use_pinecone():
|
195
202
|
logger.info(f"Deleting folder {folder_id} from pinecone index")
|
196
203
|
await delete_source_records_from_pinecone_index(source_id=folder_id, actor=actor)
|
197
204
|
|
@@ -450,7 +457,13 @@ async def delete_file_from_folder(
|
|
450
457
|
|
451
458
|
await server.remove_file_from_context_windows(source_id=folder_id, file_id=deleted_file.id, actor=actor)
|
452
459
|
|
453
|
-
if
|
460
|
+
if should_use_tpuf():
|
461
|
+
logger.info(f"Deleting file {file_id} from Turbopuffer")
|
462
|
+
from letta.helpers.tpuf_client import TurbopufferClient
|
463
|
+
|
464
|
+
tpuf_client = TurbopufferClient()
|
465
|
+
await tpuf_client.delete_file_passages(source_id=folder_id, file_id=file_id, organization_id=actor.organization_id)
|
466
|
+
elif should_use_pinecone():
|
454
467
|
logger.info(f"Deleting file {file_id} from pinecone index")
|
455
468
|
await delete_file_records_from_pinecone_index(file_id=file_id, actor=actor)
|
456
469
|
|
@@ -496,10 +509,15 @@ async def load_file_to_source_cloud(
|
|
496
509
|
else:
|
497
510
|
file_parser = MarkitdownFileParser()
|
498
511
|
|
499
|
-
|
500
|
-
if
|
512
|
+
# determine which embedder to use - turbopuffer takes precedence
|
513
|
+
if should_use_tpuf():
|
514
|
+
from letta.services.file_processor.embedder.turbopuffer_embedder import TurbopufferEmbedder
|
515
|
+
|
516
|
+
embedder = TurbopufferEmbedder(embedding_config=embedding_config)
|
517
|
+
elif should_use_pinecone():
|
501
518
|
embedder = PineconeEmbedder(embedding_config=embedding_config)
|
502
519
|
else:
|
503
520
|
embedder = OpenAIEmbedder(embedding_config=embedding_config)
|
504
|
-
|
521
|
+
|
522
|
+
file_processor = FileProcessor(file_parser=file_parser, embedder=embedder, actor=actor)
|
505
523
|
await file_processor.process(agent_states=agent_states, source_id=source_id, content=content, file_metadata=file_metadata)
|
@@ -25,6 +25,11 @@ async def list_groups(
|
|
25
25
|
after: Optional[str] = Query(None, description="Cursor for pagination"),
|
26
26
|
limit: Optional[int] = Query(None, description="Limit for pagination"),
|
27
27
|
project_id: Optional[str] = Query(None, description="Search groups by project id"),
|
28
|
+
show_hidden_groups: bool | None = Query(
|
29
|
+
False,
|
30
|
+
include_in_schema=False,
|
31
|
+
description="If set to True, include groups marked as hidden in the results.",
|
32
|
+
),
|
28
33
|
):
|
29
34
|
"""
|
30
35
|
Fetch all multi-agent groups matching query.
|
@@ -37,6 +42,7 @@ async def list_groups(
|
|
37
42
|
before=before,
|
38
43
|
after=after,
|
39
44
|
limit=limit,
|
45
|
+
show_hidden_groups=show_hidden_groups,
|
40
46
|
)
|
41
47
|
|
42
48
|
|
@@ -1,6 +1,7 @@
|
|
1
|
-
from typing import Optional
|
1
|
+
from typing import List, Optional
|
2
2
|
|
3
|
-
from fastapi import APIRouter, Body, Depends, Header, HTTPException
|
3
|
+
from fastapi import APIRouter, Body, Depends, Header, HTTPException, Query
|
4
|
+
from pydantic import BaseModel
|
4
5
|
|
5
6
|
from letta.schemas.agent import AgentState, InternalTemplateAgentCreate
|
6
7
|
from letta.schemas.block import Block, InternalTemplateBlockCreate
|
@@ -16,9 +17,6 @@ async def create_group(
|
|
16
17
|
group: InternalTemplateGroupCreate = Body(...),
|
17
18
|
server: "SyncServer" = Depends(get_letta_server),
|
18
19
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
19
|
-
x_project: Optional[str] = Header(
|
20
|
-
None, alias="X-Project", description="The project slug to associate with the group (cloud only)."
|
21
|
-
), # Only handled by next js middleware
|
22
20
|
):
|
23
21
|
"""
|
24
22
|
Create a new multi-agent group with the specified configuration.
|
@@ -35,9 +33,6 @@ async def create_agent(
|
|
35
33
|
agent: InternalTemplateAgentCreate = Body(...),
|
36
34
|
server: "SyncServer" = Depends(get_letta_server),
|
37
35
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
38
|
-
x_project: Optional[str] = Header(
|
39
|
-
None, alias="X-Project", description="The project slug to associate with the agent (cloud only)."
|
40
|
-
), # Only handled by next js middleware
|
41
36
|
):
|
42
37
|
"""
|
43
38
|
Create a new agent with template-related fields.
|
@@ -54,15 +49,226 @@ async def create_block(
|
|
54
49
|
block: InternalTemplateBlockCreate = Body(...),
|
55
50
|
server: "SyncServer" = Depends(get_letta_server),
|
56
51
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
57
|
-
x_project: Optional[str] = Header(
|
58
|
-
None, alias="X-Project", description="The project slug to associate with the block (cloud only)."
|
59
|
-
), # Only handled by next js middleware
|
60
52
|
):
|
61
53
|
"""
|
62
54
|
Create a new block with template-related fields.
|
63
55
|
"""
|
64
56
|
try:
|
65
57
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
66
|
-
|
58
|
+
block_obj = Block(**block.model_dump())
|
59
|
+
return await server.block_manager.create_or_update_block_async(block_obj, actor=actor)
|
60
|
+
except Exception as e:
|
61
|
+
raise HTTPException(status_code=500, detail=str(e))
|
62
|
+
|
63
|
+
|
64
|
+
class DeploymentEntity(BaseModel):
|
65
|
+
"""A deployment entity."""
|
66
|
+
|
67
|
+
id: str
|
68
|
+
type: str
|
69
|
+
name: Optional[str] = None
|
70
|
+
description: Optional[str] = None
|
71
|
+
|
72
|
+
|
73
|
+
class ListDeploymentEntitiesResponse(BaseModel):
|
74
|
+
"""Response model for listing deployment entities."""
|
75
|
+
|
76
|
+
entities: List[DeploymentEntity] = []
|
77
|
+
total_count: int
|
78
|
+
deployment_id: str
|
79
|
+
message: str
|
80
|
+
|
81
|
+
|
82
|
+
class DeleteDeploymentResponse(BaseModel):
|
83
|
+
"""Response model for delete deployment operation."""
|
84
|
+
|
85
|
+
deleted_blocks: List[str] = []
|
86
|
+
deleted_agents: List[str] = []
|
87
|
+
deleted_groups: List[str] = []
|
88
|
+
message: str
|
89
|
+
|
90
|
+
|
91
|
+
@router.get("/deployment/{deployment_id}", response_model=ListDeploymentEntitiesResponse, operation_id="list_deployment_entities")
|
92
|
+
async def list_deployment_entities(
|
93
|
+
deployment_id: str,
|
94
|
+
server: "SyncServer" = Depends(get_letta_server),
|
95
|
+
actor_id: Optional[str] = Header(None, alias="user_id"),
|
96
|
+
entity_types: Optional[List[str]] = Query(None, description="Filter by entity types (block, agent, group)"),
|
97
|
+
):
|
98
|
+
"""
|
99
|
+
List all entities (blocks, agents, groups) with the specified deployment_id.
|
100
|
+
Optionally filter by entity types.
|
101
|
+
"""
|
102
|
+
try:
|
103
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
104
|
+
|
105
|
+
entities = []
|
106
|
+
|
107
|
+
# Parse entity_types filter - support both array and comma-separated string
|
108
|
+
allowed_types = {"block", "agent", "group"}
|
109
|
+
if entity_types is None:
|
110
|
+
# If no filter specified, include all types
|
111
|
+
types_to_include = allowed_types
|
112
|
+
else:
|
113
|
+
# Handle comma-separated strings in a single item
|
114
|
+
if len(entity_types) == 1 and "," in entity_types[0]:
|
115
|
+
entity_types = [t.strip() for t in entity_types[0].split(",")]
|
116
|
+
|
117
|
+
# Validate and filter types
|
118
|
+
types_to_include = {t.lower() for t in entity_types if t.lower() in allowed_types}
|
119
|
+
if not types_to_include:
|
120
|
+
types_to_include = allowed_types # Default to all if invalid types provided
|
121
|
+
|
122
|
+
# Query blocks if requested
|
123
|
+
if "block" in types_to_include:
|
124
|
+
from sqlalchemy import select
|
125
|
+
|
126
|
+
from letta.orm.block import Block as BlockModel
|
127
|
+
from letta.server.db import db_registry
|
128
|
+
|
129
|
+
async with db_registry.async_session() as session:
|
130
|
+
block_query = select(BlockModel).where(
|
131
|
+
BlockModel.deployment_id == deployment_id, BlockModel.organization_id == actor.organization_id
|
132
|
+
)
|
133
|
+
result = await session.execute(block_query)
|
134
|
+
blocks = result.scalars().all()
|
135
|
+
|
136
|
+
for block in blocks:
|
137
|
+
entities.append(
|
138
|
+
DeploymentEntity(
|
139
|
+
id=block.id,
|
140
|
+
type="block",
|
141
|
+
name=getattr(block, "template_name", None) or getattr(block, "label", None),
|
142
|
+
description=block.description,
|
143
|
+
)
|
144
|
+
)
|
145
|
+
|
146
|
+
# Query agents if requested
|
147
|
+
if "agent" in types_to_include:
|
148
|
+
from letta.orm.agent import Agent as AgentModel
|
149
|
+
|
150
|
+
async with db_registry.async_session() as session:
|
151
|
+
agent_query = select(AgentModel).where(
|
152
|
+
AgentModel.deployment_id == deployment_id, AgentModel.organization_id == actor.organization_id
|
153
|
+
)
|
154
|
+
result = await session.execute(agent_query)
|
155
|
+
agents = result.scalars().all()
|
156
|
+
|
157
|
+
for agent in agents:
|
158
|
+
entities.append(DeploymentEntity(id=agent.id, type="agent", name=agent.name, description=agent.description))
|
159
|
+
|
160
|
+
# Query groups if requested
|
161
|
+
if "group" in types_to_include:
|
162
|
+
from letta.orm.group import Group as GroupModel
|
163
|
+
|
164
|
+
async with db_registry.async_session() as session:
|
165
|
+
group_query = select(GroupModel).where(
|
166
|
+
GroupModel.deployment_id == deployment_id, GroupModel.organization_id == actor.organization_id
|
167
|
+
)
|
168
|
+
result = await session.execute(group_query)
|
169
|
+
groups = result.scalars().all()
|
170
|
+
|
171
|
+
for group in groups:
|
172
|
+
entities.append(
|
173
|
+
DeploymentEntity(
|
174
|
+
id=group.id,
|
175
|
+
type="group",
|
176
|
+
name=None, # Groups don't have a name field
|
177
|
+
description=group.description,
|
178
|
+
)
|
179
|
+
)
|
180
|
+
|
181
|
+
message = f"Found {len(entities)} entities for deployment {deployment_id}"
|
182
|
+
if entity_types:
|
183
|
+
message += f" (filtered by types: {', '.join(types_to_include)})"
|
184
|
+
|
185
|
+
return ListDeploymentEntitiesResponse(entities=entities, total_count=len(entities), deployment_id=deployment_id, message=message)
|
186
|
+
except Exception as e:
|
187
|
+
raise HTTPException(status_code=500, detail=str(e))
|
188
|
+
|
189
|
+
|
190
|
+
@router.delete("/deployment/{deployment_id}", response_model=DeleteDeploymentResponse, operation_id="delete_deployment")
|
191
|
+
async def delete_deployment(
|
192
|
+
deployment_id: str,
|
193
|
+
server: "SyncServer" = Depends(get_letta_server),
|
194
|
+
actor_id: Optional[str] = Header(None, alias="user_id"),
|
195
|
+
):
|
196
|
+
"""
|
197
|
+
Delete all entities (blocks, agents, groups) with the specified deployment_id.
|
198
|
+
Deletion order: blocks -> agents -> groups to maintain referential integrity.
|
199
|
+
"""
|
200
|
+
try:
|
201
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
202
|
+
|
203
|
+
deleted_blocks = []
|
204
|
+
deleted_agents = []
|
205
|
+
deleted_groups = []
|
206
|
+
|
207
|
+
# First delete blocks
|
208
|
+
from sqlalchemy import select
|
209
|
+
|
210
|
+
from letta.orm.block import Block as BlockModel
|
211
|
+
from letta.server.db import db_registry
|
212
|
+
|
213
|
+
async with db_registry.async_session() as session:
|
214
|
+
# Get all blocks with the deployment_id
|
215
|
+
block_query = select(BlockModel).where(
|
216
|
+
BlockModel.deployment_id == deployment_id, BlockModel.organization_id == actor.organization_id
|
217
|
+
)
|
218
|
+
result = await session.execute(block_query)
|
219
|
+
blocks = result.scalars().all()
|
220
|
+
|
221
|
+
for block in blocks:
|
222
|
+
try:
|
223
|
+
await server.block_manager.delete_block_async(block.id, actor)
|
224
|
+
deleted_blocks.append(block.id)
|
225
|
+
except Exception as e:
|
226
|
+
# Continue deleting other blocks even if one fails
|
227
|
+
print(f"Failed to delete block {block.id}: {e}")
|
228
|
+
|
229
|
+
# Then delete agents
|
230
|
+
from letta.orm.agent import Agent as AgentModel
|
231
|
+
|
232
|
+
async with db_registry.async_session() as session:
|
233
|
+
# Get all agents with the deployment_id
|
234
|
+
agent_query = select(AgentModel).where(
|
235
|
+
AgentModel.deployment_id == deployment_id, AgentModel.organization_id == actor.organization_id
|
236
|
+
)
|
237
|
+
result = await session.execute(agent_query)
|
238
|
+
agents = result.scalars().all()
|
239
|
+
|
240
|
+
for agent in agents:
|
241
|
+
try:
|
242
|
+
await server.agent_manager.delete_agent_async(agent.id, actor)
|
243
|
+
deleted_agents.append(agent.id)
|
244
|
+
except Exception as e:
|
245
|
+
# Continue deleting other agents even if one fails
|
246
|
+
print(f"Failed to delete agent {agent.id}: {e}")
|
247
|
+
|
248
|
+
# Finally delete groups
|
249
|
+
from letta.orm.group import Group as GroupModel
|
250
|
+
|
251
|
+
async with db_registry.async_session() as session:
|
252
|
+
# Get all groups with the deployment_id
|
253
|
+
group_query = select(GroupModel).where(
|
254
|
+
GroupModel.deployment_id == deployment_id, GroupModel.organization_id == actor.organization_id
|
255
|
+
)
|
256
|
+
result = await session.execute(group_query)
|
257
|
+
groups = result.scalars().all()
|
258
|
+
|
259
|
+
for group in groups:
|
260
|
+
try:
|
261
|
+
await server.group_manager.delete_group_async(group.id, actor)
|
262
|
+
deleted_groups.append(group.id)
|
263
|
+
except Exception as e:
|
264
|
+
# Continue deleting other groups even if one fails
|
265
|
+
print(f"Failed to delete group {group.id}: {e}")
|
266
|
+
|
267
|
+
total_deleted = len(deleted_blocks) + len(deleted_agents) + len(deleted_groups)
|
268
|
+
message = f"Successfully deleted {total_deleted} entities from deployment {deployment_id}"
|
269
|
+
|
270
|
+
return DeleteDeploymentResponse(
|
271
|
+
deleted_blocks=deleted_blocks, deleted_agents=deleted_agents, deleted_groups=deleted_groups, message=message
|
272
|
+
)
|
67
273
|
except Exception as e:
|
68
274
|
raise HTTPException(status_code=500, detail=str(e))
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import List, Optional
|
1
|
+
from typing import List, Literal, Optional
|
2
2
|
|
3
3
|
from fastapi import APIRouter, Body, Depends, Header, Query
|
4
4
|
from fastapi.exceptions import HTTPException
|
@@ -25,9 +25,9 @@ logger = get_logger(__name__)
|
|
25
25
|
@router.post(
|
26
26
|
"/batches",
|
27
27
|
response_model=BatchJob,
|
28
|
-
operation_id="
|
28
|
+
operation_id="create_batch_run",
|
29
29
|
)
|
30
|
-
async def
|
30
|
+
async def create_batch_run(
|
31
31
|
request: Request,
|
32
32
|
payload: CreateBatch = Body(..., description="Messages and config for all agents"),
|
33
33
|
server: SyncServer = Depends(get_letta_server),
|
@@ -127,25 +127,21 @@ async def list_batch_runs(
|
|
127
127
|
)
|
128
128
|
async def list_batch_messages(
|
129
129
|
batch_id: str,
|
130
|
-
|
131
|
-
|
132
|
-
|
130
|
+
before: Optional[str] = Query(
|
131
|
+
None, description="Message ID cursor for pagination. Returns messages that come before this message ID in the specified sort order"
|
132
|
+
),
|
133
|
+
after: Optional[str] = Query(
|
134
|
+
None, description="Message ID cursor for pagination. Returns messages that come after this message ID in the specified sort order"
|
135
|
+
),
|
136
|
+
limit: Optional[int] = Query(100, description="Maximum number of messages to return"),
|
137
|
+
order: Literal["asc", "desc"] = Query(
|
138
|
+
"desc", description="Sort order for messages by creation time. 'asc' for oldest first, 'desc' for newest first"
|
133
139
|
),
|
134
140
|
agent_id: Optional[str] = Query(None, description="Filter messages by agent ID"),
|
135
|
-
sort_descending: bool = Query(True, description="Sort messages by creation time (true=newest first)"),
|
136
141
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
137
142
|
server: SyncServer = Depends(get_letta_server),
|
138
143
|
):
|
139
|
-
"""
|
140
|
-
Get messages for a specific batch job.
|
141
|
-
|
142
|
-
Returns messages associated with the batch in chronological order.
|
143
|
-
|
144
|
-
Pagination:
|
145
|
-
- For the first page, omit the cursor parameter
|
146
|
-
- For subsequent pages, use the ID of the last message from the previous response as the cursor
|
147
|
-
- Results will include messages before/after the cursor based on sort_descending
|
148
|
-
"""
|
144
|
+
"""Get response messages for a specific batch job."""
|
149
145
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
150
146
|
|
151
147
|
# First, verify the batch job exists and the user has access to it
|
@@ -156,9 +152,8 @@ async def list_batch_messages(
|
|
156
152
|
raise HTTPException(status_code=404, detail="Batch not found")
|
157
153
|
|
158
154
|
# Get messages directly using our efficient method
|
159
|
-
# We'll need to update the underlying implementation to use message_id as cursor
|
160
155
|
messages = await server.batch_manager.get_messages_for_letta_batch_async(
|
161
|
-
letta_batch_job_id=batch_id, limit=limit, actor=actor, agent_id=agent_id,
|
156
|
+
letta_batch_job_id=batch_id, limit=limit, actor=actor, agent_id=agent_id, ascending=(order == "asc"), before=before, after=after
|
162
157
|
)
|
163
158
|
|
164
159
|
return LettaBatchMessages(messages=messages)
|