letta-nightly 0.8.15.dev20250719104256__py3-none-any.whl → 0.8.16.dev20250721070720__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- letta/__init__.py +1 -1
- letta/agent.py +27 -11
- letta/agents/helpers.py +1 -1
- letta/agents/letta_agent.py +518 -322
- letta/agents/letta_agent_batch.py +1 -2
- letta/agents/voice_agent.py +15 -17
- letta/client/client.py +3 -3
- letta/constants.py +5 -0
- letta/embeddings.py +0 -2
- letta/errors.py +8 -0
- letta/functions/function_sets/base.py +3 -3
- letta/functions/helpers.py +2 -3
- letta/groups/sleeptime_multi_agent.py +0 -1
- letta/helpers/composio_helpers.py +2 -2
- letta/helpers/converters.py +1 -1
- letta/helpers/pinecone_utils.py +8 -0
- letta/helpers/tool_rule_solver.py +13 -18
- letta/llm_api/aws_bedrock.py +16 -2
- letta/llm_api/cohere.py +1 -1
- letta/llm_api/openai_client.py +1 -1
- letta/local_llm/grammars/gbnf_grammar_generator.py +1 -1
- letta/local_llm/llm_chat_completion_wrappers/zephyr.py +14 -14
- letta/local_llm/utils.py +1 -2
- letta/orm/agent.py +3 -3
- letta/orm/block.py +4 -4
- letta/orm/files_agents.py +0 -1
- letta/orm/identity.py +2 -0
- letta/orm/mcp_server.py +0 -2
- letta/orm/message.py +140 -14
- letta/orm/organization.py +5 -5
- letta/orm/passage.py +4 -4
- letta/orm/source.py +1 -1
- letta/orm/sqlalchemy_base.py +61 -39
- letta/orm/step.py +2 -0
- letta/otel/db_pool_monitoring.py +308 -0
- letta/otel/metric_registry.py +94 -1
- letta/otel/sqlalchemy_instrumentation.py +548 -0
- letta/otel/sqlalchemy_instrumentation_integration.py +124 -0
- letta/otel/tracing.py +37 -1
- letta/schemas/agent.py +0 -3
- letta/schemas/agent_file.py +283 -0
- letta/schemas/block.py +0 -3
- letta/schemas/file.py +28 -26
- letta/schemas/letta_message.py +15 -4
- letta/schemas/memory.py +1 -1
- letta/schemas/message.py +31 -26
- letta/schemas/openai/chat_completion_response.py +0 -1
- letta/schemas/providers.py +20 -0
- letta/schemas/source.py +11 -13
- letta/schemas/step.py +12 -0
- letta/schemas/tool.py +0 -4
- letta/serialize_schemas/marshmallow_agent.py +14 -1
- letta/serialize_schemas/marshmallow_block.py +23 -1
- letta/serialize_schemas/marshmallow_message.py +1 -3
- letta/serialize_schemas/marshmallow_tool.py +23 -1
- letta/server/db.py +110 -6
- letta/server/rest_api/app.py +85 -73
- letta/server/rest_api/routers/v1/agents.py +68 -53
- letta/server/rest_api/routers/v1/blocks.py +2 -2
- letta/server/rest_api/routers/v1/jobs.py +3 -0
- letta/server/rest_api/routers/v1/organizations.py +2 -2
- letta/server/rest_api/routers/v1/sources.py +18 -2
- letta/server/rest_api/routers/v1/tools.py +11 -12
- letta/server/rest_api/routers/v1/users.py +1 -1
- letta/server/rest_api/streaming_response.py +13 -5
- letta/server/rest_api/utils.py +8 -25
- letta/server/server.py +11 -4
- letta/server/ws_api/server.py +2 -2
- letta/services/agent_file_manager.py +616 -0
- letta/services/agent_manager.py +133 -46
- letta/services/block_manager.py +38 -17
- letta/services/file_manager.py +106 -21
- letta/services/file_processor/file_processor.py +93 -0
- letta/services/files_agents_manager.py +28 -0
- letta/services/group_manager.py +4 -5
- letta/services/helpers/agent_manager_helper.py +57 -9
- letta/services/identity_manager.py +22 -0
- letta/services/job_manager.py +210 -91
- letta/services/llm_batch_manager.py +9 -6
- letta/services/mcp/stdio_client.py +1 -2
- letta/services/mcp_manager.py +0 -1
- letta/services/message_manager.py +49 -26
- letta/services/passage_manager.py +0 -1
- letta/services/provider_manager.py +1 -1
- letta/services/source_manager.py +114 -5
- letta/services/step_manager.py +36 -4
- letta/services/telemetry_manager.py +9 -2
- letta/services/tool_executor/builtin_tool_executor.py +5 -1
- letta/services/tool_executor/core_tool_executor.py +3 -3
- letta/services/tool_manager.py +95 -20
- letta/services/user_manager.py +4 -12
- letta/settings.py +23 -6
- letta/system.py +1 -1
- letta/utils.py +26 -2
- {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/METADATA +3 -2
- {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/RECORD +99 -94
- {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/entry_points.txt +0 -0
letta/server/rest_api/app.py
CHANGED
@@ -2,8 +2,10 @@ import importlib.util
|
|
2
2
|
import json
|
3
3
|
import logging
|
4
4
|
import os
|
5
|
+
import platform
|
5
6
|
import sys
|
6
7
|
from contextlib import asynccontextmanager
|
8
|
+
from functools import partial
|
7
9
|
from pathlib import Path
|
8
10
|
from typing import Optional
|
9
11
|
|
@@ -34,32 +36,25 @@ from letta.server.db import db_registry
|
|
34
36
|
from letta.server.rest_api.auth.index import setup_auth_router # TODO: probably remove right?
|
35
37
|
from letta.server.rest_api.interface import StreamingServerInterface
|
36
38
|
from letta.server.rest_api.routers.openai.chat_completions.chat_completions import router as openai_chat_completions_router
|
37
|
-
|
38
|
-
# from letta.orm.utilities import get_db_session # TODO(ethan) reenable once we merge ORM
|
39
39
|
from letta.server.rest_api.routers.v1 import ROUTERS as v1_routes
|
40
40
|
from letta.server.rest_api.routers.v1.organizations import router as organizations_router
|
41
41
|
from letta.server.rest_api.routers.v1.users import router as users_router # TODO: decide on admin
|
42
42
|
from letta.server.rest_api.static_files import mount_static_files
|
43
|
+
from letta.server.rest_api.utils import SENTRY_ENABLED
|
43
44
|
from letta.server.server import SyncServer
|
44
45
|
from letta.settings import settings
|
45
46
|
|
46
|
-
|
47
|
+
if SENTRY_ENABLED:
|
48
|
+
import sentry_sdk
|
49
|
+
|
50
|
+
IS_WINDOWS = platform.system() == "Windows"
|
51
|
+
|
47
52
|
# NOTE(charles): @ethan I had to add this to get the global as the bottom to work
|
48
|
-
interface:
|
53
|
+
interface: type = StreamingServerInterface
|
49
54
|
server = SyncServer(default_interface_factory=lambda: interface())
|
50
55
|
logger = get_logger(__name__)
|
51
56
|
|
52
57
|
|
53
|
-
import logging
|
54
|
-
import platform
|
55
|
-
|
56
|
-
from fastapi import FastAPI
|
57
|
-
|
58
|
-
is_windows = platform.system() == "Windows"
|
59
|
-
|
60
|
-
log = logging.getLogger("uvicorn")
|
61
|
-
|
62
|
-
|
63
58
|
def generate_openapi_schema(app: FastAPI):
|
64
59
|
# Update the OpenAPI schema
|
65
60
|
if not app.openapi_schema:
|
@@ -157,6 +152,17 @@ async def lifespan(app_: FastAPI):
|
|
157
152
|
logger.info(f"[Worker {worker_id}] Scheduler shutdown completed")
|
158
153
|
except Exception as e:
|
159
154
|
logger.error(f"[Worker {worker_id}] Scheduler shutdown failed: {e}", exc_info=True)
|
155
|
+
|
156
|
+
# Cleanup SQLAlchemy instrumentation
|
157
|
+
if not settings.disable_tracing and settings.sqlalchemy_tracing:
|
158
|
+
try:
|
159
|
+
from letta.otel.sqlalchemy_instrumentation_integration import teardown_letta_db_instrumentation
|
160
|
+
|
161
|
+
teardown_letta_db_instrumentation()
|
162
|
+
logger.info(f"[Worker {worker_id}] SQLAlchemy instrumentation shutdown completed")
|
163
|
+
except Exception as e:
|
164
|
+
logger.warning(f"[Worker {worker_id}] SQLAlchemy instrumentation shutdown failed: {e}")
|
165
|
+
|
160
166
|
logger.info(f"[Worker {worker_id}] Lifespan shutdown completed")
|
161
167
|
|
162
168
|
|
@@ -166,9 +172,7 @@ def create_application() -> "FastAPI":
|
|
166
172
|
# server = SyncServer(default_interface_factory=lambda: interface())
|
167
173
|
print(f"\n[[ Letta server // v{letta_version} ]]")
|
168
174
|
|
169
|
-
if
|
170
|
-
import sentry_sdk
|
171
|
-
|
175
|
+
if SENTRY_ENABLED:
|
172
176
|
sentry_sdk.init(
|
173
177
|
dsn=os.getenv("SENTRY_DSN"),
|
174
178
|
traces_sample_rate=1.0,
|
@@ -176,6 +180,7 @@ def create_application() -> "FastAPI":
|
|
176
180
|
"continuous_profiling_auto_start": True,
|
177
181
|
},
|
178
182
|
)
|
183
|
+
logger.info("Sentry enabled.")
|
179
184
|
|
180
185
|
debug_mode = "--debug" in sys.argv
|
181
186
|
app = FastAPI(
|
@@ -188,31 +193,13 @@ def create_application() -> "FastAPI":
|
|
188
193
|
lifespan=lifespan,
|
189
194
|
)
|
190
195
|
|
191
|
-
|
192
|
-
|
193
|
-
return JSONResponse(
|
194
|
-
status_code=400,
|
195
|
-
content={
|
196
|
-
"detail": str(exc),
|
197
|
-
"expected_type": exc.expected_type,
|
198
|
-
"actual_type": exc.actual_type,
|
199
|
-
},
|
200
|
-
)
|
196
|
+
# === Exception Handlers ===
|
197
|
+
# TODO (cliandy): move to separate file
|
201
198
|
|
202
199
|
@app.exception_handler(Exception)
|
203
200
|
async def generic_error_handler(request: Request, exc: Exception):
|
204
|
-
|
205
|
-
|
206
|
-
print(f"Unhandled error: {str(exc)}")
|
207
|
-
|
208
|
-
import traceback
|
209
|
-
|
210
|
-
# Print the stack trace
|
211
|
-
print(f"Stack trace: {traceback.format_exc()}")
|
212
|
-
|
213
|
-
if (os.getenv("SENTRY_DSN") is not None) and (os.getenv("SENTRY_DSN") != ""):
|
214
|
-
import sentry_sdk
|
215
|
-
|
201
|
+
logger.error(f"Unhandled error: {str(exc)}", exc_info=True)
|
202
|
+
if SENTRY_ENABLED:
|
216
203
|
sentry_sdk.capture_exception(exc)
|
217
204
|
|
218
205
|
return JSONResponse(
|
@@ -224,62 +211,70 @@ def create_application() -> "FastAPI":
|
|
224
211
|
},
|
225
212
|
)
|
226
213
|
|
227
|
-
|
228
|
-
|
229
|
-
|
214
|
+
async def error_handler_with_code(request: Request, exc: Exception, code: int, detail: str | None = None):
|
215
|
+
logger.error(f"{type(exc).__name__}", exc_info=exc)
|
216
|
+
if SENTRY_ENABLED:
|
217
|
+
sentry_sdk.capture_exception(exc)
|
230
218
|
|
219
|
+
if not detail:
|
220
|
+
detail = str(exc)
|
231
221
|
return JSONResponse(
|
232
|
-
status_code=
|
233
|
-
content={"detail":
|
222
|
+
status_code=code,
|
223
|
+
content={"detail": detail},
|
234
224
|
)
|
235
225
|
|
236
|
-
|
237
|
-
|
238
|
-
|
226
|
+
_error_handler_400 = partial(error_handler_with_code, code=400)
|
227
|
+
_error_handler_404 = partial(error_handler_with_code, code=404)
|
228
|
+
_error_handler_404_agent = partial(_error_handler_404, detail="Agent not found")
|
229
|
+
_error_handler_404_user = partial(_error_handler_404, detail="User not found")
|
230
|
+
_error_handler_409 = partial(error_handler_with_code, code=409)
|
239
231
|
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
232
|
+
app.add_exception_handler(ValueError, _error_handler_400)
|
233
|
+
app.add_exception_handler(NoResultFound, _error_handler_404)
|
234
|
+
app.add_exception_handler(LettaAgentNotFoundError, _error_handler_404_agent)
|
235
|
+
app.add_exception_handler(LettaUserNotFoundError, _error_handler_404_user)
|
236
|
+
app.add_exception_handler(ForeignKeyConstraintViolationError, _error_handler_409)
|
237
|
+
app.add_exception_handler(UniqueConstraintViolationError, _error_handler_409)
|
244
238
|
|
245
|
-
@app.exception_handler(
|
246
|
-
async def
|
247
|
-
logger.error(
|
239
|
+
@app.exception_handler(IncompatibleAgentType)
|
240
|
+
async def handle_incompatible_agent_type(request: Request, exc: IncompatibleAgentType):
|
241
|
+
logger.error("Incompatible agent types. Expected: %s, Actual: %s", exc.expected_type, exc.actual_type)
|
242
|
+
if SENTRY_ENABLED:
|
243
|
+
sentry_sdk.capture_exception(exc)
|
248
244
|
|
249
245
|
return JSONResponse(
|
250
|
-
status_code=
|
251
|
-
content={
|
246
|
+
status_code=400,
|
247
|
+
content={
|
248
|
+
"detail": str(exc),
|
249
|
+
"expected_type": exc.expected_type,
|
250
|
+
"actual_type": exc.actual_type,
|
251
|
+
},
|
252
252
|
)
|
253
253
|
|
254
254
|
@app.exception_handler(DatabaseTimeoutError)
|
255
255
|
async def database_timeout_error_handler(request: Request, exc: DatabaseTimeoutError):
|
256
256
|
logger.error(f"Timeout occurred: {exc}. Original exception: {exc.original_exception}")
|
257
|
+
if SENTRY_ENABLED:
|
258
|
+
sentry_sdk.capture_exception(exc)
|
259
|
+
|
257
260
|
return JSONResponse(
|
258
261
|
status_code=503,
|
259
262
|
content={"detail": "The database is temporarily unavailable. Please try again later."},
|
260
263
|
)
|
261
264
|
|
262
|
-
@app.exception_handler(ValueError)
|
263
|
-
async def value_error_handler(request: Request, exc: ValueError):
|
264
|
-
return JSONResponse(status_code=400, content={"detail": str(exc)})
|
265
|
-
|
266
|
-
@app.exception_handler(LettaAgentNotFoundError)
|
267
|
-
async def agent_not_found_handler(request: Request, exc: LettaAgentNotFoundError):
|
268
|
-
return JSONResponse(status_code=404, content={"detail": "Agent not found"})
|
269
|
-
|
270
|
-
@app.exception_handler(LettaUserNotFoundError)
|
271
|
-
async def user_not_found_handler(request: Request, exc: LettaUserNotFoundError):
|
272
|
-
return JSONResponse(status_code=404, content={"detail": "User not found"})
|
273
|
-
|
274
265
|
@app.exception_handler(BedrockPermissionError)
|
275
266
|
async def bedrock_permission_error_handler(request, exc: BedrockPermissionError):
|
267
|
+
logger.error(f"Bedrock permission denied.")
|
268
|
+
if SENTRY_ENABLED:
|
269
|
+
sentry_sdk.capture_exception(exc)
|
270
|
+
|
276
271
|
return JSONResponse(
|
277
272
|
status_code=403,
|
278
273
|
content={
|
279
274
|
"error": {
|
280
275
|
"type": "bedrock_permission_denied",
|
281
276
|
"message": "Unable to access the required AI model. Please check your Bedrock permissions or contact support.",
|
282
|
-
"
|
277
|
+
"detail": {str(exc)},
|
283
278
|
}
|
284
279
|
},
|
285
280
|
)
|
@@ -290,6 +285,9 @@ def create_application() -> "FastAPI":
|
|
290
285
|
print(f"▶ Using secure mode with password: {random_password}")
|
291
286
|
app.add_middleware(CheckPasswordMiddleware)
|
292
287
|
|
288
|
+
# Add reverse proxy middleware to handle X-Forwarded-* headers
|
289
|
+
# app.add_middleware(ReverseProxyMiddleware, base_path=settings.server_base_path)
|
290
|
+
|
293
291
|
app.add_middleware(
|
294
292
|
CORSMiddleware,
|
295
293
|
allow_origins=settings.cors_origins,
|
@@ -314,6 +312,20 @@ def create_application() -> "FastAPI":
|
|
314
312
|
)
|
315
313
|
setup_metrics(endpoint=otlp_endpoint, app=app, service_name=service_name)
|
316
314
|
|
315
|
+
# Set up SQLAlchemy synchronous operation instrumentation
|
316
|
+
if settings.sqlalchemy_tracing:
|
317
|
+
from letta.otel.sqlalchemy_instrumentation_integration import setup_letta_db_instrumentation
|
318
|
+
|
319
|
+
try:
|
320
|
+
setup_letta_db_instrumentation(
|
321
|
+
enable_joined_monitoring=True, # Monitor joined loading operations
|
322
|
+
sql_truncate_length=1500, # Longer SQL statements for debugging
|
323
|
+
)
|
324
|
+
print("▶ SQLAlchemy synchronous operation instrumentation enabled")
|
325
|
+
except Exception as e:
|
326
|
+
logger.warning(f"Failed to setup SQLAlchemy instrumentation: {e}")
|
327
|
+
# Don't fail startup if instrumentation fails
|
328
|
+
|
317
329
|
for route in v1_routes:
|
318
330
|
app.include_router(route, prefix=API_PREFIX)
|
319
331
|
# this gives undocumented routes for "latest" and bare api calls.
|
@@ -371,8 +383,8 @@ def start_server(
|
|
371
383
|
|
372
384
|
# Experimental UV Loop Support
|
373
385
|
try:
|
374
|
-
if
|
375
|
-
print("Running server on uvloop...")
|
386
|
+
if settings.use_uvloop:
|
387
|
+
print("Running server asyncio loop on uvloop...")
|
376
388
|
import asyncio
|
377
389
|
|
378
390
|
import uvloop
|
@@ -383,7 +395,7 @@ def start_server(
|
|
383
395
|
|
384
396
|
if (os.getenv("LOCAL_HTTPS") == "true") or "--localhttps" in sys.argv:
|
385
397
|
print(f"▶ Server running at: https://{host or 'localhost'}:{port or REST_DEFAULT_PORT}")
|
386
|
-
print(
|
398
|
+
print("▶ View using ADE at: https://app.letta.com/development-servers/local/dashboard\n")
|
387
399
|
if importlib.util.find_spec("granian") is not None and settings.use_granian:
|
388
400
|
from granian import Granian
|
389
401
|
|
@@ -417,7 +429,7 @@ def start_server(
|
|
417
429
|
)
|
418
430
|
|
419
431
|
else:
|
420
|
-
if
|
432
|
+
if IS_WINDOWS:
|
421
433
|
# Windows doesn't those the fancy unicode characters
|
422
434
|
print(f"Server running at: http://{host or 'localhost'}:{port or REST_DEFAULT_PORT}")
|
423
435
|
print(f"View using ADE at: https://app.letta.com/development-servers/local/dashboard\n")
|
@@ -636,6 +636,9 @@ async def list_messages(
|
|
636
636
|
use_assistant_message: bool = Query(True, description="Whether to use assistant messages"),
|
637
637
|
assistant_message_tool_name: str = Query(DEFAULT_MESSAGE_TOOL, description="The name of the designated message tool."),
|
638
638
|
assistant_message_tool_kwarg: str = Query(DEFAULT_MESSAGE_TOOL_KWARG, description="The name of the message argument."),
|
639
|
+
include_err: bool | None = Query(
|
640
|
+
None, description="Whether to include error messages and error statuses. For debugging purposes only."
|
641
|
+
),
|
639
642
|
actor_id: str | None = Header(None, alias="user_id"), # Extract user_id from header, default to None if not present
|
640
643
|
):
|
641
644
|
"""
|
@@ -654,6 +657,7 @@ async def list_messages(
|
|
654
657
|
use_assistant_message=use_assistant_message,
|
655
658
|
assistant_message_tool_name=assistant_message_tool_name,
|
656
659
|
assistant_message_tool_kwarg=assistant_message_tool_kwarg,
|
660
|
+
include_err=include_err,
|
657
661
|
actor=actor,
|
658
662
|
)
|
659
663
|
|
@@ -701,28 +705,32 @@ async def send_message(
|
|
701
705
|
model_compatible = agent.llm_config.model_endpoint_type in ["anthropic", "openai", "together", "google_ai", "google_vertex", "bedrock"]
|
702
706
|
|
703
707
|
# Create a new run for execution tracking
|
704
|
-
|
705
|
-
|
706
|
-
|
707
|
-
|
708
|
-
|
709
|
-
|
710
|
-
|
711
|
-
|
712
|
-
|
713
|
-
|
714
|
-
|
715
|
-
|
716
|
-
|
717
|
-
|
708
|
+
if settings.track_agent_run:
|
709
|
+
job_status = JobStatus.created
|
710
|
+
run = await server.job_manager.create_job_async(
|
711
|
+
pydantic_job=Run(
|
712
|
+
user_id=actor.id,
|
713
|
+
status=job_status,
|
714
|
+
metadata={
|
715
|
+
"job_type": "send_message",
|
716
|
+
"agent_id": agent_id,
|
717
|
+
},
|
718
|
+
request_config=LettaRequestConfig(
|
719
|
+
use_assistant_message=request.use_assistant_message,
|
720
|
+
assistant_message_tool_name=request.assistant_message_tool_name,
|
721
|
+
assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
|
722
|
+
include_return_message_types=request.include_return_message_types,
|
723
|
+
),
|
718
724
|
),
|
719
|
-
|
720
|
-
|
721
|
-
|
725
|
+
actor=actor,
|
726
|
+
)
|
727
|
+
else:
|
728
|
+
run = None
|
729
|
+
|
722
730
|
job_update_metadata = None
|
723
731
|
# TODO (cliandy): clean this up
|
724
732
|
redis_client = await get_redis_client()
|
725
|
-
await redis_client.set(f"{REDIS_RUN_ID_PREFIX}:{agent_id}", run.id)
|
733
|
+
await redis_client.set(f"{REDIS_RUN_ID_PREFIX}:{agent_id}", run.id if run else None)
|
726
734
|
|
727
735
|
try:
|
728
736
|
if agent_eligible and model_compatible:
|
@@ -737,7 +745,7 @@ async def send_message(
|
|
737
745
|
job_manager=server.job_manager,
|
738
746
|
actor=actor,
|
739
747
|
group=agent.multi_agent_group,
|
740
|
-
current_run_id=run.id,
|
748
|
+
current_run_id=run.id if run else None,
|
741
749
|
)
|
742
750
|
else:
|
743
751
|
agent_loop = LettaAgent(
|
@@ -750,7 +758,7 @@ async def send_message(
|
|
750
758
|
actor=actor,
|
751
759
|
step_manager=server.step_manager,
|
752
760
|
telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
|
753
|
-
current_run_id=run.id,
|
761
|
+
current_run_id=run.id if run else None,
|
754
762
|
# summarizer settings to be added here
|
755
763
|
summarizer_mode=(
|
756
764
|
SummarizationMode.STATIC_MESSAGE_BUFFER
|
@@ -786,12 +794,13 @@ async def send_message(
|
|
786
794
|
job_status = JobStatus.failed
|
787
795
|
raise
|
788
796
|
finally:
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
797
|
+
if settings.track_agent_run:
|
798
|
+
await server.job_manager.safe_update_job_status_async(
|
799
|
+
job_id=run.id,
|
800
|
+
new_status=job_status,
|
801
|
+
actor=actor,
|
802
|
+
metadata=job_update_metadata,
|
803
|
+
)
|
795
804
|
|
796
805
|
|
797
806
|
# noinspection PyInconsistentReturns
|
@@ -832,29 +841,32 @@ async def send_message_streaming(
|
|
832
841
|
not_letta_endpoint = agent.llm_config.model_endpoint != LETTA_MODEL_ENDPOINT
|
833
842
|
|
834
843
|
# Create a new job for execution tracking
|
835
|
-
|
836
|
-
|
837
|
-
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
842
|
-
|
843
|
-
|
844
|
-
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
844
|
+
if settings.track_agent_run:
|
845
|
+
job_status = JobStatus.created
|
846
|
+
run = await server.job_manager.create_job_async(
|
847
|
+
pydantic_job=Run(
|
848
|
+
user_id=actor.id,
|
849
|
+
status=job_status,
|
850
|
+
metadata={
|
851
|
+
"job_type": "send_message_streaming",
|
852
|
+
"agent_id": agent_id,
|
853
|
+
},
|
854
|
+
request_config=LettaRequestConfig(
|
855
|
+
use_assistant_message=request.use_assistant_message,
|
856
|
+
assistant_message_tool_name=request.assistant_message_tool_name,
|
857
|
+
assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
|
858
|
+
include_return_message_types=request.include_return_message_types,
|
859
|
+
),
|
849
860
|
),
|
850
|
-
|
851
|
-
|
852
|
-
|
861
|
+
actor=actor,
|
862
|
+
)
|
863
|
+
else:
|
864
|
+
run = None
|
853
865
|
|
854
866
|
job_update_metadata = None
|
855
867
|
# TODO (cliandy): clean this up
|
856
868
|
redis_client = await get_redis_client()
|
857
|
-
await redis_client.set(f"{REDIS_RUN_ID_PREFIX}:{agent_id}", run.id)
|
869
|
+
await redis_client.set(f"{REDIS_RUN_ID_PREFIX}:{agent_id}", run.id if run else None)
|
858
870
|
|
859
871
|
try:
|
860
872
|
if agent_eligible and model_compatible:
|
@@ -871,7 +883,7 @@ async def send_message_streaming(
|
|
871
883
|
step_manager=server.step_manager,
|
872
884
|
telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
|
873
885
|
group=agent.multi_agent_group,
|
874
|
-
current_run_id=run.id,
|
886
|
+
current_run_id=run.id if run else None,
|
875
887
|
)
|
876
888
|
else:
|
877
889
|
agent_loop = LettaAgent(
|
@@ -884,7 +896,7 @@ async def send_message_streaming(
|
|
884
896
|
actor=actor,
|
885
897
|
step_manager=server.step_manager,
|
886
898
|
telemetry_manager=server.telemetry_manager if settings.llm_api_logging else NoopTelemetryManager(),
|
887
|
-
current_run_id=run.id,
|
899
|
+
current_run_id=run.id if run else None,
|
888
900
|
# summarizer settings to be added here
|
889
901
|
summarizer_mode=(
|
890
902
|
SummarizationMode.STATIC_MESSAGE_BUFFER
|
@@ -937,12 +949,13 @@ async def send_message_streaming(
|
|
937
949
|
job_status = JobStatus.failed
|
938
950
|
raise
|
939
951
|
finally:
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
952
|
+
if settings.track_agent_run:
|
953
|
+
await server.job_manager.safe_update_job_status_async(
|
954
|
+
job_id=run.id,
|
955
|
+
new_status=job_status,
|
956
|
+
actor=actor,
|
957
|
+
metadata=job_update_metadata,
|
958
|
+
)
|
946
959
|
|
947
960
|
|
948
961
|
@router.post("/{agent_id}/messages/cancel", operation_id="cancel_agent_run")
|
@@ -959,6 +972,8 @@ async def cancel_agent_run(
|
|
959
972
|
"""
|
960
973
|
|
961
974
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
975
|
+
if not settings.track_agent_run:
|
976
|
+
raise HTTPException(status_code=400, detail="Agent run tracking is disabled")
|
962
977
|
if not run_ids:
|
963
978
|
redis_client = await get_redis_client()
|
964
979
|
run_id = await redis_client.get(f"{REDIS_RUN_ID_PREFIX}:{agent_id}")
|
@@ -1156,7 +1171,7 @@ async def list_agent_groups(
|
|
1156
1171
|
):
|
1157
1172
|
"""Lists the groups for an agent"""
|
1158
1173
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
1159
|
-
|
1174
|
+
logger.info("in list agents with manager_type", manager_type)
|
1160
1175
|
return server.agent_manager.list_groups(agent_id=agent_id, manager_type=manager_type, actor=actor)
|
1161
1176
|
|
1162
1177
|
|
@@ -72,14 +72,14 @@ async def modify_block(
|
|
72
72
|
return await server.block_manager.update_block_async(block_id=block_id, block_update=block_update, actor=actor)
|
73
73
|
|
74
74
|
|
75
|
-
@router.delete("/{block_id}",
|
75
|
+
@router.delete("/{block_id}", operation_id="delete_block")
|
76
76
|
async def delete_block(
|
77
77
|
block_id: str,
|
78
78
|
server: SyncServer = Depends(get_letta_server),
|
79
79
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
80
80
|
):
|
81
81
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
82
|
-
|
82
|
+
await server.block_manager.delete_block_async(block_id=block_id, actor=actor)
|
83
83
|
|
84
84
|
|
85
85
|
@router.get("/{block_id}", response_model=Block, operation_id="retrieve_block")
|
@@ -7,6 +7,7 @@ from letta.schemas.enums import JobStatus
|
|
7
7
|
from letta.schemas.job import Job
|
8
8
|
from letta.server.rest_api.utils import get_letta_server
|
9
9
|
from letta.server.server import SyncServer
|
10
|
+
from letta.settings import settings
|
10
11
|
|
11
12
|
router = APIRouter(prefix="/jobs", tags=["jobs"])
|
12
13
|
|
@@ -93,6 +94,8 @@ async def cancel_job(
|
|
93
94
|
agent execution to terminate as soon as possible.
|
94
95
|
"""
|
95
96
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
97
|
+
if not settings.track_agent_run:
|
98
|
+
raise HTTPException(status_code=400, detail="Agent run tracking is disabled")
|
96
99
|
|
97
100
|
try:
|
98
101
|
# First check if the job exists and is in a cancellable state
|
@@ -52,7 +52,7 @@ async def delete_org(
|
|
52
52
|
try:
|
53
53
|
org = await server.organization_manager.get_organization_by_id_async(org_id=org_id)
|
54
54
|
if org is None:
|
55
|
-
raise HTTPException(status_code=404, detail=
|
55
|
+
raise HTTPException(status_code=404, detail="Organization does not exist")
|
56
56
|
await server.organization_manager.delete_organization_by_id_async(org_id=org_id)
|
57
57
|
except HTTPException:
|
58
58
|
raise
|
@@ -70,7 +70,7 @@ async def update_org(
|
|
70
70
|
try:
|
71
71
|
org = await server.organization_manager.get_organization_by_id_async(org_id=org_id)
|
72
72
|
if org is None:
|
73
|
-
raise HTTPException(status_code=404, detail=
|
73
|
+
raise HTTPException(status_code=404, detail="Organization does not exist")
|
74
74
|
org = await server.organization_manager.update_organization_async(org_id=org_id, name=request.name)
|
75
75
|
except HTTPException:
|
76
76
|
raise
|
@@ -99,6 +99,7 @@ async def get_source_id_by_name(
|
|
99
99
|
async def get_sources_metadata(
|
100
100
|
server: "SyncServer" = Depends(get_letta_server),
|
101
101
|
actor_id: Optional[str] = Header(None, alias="user_id"),
|
102
|
+
include_detailed_per_source_metadata: bool = False,
|
102
103
|
):
|
103
104
|
"""
|
104
105
|
Get aggregated metadata for all sources in an organization.
|
@@ -107,10 +108,12 @@ async def get_sources_metadata(
|
|
107
108
|
- Total number of sources
|
108
109
|
- Total number of files across all sources
|
109
110
|
- Total size of all files
|
110
|
-
- Per-source breakdown with file details (file_name, file_size per file)
|
111
|
+
- Per-source breakdown with file details (file_name, file_size per file) if include_detailed_per_source_metadata is True
|
111
112
|
"""
|
112
113
|
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
113
|
-
return await server.file_manager.get_organization_sources_metadata(
|
114
|
+
return await server.file_manager.get_organization_sources_metadata(
|
115
|
+
actor=actor, include_detailed_per_source_metadata=include_detailed_per_source_metadata
|
116
|
+
)
|
114
117
|
|
115
118
|
|
116
119
|
@router.get("/", response_model=List[Source], operation_id="list_sources")
|
@@ -321,6 +324,19 @@ async def upload_file_to_source(
|
|
321
324
|
return file_metadata
|
322
325
|
|
323
326
|
|
327
|
+
@router.get("/{source_id}/agents", response_model=List[str], operation_id="get_agents_for_source")
|
328
|
+
async def get_agents_for_source(
|
329
|
+
source_id: str,
|
330
|
+
server: SyncServer = Depends(get_letta_server),
|
331
|
+
actor_id: Optional[str] = Header(None, alias="user_id"),
|
332
|
+
):
|
333
|
+
"""
|
334
|
+
Get all agent IDs that have the specified source attached.
|
335
|
+
"""
|
336
|
+
actor = await server.user_manager.get_actor_or_default_async(actor_id=actor_id)
|
337
|
+
return await server.source_manager.get_agents_for_source_id(source_id=source_id, actor=actor)
|
338
|
+
|
339
|
+
|
324
340
|
@router.get("/{source_id}/passages", response_model=List[Passage], operation_id="list_source_passages")
|
325
341
|
async def list_source_passages(
|
326
342
|
source_id: str,
|