letta-nightly 0.7.30.dev20250603104343__py3-none-any.whl → 0.8.0.dev20250604201135__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 +7 -1
- letta/agent.py +14 -7
- letta/agents/base_agent.py +1 -0
- letta/agents/ephemeral_summary_agent.py +104 -0
- letta/agents/helpers.py +35 -3
- letta/agents/letta_agent.py +492 -176
- letta/agents/letta_agent_batch.py +22 -16
- letta/agents/prompts/summary_system_prompt.txt +62 -0
- letta/agents/voice_agent.py +22 -7
- letta/agents/voice_sleeptime_agent.py +13 -8
- letta/constants.py +33 -1
- letta/data_sources/connectors.py +52 -36
- letta/errors.py +4 -0
- letta/functions/ast_parsers.py +13 -30
- letta/functions/function_sets/base.py +3 -1
- letta/functions/functions.py +2 -0
- letta/functions/mcp_client/base_client.py +151 -97
- letta/functions/mcp_client/sse_client.py +49 -31
- letta/functions/mcp_client/stdio_client.py +107 -106
- letta/functions/schema_generator.py +22 -22
- letta/groups/helpers.py +3 -4
- letta/groups/sleeptime_multi_agent.py +4 -4
- letta/groups/sleeptime_multi_agent_v2.py +22 -0
- letta/helpers/composio_helpers.py +16 -0
- letta/helpers/converters.py +20 -0
- letta/helpers/datetime_helpers.py +1 -6
- letta/helpers/tool_rule_solver.py +2 -1
- letta/interfaces/anthropic_streaming_interface.py +17 -2
- letta/interfaces/openai_chat_completions_streaming_interface.py +1 -0
- letta/interfaces/openai_streaming_interface.py +18 -2
- letta/llm_api/anthropic_client.py +24 -3
- letta/llm_api/google_ai_client.py +0 -15
- letta/llm_api/google_vertex_client.py +6 -5
- letta/llm_api/llm_client_base.py +15 -0
- letta/llm_api/openai.py +2 -2
- letta/llm_api/openai_client.py +60 -8
- letta/orm/__init__.py +2 -0
- letta/orm/agent.py +45 -43
- letta/orm/base.py +0 -2
- letta/orm/block.py +1 -0
- letta/orm/custom_columns.py +13 -0
- letta/orm/enums.py +5 -0
- letta/orm/file.py +3 -1
- letta/orm/files_agents.py +68 -0
- letta/orm/mcp_server.py +48 -0
- letta/orm/message.py +1 -0
- letta/orm/organization.py +11 -2
- letta/orm/passage.py +25 -10
- letta/orm/sandbox_config.py +5 -2
- letta/orm/sqlalchemy_base.py +171 -110
- letta/prompts/system/memgpt_base.txt +6 -1
- letta/prompts/system/memgpt_v2_chat.txt +57 -0
- letta/prompts/system/sleeptime.txt +2 -0
- letta/prompts/system/sleeptime_v2.txt +28 -0
- letta/schemas/agent.py +87 -20
- letta/schemas/block.py +7 -1
- letta/schemas/file.py +57 -0
- letta/schemas/mcp.py +74 -0
- letta/schemas/memory.py +5 -2
- letta/schemas/message.py +9 -0
- letta/schemas/openai/openai.py +0 -6
- letta/schemas/providers.py +33 -4
- letta/schemas/tool.py +26 -21
- letta/schemas/tool_execution_result.py +5 -0
- letta/server/db.py +23 -8
- letta/server/rest_api/app.py +73 -56
- letta/server/rest_api/interface.py +4 -4
- letta/server/rest_api/routers/v1/agents.py +132 -47
- letta/server/rest_api/routers/v1/blocks.py +3 -2
- letta/server/rest_api/routers/v1/embeddings.py +3 -3
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/jobs.py +14 -17
- letta/server/rest_api/routers/v1/organizations.py +10 -10
- letta/server/rest_api/routers/v1/providers.py +12 -10
- letta/server/rest_api/routers/v1/runs.py +3 -3
- letta/server/rest_api/routers/v1/sandbox_configs.py +12 -12
- letta/server/rest_api/routers/v1/sources.py +108 -43
- letta/server/rest_api/routers/v1/steps.py +8 -6
- letta/server/rest_api/routers/v1/tools.py +134 -95
- letta/server/rest_api/utils.py +12 -1
- letta/server/server.py +272 -73
- letta/services/agent_manager.py +246 -313
- letta/services/block_manager.py +30 -9
- letta/services/context_window_calculator/__init__.py +0 -0
- letta/services/context_window_calculator/context_window_calculator.py +150 -0
- letta/services/context_window_calculator/token_counter.py +82 -0
- letta/services/file_processor/__init__.py +0 -0
- letta/services/file_processor/chunker/__init__.py +0 -0
- letta/services/file_processor/chunker/llama_index_chunker.py +29 -0
- letta/services/file_processor/embedder/__init__.py +0 -0
- letta/services/file_processor/embedder/openai_embedder.py +84 -0
- letta/services/file_processor/file_processor.py +123 -0
- letta/services/file_processor/parser/__init__.py +0 -0
- letta/services/file_processor/parser/base_parser.py +9 -0
- letta/services/file_processor/parser/mistral_parser.py +54 -0
- letta/services/file_processor/types.py +0 -0
- letta/services/files_agents_manager.py +184 -0
- letta/services/group_manager.py +118 -0
- letta/services/helpers/agent_manager_helper.py +76 -21
- letta/services/helpers/tool_execution_helper.py +3 -0
- letta/services/helpers/tool_parser_helper.py +100 -0
- letta/services/identity_manager.py +44 -42
- letta/services/job_manager.py +21 -10
- letta/services/mcp/base_client.py +5 -2
- letta/services/mcp/sse_client.py +3 -5
- letta/services/mcp/stdio_client.py +3 -5
- letta/services/mcp_manager.py +281 -0
- letta/services/message_manager.py +40 -26
- letta/services/organization_manager.py +55 -19
- letta/services/passage_manager.py +211 -13
- letta/services/provider_manager.py +48 -2
- letta/services/sandbox_config_manager.py +105 -0
- letta/services/source_manager.py +4 -5
- letta/services/step_manager.py +9 -6
- letta/services/summarizer/summarizer.py +50 -23
- letta/services/telemetry_manager.py +7 -0
- letta/services/tool_executor/tool_execution_manager.py +11 -52
- letta/services/tool_executor/tool_execution_sandbox.py +4 -34
- letta/services/tool_executor/tool_executor.py +107 -105
- letta/services/tool_manager.py +56 -17
- letta/services/tool_sandbox/base.py +39 -92
- letta/services/tool_sandbox/e2b_sandbox.py +16 -11
- letta/services/tool_sandbox/local_sandbox.py +51 -23
- letta/services/user_manager.py +36 -3
- letta/settings.py +10 -3
- letta/templates/__init__.py +0 -0
- letta/templates/sandbox_code_file.py.j2 +47 -0
- letta/templates/template_helper.py +16 -0
- letta/tracing.py +30 -1
- letta/types/__init__.py +7 -0
- letta/utils.py +25 -1
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/METADATA +7 -2
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/RECORD +136 -110
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/LICENSE +0 -0
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/WHEEL +0 -0
- {letta_nightly-0.7.30.dev20250603104343.dist-info → letta_nightly-0.8.0.dev20250604201135.dist-info}/entry_points.txt +0 -0
@@ -9,7 +9,7 @@ router = APIRouter(prefix="/embeddings", tags=["embeddings"])
|
|
9
9
|
|
10
10
|
|
11
11
|
@router.get("/total_storage_size", response_model=float, operation_id="get_total_storage_size")
|
12
|
-
def get_embeddings_total_storage_size(
|
12
|
+
async def get_embeddings_total_storage_size(
|
13
13
|
server: SyncServer = Depends(get_letta_server),
|
14
14
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
15
15
|
storage_unit: Optional[str] = Header("GB", alias="storage_unit"), # Extract storage unit from header, default to GB
|
@@ -17,5 +17,5 @@ def get_embeddings_total_storage_size(
|
|
17
17
|
"""
|
18
18
|
Get the total size of all embeddings in the database for a user in the storage unit given.
|
19
19
|
"""
|
20
|
-
actor = server.user_manager.
|
21
|
-
return server.passage_manager.
|
20
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
21
|
+
return await server.passage_manager.estimate_embeddings_size_async(actor=actor, storage_unit=storage_unit)
|
@@ -52,7 +52,7 @@ def count_groups(
|
|
52
52
|
|
53
53
|
|
54
54
|
@router.get("/{group_id}", response_model=Group, operation_id="retrieve_group")
|
55
|
-
def retrieve_group(
|
55
|
+
async def retrieve_group(
|
56
56
|
group_id: str,
|
57
57
|
server: "SyncServer" = Depends(get_letta_server),
|
58
58
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
@@ -60,10 +60,10 @@ def retrieve_group(
|
|
60
60
|
"""
|
61
61
|
Retrieve the group by id.
|
62
62
|
"""
|
63
|
-
actor = server.user_manager.
|
63
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
64
64
|
|
65
65
|
try:
|
66
|
-
return server.group_manager.
|
66
|
+
return await server.group_manager.retrieve_group_async(group_id=group_id, actor=actor)
|
67
67
|
except NoResultFound as e:
|
68
68
|
raise HTTPException(status_code=404, detail=str(e))
|
69
69
|
|
@@ -12,7 +12,7 @@ router = APIRouter(prefix="/jobs", tags=["jobs"])
|
|
12
12
|
|
13
13
|
|
14
14
|
@router.get("/", response_model=List[Job], operation_id="list_jobs")
|
15
|
-
def list_jobs(
|
15
|
+
async def list_jobs(
|
16
16
|
server: "SyncServer" = Depends(get_letta_server),
|
17
17
|
source_id: Optional[str] = Query(None, description="Only list jobs associated with the source."),
|
18
18
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
@@ -20,33 +20,30 @@ def list_jobs(
|
|
20
20
|
"""
|
21
21
|
List all jobs.
|
22
22
|
"""
|
23
|
-
actor = server.user_manager.
|
23
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
24
24
|
|
25
25
|
# TODO: add filtering by status
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
# TODO: Probably change this
|
31
|
-
jobs = [job for job in jobs if job.metadata.get("source_id") == source_id]
|
32
|
-
return jobs
|
26
|
+
return await server.job_manager.list_jobs_async(
|
27
|
+
actor=actor,
|
28
|
+
source_id=source_id,
|
29
|
+
)
|
33
30
|
|
34
31
|
|
35
32
|
@router.get("/active", response_model=List[Job], operation_id="list_active_jobs")
|
36
33
|
async def list_active_jobs(
|
37
34
|
server: "SyncServer" = Depends(get_letta_server),
|
38
35
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
36
|
+
source_id: Optional[str] = Query(None, description="Only list jobs associated with the source."),
|
39
37
|
):
|
40
38
|
"""
|
41
39
|
List all active jobs.
|
42
40
|
"""
|
43
41
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
44
|
-
|
45
|
-
return await server.job_manager.list_jobs_async(actor=actor, statuses=[JobStatus.created, JobStatus.running])
|
42
|
+
return await server.job_manager.list_jobs_async(actor=actor, statuses=[JobStatus.created, JobStatus.running], source_id=source_id)
|
46
43
|
|
47
44
|
|
48
45
|
@router.get("/{job_id}", response_model=Job, operation_id="retrieve_job")
|
49
|
-
def retrieve_job(
|
46
|
+
async def retrieve_job(
|
50
47
|
job_id: str,
|
51
48
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
52
49
|
server: "SyncServer" = Depends(get_letta_server),
|
@@ -54,16 +51,16 @@ def retrieve_job(
|
|
54
51
|
"""
|
55
52
|
Get the status of a job.
|
56
53
|
"""
|
57
|
-
actor = server.user_manager.
|
54
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
58
55
|
|
59
56
|
try:
|
60
|
-
return server.job_manager.
|
57
|
+
return await server.job_manager.get_job_by_id_async(job_id=job_id, actor=actor)
|
61
58
|
except NoResultFound:
|
62
59
|
raise HTTPException(status_code=404, detail="Job not found")
|
63
60
|
|
64
61
|
|
65
62
|
@router.delete("/{job_id}", response_model=Job, operation_id="delete_job")
|
66
|
-
def delete_job(
|
63
|
+
async def delete_job(
|
67
64
|
job_id: str,
|
68
65
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
69
66
|
server: "SyncServer" = Depends(get_letta_server),
|
@@ -71,10 +68,10 @@ def delete_job(
|
|
71
68
|
"""
|
72
69
|
Delete a job by its job_id.
|
73
70
|
"""
|
74
|
-
actor = server.user_manager.
|
71
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
75
72
|
|
76
73
|
try:
|
77
|
-
job = server.job_manager.
|
74
|
+
job = await server.job_manager.delete_job_by_id_async(job_id=job_id, actor=actor)
|
78
75
|
return job
|
79
76
|
except NoResultFound:
|
80
77
|
raise HTTPException(status_code=404, detail="Job not found")
|
@@ -13,7 +13,7 @@ router = APIRouter(prefix="/orgs", tags=["organization", "admin"])
|
|
13
13
|
|
14
14
|
|
15
15
|
@router.get("/", tags=["admin"], response_model=List[Organization], operation_id="list_orgs")
|
16
|
-
def get_all_orgs(
|
16
|
+
async def get_all_orgs(
|
17
17
|
after: Optional[str] = Query(None),
|
18
18
|
limit: Optional[int] = Query(50),
|
19
19
|
server: "SyncServer" = Depends(get_letta_server),
|
@@ -22,7 +22,7 @@ def get_all_orgs(
|
|
22
22
|
Get a list of all orgs in the database
|
23
23
|
"""
|
24
24
|
try:
|
25
|
-
orgs = server.organization_manager.
|
25
|
+
orgs = await server.organization_manager.list_organizations_async(after=after, limit=limit)
|
26
26
|
except HTTPException:
|
27
27
|
raise
|
28
28
|
except Exception as e:
|
@@ -31,7 +31,7 @@ def get_all_orgs(
|
|
31
31
|
|
32
32
|
|
33
33
|
@router.post("/", tags=["admin"], response_model=Organization, operation_id="create_organization")
|
34
|
-
def create_org(
|
34
|
+
async def create_org(
|
35
35
|
request: OrganizationCreate = Body(...),
|
36
36
|
server: "SyncServer" = Depends(get_letta_server),
|
37
37
|
):
|
@@ -39,21 +39,21 @@ def create_org(
|
|
39
39
|
Create a new org in the database
|
40
40
|
"""
|
41
41
|
org = Organization(**request.model_dump())
|
42
|
-
org = server.organization_manager.
|
42
|
+
org = await server.organization_manager.create_organization_async(pydantic_org=org)
|
43
43
|
return org
|
44
44
|
|
45
45
|
|
46
46
|
@router.delete("/", tags=["admin"], response_model=Organization, operation_id="delete_organization_by_id")
|
47
|
-
def delete_org(
|
47
|
+
async def delete_org(
|
48
48
|
org_id: str = Query(..., description="The org_id key to be deleted."),
|
49
49
|
server: "SyncServer" = Depends(get_letta_server),
|
50
50
|
):
|
51
51
|
# TODO make a soft deletion, instead of a hard deletion
|
52
52
|
try:
|
53
|
-
org = server.organization_manager.
|
53
|
+
org = await server.organization_manager.get_organization_by_id_async(org_id=org_id)
|
54
54
|
if org is None:
|
55
55
|
raise HTTPException(status_code=404, detail=f"Organization does not exist")
|
56
|
-
server.organization_manager.
|
56
|
+
await server.organization_manager.delete_organization_by_id_async(org_id=org_id)
|
57
57
|
except HTTPException:
|
58
58
|
raise
|
59
59
|
except Exception as e:
|
@@ -62,16 +62,16 @@ def delete_org(
|
|
62
62
|
|
63
63
|
|
64
64
|
@router.patch("/", tags=["admin"], response_model=Organization, operation_id="update_organization")
|
65
|
-
def update_org(
|
65
|
+
async def update_org(
|
66
66
|
org_id: str = Query(..., description="The org_id key to be updated."),
|
67
67
|
request: OrganizationUpdate = Body(...),
|
68
68
|
server: "SyncServer" = Depends(get_letta_server),
|
69
69
|
):
|
70
70
|
try:
|
71
|
-
org = server.organization_manager.
|
71
|
+
org = await server.organization_manager.get_organization_by_id_async(org_id=org_id)
|
72
72
|
if org is None:
|
73
73
|
raise HTTPException(status_code=404, detail=f"Organization does not exist")
|
74
|
-
org = server.organization_manager.
|
74
|
+
org = await server.organization_manager.update_organization_async(org_id=org_id, name=request.name)
|
75
75
|
except HTTPException:
|
76
76
|
raise
|
77
77
|
except Exception as e:
|
@@ -16,7 +16,7 @@ router = APIRouter(prefix="/providers", tags=["providers"])
|
|
16
16
|
|
17
17
|
|
18
18
|
@router.get("/", response_model=List[Provider], operation_id="list_providers")
|
19
|
-
def list_providers(
|
19
|
+
async def list_providers(
|
20
20
|
name: Optional[str] = Query(None),
|
21
21
|
provider_type: Optional[ProviderType] = Query(None),
|
22
22
|
after: Optional[str] = Query(None),
|
@@ -28,8 +28,10 @@ def list_providers(
|
|
28
28
|
Get a list of all custom providers in the database
|
29
29
|
"""
|
30
30
|
try:
|
31
|
-
actor = server.user_manager.
|
32
|
-
providers = server.provider_manager.
|
31
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
32
|
+
providers = await server.provider_manager.list_providers_async(
|
33
|
+
after=after, limit=limit, actor=actor, name=name, provider_type=provider_type
|
34
|
+
)
|
33
35
|
except HTTPException:
|
34
36
|
raise
|
35
37
|
except Exception as e:
|
@@ -38,7 +40,7 @@ def list_providers(
|
|
38
40
|
|
39
41
|
|
40
42
|
@router.post("/", response_model=Provider, operation_id="create_provider")
|
41
|
-
def create_provider(
|
43
|
+
async def create_provider(
|
42
44
|
request: ProviderCreate = Body(...),
|
43
45
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
44
46
|
server: "SyncServer" = Depends(get_letta_server),
|
@@ -46,16 +48,16 @@ def create_provider(
|
|
46
48
|
"""
|
47
49
|
Create a new custom provider
|
48
50
|
"""
|
49
|
-
actor = server.user_manager.
|
51
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
50
52
|
|
51
53
|
provider = ProviderCreate(**request.model_dump())
|
52
54
|
|
53
|
-
provider = server.provider_manager.
|
55
|
+
provider = await server.provider_manager.create_provider_async(provider, actor=actor)
|
54
56
|
return provider
|
55
57
|
|
56
58
|
|
57
59
|
@router.patch("/{provider_id}", response_model=Provider, operation_id="modify_provider")
|
58
|
-
def modify_provider(
|
60
|
+
async def modify_provider(
|
59
61
|
provider_id: str,
|
60
62
|
request: ProviderUpdate = Body(...),
|
61
63
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
@@ -85,7 +87,7 @@ def check_provider(
|
|
85
87
|
|
86
88
|
|
87
89
|
@router.delete("/{provider_id}", response_model=None, operation_id="delete_provider")
|
88
|
-
def delete_provider(
|
90
|
+
async def delete_provider(
|
89
91
|
provider_id: str,
|
90
92
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
91
93
|
server: "SyncServer" = Depends(get_letta_server),
|
@@ -94,8 +96,8 @@ def delete_provider(
|
|
94
96
|
Delete an existing custom provider
|
95
97
|
"""
|
96
98
|
try:
|
97
|
-
actor = server.user_manager.
|
98
|
-
server.provider_manager.
|
99
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
100
|
+
await server.provider_manager.delete_provider_by_id_async(provider_id=provider_id, actor=actor)
|
99
101
|
return JSONResponse(status_code=status.HTTP_200_OK, content={"message": f"Provider id={provider_id} successfully deleted"})
|
100
102
|
except NoResultFound:
|
101
103
|
raise HTTPException(status_code=404, detail=f"Provider provider_id={provider_id} not found for user_id={actor.id}.")
|
@@ -199,7 +199,7 @@ async def list_run_steps(
|
|
199
199
|
|
200
200
|
|
201
201
|
@router.delete("/{run_id}", response_model=Run, operation_id="delete_run")
|
202
|
-
def delete_run(
|
202
|
+
async def delete_run(
|
203
203
|
run_id: str,
|
204
204
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
205
205
|
server: "SyncServer" = Depends(get_letta_server),
|
@@ -207,10 +207,10 @@ def delete_run(
|
|
207
207
|
"""
|
208
208
|
Delete a run by its run_id.
|
209
209
|
"""
|
210
|
-
actor = server.user_manager.
|
210
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
211
211
|
|
212
212
|
try:
|
213
|
-
job = server.job_manager.
|
213
|
+
job = await server.job_manager.delete_job_by_id_async(job_id=run_id, actor=actor)
|
214
214
|
return Run.from_job(job)
|
215
215
|
except NoResultFound:
|
216
216
|
raise HTTPException(status_code=404, detail="Run not found")
|
@@ -90,13 +90,13 @@ async def update_sandbox_config(
|
|
90
90
|
|
91
91
|
|
92
92
|
@router.delete("/{sandbox_config_id}", status_code=204)
|
93
|
-
def delete_sandbox_config(
|
93
|
+
async def delete_sandbox_config(
|
94
94
|
sandbox_config_id: str,
|
95
95
|
server: SyncServer = Depends(get_letta_server),
|
96
96
|
actor_id: str = Depends(get_user_id),
|
97
97
|
):
|
98
|
-
actor = server.user_manager.
|
99
|
-
server.sandbox_config_manager.
|
98
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
99
|
+
await server.sandbox_config_manager.delete_sandbox_config_async(sandbox_config_id, actor)
|
100
100
|
|
101
101
|
|
102
102
|
@router.get("/", response_model=List[PydanticSandboxConfig])
|
@@ -158,35 +158,35 @@ async def force_recreate_local_sandbox_venv(
|
|
158
158
|
|
159
159
|
|
160
160
|
@router.post("/{sandbox_config_id}/environment-variable", response_model=PydanticEnvVar)
|
161
|
-
def create_sandbox_env_var(
|
161
|
+
async def create_sandbox_env_var(
|
162
162
|
sandbox_config_id: str,
|
163
163
|
env_var_create: SandboxEnvironmentVariableCreate,
|
164
164
|
server: SyncServer = Depends(get_letta_server),
|
165
165
|
actor_id: str = Depends(get_user_id),
|
166
166
|
):
|
167
|
-
actor = server.user_manager.
|
168
|
-
return server.sandbox_config_manager.
|
167
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
168
|
+
return await server.sandbox_config_manager.create_sandbox_env_var_async(env_var_create, sandbox_config_id, actor)
|
169
169
|
|
170
170
|
|
171
171
|
@router.patch("/environment-variable/{env_var_id}", response_model=PydanticEnvVar)
|
172
|
-
def update_sandbox_env_var(
|
172
|
+
async def update_sandbox_env_var(
|
173
173
|
env_var_id: str,
|
174
174
|
env_var_update: SandboxEnvironmentVariableUpdate,
|
175
175
|
server: SyncServer = Depends(get_letta_server),
|
176
176
|
actor_id: str = Depends(get_user_id),
|
177
177
|
):
|
178
|
-
actor = server.user_manager.
|
179
|
-
return server.sandbox_config_manager.
|
178
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
179
|
+
return await server.sandbox_config_manager.update_sandbox_env_var_async(env_var_id, env_var_update, actor)
|
180
180
|
|
181
181
|
|
182
182
|
@router.delete("/environment-variable/{env_var_id}", status_code=204)
|
183
|
-
def delete_sandbox_env_var(
|
183
|
+
async def delete_sandbox_env_var(
|
184
184
|
env_var_id: str,
|
185
185
|
server: SyncServer = Depends(get_letta_server),
|
186
186
|
actor_id: str = Depends(get_user_id),
|
187
187
|
):
|
188
|
-
actor = server.user_manager.
|
189
|
-
server.sandbox_config_manager.
|
188
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
189
|
+
await server.sandbox_config_manager.delete_sandbox_env_var_async(env_var_id, actor)
|
190
190
|
|
191
191
|
|
192
192
|
@router.get("/{sandbox_config_id}/environment-variable", response_model=List[PydanticEnvVar])
|
@@ -1,11 +1,16 @@
|
|
1
1
|
import asyncio
|
2
|
+
import mimetypes
|
2
3
|
import os
|
3
4
|
import tempfile
|
5
|
+
from pathlib import Path
|
4
6
|
from typing import List, Optional
|
5
7
|
|
6
|
-
from fastapi import APIRouter,
|
8
|
+
from fastapi import APIRouter, Depends, Header, HTTPException, Query, UploadFile
|
9
|
+
from starlette import status
|
7
10
|
|
8
11
|
import letta.constants as constants
|
12
|
+
from letta.log import get_logger
|
13
|
+
from letta.schemas.agent import AgentState
|
9
14
|
from letta.schemas.file import FileMetadata
|
10
15
|
from letta.schemas.job import Job
|
11
16
|
from letta.schemas.passage import Passage
|
@@ -13,9 +18,14 @@ from letta.schemas.source import Source, SourceCreate, SourceUpdate
|
|
13
18
|
from letta.schemas.user import User
|
14
19
|
from letta.server.rest_api.utils import get_letta_server
|
15
20
|
from letta.server.server import SyncServer
|
16
|
-
from letta.
|
21
|
+
from letta.services.file_processor.chunker.llama_index_chunker import LlamaIndexChunker
|
22
|
+
from letta.services.file_processor.embedder.openai_embedder import OpenAIEmbedder
|
23
|
+
from letta.services.file_processor.file_processor import FileProcessor
|
24
|
+
from letta.services.file_processor.parser.mistral_parser import MistralFileParser
|
25
|
+
from letta.settings import model_settings, settings
|
26
|
+
from letta.utils import safe_create_task, sanitize_filename
|
17
27
|
|
18
|
-
|
28
|
+
logger = get_logger(__name__)
|
19
29
|
|
20
30
|
|
21
31
|
router = APIRouter(prefix="/sources", tags=["sources"])
|
@@ -29,7 +39,8 @@ async def count_sources(
|
|
29
39
|
"""
|
30
40
|
Count all data sources created by a user.
|
31
41
|
"""
|
32
|
-
|
42
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
43
|
+
return await server.source_manager.size_async(actor=actor)
|
33
44
|
|
34
45
|
|
35
46
|
@router.get("/{source_id}", response_model=Source, operation_id="retrieve_source")
|
@@ -41,7 +52,7 @@ async def retrieve_source(
|
|
41
52
|
"""
|
42
53
|
Get all sources
|
43
54
|
"""
|
44
|
-
actor = server.user_manager.
|
55
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
45
56
|
|
46
57
|
source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
|
47
58
|
if not source:
|
@@ -58,7 +69,7 @@ async def get_source_id_by_name(
|
|
58
69
|
"""
|
59
70
|
Get a source by name
|
60
71
|
"""
|
61
|
-
actor = server.user_manager.
|
72
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
62
73
|
|
63
74
|
source = await server.source_manager.get_source_by_name(source_name=source_name, actor=actor)
|
64
75
|
if not source:
|
@@ -74,7 +85,7 @@ async def list_sources(
|
|
74
85
|
"""
|
75
86
|
List all data sources created by a user.
|
76
87
|
"""
|
77
|
-
actor = server.user_manager.
|
88
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
78
89
|
return await server.source_manager.list_sources(actor=actor)
|
79
90
|
|
80
91
|
|
@@ -87,14 +98,14 @@ async def create_source(
|
|
87
98
|
"""
|
88
99
|
Create a new data source.
|
89
100
|
"""
|
90
|
-
actor = server.user_manager.
|
101
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
91
102
|
|
92
103
|
# TODO: need to asyncify this
|
93
104
|
if not source_create.embedding_config:
|
94
105
|
if not source_create.embedding:
|
95
106
|
# TODO: modify error type
|
96
107
|
raise ValueError("Must specify either embedding or embedding_config in request")
|
97
|
-
source_create.embedding_config = server.
|
108
|
+
source_create.embedding_config = await server.get_embedding_config_from_handle_async(
|
98
109
|
handle=source_create.embedding,
|
99
110
|
embedding_chunk_size=source_create.embedding_chunk_size or constants.DEFAULT_EMBEDDING_CHUNK_SIZE,
|
100
111
|
actor=actor,
|
@@ -120,7 +131,7 @@ async def modify_source(
|
|
120
131
|
Update the name or documentation of an existing data source.
|
121
132
|
"""
|
122
133
|
# TODO: allow updating the handle/embedding config
|
123
|
-
actor = server.user_manager.
|
134
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
124
135
|
if not await server.source_manager.get_source_by_id(source_id=source_id, actor=actor):
|
125
136
|
raise HTTPException(status_code=404, detail=f"Source with id={source_id} does not exist.")
|
126
137
|
return await server.source_manager.update_source(source_id=source_id, source_update=source, actor=actor)
|
@@ -135,15 +146,19 @@ async def delete_source(
|
|
135
146
|
"""
|
136
147
|
Delete a data source.
|
137
148
|
"""
|
138
|
-
actor = server.user_manager.
|
139
|
-
source = await server.source_manager.get_source_by_id(source_id=source_id)
|
140
|
-
|
141
|
-
|
142
|
-
|
149
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
150
|
+
source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
|
151
|
+
agent_states = await server.source_manager.list_attached_agents(source_id=source_id, actor=actor)
|
152
|
+
files = await server.source_manager.list_files(source_id, actor)
|
153
|
+
file_ids = [f.id for f in files]
|
154
|
+
|
155
|
+
for agent_state in agent_states:
|
156
|
+
await server.remove_files_from_context_window(agent_state=agent_state, file_ids=file_ids, actor=actor)
|
157
|
+
|
158
|
+
if agent_state.enable_sleeptime:
|
143
159
|
try:
|
144
|
-
|
145
|
-
|
146
|
-
server.block_manager.delete_block(block.id, actor)
|
160
|
+
block = await server.agent_manager.get_block_with_label_async(agent_id=agent_state.id, block_label=source.name, actor=actor)
|
161
|
+
await server.block_manager.delete_block_async(block.id, actor)
|
147
162
|
except:
|
148
163
|
pass
|
149
164
|
await server.delete_source(source_id=source_id, actor=actor)
|
@@ -153,18 +168,49 @@ async def delete_source(
|
|
153
168
|
async def upload_file_to_source(
|
154
169
|
file: UploadFile,
|
155
170
|
source_id: str,
|
156
|
-
background_tasks: BackgroundTasks,
|
157
171
|
server: "SyncServer" = Depends(get_letta_server),
|
158
|
-
actor_id: Optional[str] = Header(None, alias="user_id"),
|
172
|
+
actor_id: Optional[str] = Header(None, alias="user_id"),
|
159
173
|
):
|
160
174
|
"""
|
161
175
|
Upload a file to a data source.
|
162
176
|
"""
|
163
|
-
|
177
|
+
allowed_media_types = {"application/pdf", "text/plain", "application/json"}
|
178
|
+
|
179
|
+
# Normalize incoming Content-Type header (strip charset or any parameters).
|
180
|
+
raw_ct = file.content_type or ""
|
181
|
+
media_type = raw_ct.split(";", 1)[0].strip().lower()
|
182
|
+
|
183
|
+
# If client didn’t supply a Content-Type or it’s not one of the allowed types,
|
184
|
+
# attempt to infer from filename extension.
|
185
|
+
if media_type not in allowed_media_types and file.filename:
|
186
|
+
guessed, _ = mimetypes.guess_type(file.filename)
|
187
|
+
media_type = (guessed or "").lower()
|
188
|
+
|
189
|
+
if media_type not in allowed_media_types:
|
190
|
+
ext = Path(file.filename).suffix.lower()
|
191
|
+
ext_map = {
|
192
|
+
".pdf": "application/pdf",
|
193
|
+
".txt": "text/plain",
|
194
|
+
".json": "application/json",
|
195
|
+
}
|
196
|
+
media_type = ext_map.get(ext, media_type)
|
197
|
+
|
198
|
+
# If still not allowed, reject with 415.
|
199
|
+
if media_type not in allowed_media_types:
|
200
|
+
raise HTTPException(
|
201
|
+
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
202
|
+
detail=(f"Unsupported file type: {media_type or 'unknown'} " f"(filename: {file.filename}). Only PDF, .txt, or .json allowed."),
|
203
|
+
)
|
204
|
+
|
205
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
164
206
|
|
165
207
|
source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
|
166
|
-
|
167
|
-
|
208
|
+
if source is None:
|
209
|
+
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Source with id={source_id} not found.")
|
210
|
+
content = await file.read()
|
211
|
+
|
212
|
+
# sanitize filename
|
213
|
+
file.filename = sanitize_filename(file.filename)
|
168
214
|
|
169
215
|
# create job
|
170
216
|
job = Job(
|
@@ -172,17 +218,28 @@ async def upload_file_to_source(
|
|
172
218
|
metadata={"type": "embedding", "filename": file.filename, "source_id": source_id},
|
173
219
|
completed_at=None,
|
174
220
|
)
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
221
|
+
job = await server.job_manager.create_job_async(job, actor=actor)
|
222
|
+
|
223
|
+
# TODO: Do we need to pull in the full agent_states? Can probably simplify here right?
|
224
|
+
agent_states = await server.source_manager.list_attached_agents(source_id=source_id, actor=actor)
|
225
|
+
|
226
|
+
# NEW: Cloud based file processing
|
227
|
+
if settings.mistral_api_key and model_settings.openai_api_key:
|
228
|
+
logger.info("Running experimental cloud based file processing...")
|
229
|
+
safe_create_task(
|
230
|
+
load_file_to_source_cloud(server, agent_states, content, file, job, source_id, actor),
|
231
|
+
logger=logger,
|
232
|
+
label="file_processor.process",
|
233
|
+
)
|
234
|
+
else:
|
235
|
+
# create background tasks
|
236
|
+
safe_create_task(
|
237
|
+
load_file_to_source_async(server, source_id=source.id, filename=file.filename, job_id=job.id, bytes=content, actor=actor),
|
238
|
+
logger=logger,
|
239
|
+
label="load_file_to_source_async",
|
240
|
+
)
|
241
|
+
safe_create_task(sleeptime_document_ingest_async(server, source_id, actor), logger=logger, label="sleeptime_document_ingest_async")
|
181
242
|
|
182
|
-
# return job information
|
183
|
-
# Is this necessary? Can we just return the job from create_job?
|
184
|
-
job = server.job_manager.get_job_by_id(job_id=job_id, actor=actor)
|
185
|
-
assert job is not None, "Job not found"
|
186
243
|
return job
|
187
244
|
|
188
245
|
|
@@ -198,7 +255,7 @@ async def list_source_passages(
|
|
198
255
|
"""
|
199
256
|
List all passages associated with a data source.
|
200
257
|
"""
|
201
|
-
actor = server.user_manager.
|
258
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
202
259
|
return await server.agent_manager.list_passages_async(
|
203
260
|
actor=actor,
|
204
261
|
source_id=source_id,
|
@@ -219,7 +276,7 @@ async def list_source_files(
|
|
219
276
|
"""
|
220
277
|
List paginated files associated with a data source.
|
221
278
|
"""
|
222
|
-
actor = server.user_manager.
|
279
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
223
280
|
return await server.source_manager.list_files(source_id=source_id, limit=limit, after=after, actor=actor)
|
224
281
|
|
225
282
|
|
@@ -229,29 +286,27 @@ async def list_source_files(
|
|
229
286
|
async def delete_file_from_source(
|
230
287
|
source_id: str,
|
231
288
|
file_id: str,
|
232
|
-
background_tasks: BackgroundTasks,
|
233
289
|
server: "SyncServer" = Depends(get_letta_server),
|
234
290
|
actor_id: Optional[str] = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
235
291
|
):
|
236
292
|
"""
|
237
293
|
Delete a data source.
|
238
294
|
"""
|
239
|
-
actor = server.user_manager.
|
295
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
240
296
|
|
241
297
|
deleted_file = await server.source_manager.delete_file(file_id=file_id, actor=actor)
|
242
298
|
|
243
|
-
|
299
|
+
await server.remove_file_from_context_windows(source_id=source_id, file_id=deleted_file.id, actor=actor)
|
300
|
+
|
244
301
|
asyncio.create_task(sleeptime_document_ingest_async(server, source_id, actor, clear_history=True))
|
245
302
|
if deleted_file is None:
|
246
303
|
raise HTTPException(status_code=404, detail=f"File with id={file_id} not found.")
|
247
304
|
|
248
305
|
|
249
|
-
async def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str,
|
306
|
+
async def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, filename: str, bytes: bytes, actor: User):
|
250
307
|
# Create a temporary directory (deleted after the context manager exits)
|
251
308
|
with tempfile.TemporaryDirectory() as tmpdirname:
|
252
|
-
|
253
|
-
sanitized_filename = sanitize_filename(file.filename)
|
254
|
-
file_path = os.path.join(tmpdirname, sanitized_filename)
|
309
|
+
file_path = os.path.join(tmpdirname, filename)
|
255
310
|
|
256
311
|
# Write the file to the sanitized path
|
257
312
|
with open(file_path, "wb") as buffer:
|
@@ -266,4 +321,14 @@ async def sleeptime_document_ingest_async(server: SyncServer, source_id: str, ac
|
|
266
321
|
agents = await server.source_manager.list_attached_agents(source_id=source_id, actor=actor)
|
267
322
|
for agent in agents:
|
268
323
|
if agent.enable_sleeptime:
|
269
|
-
server.
|
324
|
+
await server.sleeptime_document_ingest_async(agent, source, actor, clear_history)
|
325
|
+
|
326
|
+
|
327
|
+
async def load_file_to_source_cloud(
|
328
|
+
server: SyncServer, agent_states: List[AgentState], content: bytes, file: UploadFile, job: Job, source_id: str, actor: User
|
329
|
+
):
|
330
|
+
file_processor = MistralFileParser()
|
331
|
+
text_chunker = LlamaIndexChunker()
|
332
|
+
embedder = OpenAIEmbedder()
|
333
|
+
file_processor = FileProcessor(file_parser=file_processor, text_chunker=text_chunker, embedder=embedder, actor=actor)
|
334
|
+
await file_processor.process(server=server, agent_states=agent_states, source_id=source_id, content=content, file=file, job=job)
|