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.
- letta/__init__.py +1 -1
- letta/agent.py +16 -12
- letta/agents/base_agent.py +1 -1
- letta/agents/helpers.py +13 -2
- letta/agents/letta_agent.py +72 -34
- letta/agents/letta_agent_batch.py +1 -2
- letta/agents/voice_agent.py +19 -13
- letta/agents/voice_sleeptime_agent.py +23 -6
- letta/constants.py +18 -0
- letta/data_sources/__init__.py +0 -0
- letta/data_sources/redis_client.py +282 -0
- letta/errors.py +0 -4
- letta/functions/function_sets/files.py +58 -0
- letta/functions/schema_generator.py +18 -1
- letta/groups/sleeptime_multi_agent_v2.py +13 -3
- letta/helpers/datetime_helpers.py +47 -3
- letta/helpers/decorators.py +69 -0
- letta/{services/helpers/noop_helper.py → helpers/singleton.py} +5 -0
- letta/interfaces/anthropic_streaming_interface.py +43 -24
- letta/interfaces/openai_streaming_interface.py +21 -19
- letta/llm_api/anthropic.py +1 -1
- letta/llm_api/anthropic_client.py +30 -16
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/helpers.py +36 -30
- letta/llm_api/llm_api_tools.py +1 -1
- letta/llm_api/llm_client_base.py +29 -1
- letta/llm_api/openai.py +1 -1
- letta/llm_api/openai_client.py +6 -8
- letta/local_llm/chat_completion_proxy.py +1 -1
- letta/memory.py +1 -1
- letta/orm/enums.py +1 -0
- letta/orm/file.py +80 -3
- letta/orm/files_agents.py +13 -0
- letta/orm/passage.py +2 -0
- letta/orm/sqlalchemy_base.py +34 -11
- letta/otel/__init__.py +0 -0
- letta/otel/context.py +25 -0
- letta/otel/events.py +0 -0
- letta/otel/metric_registry.py +122 -0
- letta/otel/metrics.py +66 -0
- letta/otel/resource.py +26 -0
- letta/{tracing.py → otel/tracing.py} +55 -78
- letta/plugins/README.md +22 -0
- letta/plugins/__init__.py +0 -0
- letta/plugins/defaults.py +11 -0
- letta/plugins/plugins.py +72 -0
- letta/schemas/enums.py +8 -0
- letta/schemas/file.py +12 -0
- letta/schemas/letta_request.py +6 -0
- letta/schemas/passage.py +1 -0
- letta/schemas/tool.py +4 -0
- letta/server/db.py +7 -7
- letta/server/rest_api/app.py +8 -6
- letta/server/rest_api/routers/v1/agents.py +46 -37
- letta/server/rest_api/routers/v1/groups.py +3 -3
- letta/server/rest_api/routers/v1/sources.py +26 -3
- letta/server/rest_api/routers/v1/tools.py +7 -2
- letta/server/rest_api/utils.py +9 -6
- letta/server/server.py +25 -13
- letta/services/agent_manager.py +186 -194
- letta/services/block_manager.py +1 -1
- letta/services/context_window_calculator/context_window_calculator.py +1 -1
- letta/services/context_window_calculator/token_counter.py +3 -2
- letta/services/file_processor/chunker/line_chunker.py +34 -0
- letta/services/file_processor/file_processor.py +43 -12
- letta/services/file_processor/parser/mistral_parser.py +11 -1
- letta/services/files_agents_manager.py +96 -7
- letta/services/group_manager.py +6 -6
- letta/services/helpers/agent_manager_helper.py +404 -3
- letta/services/identity_manager.py +1 -1
- letta/services/job_manager.py +1 -1
- letta/services/llm_batch_manager.py +1 -1
- letta/services/mcp/stdio_client.py +5 -1
- letta/services/mcp_manager.py +4 -4
- letta/services/message_manager.py +1 -1
- letta/services/organization_manager.py +1 -1
- letta/services/passage_manager.py +604 -19
- letta/services/per_agent_lock_manager.py +1 -1
- letta/services/provider_manager.py +1 -1
- letta/services/sandbox_config_manager.py +1 -1
- letta/services/source_manager.py +178 -19
- letta/services/step_manager.py +2 -2
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/telemetry_manager.py +1 -1
- letta/services/tool_executor/builtin_tool_executor.py +117 -0
- letta/services/tool_executor/composio_tool_executor.py +53 -0
- letta/services/tool_executor/core_tool_executor.py +474 -0
- letta/services/tool_executor/files_tool_executor.py +138 -0
- letta/services/tool_executor/mcp_tool_executor.py +45 -0
- letta/services/tool_executor/multi_agent_tool_executor.py +123 -0
- letta/services/tool_executor/tool_execution_manager.py +34 -14
- letta/services/tool_executor/tool_execution_sandbox.py +1 -1
- letta/services/tool_executor/tool_executor.py +3 -802
- letta/services/tool_executor/tool_executor_base.py +43 -0
- letta/services/tool_manager.py +55 -59
- letta/services/tool_sandbox/e2b_sandbox.py +1 -1
- letta/services/tool_sandbox/local_sandbox.py +6 -3
- letta/services/user_manager.py +6 -3
- letta/settings.py +23 -2
- letta/utils.py +7 -2
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/METADATA +4 -2
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/RECORD +105 -83
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.0.dev20250606195656.dist-info → letta_nightly-0.8.3.dev20250607000559.dist-info}/WHEEL +0 -0
- {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
|
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
|
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
|
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
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
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
|
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
|
-
|
149
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
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):
|
letta/plugins/README.md
ADDED
@@ -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
|
letta/plugins/plugins.py
ADDED
@@ -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,
|
letta/schemas/letta_request.py
CHANGED
@@ -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
|
-
|
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()
|