letta-nightly 0.12.1.dev20251023104211__py3-none-any.whl → 0.13.0.dev20251024223017__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.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +2 -3
- letta/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/simple_llm_request_adapter.py +8 -5
- letta/adapters/simple_llm_stream_adapter.py +22 -6
- letta/agents/agent_loop.py +10 -3
- letta/agents/base_agent.py +4 -1
- letta/agents/helpers.py +41 -9
- letta/agents/letta_agent.py +11 -10
- letta/agents/letta_agent_v2.py +47 -37
- letta/agents/letta_agent_v3.py +395 -300
- letta/agents/voice_agent.py +8 -6
- letta/agents/voice_sleeptime_agent.py +3 -3
- letta/constants.py +30 -7
- letta/errors.py +20 -0
- letta/functions/function_sets/base.py +55 -3
- letta/functions/mcp_client/types.py +33 -57
- letta/functions/schema_generator.py +135 -23
- letta/groups/sleeptime_multi_agent_v3.py +6 -11
- letta/groups/sleeptime_multi_agent_v4.py +227 -0
- letta/helpers/converters.py +78 -4
- letta/helpers/crypto_utils.py +6 -2
- letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
- letta/interfaces/anthropic_streaming_interface.py +3 -4
- letta/interfaces/gemini_streaming_interface.py +4 -6
- letta/interfaces/openai_streaming_interface.py +63 -28
- letta/llm_api/anthropic_client.py +7 -4
- letta/llm_api/deepseek_client.py +6 -4
- letta/llm_api/google_ai_client.py +3 -12
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/helpers.py +90 -61
- letta/llm_api/llm_api_tools.py +4 -1
- letta/llm_api/openai.py +12 -12
- letta/llm_api/openai_client.py +53 -16
- letta/local_llm/constants.py +4 -3
- letta/local_llm/json_parser.py +5 -2
- letta/local_llm/utils.py +2 -3
- letta/log.py +171 -7
- letta/orm/agent.py +43 -9
- letta/orm/archive.py +4 -0
- letta/orm/custom_columns.py +15 -0
- letta/orm/identity.py +11 -11
- letta/orm/mcp_server.py +9 -0
- letta/orm/message.py +6 -1
- letta/orm/run_metrics.py +7 -2
- letta/orm/sqlalchemy_base.py +2 -2
- letta/orm/tool.py +3 -0
- letta/otel/tracing.py +2 -0
- letta/prompts/prompt_generator.py +7 -2
- letta/schemas/agent.py +41 -10
- letta/schemas/agent_file.py +3 -0
- letta/schemas/archive.py +4 -2
- letta/schemas/block.py +2 -1
- letta/schemas/enums.py +36 -3
- letta/schemas/file.py +3 -3
- letta/schemas/folder.py +2 -1
- letta/schemas/group.py +2 -1
- letta/schemas/identity.py +18 -9
- letta/schemas/job.py +3 -1
- letta/schemas/letta_message.py +71 -12
- letta/schemas/letta_request.py +7 -3
- letta/schemas/letta_stop_reason.py +0 -25
- letta/schemas/llm_config.py +8 -2
- letta/schemas/mcp.py +80 -83
- letta/schemas/mcp_server.py +349 -0
- letta/schemas/memory.py +20 -8
- letta/schemas/message.py +212 -67
- letta/schemas/providers/anthropic.py +13 -6
- letta/schemas/providers/azure.py +6 -4
- letta/schemas/providers/base.py +8 -4
- letta/schemas/providers/bedrock.py +6 -2
- letta/schemas/providers/cerebras.py +7 -3
- letta/schemas/providers/deepseek.py +2 -1
- letta/schemas/providers/google_gemini.py +15 -6
- letta/schemas/providers/groq.py +2 -1
- letta/schemas/providers/lmstudio.py +9 -6
- letta/schemas/providers/mistral.py +2 -1
- letta/schemas/providers/openai.py +7 -2
- letta/schemas/providers/together.py +9 -3
- letta/schemas/providers/xai.py +7 -3
- letta/schemas/run.py +7 -2
- letta/schemas/run_metrics.py +2 -1
- letta/schemas/sandbox_config.py +2 -2
- letta/schemas/secret.py +3 -158
- letta/schemas/source.py +2 -2
- letta/schemas/step.py +2 -2
- letta/schemas/tool.py +24 -1
- letta/schemas/usage.py +0 -1
- letta/server/rest_api/app.py +123 -7
- letta/server/rest_api/dependencies.py +3 -0
- letta/server/rest_api/interface.py +7 -4
- letta/server/rest_api/redis_stream_manager.py +16 -1
- letta/server/rest_api/routers/v1/__init__.py +7 -0
- letta/server/rest_api/routers/v1/agents.py +332 -322
- letta/server/rest_api/routers/v1/archives.py +127 -40
- letta/server/rest_api/routers/v1/blocks.py +54 -6
- letta/server/rest_api/routers/v1/chat_completions.py +146 -0
- letta/server/rest_api/routers/v1/folders.py +27 -35
- letta/server/rest_api/routers/v1/groups.py +23 -35
- letta/server/rest_api/routers/v1/identities.py +24 -10
- letta/server/rest_api/routers/v1/internal_runs.py +107 -0
- letta/server/rest_api/routers/v1/internal_templates.py +162 -179
- letta/server/rest_api/routers/v1/jobs.py +15 -27
- letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
- letta/server/rest_api/routers/v1/messages.py +23 -34
- letta/server/rest_api/routers/v1/organizations.py +6 -27
- letta/server/rest_api/routers/v1/providers.py +35 -62
- letta/server/rest_api/routers/v1/runs.py +30 -43
- letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
- letta/server/rest_api/routers/v1/sources.py +26 -42
- letta/server/rest_api/routers/v1/steps.py +16 -29
- letta/server/rest_api/routers/v1/tools.py +17 -13
- letta/server/rest_api/routers/v1/users.py +5 -17
- letta/server/rest_api/routers/v1/voice.py +18 -27
- letta/server/rest_api/streaming_response.py +5 -2
- letta/server/rest_api/utils.py +187 -25
- letta/server/server.py +27 -22
- letta/server/ws_api/server.py +5 -4
- letta/services/agent_manager.py +148 -26
- letta/services/agent_serialization_manager.py +6 -1
- letta/services/archive_manager.py +168 -15
- letta/services/block_manager.py +14 -4
- letta/services/file_manager.py +33 -29
- letta/services/group_manager.py +10 -0
- letta/services/helpers/agent_manager_helper.py +65 -11
- letta/services/identity_manager.py +105 -4
- letta/services/job_manager.py +11 -1
- letta/services/mcp/base_client.py +2 -2
- letta/services/mcp/oauth_utils.py +33 -8
- letta/services/mcp_manager.py +174 -78
- letta/services/mcp_server_manager.py +1331 -0
- letta/services/message_manager.py +109 -4
- letta/services/organization_manager.py +4 -4
- letta/services/passage_manager.py +9 -25
- letta/services/provider_manager.py +91 -15
- letta/services/run_manager.py +72 -15
- letta/services/sandbox_config_manager.py +45 -3
- letta/services/source_manager.py +15 -8
- letta/services/step_manager.py +24 -1
- letta/services/streaming_service.py +581 -0
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/tool_executor/core_tool_executor.py +111 -0
- letta/services/tool_executor/files_tool_executor.py +5 -3
- letta/services/tool_executor/sandbox_tool_executor.py +2 -2
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_manager.py +10 -3
- letta/services/tool_sandbox/base.py +61 -1
- letta/services/tool_sandbox/local_sandbox.py +1 -3
- letta/services/user_manager.py +2 -2
- letta/settings.py +49 -5
- letta/system.py +14 -5
- letta/utils.py +73 -1
- letta/validators.py +105 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/RECORD +157 -151
- letta/schemas/letta_ping.py +0 -28
- letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,8 +5,8 @@ from fastapi import APIRouter, Body, Depends, HTTPException, Query
|
|
|
5
5
|
from pydantic import Field
|
|
6
6
|
|
|
7
7
|
from letta.data_sources.redis_client import NoopAsyncRedisClient, get_redis_client
|
|
8
|
+
from letta.errors import LettaExpiredError, LettaInvalidArgumentError
|
|
8
9
|
from letta.helpers.datetime_helpers import get_utc_time
|
|
9
|
-
from letta.orm.errors import NoResultFound
|
|
10
10
|
from letta.schemas.enums import RunStatus
|
|
11
11
|
from letta.schemas.letta_message import LettaMessageUnion
|
|
12
12
|
from letta.schemas.letta_request import RetrieveStreamRequest
|
|
@@ -151,28 +151,25 @@ async def retrieve_run(
|
|
|
151
151
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
152
152
|
runs_manager = RunManager()
|
|
153
153
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
return run
|
|
174
|
-
except NoResultFound:
|
|
175
|
-
raise HTTPException(status_code=404, detail="Run not found")
|
|
154
|
+
run = await runs_manager.get_run_by_id(run_id=run_id, actor=actor)
|
|
155
|
+
|
|
156
|
+
use_lettuce = run.metadata and run.metadata.get("lettuce")
|
|
157
|
+
if use_lettuce and run.status not in [RunStatus.completed, RunStatus.failed, RunStatus.cancelled]:
|
|
158
|
+
lettuce_client = await LettuceClient.create()
|
|
159
|
+
status = await lettuce_client.get_status(run_id=run_id)
|
|
160
|
+
|
|
161
|
+
# Map the status to our enum
|
|
162
|
+
run_status = run.status
|
|
163
|
+
if status == "RUNNING":
|
|
164
|
+
run_status = RunStatus.running
|
|
165
|
+
elif status == "COMPLETED":
|
|
166
|
+
run_status = RunStatus.completed
|
|
167
|
+
elif status == "FAILED":
|
|
168
|
+
run_status = RunStatus.failed
|
|
169
|
+
elif status == "CANCELLED":
|
|
170
|
+
run_status = RunStatus.cancelled
|
|
171
|
+
run.status = run_status
|
|
172
|
+
return run
|
|
176
173
|
|
|
177
174
|
|
|
178
175
|
RunMessagesResponse = Annotated[
|
|
@@ -218,11 +215,7 @@ async def retrieve_run_usage(
|
|
|
218
215
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
219
216
|
runs_manager = RunManager()
|
|
220
217
|
|
|
221
|
-
|
|
222
|
-
usage = await runs_manager.get_run_usage(run_id=run_id, actor=actor)
|
|
223
|
-
return usage
|
|
224
|
-
except NoResultFound:
|
|
225
|
-
raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found")
|
|
218
|
+
return await runs_manager.get_run_usage(run_id=run_id, actor=actor)
|
|
226
219
|
|
|
227
220
|
|
|
228
221
|
@router.get("/{run_id}/metrics", response_model=RunMetrics, operation_id="retrieve_metrics_for_run")
|
|
@@ -234,12 +227,9 @@ async def retrieve_metrics_for_run(
|
|
|
234
227
|
"""
|
|
235
228
|
Get run metrics by run ID.
|
|
236
229
|
"""
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return await runs_manager.get_run_metrics_async(run_id=run_id, actor=actor)
|
|
241
|
-
except NoResultFound:
|
|
242
|
-
raise HTTPException(status_code=404, detail="Run metrics not found")
|
|
230
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
231
|
+
runs_manager = RunManager()
|
|
232
|
+
return await runs_manager.get_run_metrics_async(run_id=run_id, actor=actor)
|
|
243
233
|
|
|
244
234
|
|
|
245
235
|
@router.get(
|
|
@@ -275,7 +265,7 @@ async def list_run_steps(
|
|
|
275
265
|
)
|
|
276
266
|
|
|
277
267
|
|
|
278
|
-
@router.delete("/{run_id}", response_model=
|
|
268
|
+
@router.delete("/{run_id}", response_model=None, operation_id="delete_run")
|
|
279
269
|
async def delete_run(
|
|
280
270
|
run_id: str,
|
|
281
271
|
headers: HeaderParams = Depends(get_headers),
|
|
@@ -286,7 +276,7 @@ async def delete_run(
|
|
|
286
276
|
"""
|
|
287
277
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
288
278
|
runs_manager = RunManager()
|
|
289
|
-
return await runs_manager.
|
|
279
|
+
return await runs_manager.delete_run_by_id(run_id=run_id, actor=actor)
|
|
290
280
|
|
|
291
281
|
|
|
292
282
|
@router.post(
|
|
@@ -330,16 +320,13 @@ async def retrieve_stream(
|
|
|
330
320
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
331
321
|
runs_manager = RunManager()
|
|
332
322
|
|
|
333
|
-
|
|
334
|
-
run = await runs_manager.get_run_by_id(run_id=run_id, actor=actor)
|
|
335
|
-
except NoResultFound:
|
|
336
|
-
raise HTTPException(status_code=404, detail="Run not found")
|
|
323
|
+
run = await runs_manager.get_run_by_id(run_id=run_id, actor=actor)
|
|
337
324
|
|
|
338
325
|
if not run.background:
|
|
339
|
-
raise
|
|
326
|
+
raise LettaInvalidArgumentError("Run was not created in background mode, so it cannot be retrieved.")
|
|
340
327
|
|
|
341
328
|
if run.created_at < get_utc_time() - timedelta(hours=3):
|
|
342
|
-
raise
|
|
329
|
+
raise LettaExpiredError("Run was created more than 3 hours ago, and is now expired.")
|
|
343
330
|
|
|
344
331
|
redis_client = await get_redis_client()
|
|
345
332
|
|
|
@@ -370,7 +357,7 @@ async def retrieve_stream(
|
|
|
370
357
|
)
|
|
371
358
|
|
|
372
359
|
if request.include_pings and settings.enable_keepalive:
|
|
373
|
-
stream = add_keepalive_to_stream(stream, keepalive_interval=settings.keepalive_interval)
|
|
360
|
+
stream = add_keepalive_to_stream(stream, keepalive_interval=settings.keepalive_interval, run_id=run_id)
|
|
374
361
|
|
|
375
362
|
return StreamingResponseWithStatusCode(
|
|
376
363
|
stream,
|
|
@@ -15,12 +15,14 @@ from letta.schemas.environment_variables import (
|
|
|
15
15
|
from letta.schemas.sandbox_config import (
|
|
16
16
|
LocalSandboxConfig,
|
|
17
17
|
SandboxConfig as PydanticSandboxConfig,
|
|
18
|
+
SandboxConfigBase,
|
|
18
19
|
SandboxConfigCreate,
|
|
19
20
|
SandboxConfigUpdate,
|
|
20
21
|
)
|
|
21
22
|
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
|
22
23
|
from letta.server.server import SyncServer
|
|
23
24
|
from letta.services.helpers.tool_execution_helper import create_venv_for_local_sandbox, install_pip_requirements_for_sandbox
|
|
25
|
+
from letta.validators import SandboxConfigId
|
|
24
26
|
|
|
25
27
|
router = APIRouter(prefix="/sandbox-config", tags=["sandbox-config"])
|
|
26
28
|
|
|
@@ -87,8 +89,8 @@ async def create_custom_local_sandbox_config(
|
|
|
87
89
|
|
|
88
90
|
@router.patch("/{sandbox_config_id}", response_model=PydanticSandboxConfig)
|
|
89
91
|
async def update_sandbox_config(
|
|
90
|
-
sandbox_config_id: str,
|
|
91
92
|
config_update: SandboxConfigUpdate,
|
|
93
|
+
sandbox_config_id: SandboxConfigId,
|
|
92
94
|
server: SyncServer = Depends(get_letta_server),
|
|
93
95
|
headers: HeaderParams = Depends(get_headers),
|
|
94
96
|
):
|
|
@@ -98,7 +100,7 @@ async def update_sandbox_config(
|
|
|
98
100
|
|
|
99
101
|
@router.delete("/{sandbox_config_id}", status_code=204)
|
|
100
102
|
async def delete_sandbox_config(
|
|
101
|
-
sandbox_config_id:
|
|
103
|
+
sandbox_config_id: SandboxConfigId,
|
|
102
104
|
server: SyncServer = Depends(get_letta_server),
|
|
103
105
|
headers: HeaderParams = Depends(get_headers),
|
|
104
106
|
):
|
|
@@ -157,8 +159,8 @@ async def force_recreate_local_sandbox_venv(
|
|
|
157
159
|
|
|
158
160
|
@router.post("/{sandbox_config_id}/environment-variable", response_model=PydanticEnvVar)
|
|
159
161
|
async def create_sandbox_env_var(
|
|
160
|
-
sandbox_config_id: str,
|
|
161
162
|
env_var_create: SandboxEnvironmentVariableCreate,
|
|
163
|
+
sandbox_config_id: SandboxConfigId,
|
|
162
164
|
server: SyncServer = Depends(get_letta_server),
|
|
163
165
|
headers: HeaderParams = Depends(get_headers),
|
|
164
166
|
):
|
|
@@ -189,7 +191,7 @@ async def delete_sandbox_env_var(
|
|
|
189
191
|
|
|
190
192
|
@router.get("/{sandbox_config_id}/environment-variable", response_model=List[PydanticEnvVar])
|
|
191
193
|
async def list_sandbox_env_vars(
|
|
192
|
-
sandbox_config_id:
|
|
194
|
+
sandbox_config_id: SandboxConfigId,
|
|
193
195
|
limit: int = Query(1000, description="Number of results to return"),
|
|
194
196
|
after: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
|
|
195
197
|
server: SyncServer = Depends(get_letta_server),
|
|
@@ -9,6 +9,7 @@ from starlette import status
|
|
|
9
9
|
from starlette.responses import Response
|
|
10
10
|
|
|
11
11
|
import letta.constants as constants
|
|
12
|
+
from letta.errors import LettaInvalidArgumentError, LettaUnsupportedFileUploadError
|
|
12
13
|
from letta.helpers.pinecone_utils import (
|
|
13
14
|
delete_file_records_from_pinecone_index,
|
|
14
15
|
delete_source_records_from_pinecone_index,
|
|
@@ -20,9 +21,9 @@ from letta.otel.tracing import trace_method
|
|
|
20
21
|
from letta.schemas.agent import AgentState
|
|
21
22
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
22
23
|
from letta.schemas.enums import DuplicateFileHandling, FileProcessingStatus
|
|
23
|
-
from letta.schemas.file import FileMetadata
|
|
24
|
+
from letta.schemas.file import FileMetadata, FileMetadataBase
|
|
24
25
|
from letta.schemas.passage import Passage
|
|
25
|
-
from letta.schemas.source import Source, SourceCreate, SourceUpdate
|
|
26
|
+
from letta.schemas.source import BaseSource, Source, SourceCreate, SourceUpdate
|
|
26
27
|
from letta.schemas.source_metadata import OrganizationSourcesStats
|
|
27
28
|
from letta.schemas.user import User
|
|
28
29
|
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
|
@@ -35,6 +36,7 @@ from letta.services.file_processor.parser.markitdown_parser import MarkitdownFil
|
|
|
35
36
|
from letta.services.file_processor.parser.mistral_parser import MistralFileParser
|
|
36
37
|
from letta.settings import settings
|
|
37
38
|
from letta.utils import safe_create_file_processing_task, safe_create_task, sanitize_filename
|
|
39
|
+
from letta.validators import FileId, SourceId
|
|
38
40
|
|
|
39
41
|
logger = get_logger(__name__)
|
|
40
42
|
|
|
@@ -58,7 +60,7 @@ async def count_sources(
|
|
|
58
60
|
|
|
59
61
|
@router.get("/{source_id}", response_model=Source, operation_id="retrieve_source", deprecated=True)
|
|
60
62
|
async def retrieve_source(
|
|
61
|
-
source_id:
|
|
63
|
+
source_id: SourceId,
|
|
62
64
|
server: "SyncServer" = Depends(get_letta_server),
|
|
63
65
|
headers: HeaderParams = Depends(get_headers),
|
|
64
66
|
):
|
|
@@ -66,10 +68,7 @@ async def retrieve_source(
|
|
|
66
68
|
Get all sources
|
|
67
69
|
"""
|
|
68
70
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
69
|
-
|
|
70
71
|
source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
|
|
71
|
-
if not source:
|
|
72
|
-
raise HTTPException(status_code=404, detail=f"Source with id={source_id} not found.")
|
|
73
72
|
return source
|
|
74
73
|
|
|
75
74
|
|
|
@@ -85,8 +84,6 @@ async def get_source_id_by_name(
|
|
|
85
84
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
86
85
|
|
|
87
86
|
source = await server.source_manager.get_source_by_name(source_name=source_name, actor=actor)
|
|
88
|
-
if not source:
|
|
89
|
-
raise HTTPException(status_code=404, detail=f"Source with name={source_name} not found.")
|
|
90
87
|
return source.id
|
|
91
88
|
|
|
92
89
|
|
|
@@ -138,8 +135,9 @@ async def create_source(
|
|
|
138
135
|
if not source_create.embedding_config:
|
|
139
136
|
if not source_create.embedding:
|
|
140
137
|
if settings.default_embedding_handle is None:
|
|
141
|
-
|
|
142
|
-
|
|
138
|
+
raise LettaInvalidArgumentError(
|
|
139
|
+
"Must specify either embedding or embedding_config in request", argument_name="default_embedding_handle"
|
|
140
|
+
)
|
|
143
141
|
else:
|
|
144
142
|
source_create.embedding = settings.default_embedding_handle
|
|
145
143
|
source_create.embedding_config = await server.get_embedding_config_from_handle_async(
|
|
@@ -159,8 +157,8 @@ async def create_source(
|
|
|
159
157
|
|
|
160
158
|
@router.patch("/{source_id}", response_model=Source, operation_id="modify_source", deprecated=True)
|
|
161
159
|
async def modify_source(
|
|
162
|
-
source_id: str,
|
|
163
160
|
source: SourceUpdate,
|
|
161
|
+
source_id: SourceId,
|
|
164
162
|
server: "SyncServer" = Depends(get_letta_server),
|
|
165
163
|
headers: HeaderParams = Depends(get_headers),
|
|
166
164
|
):
|
|
@@ -169,14 +167,13 @@ async def modify_source(
|
|
|
169
167
|
"""
|
|
170
168
|
# TODO: allow updating the handle/embedding config
|
|
171
169
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
172
|
-
|
|
173
|
-
raise HTTPException(status_code=404, detail=f"Source with id={source_id} does not exist.")
|
|
170
|
+
await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
|
|
174
171
|
return await server.source_manager.update_source(source_id=source_id, source_update=source, actor=actor)
|
|
175
172
|
|
|
176
173
|
|
|
177
174
|
@router.delete("/{source_id}", response_model=None, operation_id="delete_source", deprecated=True)
|
|
178
175
|
async def delete_source(
|
|
179
|
-
source_id:
|
|
176
|
+
source_id: SourceId,
|
|
180
177
|
server: "SyncServer" = Depends(get_letta_server),
|
|
181
178
|
headers: HeaderParams = Depends(get_headers),
|
|
182
179
|
):
|
|
@@ -203,18 +200,16 @@ async def delete_source(
|
|
|
203
200
|
await server.remove_files_from_context_window(agent_state=agent_state, file_ids=file_ids, actor=actor)
|
|
204
201
|
|
|
205
202
|
if agent_state.enable_sleeptime:
|
|
206
|
-
|
|
207
|
-
|
|
203
|
+
block = await server.agent_manager.get_block_with_label_async(agent_id=agent_state.id, block_label=source.name, actor=actor)
|
|
204
|
+
if block:
|
|
208
205
|
await server.block_manager.delete_block_async(block.id, actor)
|
|
209
|
-
except:
|
|
210
|
-
pass
|
|
211
206
|
await server.delete_source(source_id=source_id, actor=actor)
|
|
212
207
|
|
|
213
208
|
|
|
214
209
|
@router.post("/{source_id}/upload", response_model=FileMetadata, operation_id="upload_file_to_source", deprecated=True)
|
|
215
210
|
async def upload_file_to_source(
|
|
216
211
|
file: UploadFile,
|
|
217
|
-
source_id:
|
|
212
|
+
source_id: SourceId,
|
|
218
213
|
duplicate_handling: DuplicateFileHandling = Query(DuplicateFileHandling.SUFFIX, description="How to handle duplicate filenames"),
|
|
219
214
|
name: Optional[str] = Query(None, description="Optional custom name to override the uploaded file's name"),
|
|
220
215
|
server: "SyncServer" = Depends(get_letta_server),
|
|
@@ -246,9 +241,8 @@ async def upload_file_to_source(
|
|
|
246
241
|
|
|
247
242
|
# If still not allowed, reject with 415.
|
|
248
243
|
if media_type not in allowed_media_types:
|
|
249
|
-
raise
|
|
250
|
-
|
|
251
|
-
detail=(
|
|
244
|
+
raise LettaUnsupportedFileUploadError(
|
|
245
|
+
message=(
|
|
252
246
|
f"Unsupported file type: {media_type or 'unknown'} "
|
|
253
247
|
f"(filename: {file.filename}). "
|
|
254
248
|
f"Supported types: PDF, text files (.txt, .md), JSON, and code files (.py, .js, .java, etc.)."
|
|
@@ -258,8 +252,6 @@ async def upload_file_to_source(
|
|
|
258
252
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
259
253
|
|
|
260
254
|
source = await server.source_manager.get_source_by_id(source_id=source_id, actor=actor)
|
|
261
|
-
if source is None:
|
|
262
|
-
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Source with id={source_id} not found.")
|
|
263
255
|
|
|
264
256
|
content = await file.read()
|
|
265
257
|
|
|
@@ -278,8 +270,9 @@ async def upload_file_to_source(
|
|
|
278
270
|
if existing_file:
|
|
279
271
|
# Duplicate found, handle based on strategy
|
|
280
272
|
if duplicate_handling == DuplicateFileHandling.ERROR:
|
|
281
|
-
raise
|
|
282
|
-
|
|
273
|
+
raise LettaInvalidArgumentError(
|
|
274
|
+
message=f"File '{original_filename}' already exists in source '{source.name}'",
|
|
275
|
+
argument_name="duplicate_handling",
|
|
283
276
|
)
|
|
284
277
|
elif duplicate_handling == DuplicateFileHandling.SKIP:
|
|
285
278
|
# Return existing file metadata with custom header to indicate it was skipped
|
|
@@ -331,7 +324,7 @@ async def upload_file_to_source(
|
|
|
331
324
|
|
|
332
325
|
@router.get("/{source_id}/agents", response_model=List[str], operation_id="get_agents_for_source", deprecated=True)
|
|
333
326
|
async def get_agents_for_source(
|
|
334
|
-
source_id:
|
|
327
|
+
source_id: SourceId,
|
|
335
328
|
server: SyncServer = Depends(get_letta_server),
|
|
336
329
|
headers: HeaderParams = Depends(get_headers),
|
|
337
330
|
):
|
|
@@ -344,7 +337,7 @@ async def get_agents_for_source(
|
|
|
344
337
|
|
|
345
338
|
@router.get("/{source_id}/passages", response_model=List[Passage], operation_id="list_source_passages", deprecated=True)
|
|
346
339
|
async def list_source_passages(
|
|
347
|
-
source_id:
|
|
340
|
+
source_id: SourceId,
|
|
348
341
|
after: Optional[str] = Query(None, description="Message after which to retrieve the returned messages."),
|
|
349
342
|
before: Optional[str] = Query(None, description="Message before which to retrieve the returned messages."),
|
|
350
343
|
limit: int = Query(100, description="Maximum number of messages to retrieve."),
|
|
@@ -366,7 +359,7 @@ async def list_source_passages(
|
|
|
366
359
|
|
|
367
360
|
@router.get("/{source_id}/files", response_model=List[FileMetadata], operation_id="list_source_files", deprecated=True)
|
|
368
361
|
async def list_source_files(
|
|
369
|
-
source_id:
|
|
362
|
+
source_id: SourceId,
|
|
370
363
|
limit: int = Query(1000, description="Number of files to return"),
|
|
371
364
|
after: Optional[str] = Query(None, description="Pagination cursor to fetch the next set of results"),
|
|
372
365
|
include_content: bool = Query(False, description="Whether to include full file content"),
|
|
@@ -394,8 +387,8 @@ async def list_source_files(
|
|
|
394
387
|
|
|
395
388
|
@router.get("/{source_id}/files/{file_id}", response_model=FileMetadata, operation_id="get_file_metadata", deprecated=True)
|
|
396
389
|
async def get_file_metadata(
|
|
397
|
-
source_id:
|
|
398
|
-
file_id:
|
|
390
|
+
source_id: SourceId,
|
|
391
|
+
file_id: FileId,
|
|
399
392
|
include_content: bool = Query(False, description="Whether to include full file content"),
|
|
400
393
|
server: "SyncServer" = Depends(get_letta_server),
|
|
401
394
|
headers: HeaderParams = Depends(get_headers),
|
|
@@ -410,13 +403,6 @@ async def get_file_metadata(
|
|
|
410
403
|
file_id=file_id, actor=actor, include_content=include_content, strip_directory_prefix=True
|
|
411
404
|
)
|
|
412
405
|
|
|
413
|
-
if not file_metadata:
|
|
414
|
-
raise HTTPException(status_code=404, detail=f"File with id={file_id} not found.")
|
|
415
|
-
|
|
416
|
-
# Verify the file belongs to the specified source
|
|
417
|
-
if file_metadata.source_id != source_id:
|
|
418
|
-
raise HTTPException(status_code=404, detail=f"File with id={file_id} not found in source {source_id}.")
|
|
419
|
-
|
|
420
406
|
# Check and update file status (timeout check and pinecone embedding sync)
|
|
421
407
|
file_metadata = await server.file_manager.check_and_update_file_status(file_metadata, actor)
|
|
422
408
|
|
|
@@ -427,8 +413,8 @@ async def get_file_metadata(
|
|
|
427
413
|
# it's still good practice to return a status indicating the success or failure of the deletion
|
|
428
414
|
@router.delete("/{source_id}/{file_id}", status_code=204, operation_id="delete_file_from_source", deprecated=True)
|
|
429
415
|
async def delete_file_from_source(
|
|
430
|
-
source_id:
|
|
431
|
-
file_id:
|
|
416
|
+
source_id: SourceId,
|
|
417
|
+
file_id: FileId,
|
|
432
418
|
server: "SyncServer" = Depends(get_letta_server),
|
|
433
419
|
headers: HeaderParams = Depends(get_headers),
|
|
434
420
|
):
|
|
@@ -452,8 +438,6 @@ async def delete_file_from_source(
|
|
|
452
438
|
await delete_file_records_from_pinecone_index(file_id=file_id, actor=actor)
|
|
453
439
|
|
|
454
440
|
safe_create_task(sleeptime_document_ingest_async(server, source_id, actor, clear_history=True), label="document_ingest_after_delete")
|
|
455
|
-
if deleted_file is None:
|
|
456
|
-
raise HTTPException(status_code=404, detail=f"File with id={file_id} not found.")
|
|
457
441
|
|
|
458
442
|
|
|
459
443
|
async def load_file_to_source_async(server: SyncServer, source_id: str, job_id: str, filename: str, bytes: bytes, actor: User):
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
from typing import List, Literal, Optional
|
|
3
3
|
|
|
4
|
-
from fastapi import APIRouter, Body, Depends, Header,
|
|
4
|
+
from fastapi import APIRouter, Body, Depends, Header, Query
|
|
5
5
|
from pydantic import BaseModel, Field
|
|
6
6
|
|
|
7
|
-
from letta.orm.errors import NoResultFound
|
|
8
7
|
from letta.schemas.letta_message import LettaMessageUnion
|
|
9
8
|
from letta.schemas.message import Message
|
|
10
9
|
from letta.schemas.provider_trace import ProviderTrace
|
|
11
|
-
from letta.schemas.step import Step
|
|
10
|
+
from letta.schemas.step import Step, StepBase
|
|
12
11
|
from letta.schemas.step_metrics import StepMetrics
|
|
13
12
|
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
|
14
13
|
from letta.server.server import SyncServer
|
|
15
14
|
from letta.services.step_manager import FeedbackType
|
|
16
15
|
from letta.settings import settings
|
|
16
|
+
from letta.validators import StepId
|
|
17
17
|
|
|
18
18
|
router = APIRouter(prefix="/steps", tags=["steps"])
|
|
19
19
|
|
|
@@ -70,39 +70,33 @@ async def list_steps(
|
|
|
70
70
|
|
|
71
71
|
@router.get("/{step_id}", response_model=Step, operation_id="retrieve_step")
|
|
72
72
|
async def retrieve_step(
|
|
73
|
-
step_id:
|
|
73
|
+
step_id: StepId,
|
|
74
74
|
headers: HeaderParams = Depends(get_headers),
|
|
75
75
|
server: SyncServer = Depends(get_letta_server),
|
|
76
76
|
):
|
|
77
77
|
"""
|
|
78
78
|
Get a step by ID.
|
|
79
79
|
"""
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return await server.step_manager.get_step_async(step_id=step_id, actor=actor)
|
|
83
|
-
except NoResultFound:
|
|
84
|
-
raise HTTPException(status_code=404, detail="Step not found")
|
|
80
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
81
|
+
return await server.step_manager.get_step_async(step_id=step_id, actor=actor)
|
|
85
82
|
|
|
86
83
|
|
|
87
84
|
@router.get("/{step_id}/metrics", response_model=StepMetrics, operation_id="retrieve_metrics_for_step")
|
|
88
85
|
async def retrieve_metrics_for_step(
|
|
89
|
-
step_id:
|
|
86
|
+
step_id: StepId,
|
|
90
87
|
headers: HeaderParams = Depends(get_headers),
|
|
91
88
|
server: SyncServer = Depends(get_letta_server),
|
|
92
89
|
):
|
|
93
90
|
"""
|
|
94
91
|
Get step metrics by step ID.
|
|
95
92
|
"""
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return await server.step_manager.get_step_metrics_async(step_id=step_id, actor=actor)
|
|
99
|
-
except NoResultFound:
|
|
100
|
-
raise HTTPException(status_code=404, detail="Step metrics not found")
|
|
93
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
94
|
+
return await server.step_manager.get_step_metrics_async(step_id=step_id, actor=actor)
|
|
101
95
|
|
|
102
96
|
|
|
103
97
|
@router.get("/{step_id}/trace", response_model=Optional[ProviderTrace], operation_id="retrieve_trace_for_step")
|
|
104
98
|
async def retrieve_trace_for_step(
|
|
105
|
-
step_id:
|
|
99
|
+
step_id: StepId,
|
|
106
100
|
server: SyncServer = Depends(get_letta_server),
|
|
107
101
|
headers: HeaderParams = Depends(get_headers),
|
|
108
102
|
):
|
|
@@ -125,7 +119,7 @@ class ModifyFeedbackRequest(BaseModel):
|
|
|
125
119
|
|
|
126
120
|
@router.patch("/{step_id}/feedback", response_model=Step, operation_id="modify_feedback_for_step")
|
|
127
121
|
async def modify_feedback_for_step(
|
|
128
|
-
step_id:
|
|
122
|
+
step_id: StepId,
|
|
129
123
|
request: ModifyFeedbackRequest = Body(...),
|
|
130
124
|
headers: HeaderParams = Depends(get_headers),
|
|
131
125
|
server: SyncServer = Depends(get_letta_server),
|
|
@@ -133,16 +127,13 @@ async def modify_feedback_for_step(
|
|
|
133
127
|
"""
|
|
134
128
|
Modify feedback for a given step.
|
|
135
129
|
"""
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
return await server.step_manager.add_feedback_async(step_id=step_id, feedback=request.feedback, tags=request.tags, actor=actor)
|
|
139
|
-
except NoResultFound:
|
|
140
|
-
raise HTTPException(status_code=404, detail="Step not found")
|
|
130
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=headers.actor_id)
|
|
131
|
+
return await server.step_manager.add_feedback_async(step_id=step_id, feedback=request.feedback, tags=request.tags, actor=actor)
|
|
141
132
|
|
|
142
133
|
|
|
143
134
|
@router.get("/{step_id}/messages", response_model=List[LettaMessageUnion], operation_id="list_messages_for_step")
|
|
144
135
|
async def list_messages_for_step(
|
|
145
|
-
step_id:
|
|
136
|
+
step_id: StepId,
|
|
146
137
|
headers: HeaderParams = Depends(get_headers),
|
|
147
138
|
server: SyncServer = Depends(get_letta_server),
|
|
148
139
|
before: Optional[str] = Query(
|
|
@@ -169,8 +160,8 @@ async def list_messages_for_step(
|
|
|
169
160
|
|
|
170
161
|
@router.patch("/{step_id}/transaction/{transaction_id}", response_model=Step, operation_id="update_step_transaction_id")
|
|
171
162
|
async def update_step_transaction_id(
|
|
172
|
-
step_id: str,
|
|
173
163
|
transaction_id: str,
|
|
164
|
+
step_id: StepId,
|
|
174
165
|
headers: HeaderParams = Depends(get_headers),
|
|
175
166
|
server: SyncServer = Depends(get_letta_server),
|
|
176
167
|
):
|
|
@@ -178,8 +169,4 @@ async def update_step_transaction_id(
|
|
|
178
169
|
Update the transaction ID for a step.
|
|
179
170
|
"""
|
|
180
171
|
actor = server.user_manager.get_user_or_default(user_id=headers.actor_id)
|
|
181
|
-
|
|
182
|
-
try:
|
|
183
|
-
return await server.step_manager.update_step_transaction_id(actor=actor, step_id=step_id, transaction_id=transaction_id)
|
|
184
|
-
except NoResultFound:
|
|
185
|
-
raise HTTPException(status_code=404, detail="Step not found")
|
|
172
|
+
return await server.step_manager.update_step_transaction_id(actor=actor, step_id=step_id, transaction_id=transaction_id)
|
|
@@ -30,14 +30,17 @@ from letta.schemas.letta_message_content import TextContent
|
|
|
30
30
|
from letta.schemas.mcp import UpdateSSEMCPServer, UpdateStdioMCPServer, UpdateStreamableHTTPMCPServer
|
|
31
31
|
from letta.schemas.message import Message
|
|
32
32
|
from letta.schemas.pip_requirement import PipRequirement
|
|
33
|
-
from letta.schemas.tool import Tool, ToolCreate, ToolRunFromSource, ToolUpdate
|
|
33
|
+
from letta.schemas.tool import BaseTool, Tool, ToolCreate, ToolRunFromSource, ToolUpdate
|
|
34
34
|
from letta.server.rest_api.dependencies import HeaderParams, get_headers, get_letta_server
|
|
35
35
|
from letta.server.rest_api.streaming_response import StreamingResponseWithStatusCode
|
|
36
36
|
from letta.server.server import SyncServer
|
|
37
37
|
from letta.services.mcp.oauth_utils import MCPOAuthSession, drill_down_exception, oauth_stream_event
|
|
38
38
|
from letta.services.mcp.stdio_client import AsyncStdioMCPClient
|
|
39
39
|
from letta.services.mcp.types import OauthStreamEvent
|
|
40
|
+
from letta.services.summarizer.summarizer import traceback
|
|
40
41
|
from letta.settings import tool_settings
|
|
42
|
+
from letta.utils import asyncio
|
|
43
|
+
from letta.validators import ToolId
|
|
41
44
|
|
|
42
45
|
router = APIRouter(prefix="/tools", tags=["tools"])
|
|
43
46
|
|
|
@@ -46,7 +49,7 @@ logger = get_logger(__name__)
|
|
|
46
49
|
|
|
47
50
|
@router.delete("/{tool_id}", operation_id="delete_tool")
|
|
48
51
|
async def delete_tool(
|
|
49
|
-
tool_id:
|
|
52
|
+
tool_id: ToolId,
|
|
50
53
|
server: SyncServer = Depends(get_letta_server),
|
|
51
54
|
headers: HeaderParams = Depends(get_headers),
|
|
52
55
|
):
|
|
@@ -149,7 +152,7 @@ async def count_tools(
|
|
|
149
152
|
|
|
150
153
|
@router.get("/{tool_id}", response_model=Tool, operation_id="retrieve_tool")
|
|
151
154
|
async def retrieve_tool(
|
|
152
|
-
tool_id:
|
|
155
|
+
tool_id: ToolId,
|
|
153
156
|
server: SyncServer = Depends(get_letta_server),
|
|
154
157
|
headers: HeaderParams = Depends(get_headers),
|
|
155
158
|
):
|
|
@@ -297,7 +300,7 @@ async def upsert_tool(
|
|
|
297
300
|
|
|
298
301
|
@router.patch("/{tool_id}", response_model=Tool, operation_id="modify_tool")
|
|
299
302
|
async def modify_tool(
|
|
300
|
-
tool_id:
|
|
303
|
+
tool_id: ToolId,
|
|
301
304
|
request: ToolUpdate = Body(...),
|
|
302
305
|
server: SyncServer = Depends(get_letta_server),
|
|
303
306
|
headers: HeaderParams = Depends(get_headers),
|
|
@@ -443,14 +446,11 @@ async def add_mcp_tool(
|
|
|
443
446
|
argument_name="mcp_tool_name",
|
|
444
447
|
)
|
|
445
448
|
|
|
446
|
-
#
|
|
447
|
-
if mcp_tool.health:
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
mcp_tool_name=mcp_tool_name,
|
|
452
|
-
reasons=mcp_tool.health.reasons,
|
|
453
|
-
)
|
|
449
|
+
# Log warning if tool has invalid schema but allow attachment
|
|
450
|
+
if mcp_tool.health and mcp_tool.health.status == "INVALID":
|
|
451
|
+
logger.warning(
|
|
452
|
+
f"Attaching MCP tool {mcp_tool_name} from server {mcp_server_name} with invalid schema. Reasons: {mcp_tool.health.reasons}"
|
|
453
|
+
)
|
|
454
454
|
|
|
455
455
|
tool_create = ToolCreate.from_mcp(mcp_server_name=mcp_server_name, mcp_tool=mcp_tool)
|
|
456
456
|
# For config-based servers, use the server name as ID since they don't have database IDs
|
|
@@ -666,7 +666,11 @@ async def connect_mcp_server(
|
|
|
666
666
|
detailed_error = drill_down_exception(e)
|
|
667
667
|
logger.error(f"Error in OAuth stream:\n{detailed_error}")
|
|
668
668
|
yield oauth_stream_event(OauthStreamEvent.ERROR, message=f"Internal error: {detailed_error}")
|
|
669
|
-
|
|
669
|
+
# TODO: investigate cancelled by cancel scope errors here during oauth exchange flow
|
|
670
|
+
except asyncio.CancelledError as e:
|
|
671
|
+
logger.error(f"CancelledError: {e!r}")
|
|
672
|
+
tb = "".join(traceback.format_stack())
|
|
673
|
+
logger.error(f"Stack trace at cancellation:\n{tb}")
|
|
670
674
|
finally:
|
|
671
675
|
if client:
|
|
672
676
|
try:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, List, Optional
|
|
2
2
|
|
|
3
|
-
from fastapi import APIRouter, Body, Depends,
|
|
3
|
+
from fastapi import APIRouter, Body, Depends, Query
|
|
4
4
|
|
|
5
5
|
from letta.schemas.user import User, UserCreate, UserUpdate
|
|
6
6
|
from letta.server.rest_api.dependencies import get_letta_server
|
|
@@ -22,13 +22,7 @@ async def list_users(
|
|
|
22
22
|
"""
|
|
23
23
|
Get a list of all users in the database
|
|
24
24
|
"""
|
|
25
|
-
|
|
26
|
-
users = await server.user_manager.list_actors_async(after=after, limit=limit)
|
|
27
|
-
except HTTPException:
|
|
28
|
-
raise
|
|
29
|
-
except Exception as e:
|
|
30
|
-
raise HTTPException(status_code=500, detail=f"{e}")
|
|
31
|
-
return users
|
|
25
|
+
return await server.user_manager.list_actors_async(after=after, limit=limit)
|
|
32
26
|
|
|
33
27
|
|
|
34
28
|
@router.post("/", tags=["admin"], response_model=User, operation_id="create_user")
|
|
@@ -62,13 +56,7 @@ async def delete_user(
|
|
|
62
56
|
server: "SyncServer" = Depends(get_letta_server),
|
|
63
57
|
):
|
|
64
58
|
# TODO make a soft deletion, instead of a hard deletion
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
raise HTTPException(status_code=404, detail="User does not exist")
|
|
69
|
-
await server.user_manager.delete_actor_by_id_async(user_id=user_id)
|
|
70
|
-
except HTTPException:
|
|
71
|
-
raise
|
|
72
|
-
except Exception as e:
|
|
73
|
-
raise HTTPException(status_code=500, detail=f"{e}")
|
|
59
|
+
# Get the user first so we can return it after deletion
|
|
60
|
+
user = await server.user_manager.get_actor_by_id_async(actor_id=user_id)
|
|
61
|
+
await server.user_manager.delete_actor_by_id_async(user_id=user_id)
|
|
74
62
|
return user
|