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.
Files changed (99) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +27 -11
  3. letta/agents/helpers.py +1 -1
  4. letta/agents/letta_agent.py +518 -322
  5. letta/agents/letta_agent_batch.py +1 -2
  6. letta/agents/voice_agent.py +15 -17
  7. letta/client/client.py +3 -3
  8. letta/constants.py +5 -0
  9. letta/embeddings.py +0 -2
  10. letta/errors.py +8 -0
  11. letta/functions/function_sets/base.py +3 -3
  12. letta/functions/helpers.py +2 -3
  13. letta/groups/sleeptime_multi_agent.py +0 -1
  14. letta/helpers/composio_helpers.py +2 -2
  15. letta/helpers/converters.py +1 -1
  16. letta/helpers/pinecone_utils.py +8 -0
  17. letta/helpers/tool_rule_solver.py +13 -18
  18. letta/llm_api/aws_bedrock.py +16 -2
  19. letta/llm_api/cohere.py +1 -1
  20. letta/llm_api/openai_client.py +1 -1
  21. letta/local_llm/grammars/gbnf_grammar_generator.py +1 -1
  22. letta/local_llm/llm_chat_completion_wrappers/zephyr.py +14 -14
  23. letta/local_llm/utils.py +1 -2
  24. letta/orm/agent.py +3 -3
  25. letta/orm/block.py +4 -4
  26. letta/orm/files_agents.py +0 -1
  27. letta/orm/identity.py +2 -0
  28. letta/orm/mcp_server.py +0 -2
  29. letta/orm/message.py +140 -14
  30. letta/orm/organization.py +5 -5
  31. letta/orm/passage.py +4 -4
  32. letta/orm/source.py +1 -1
  33. letta/orm/sqlalchemy_base.py +61 -39
  34. letta/orm/step.py +2 -0
  35. letta/otel/db_pool_monitoring.py +308 -0
  36. letta/otel/metric_registry.py +94 -1
  37. letta/otel/sqlalchemy_instrumentation.py +548 -0
  38. letta/otel/sqlalchemy_instrumentation_integration.py +124 -0
  39. letta/otel/tracing.py +37 -1
  40. letta/schemas/agent.py +0 -3
  41. letta/schemas/agent_file.py +283 -0
  42. letta/schemas/block.py +0 -3
  43. letta/schemas/file.py +28 -26
  44. letta/schemas/letta_message.py +15 -4
  45. letta/schemas/memory.py +1 -1
  46. letta/schemas/message.py +31 -26
  47. letta/schemas/openai/chat_completion_response.py +0 -1
  48. letta/schemas/providers.py +20 -0
  49. letta/schemas/source.py +11 -13
  50. letta/schemas/step.py +12 -0
  51. letta/schemas/tool.py +0 -4
  52. letta/serialize_schemas/marshmallow_agent.py +14 -1
  53. letta/serialize_schemas/marshmallow_block.py +23 -1
  54. letta/serialize_schemas/marshmallow_message.py +1 -3
  55. letta/serialize_schemas/marshmallow_tool.py +23 -1
  56. letta/server/db.py +110 -6
  57. letta/server/rest_api/app.py +85 -73
  58. letta/server/rest_api/routers/v1/agents.py +68 -53
  59. letta/server/rest_api/routers/v1/blocks.py +2 -2
  60. letta/server/rest_api/routers/v1/jobs.py +3 -0
  61. letta/server/rest_api/routers/v1/organizations.py +2 -2
  62. letta/server/rest_api/routers/v1/sources.py +18 -2
  63. letta/server/rest_api/routers/v1/tools.py +11 -12
  64. letta/server/rest_api/routers/v1/users.py +1 -1
  65. letta/server/rest_api/streaming_response.py +13 -5
  66. letta/server/rest_api/utils.py +8 -25
  67. letta/server/server.py +11 -4
  68. letta/server/ws_api/server.py +2 -2
  69. letta/services/agent_file_manager.py +616 -0
  70. letta/services/agent_manager.py +133 -46
  71. letta/services/block_manager.py +38 -17
  72. letta/services/file_manager.py +106 -21
  73. letta/services/file_processor/file_processor.py +93 -0
  74. letta/services/files_agents_manager.py +28 -0
  75. letta/services/group_manager.py +4 -5
  76. letta/services/helpers/agent_manager_helper.py +57 -9
  77. letta/services/identity_manager.py +22 -0
  78. letta/services/job_manager.py +210 -91
  79. letta/services/llm_batch_manager.py +9 -6
  80. letta/services/mcp/stdio_client.py +1 -2
  81. letta/services/mcp_manager.py +0 -1
  82. letta/services/message_manager.py +49 -26
  83. letta/services/passage_manager.py +0 -1
  84. letta/services/provider_manager.py +1 -1
  85. letta/services/source_manager.py +114 -5
  86. letta/services/step_manager.py +36 -4
  87. letta/services/telemetry_manager.py +9 -2
  88. letta/services/tool_executor/builtin_tool_executor.py +5 -1
  89. letta/services/tool_executor/core_tool_executor.py +3 -3
  90. letta/services/tool_manager.py +95 -20
  91. letta/services/user_manager.py +4 -12
  92. letta/settings.py +23 -6
  93. letta/system.py +1 -1
  94. letta/utils.py +26 -2
  95. {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/METADATA +3 -2
  96. {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/RECORD +99 -94
  97. {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/LICENSE +0 -0
  98. {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/WHEEL +0 -0
  99. {letta_nightly-0.8.15.dev20250719104256.dist-info → letta_nightly-0.8.16.dev20250721070720.dist-info}/entry_points.txt +0 -0
@@ -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
- # TODO(ethan)
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: StreamingServerInterface = StreamingServerInterface
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 (os.getenv("SENTRY_DSN") is not None) and (os.getenv("SENTRY_DSN") != ""):
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
- @app.exception_handler(IncompatibleAgentType)
192
- async def handle_incompatible_agent_type(request: Request, exc: IncompatibleAgentType):
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
- # Log the actual error for debugging
205
- log.error(f"Unhandled error: {str(exc)}", exc_info=True)
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
- @app.exception_handler(NoResultFound)
228
- async def no_result_found_handler(request: Request, exc: NoResultFound):
229
- logger.error(f"NoResultFound: {exc}")
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=404,
233
- content={"detail": str(exc)},
222
+ status_code=code,
223
+ content={"detail": detail},
234
224
  )
235
225
 
236
- @app.exception_handler(ForeignKeyConstraintViolationError)
237
- async def foreign_key_constraint_handler(request: Request, exc: ForeignKeyConstraintViolationError):
238
- logger.error(f"ForeignKeyConstraintViolationError: {exc}")
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
- return JSONResponse(
241
- status_code=409,
242
- content={"detail": str(exc)},
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(UniqueConstraintViolationError)
246
- async def unique_key_constraint_handler(request: Request, exc: UniqueConstraintViolationError):
247
- logger.error(f"UniqueConstraintViolationError: {exc}")
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=409,
251
- content={"detail": str(exc)},
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
- "details": {"model_arn": exc.model_arn, "reason": str(exc)},
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 importlib.util.find_spec("uvloop") is not None and settings.use_uvloop:
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(f"▶ View using ADE at: https://app.letta.com/development-servers/local/dashboard\n")
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 is_windows:
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
- job_status = JobStatus.created
705
- run = await server.job_manager.create_job_async(
706
- pydantic_job=Run(
707
- user_id=actor.id,
708
- status=job_status,
709
- metadata={
710
- "job_type": "send_message",
711
- "agent_id": agent_id,
712
- },
713
- request_config=LettaRequestConfig(
714
- use_assistant_message=request.use_assistant_message,
715
- assistant_message_tool_name=request.assistant_message_tool_name,
716
- assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
717
- include_return_message_types=request.include_return_message_types,
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
- actor=actor,
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
- await server.job_manager.safe_update_job_status_async(
790
- job_id=run.id,
791
- new_status=job_status,
792
- actor=actor,
793
- metadata=job_update_metadata,
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
- job_status = JobStatus.created
836
- run = await server.job_manager.create_job_async(
837
- pydantic_job=Run(
838
- user_id=actor.id,
839
- status=job_status,
840
- metadata={
841
- "job_type": "send_message_streaming",
842
- "agent_id": agent_id,
843
- },
844
- request_config=LettaRequestConfig(
845
- use_assistant_message=request.use_assistant_message,
846
- assistant_message_tool_name=request.assistant_message_tool_name,
847
- assistant_message_tool_kwarg=request.assistant_message_tool_kwarg,
848
- include_return_message_types=request.include_return_message_types,
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
- actor=actor,
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
- await server.job_manager.safe_update_job_status_async(
941
- job_id=run.id,
942
- new_status=job_status,
943
- actor=actor,
944
- metadata=job_update_metadata,
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
- print("in list agents with manager_type", manager_type)
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}", response_model=Block, operation_id="delete_block")
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
- return await server.block_manager.delete_block_async(block_id=block_id, actor=actor)
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=f"Organization does not exist")
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=f"Organization does not exist")
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(actor=actor)
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,