letta-nightly 0.8.0.dev20250606195656__py3-none-any.whl → 0.8.3.dev20250607000559__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 (105) hide show
  1. letta/__init__.py +1 -1
  2. letta/agent.py +16 -12
  3. letta/agents/base_agent.py +1 -1
  4. letta/agents/helpers.py +13 -2
  5. letta/agents/letta_agent.py +72 -34
  6. letta/agents/letta_agent_batch.py +1 -2
  7. letta/agents/voice_agent.py +19 -13
  8. letta/agents/voice_sleeptime_agent.py +23 -6
  9. letta/constants.py +18 -0
  10. letta/data_sources/__init__.py +0 -0
  11. letta/data_sources/redis_client.py +282 -0
  12. letta/errors.py +0 -4
  13. letta/functions/function_sets/files.py +58 -0
  14. letta/functions/schema_generator.py +18 -1
  15. letta/groups/sleeptime_multi_agent_v2.py +13 -3
  16. letta/helpers/datetime_helpers.py +47 -3
  17. letta/helpers/decorators.py +69 -0
  18. letta/{services/helpers/noop_helper.py → helpers/singleton.py} +5 -0
  19. letta/interfaces/anthropic_streaming_interface.py +43 -24
  20. letta/interfaces/openai_streaming_interface.py +21 -19
  21. letta/llm_api/anthropic.py +1 -1
  22. letta/llm_api/anthropic_client.py +30 -16
  23. letta/llm_api/google_vertex_client.py +1 -1
  24. letta/llm_api/helpers.py +36 -30
  25. letta/llm_api/llm_api_tools.py +1 -1
  26. letta/llm_api/llm_client_base.py +29 -1
  27. letta/llm_api/openai.py +1 -1
  28. letta/llm_api/openai_client.py +6 -8
  29. letta/local_llm/chat_completion_proxy.py +1 -1
  30. letta/memory.py +1 -1
  31. letta/orm/enums.py +1 -0
  32. letta/orm/file.py +80 -3
  33. letta/orm/files_agents.py +13 -0
  34. letta/orm/passage.py +2 -0
  35. letta/orm/sqlalchemy_base.py +34 -11
  36. letta/otel/__init__.py +0 -0
  37. letta/otel/context.py +25 -0
  38. letta/otel/events.py +0 -0
  39. letta/otel/metric_registry.py +122 -0
  40. letta/otel/metrics.py +66 -0
  41. letta/otel/resource.py +26 -0
  42. letta/{tracing.py → otel/tracing.py} +55 -78
  43. letta/plugins/README.md +22 -0
  44. letta/plugins/__init__.py +0 -0
  45. letta/plugins/defaults.py +11 -0
  46. letta/plugins/plugins.py +72 -0
  47. letta/schemas/enums.py +8 -0
  48. letta/schemas/file.py +12 -0
  49. letta/schemas/letta_request.py +6 -0
  50. letta/schemas/passage.py +1 -0
  51. letta/schemas/tool.py +4 -0
  52. letta/server/db.py +7 -7
  53. letta/server/rest_api/app.py +8 -6
  54. letta/server/rest_api/routers/v1/agents.py +46 -37
  55. letta/server/rest_api/routers/v1/groups.py +3 -3
  56. letta/server/rest_api/routers/v1/sources.py +26 -3
  57. letta/server/rest_api/routers/v1/tools.py +7 -2
  58. letta/server/rest_api/utils.py +9 -6
  59. letta/server/server.py +25 -13
  60. letta/services/agent_manager.py +186 -194
  61. letta/services/block_manager.py +1 -1
  62. letta/services/context_window_calculator/context_window_calculator.py +1 -1
  63. letta/services/context_window_calculator/token_counter.py +3 -2
  64. letta/services/file_processor/chunker/line_chunker.py +34 -0
  65. letta/services/file_processor/file_processor.py +43 -12
  66. letta/services/file_processor/parser/mistral_parser.py +11 -1
  67. letta/services/files_agents_manager.py +96 -7
  68. letta/services/group_manager.py +6 -6
  69. letta/services/helpers/agent_manager_helper.py +404 -3
  70. letta/services/identity_manager.py +1 -1
  71. letta/services/job_manager.py +1 -1
  72. letta/services/llm_batch_manager.py +1 -1
  73. letta/services/mcp/stdio_client.py +5 -1
  74. letta/services/mcp_manager.py +4 -4
  75. letta/services/message_manager.py +1 -1
  76. letta/services/organization_manager.py +1 -1
  77. letta/services/passage_manager.py +604 -19
  78. letta/services/per_agent_lock_manager.py +1 -1
  79. letta/services/provider_manager.py +1 -1
  80. letta/services/sandbox_config_manager.py +1 -1
  81. letta/services/source_manager.py +178 -19
  82. letta/services/step_manager.py +2 -2
  83. letta/services/summarizer/summarizer.py +1 -1
  84. letta/services/telemetry_manager.py +1 -1
  85. letta/services/tool_executor/builtin_tool_executor.py +117 -0
  86. letta/services/tool_executor/composio_tool_executor.py +53 -0
  87. letta/services/tool_executor/core_tool_executor.py +474 -0
  88. letta/services/tool_executor/files_tool_executor.py +138 -0
  89. letta/services/tool_executor/mcp_tool_executor.py +45 -0
  90. letta/services/tool_executor/multi_agent_tool_executor.py +123 -0
  91. letta/services/tool_executor/tool_execution_manager.py +34 -14
  92. letta/services/tool_executor/tool_execution_sandbox.py +1 -1
  93. letta/services/tool_executor/tool_executor.py +3 -802
  94. letta/services/tool_executor/tool_executor_base.py +43 -0
  95. letta/services/tool_manager.py +55 -59
  96. letta/services/tool_sandbox/e2b_sandbox.py +1 -1
  97. letta/services/tool_sandbox/local_sandbox.py +6 -3
  98. letta/services/user_manager.py +6 -3
  99. letta/settings.py +23 -2
  100. letta/utils.py +7 -2
  101. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/METADATA +4 -2
  102. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/RECORD +105 -83
  103. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/LICENSE +0 -0
  104. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/WHEEL +0 -0
  105. {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,122 @@
1
+ from dataclasses import dataclass, field
2
+ from functools import partial
3
+
4
+ from opentelemetry import metrics
5
+ from opentelemetry.metrics import Counter, Histogram
6
+
7
+ from letta.helpers.singleton import singleton
8
+ from letta.otel.metrics import get_letta_meter
9
+
10
+
11
+ @singleton
12
+ @dataclass(frozen=True)
13
+ class MetricRegistry:
14
+ """Registry of all application metrics
15
+
16
+ Metrics are composed of the following:
17
+ - name
18
+ - description
19
+ - unit: UCUM unit of the metric (i.e. 'By' for bytes, 'ms' for milliseconds, '1' for count
20
+ - bucket_bounds (list[float] | None): the explicit bucket bounds for histogram metrics
21
+
22
+ and instruments are of types Counter, Histogram, and Gauge
23
+
24
+ The relationship between the various models is as follows:
25
+ project_id -N:1-> base_template_id -N:1-> template_id -N:1-> agent_id
26
+ agent_id -1:1+-> model_name
27
+ agent_id -1:N -> tool_name
28
+ """
29
+
30
+ Instrument = Counter | Histogram
31
+ _metrics: dict[str, Instrument] = field(default_factory=dict, init=False)
32
+ _meter: metrics.Meter = field(init=False)
33
+
34
+ def __post_init__(self):
35
+ object.__setattr__(self, "_meter", get_letta_meter())
36
+
37
+ def _get_or_create_metric(self, name: str, factory):
38
+ """Lazy initialization of metrics."""
39
+ if name not in self._metrics:
40
+ self._metrics[name] = factory()
41
+ return self._metrics[name]
42
+
43
+ # (includes base attributes: project, template_base, template, agent)
44
+ @property
45
+ def user_message_counter(self) -> Counter:
46
+ return self._get_or_create_metric(
47
+ "count_user_message",
48
+ partial(
49
+ self._meter.create_counter,
50
+ name="count_user_message",
51
+ description="Counts the number of messages sent by the user",
52
+ unit="1",
53
+ ),
54
+ )
55
+
56
+ # (includes tool_name, tool_execution_success, & step_id on failure)
57
+ @property
58
+ def tool_execution_counter(self) -> Counter:
59
+ return self._get_or_create_metric(
60
+ "count_tool_execution",
61
+ partial(self._meter.create_counter, name="count_tool_execution", description="Counts the number of tools executed.", unit="1"),
62
+ )
63
+
64
+ # project_id + model
65
+ @property
66
+ def ttft_ms_histogram(self) -> Histogram:
67
+ return self._get_or_create_metric(
68
+ "hist_ttft_ms",
69
+ partial(self._meter.create_histogram, name="hist_ttft_ms", description="Histogram for the Time to First Token (ms)", unit="ms"),
70
+ )
71
+
72
+ # (includes model name)
73
+ @property
74
+ def llm_execution_time_ms_histogram(self) -> Histogram:
75
+ return self._get_or_create_metric(
76
+ "hist_llm_execution_time_ms",
77
+ partial(
78
+ self._meter.create_histogram,
79
+ name="hist_llm_execution_time_ms",
80
+ description="Histogram for LLM execution time (ms)",
81
+ unit="ms",
82
+ ),
83
+ )
84
+
85
+ # (includes tool name)
86
+ @property
87
+ def tool_execution_time_ms_histogram(self) -> Histogram:
88
+ return self._get_or_create_metric(
89
+ "hist_tool_execution_time_ms",
90
+ partial(
91
+ self._meter.create_histogram,
92
+ name="hist_tool_execution_time_ms",
93
+ description="Histogram for tool execution time (ms)",
94
+ unit="ms",
95
+ ),
96
+ )
97
+
98
+ # TODO (cliandy): instrument this
99
+ @property
100
+ def message_cost(self) -> Histogram:
101
+ return self._get_or_create_metric(
102
+ "hist_message_cost_usd",
103
+ partial(
104
+ self._meter.create_histogram,
105
+ name="hist_message_cost_usd",
106
+ description="Histogram for cost of messages (usd) per step",
107
+ unit="usd",
108
+ ),
109
+ )
110
+
111
+ # (includes model name)
112
+ @property
113
+ def message_output_tokens(self) -> Histogram:
114
+ return self._get_or_create_metric(
115
+ "hist_message_output_tokens",
116
+ partial(
117
+ self._meter.create_histogram,
118
+ name="hist_message_output_tokens",
119
+ description="Histogram for output tokens generated by LLM per step",
120
+ unit="1",
121
+ ),
122
+ )
letta/otel/metrics.py ADDED
@@ -0,0 +1,66 @@
1
+ from fastapi import FastAPI, Request
2
+ from opentelemetry import metrics
3
+ from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
4
+ from opentelemetry.metrics import NoOpMeter
5
+ from opentelemetry.sdk.metrics import MeterProvider
6
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
7
+
8
+ from letta.log import get_logger
9
+ from letta.otel.context import add_ctx_attribute
10
+ from letta.otel.resource import get_resource, is_pytest_environment
11
+
12
+ logger = get_logger(__name__)
13
+
14
+ _meter: metrics.Meter = NoOpMeter("noop")
15
+ _is_metrics_initialized: bool = False
16
+
17
+
18
+ async def _otel_metric_middleware(request: Request, call_next):
19
+ if not _is_metrics_initialized:
20
+ return await call_next(request)
21
+
22
+ header_attributes = {
23
+ "x-organization-id": "organization.id",
24
+ "x-project-id": "project.id",
25
+ "x-base-template-id": "base_template.id",
26
+ "x-template-id": "template.id",
27
+ "x-agent-id": "agent.id",
28
+ }
29
+ try:
30
+ for header_key, otel_key in header_attributes.items():
31
+ header_value = request.headers.get(header_key)
32
+ if header_value:
33
+ add_ctx_attribute(otel_key, header_value)
34
+ return await call_next(request)
35
+ except Exception:
36
+ raise
37
+
38
+
39
+ def setup_metrics(
40
+ endpoint: str,
41
+ app: FastAPI | None = None,
42
+ service_name: str = "memgpt-server",
43
+ ) -> None:
44
+ if is_pytest_environment():
45
+ return
46
+ assert endpoint
47
+
48
+ global _is_metrics_initialized, _meter
49
+
50
+ otlp_metric_exporter = OTLPMetricExporter(endpoint=endpoint)
51
+ metric_reader = PeriodicExportingMetricReader(exporter=otlp_metric_exporter)
52
+ meter_provider = MeterProvider(resource=get_resource(service_name), metric_readers=[metric_reader])
53
+ metrics.set_meter_provider(meter_provider)
54
+ _meter = metrics.get_meter(__name__)
55
+
56
+ if app:
57
+ app.middleware("http")(_otel_metric_middleware)
58
+
59
+ _is_metrics_initialized = True
60
+
61
+
62
+ def get_letta_meter() -> metrics.Meter | None:
63
+ """Returns the global letta meter if metrics are initialized."""
64
+ if not _is_metrics_initialized or isinstance(_meter, NoOpMeter):
65
+ logger.warning("Metrics are not initialized or meter is not available.")
66
+ return _meter
letta/otel/resource.py ADDED
@@ -0,0 +1,26 @@
1
+ import os
2
+ import sys
3
+ import uuid
4
+
5
+ from opentelemetry.sdk.resources import Resource
6
+
7
+ from letta import __version__ as letta_version
8
+
9
+ _resources = {}
10
+
11
+
12
+ def get_resource(service_name: str) -> Resource:
13
+ _env = os.getenv("LETTA_ENVIRONMENT")
14
+ if service_name not in _resources:
15
+ resource_dict = {
16
+ "service.name": service_name,
17
+ "letta.version": letta_version,
18
+ }
19
+ if _env != "PRODUCTION":
20
+ resource_dict["device.id"] = uuid.getnode() # MAC address as unique device identifier,
21
+ _resources[(service_name, _env)] = Resource.create(resource_dict)
22
+ return _resources[(service_name, _env)]
23
+
24
+
25
+ def is_pytest_environment():
26
+ return "pytest" in sys.modules
@@ -1,6 +1,5 @@
1
1
  import inspect
2
2
  import re
3
- import sys
4
3
  import time
5
4
  from functools import wraps
6
5
  from typing import Any, Dict, List, Optional
@@ -11,15 +10,18 @@ from fastapi.responses import JSONResponse
11
10
  from opentelemetry import trace
12
11
  from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
13
12
  from opentelemetry.instrumentation.requests import RequestsInstrumentor
14
- from opentelemetry.sdk.resources import Resource
15
13
  from opentelemetry.sdk.trace import TracerProvider
16
14
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
17
15
  from opentelemetry.trace import Status, StatusCode
18
16
 
19
- from letta import __version__ as letta_version
17
+ from letta.log import get_logger
18
+ from letta.otel.resource import get_resource, is_pytest_environment
19
+ from letta.settings import settings
20
20
 
21
+ logger = get_logger(__name__) # TODO: set up logger config for this
21
22
  tracer = trace.get_tracer(__name__)
22
23
  _is_tracing_initialized = False
24
+
23
25
  _excluded_v1_endpoints_regex: List[str] = [
24
26
  # "^GET /v1/agents/(?P<agent_id>[^/]+)/messages$",
25
27
  # "^GET /v1/agents/(?P<agent_id>[^/]+)/context$",
@@ -30,11 +32,7 @@ _excluded_v1_endpoints_regex: List[str] = [
30
32
  ]
31
33
 
32
34
 
33
- def is_pytest_environment():
34
- return "pytest" in sys.modules
35
-
36
-
37
- async def trace_request_middleware(request: Request, call_next):
35
+ async def _trace_request_middleware(request: Request, call_next):
38
36
  if not _is_tracing_initialized:
39
37
  return await call_next(request)
40
38
  initial_span_name = f"{request.method} {request.url.path}"
@@ -56,7 +54,7 @@ async def trace_request_middleware(request: Request, call_next):
56
54
  raise
57
55
 
58
56
 
59
- async def update_trace_attributes(request: Request):
57
+ async def _update_trace_attributes(request: Request):
60
58
  """Dependency to update trace attributes after FastAPI has processed the request"""
61
59
  if not _is_tracing_initialized:
62
60
  return
@@ -78,35 +76,19 @@ async def update_trace_attributes(request: Request):
78
76
  for key, value in request.path_params.items():
79
77
  span.set_attribute(f"http.{key}", value)
80
78
 
81
- # Add user ID if available
82
- user_id = request.headers.get("user_id")
83
- if user_id:
84
- span.set_attribute("user.id", user_id)
85
-
86
- # Add organization_id if available
87
- organization_id = request.headers.get("x-organization-id")
88
- if organization_id:
89
- span.set_attribute("organization.id", organization_id)
90
-
91
- # Add project_id if available
92
- project_id = request.headers.get("x-project-id")
93
- if project_id:
94
- span.set_attribute("project.id", project_id)
95
-
96
- # Add agent_id if available
97
- agent_id = request.headers.get("x-agent-id")
98
- if agent_id:
99
- span.set_attribute("agent.id", agent_id)
100
-
101
- # Add template_id if available
102
- template_id = request.headers.get("x-template-id")
103
- if template_id:
104
- span.set_attribute("template.id", template_id)
105
-
106
- # Add base_template_id if available
107
- base_template_id = request.headers.get("x-base-template-id")
108
- if base_template_id:
109
- span.set_attribute("base_template.id", base_template_id)
79
+ # Add the following headers to span if available
80
+ header_attributes = {
81
+ "user_id": "user.id",
82
+ "x-organization-id": "organization.id",
83
+ "x-project-id": "project.id",
84
+ "x-agent-id": "agent.id",
85
+ "x-template-id": "template.id",
86
+ "x-base-template-id": "base_template.id",
87
+ }
88
+ for header_key, span_key in header_attributes.items():
89
+ header_value = request.headers.get(header_key)
90
+ if header_value:
91
+ span.set_attribute(span_key, header_value)
110
92
 
111
93
  # Add request body if available
112
94
  try:
@@ -117,7 +99,7 @@ async def update_trace_attributes(request: Request):
117
99
  pass
118
100
 
119
101
 
120
- async def trace_error_handler(_request: Request, exc: Exception) -> JSONResponse:
102
+ async def _trace_error_handler(_request: Request, exc: Exception) -> JSONResponse:
121
103
  status_code = getattr(exc, "status_code", 500)
122
104
  error_msg = str(exc)
123
105
 
@@ -142,49 +124,44 @@ def setup_tracing(
142
124
  ) -> None:
143
125
  if is_pytest_environment():
144
126
  return
127
+ assert endpoint
145
128
 
146
129
  global _is_tracing_initialized
147
130
 
148
- provider = TracerProvider(resource=Resource.create({"service.name": service_name}))
149
- import uuid
131
+ tracer_provider = TracerProvider(resource=get_resource(service_name))
132
+ tracer_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint)))
133
+ _is_tracing_initialized = True
134
+ trace.set_tracer_provider(tracer_provider)
150
135
 
151
- provider = TracerProvider(
152
- resource=Resource.create(
153
- {
154
- "service.name": service_name,
155
- "device.id": uuid.getnode(), # MAC address as unique device identifier,
156
- "letta.version": letta_version,
157
- }
158
- )
159
- )
160
- if endpoint:
161
- provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint)))
162
- _is_tracing_initialized = True
163
- trace.set_tracer_provider(provider)
164
-
165
- def requests_callback(span: trace.Span, _: Any, response: Any) -> None:
166
- if hasattr(response, "status_code"):
167
- span.set_status(Status(StatusCode.OK if response.status_code < 400 else StatusCode.ERROR))
168
-
169
- RequestsInstrumentor().instrument(response_hook=requests_callback)
170
-
171
- if app:
172
- # Add middleware first
173
- app.middleware("http")(trace_request_middleware)
174
-
175
- # Add dependency to v1 routes
176
- from letta.server.rest_api.routers.v1 import ROUTERS as v1_routes
177
-
178
- for router in v1_routes:
179
- for route in router.routes:
180
- full_path = ((next(iter(route.methods)) + " ") if route.methods else "") + "/v1" + route.path
181
- if not any(re.match(regex, full_path) for regex in _excluded_v1_endpoints_regex):
182
- route.dependencies.append(Depends(update_trace_attributes))
183
-
184
- # Register exception handlers
185
- app.exception_handler(HTTPException)(trace_error_handler)
186
- app.exception_handler(RequestValidationError)(trace_error_handler)
187
- app.exception_handler(Exception)(trace_error_handler)
136
+ # Instrumentors (e.g., RequestsInstrumentor)
137
+ def requests_callback(span: trace.Span, _: Any, response: Any) -> None:
138
+ if hasattr(response, "status_code"):
139
+ span.set_status(Status(StatusCode.OK if response.status_code < 400 else StatusCode.ERROR))
140
+
141
+ RequestsInstrumentor().instrument(response_hook=requests_callback)
142
+
143
+ if settings.sqlalchemy_tracing:
144
+ from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
145
+
146
+ SQLAlchemyInstrumentor().instrument()
147
+
148
+ if app:
149
+ # Add middleware first
150
+ app.middleware("http")(_trace_request_middleware)
151
+
152
+ # Add dependency to v1 routes
153
+ from letta.server.rest_api.routers.v1 import ROUTERS as V1_ROUTES
154
+
155
+ for router in V1_ROUTES:
156
+ for route in router.routes:
157
+ full_path = ((next(iter(route.methods)) + " ") if route.methods else "") + "/v1" + route.path
158
+ if not any(re.match(regex, full_path) for regex in _excluded_v1_endpoints_regex):
159
+ route.dependencies.append(Depends(_update_trace_attributes))
160
+
161
+ # Register exception handlers for tracing
162
+ app.exception_handler(HTTPException)(_trace_error_handler)
163
+ app.exception_handler(RequestValidationError)(_trace_error_handler)
164
+ app.exception_handler(Exception)(_trace_error_handler)
188
165
 
189
166
 
190
167
  def trace_method(func):
@@ -0,0 +1,22 @@
1
+ ### Plugins
2
+
3
+ Plugins enable plug and play for various components.
4
+
5
+ Plugin configurations can be set in `letta.settings.settings`.
6
+
7
+ The plugins will take a delimited list of consisting of individual plugin configs:
8
+
9
+ `<plugin_name>.<config_name>=<class_or_function>`
10
+
11
+ joined by `;`
12
+
13
+ In the default configuration, the top level keys have values `plugin_name`,
14
+ the `config_name` is nested under and the `class_or_function` is defined
15
+ after in format `<module_path>:<name>`.
16
+
17
+ ```
18
+ DEFAULT_PLUGINS = {
19
+ "experimental_check": {
20
+ "default": "letta.plugins.defaults:is_experimental_enabled",
21
+ ...
22
+ ```
File without changes
@@ -0,0 +1,11 @@
1
+ from letta.settings import settings
2
+
3
+
4
+ def is_experimental_enabled(feature_name: str, **kwargs) -> bool:
5
+ if feature_name in ("async_agent_loop", "summarize"):
6
+ if not (kwargs.get("eligibility", False) and settings.use_experimental):
7
+ return False
8
+ return True
9
+
10
+ # Err on safety here, disabling experimental if not handled here.
11
+ return False
@@ -0,0 +1,72 @@
1
+ import importlib
2
+ from typing import Protocol, runtime_checkable
3
+
4
+ from letta.settings import settings
5
+
6
+
7
+ @runtime_checkable
8
+ class SummarizerProtocol(Protocol):
9
+ """What a summarizer must implement"""
10
+
11
+ async def summarize(self, text: str) -> str: ...
12
+ def get_name(self) -> str: ...
13
+
14
+
15
+ # Currently this supports one of each plugin type. This can be expanded in the future.
16
+ DEFAULT_PLUGINS = {
17
+ "experimental_check": {
18
+ "protocol": None,
19
+ "target": "letta.plugins.defaults:is_experimental_enabled",
20
+ },
21
+ "summarizer": {
22
+ "protocol": SummarizerProtocol,
23
+ "target": "letta.services.summarizer.summarizer:Summarizer",
24
+ },
25
+ }
26
+
27
+
28
+ def get_plugin(plugin_type: str):
29
+ """Get a plugin instance"""
30
+ plugin_register = dict(DEFAULT_PLUGINS, **settings.plugin_register_dict)
31
+ if plugin_type in plugin_register:
32
+ impl_path = plugin_register[plugin_type]["target"]
33
+ module_path, name = impl_path.split(":")
34
+ module = importlib.import_module(module_path)
35
+ plugin = getattr(module, name)
36
+ if type(plugin).__name__ == "function":
37
+ return plugin
38
+ elif type(plugin).__name__ == "class":
39
+ if plugin_register["protocol"] and not isinstance(plugin, type(plugin_register["protocol"])):
40
+ raise TypeError(f'{plugin} does not implement {type(plugin_register["protocol"]).__name__}')
41
+ return plugin()
42
+ raise TypeError("Unknown plugin type")
43
+
44
+
45
+ _experimental_checker = None
46
+ _summarizer = None
47
+
48
+
49
+ # TODO handle coroutines
50
+ # Convenience functions
51
+ def get_experimental_checker():
52
+ global _experimental_checker
53
+ if _experimental_checker is None:
54
+ _experimental_checker = get_plugin("experimental_check")
55
+ return _experimental_checker
56
+
57
+
58
+ def get_summarizer():
59
+ global _summarizer
60
+ if _summarizer is None:
61
+ _summarizer = get_plugin("summarizer")
62
+ return _summarizer
63
+
64
+
65
+ def reset_experimental_checker():
66
+ global _experimental_checker
67
+ _experimental_checker = None
68
+
69
+
70
+ def reset_summarizer():
71
+ global _summarizer
72
+ _summarizer = None
letta/schemas/enums.py CHANGED
@@ -87,3 +87,11 @@ class ToolRuleType(str, Enum):
87
87
  constrain_child_tools = "constrain_child_tools"
88
88
  max_count_per_step = "max_count_per_step"
89
89
  parent_last_tool = "parent_last_tool"
90
+
91
+
92
+ class FileProcessingStatus(str, Enum):
93
+ PENDING = "pending"
94
+ PARSING = "parsing"
95
+ EMBEDDING = "embedding"
96
+ COMPLETED = "completed"
97
+ ERROR = "error"
letta/schemas/file.py CHANGED
@@ -4,6 +4,7 @@ from typing import Optional
4
4
 
5
5
  from pydantic import Field
6
6
 
7
+ from letta.schemas.enums import FileProcessingStatus
7
8
  from letta.schemas.letta_base import LettaBase
8
9
 
9
10
 
@@ -34,12 +35,22 @@ class FileMetadata(FileMetadataBase):
34
35
  file_size: Optional[int] = Field(None, description="The size of the file in bytes.")
35
36
  file_creation_date: Optional[str] = Field(None, description="The creation date of the file.")
36
37
  file_last_modified_date: Optional[str] = Field(None, description="The last modified date of the file.")
38
+ processing_status: FileProcessingStatus = Field(
39
+ default=FileProcessingStatus.PENDING,
40
+ description="The current processing status of the file (e.g. pending, parsing, embedding, completed, error).",
41
+ )
42
+ error_message: Optional[str] = Field(default=None, description="Optional error message if the file failed processing.")
37
43
 
38
44
  # orm metadata, optional fields
39
45
  created_at: Optional[datetime] = Field(default_factory=datetime.utcnow, description="The creation date of the file.")
40
46
  updated_at: Optional[datetime] = Field(default_factory=datetime.utcnow, description="The update date of the file.")
41
47
  is_deleted: bool = Field(False, description="Whether this file is deleted or not.")
42
48
 
49
+ # This is optional, and only occasionally pulled in since it can be very large
50
+ content: Optional[str] = Field(
51
+ default=None, description="Optional full-text content of the file; only populated on demand due to its size."
52
+ )
53
+
43
54
 
44
55
  class FileAgentBase(LettaBase):
45
56
  """Base class for the FileMetadata-⇄-Agent association schemas"""
@@ -67,6 +78,7 @@ class FileAgent(FileAgentBase):
67
78
  )
68
79
  agent_id: str = Field(..., description="Unique identifier of the agent.")
69
80
  file_id: str = Field(..., description="Unique identifier of the file.")
81
+ file_name: str = Field(..., description="Name of the file.")
70
82
  is_open: bool = Field(True, description="True if the agent currently has the file open.")
71
83
  visible_content: Optional[str] = Field(
72
84
  None,
@@ -3,6 +3,7 @@ from typing import List, Optional
3
3
  from pydantic import BaseModel, Field, HttpUrl
4
4
 
5
5
  from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG
6
+ from letta.schemas.letta_message import MessageType
6
7
  from letta.schemas.message import MessageCreate
7
8
 
8
9
 
@@ -21,6 +22,11 @@ class LettaRequest(BaseModel):
21
22
  description="The name of the message argument in the designated message tool.",
22
23
  )
23
24
 
25
+ # filter to only return specific message types
26
+ include_return_message_types: Optional[List[MessageType]] = Field(
27
+ default=None, description="Only return specified message types in the response. If `None` (default) returns all messages."
28
+ )
29
+
24
30
 
25
31
  class LettaStreamingRequest(LettaRequest):
26
32
  stream_tokens: bool = Field(
letta/schemas/passage.py CHANGED
@@ -23,6 +23,7 @@ class PassageBase(OrmMetadataBase):
23
23
 
24
24
  # file association
25
25
  file_id: Optional[str] = Field(None, description="The unique identifier of the file associated with the passage.")
26
+ file_name: Optional[str] = Field(None, description="The name of the file (only for source passages).")
26
27
  metadata: Optional[Dict] = Field({}, validation_alias="metadata_", description="The metadata of the passage.")
27
28
 
28
29
 
letta/schemas/tool.py CHANGED
@@ -7,6 +7,7 @@ from letta.constants import (
7
7
  FUNCTION_RETURN_CHAR_LIMIT,
8
8
  LETTA_BUILTIN_TOOL_MODULE_NAME,
9
9
  LETTA_CORE_TOOL_MODULE_NAME,
10
+ LETTA_FILES_TOOL_MODULE_NAME,
10
11
  LETTA_MULTI_AGENT_TOOL_MODULE_NAME,
11
12
  LETTA_VOICE_TOOL_MODULE_NAME,
12
13
  MCP_TOOL_TAG_NAME_PREFIX,
@@ -106,6 +107,9 @@ class Tool(BaseTool):
106
107
  elif self.tool_type in {ToolType.LETTA_BUILTIN}:
107
108
  # If it's letta voice tool, we generate the json_schema on the fly here
108
109
  self.json_schema = get_json_schema_from_module(module_name=LETTA_BUILTIN_TOOL_MODULE_NAME, function_name=self.name)
110
+ elif self.tool_type in {ToolType.LETTA_FILES_CORE}:
111
+ # If it's letta files tool, we generate the json_schema on the fly here
112
+ self.json_schema = get_json_schema_from_module(module_name=LETTA_FILES_TOOL_MODULE_NAME, function_name=self.name)
109
113
  elif self.tool_type in {ToolType.EXTERNAL_COMPOSIO}:
110
114
  # Composio schemas handled separately
111
115
  pass
letta/server/db.py CHANGED
@@ -13,8 +13,8 @@ from sqlalchemy.orm import sessionmaker
13
13
 
14
14
  from letta.config import LettaConfig
15
15
  from letta.log import get_logger
16
+ from letta.otel.tracing import trace_method
16
17
  from letta.settings import settings
17
- from letta.tracing import trace_method
18
18
 
19
19
  logger = get_logger(__name__)
20
20
 
@@ -131,7 +131,12 @@ class DatabaseRegistry:
131
131
  # Create async session factory
132
132
  self._async_engines["default"] = async_engine
133
133
  self._async_session_factories["default"] = async_sessionmaker(
134
- close_resets_only=False, autocommit=False, autoflush=False, bind=self._async_engines["default"], class_=AsyncSession
134
+ expire_on_commit=True,
135
+ close_resets_only=False,
136
+ autocommit=False,
137
+ autoflush=False,
138
+ bind=self._async_engines["default"],
139
+ class_=AsyncSession,
135
140
  )
136
141
  self._initialized["async"] = True
137
142
 
@@ -207,11 +212,6 @@ class DatabaseRegistry:
207
212
  self.initialize_sync()
208
213
  return self._engines.get(name)
209
214
 
210
- def get_async_engine(self, name: str = "default") -> AsyncEngine:
211
- """Get an async database engine by name."""
212
- self.initialize_async()
213
- return self._async_engines.get(name)
214
-
215
215
  def get_session_factory(self, name: str = "default") -> sessionmaker:
216
216
  """Get a session factory by name."""
217
217
  self.initialize_sync()