letta-nightly 0.8.4.dev20250615104252__py3-none-any.whl → 0.8.4.dev20250616104355__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 -0
- letta/agents/base_agent.py +12 -1
- letta/agents/helpers.py +5 -2
- letta/agents/letta_agent.py +98 -61
- letta/agents/voice_sleeptime_agent.py +2 -1
- letta/constants.py +3 -5
- letta/data_sources/redis_client.py +30 -10
- letta/functions/function_sets/files.py +4 -4
- letta/functions/helpers.py +6 -1
- letta/functions/mcp_client/types.py +95 -0
- letta/groups/sleeptime_multi_agent_v2.py +2 -1
- letta/helpers/decorators.py +91 -0
- letta/interfaces/anthropic_streaming_interface.py +11 -0
- letta/interfaces/openai_streaming_interface.py +244 -225
- letta/llm_api/openai_client.py +1 -1
- letta/local_llm/utils.py +5 -1
- letta/orm/enums.py +1 -0
- letta/orm/mcp_server.py +3 -0
- letta/orm/tool.py +3 -0
- letta/otel/metric_registry.py +12 -0
- letta/otel/metrics.py +16 -7
- letta/schemas/letta_response.py +6 -1
- letta/schemas/letta_stop_reason.py +22 -0
- letta/schemas/mcp.py +48 -6
- letta/schemas/openai/chat_completion_request.py +1 -1
- letta/schemas/openai/chat_completion_response.py +1 -1
- letta/schemas/pip_requirement.py +14 -0
- letta/schemas/sandbox_config.py +1 -19
- letta/schemas/tool.py +5 -0
- letta/server/rest_api/json_parser.py +39 -3
- letta/server/rest_api/routers/v1/tools.py +3 -1
- letta/server/rest_api/routers/v1/voice.py +2 -3
- letta/server/rest_api/utils.py +1 -1
- letta/server/server.py +11 -2
- letta/services/agent_manager.py +37 -29
- letta/services/helpers/tool_execution_helper.py +39 -9
- letta/services/mcp/base_client.py +13 -2
- letta/services/mcp/sse_client.py +8 -1
- letta/services/mcp/streamable_http_client.py +56 -0
- letta/services/mcp_manager.py +23 -9
- letta/services/message_manager.py +30 -3
- letta/services/tool_executor/files_tool_executor.py +2 -3
- letta/services/tool_sandbox/e2b_sandbox.py +53 -3
- letta/services/tool_sandbox/local_sandbox.py +3 -1
- letta/services/user_manager.py +22 -0
- letta/settings.py +3 -0
- {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250616104355.dist-info}/METADATA +5 -6
- {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250616104355.dist-info}/RECORD +51 -48
- {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250616104355.dist-info}/LICENSE +0 -0
- {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250616104355.dist-info}/WHEEL +0 -0
- {letta_nightly-0.8.4.dev20250615104252.dist-info → letta_nightly-0.8.4.dev20250616104355.dist-info}/entry_points.txt +0 -0
letta/otel/metrics.py
CHANGED
@@ -5,18 +5,19 @@ from typing import List
|
|
5
5
|
from fastapi import FastAPI, Request
|
6
6
|
from opentelemetry import metrics
|
7
7
|
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
|
8
|
-
from opentelemetry.metrics import NoOpMeter
|
9
|
-
from opentelemetry.sdk.metrics import MeterProvider
|
10
|
-
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
|
8
|
+
from opentelemetry.metrics import Meter, NoOpMeter
|
9
|
+
from opentelemetry.sdk.metrics import Counter, Histogram, MeterProvider
|
10
|
+
from opentelemetry.sdk.metrics.export import AggregationTemporality, PeriodicExportingMetricReader
|
11
11
|
|
12
12
|
from letta.helpers.datetime_helpers import ns_to_ms
|
13
13
|
from letta.log import get_logger
|
14
14
|
from letta.otel.context import add_ctx_attribute, get_ctx_attributes
|
15
15
|
from letta.otel.resource import get_resource, is_pytest_environment
|
16
|
+
from letta.settings import settings
|
16
17
|
|
17
18
|
logger = get_logger(__name__)
|
18
19
|
|
19
|
-
_meter:
|
20
|
+
_meter: Meter = NoOpMeter("noop")
|
20
21
|
_is_metrics_initialized: bool = False
|
21
22
|
|
22
23
|
# Endpoints to include in endpoint metrics tracking (opt-in) vs tracing.py opt-out
|
@@ -110,9 +111,17 @@ def setup_metrics(
|
|
110
111
|
assert endpoint
|
111
112
|
|
112
113
|
global _is_metrics_initialized, _meter
|
113
|
-
|
114
|
-
otlp_metric_exporter = OTLPMetricExporter(
|
114
|
+
preferred_temporality = AggregationTemporality(settings.otel_preferred_temporality)
|
115
|
+
otlp_metric_exporter = OTLPMetricExporter(
|
116
|
+
endpoint=endpoint,
|
117
|
+
preferred_temporality={
|
118
|
+
# Add more as needed here.
|
119
|
+
Counter: preferred_temporality,
|
120
|
+
Histogram: preferred_temporality,
|
121
|
+
},
|
122
|
+
)
|
115
123
|
metric_reader = PeriodicExportingMetricReader(exporter=otlp_metric_exporter)
|
124
|
+
|
116
125
|
meter_provider = MeterProvider(resource=get_resource(service_name), metric_readers=[metric_reader])
|
117
126
|
metrics.set_meter_provider(meter_provider)
|
118
127
|
_meter = metrics.get_meter(__name__)
|
@@ -123,7 +132,7 @@ def setup_metrics(
|
|
123
132
|
_is_metrics_initialized = True
|
124
133
|
|
125
134
|
|
126
|
-
def get_letta_meter() ->
|
135
|
+
def get_letta_meter() -> Meter:
|
127
136
|
"""Returns the global letta meter if metrics are initialized."""
|
128
137
|
if not _is_metrics_initialized or isinstance(_meter, NoOpMeter):
|
129
138
|
logger.warning("Metrics are not initialized or meter is not available.")
|
letta/schemas/letta_response.py
CHANGED
@@ -9,6 +9,7 @@ from pydantic import BaseModel, Field
|
|
9
9
|
from letta.helpers.json_helpers import json_dumps
|
10
10
|
from letta.schemas.enums import JobStatus, MessageStreamStatus
|
11
11
|
from letta.schemas.letta_message import LettaMessage, LettaMessageUnion
|
12
|
+
from letta.schemas.letta_stop_reason import LettaStopReason
|
12
13
|
from letta.schemas.message import Message
|
13
14
|
from letta.schemas.usage import LettaUsageStatistics
|
14
15
|
|
@@ -34,6 +35,10 @@ class LettaResponse(BaseModel):
|
|
34
35
|
}
|
35
36
|
},
|
36
37
|
)
|
38
|
+
stop_reason: LettaStopReason = Field(
|
39
|
+
...,
|
40
|
+
description="The stop reason from Letta indicating why agent loop stopped execution.",
|
41
|
+
)
|
37
42
|
usage: LettaUsageStatistics = Field(
|
38
43
|
...,
|
39
44
|
description="The usage statistics of the agent.",
|
@@ -166,7 +171,7 @@ class LettaResponse(BaseModel):
|
|
166
171
|
|
167
172
|
|
168
173
|
# The streaming response is either [DONE], [DONE_STEP], [DONE], an error, or a LettaMessage
|
169
|
-
LettaStreamingResponse = Union[LettaMessage, MessageStreamStatus, LettaUsageStatistics]
|
174
|
+
LettaStreamingResponse = Union[LettaMessage, MessageStreamStatus, LettaStopReason, LettaUsageStatistics]
|
170
175
|
|
171
176
|
|
172
177
|
class LettaBatchResponse(BaseModel):
|
@@ -0,0 +1,22 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
from typing import Literal
|
3
|
+
|
4
|
+
from pydantic import BaseModel, Field
|
5
|
+
|
6
|
+
|
7
|
+
class StopReasonType(str, Enum):
|
8
|
+
end_turn = "end_turn"
|
9
|
+
error = "error"
|
10
|
+
invalid_tool_call = "invalid_tool_call"
|
11
|
+
max_steps = "max_steps"
|
12
|
+
no_tool_call = "no_tool_call"
|
13
|
+
tool_rule = "tool_rule"
|
14
|
+
|
15
|
+
|
16
|
+
class LettaStopReason(BaseModel):
|
17
|
+
"""
|
18
|
+
The stop reason from Letta indicating why agent loop stopped execution.
|
19
|
+
"""
|
20
|
+
|
21
|
+
message_type: Literal["stop_reason"] = Field("stop_reason", description="The type of the message.")
|
22
|
+
stop_reason: StopReasonType = Field(..., description="The reason why execution stopped.")
|
letta/schemas/mcp.py
CHANGED
@@ -2,7 +2,14 @@ from typing import Any, Dict, Optional, Union
|
|
2
2
|
|
3
3
|
from pydantic import Field
|
4
4
|
|
5
|
-
from letta.functions.mcp_client.types import
|
5
|
+
from letta.functions.mcp_client.types import (
|
6
|
+
MCP_AUTH_HEADER_AUTHORIZATION,
|
7
|
+
MCP_AUTH_TOKEN_BEARER_PREFIX,
|
8
|
+
MCPServerType,
|
9
|
+
SSEServerConfig,
|
10
|
+
StdioServerConfig,
|
11
|
+
StreamableHTTPServerConfig,
|
12
|
+
)
|
6
13
|
from letta.schemas.letta_base import LettaBase
|
7
14
|
|
8
15
|
|
@@ -17,6 +24,7 @@ class MCPServer(BaseMCPServer):
|
|
17
24
|
|
18
25
|
# sse config
|
19
26
|
server_url: Optional[str] = Field(None, description="The URL of the server (MCP SSE client will connect to this URL)")
|
27
|
+
token: Optional[str] = Field(None, description="The access token or API key for the MCP server (used for SSE authentication)")
|
20
28
|
|
21
29
|
# stdio config
|
22
30
|
stdio_config: Optional[StdioServerConfig] = Field(
|
@@ -30,22 +38,38 @@ class MCPServer(BaseMCPServer):
|
|
30
38
|
last_updated_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
|
31
39
|
metadata_: Optional[Dict[str, Any]] = Field(default_factory=dict, description="A dictionary of additional metadata for the tool.")
|
32
40
|
|
33
|
-
|
34
|
-
|
35
|
-
def to_config(self) -> Union[SSEServerConfig, StdioServerConfig]:
|
41
|
+
def to_config(self) -> Union[SSEServerConfig, StdioServerConfig, StreamableHTTPServerConfig]:
|
36
42
|
if self.server_type == MCPServerType.SSE:
|
37
43
|
return SSEServerConfig(
|
38
44
|
server_name=self.server_name,
|
39
45
|
server_url=self.server_url,
|
46
|
+
auth_header=MCP_AUTH_HEADER_AUTHORIZATION if self.token else None,
|
47
|
+
auth_token=f"{MCP_AUTH_TOKEN_BEARER_PREFIX} {self.token}" if self.token else None,
|
48
|
+
custom_headers=None,
|
40
49
|
)
|
41
50
|
elif self.server_type == MCPServerType.STDIO:
|
51
|
+
if self.stdio_config is None:
|
52
|
+
raise ValueError("stdio_config is required for STDIO server type")
|
42
53
|
return self.stdio_config
|
54
|
+
elif self.server_type == MCPServerType.STREAMABLE_HTTP:
|
55
|
+
if self.server_url is None:
|
56
|
+
raise ValueError("server_url is required for STREAMABLE_HTTP server type")
|
57
|
+
return StreamableHTTPServerConfig(
|
58
|
+
server_name=self.server_name,
|
59
|
+
server_url=self.server_url,
|
60
|
+
auth_header=MCP_AUTH_HEADER_AUTHORIZATION if self.token else None,
|
61
|
+
auth_token=f"{MCP_AUTH_TOKEN_BEARER_PREFIX} {self.token}" if self.token else None,
|
62
|
+
custom_headers=None,
|
63
|
+
)
|
64
|
+
else:
|
65
|
+
raise ValueError(f"Unsupported server type: {self.server_type}")
|
43
66
|
|
44
67
|
|
45
68
|
class RegisterSSEMCPServer(LettaBase):
|
46
69
|
server_name: str = Field(..., description="The name of the server")
|
47
70
|
server_type: MCPServerType = MCPServerType.SSE
|
48
71
|
server_url: str = Field(..., description="The URL of the server (MCP SSE client will connect to this URL)")
|
72
|
+
token: Optional[str] = Field(None, description="The access token or API key for the MCP server used for authentication")
|
49
73
|
|
50
74
|
|
51
75
|
class RegisterStdioMCPServer(LettaBase):
|
@@ -54,11 +78,20 @@ class RegisterStdioMCPServer(LettaBase):
|
|
54
78
|
stdio_config: StdioServerConfig = Field(..., description="The configuration for the server (MCP 'local' client will run this command)")
|
55
79
|
|
56
80
|
|
81
|
+
class RegisterStreamableHTTPMCPServer(LettaBase):
|
82
|
+
server_name: str = Field(..., description="The name of the server")
|
83
|
+
server_type: MCPServerType = MCPServerType.STREAMABLE_HTTP
|
84
|
+
server_url: str = Field(..., description="The URL path for the streamable HTTP server (e.g., 'example/mcp')")
|
85
|
+
auth_header: Optional[str] = Field(None, description="The name of the authentication header (e.g., 'Authorization')")
|
86
|
+
auth_token: Optional[str] = Field(None, description="The authentication token or API key value")
|
87
|
+
|
88
|
+
|
57
89
|
class UpdateSSEMCPServer(LettaBase):
|
58
90
|
"""Update an SSE MCP server"""
|
59
91
|
|
60
92
|
server_name: Optional[str] = Field(None, description="The name of the server")
|
61
93
|
server_url: Optional[str] = Field(None, description="The URL of the server (MCP SSE client will connect to this URL)")
|
94
|
+
token: Optional[str] = Field(None, description="The access token or API key for the MCP server (used for SSE authentication)")
|
62
95
|
|
63
96
|
|
64
97
|
class UpdateStdioMCPServer(LettaBase):
|
@@ -70,5 +103,14 @@ class UpdateStdioMCPServer(LettaBase):
|
|
70
103
|
)
|
71
104
|
|
72
105
|
|
73
|
-
|
74
|
-
|
106
|
+
class UpdateStreamableHTTPMCPServer(LettaBase):
|
107
|
+
"""Update a Streamable HTTP MCP server"""
|
108
|
+
|
109
|
+
server_name: Optional[str] = Field(None, description="The name of the server")
|
110
|
+
server_url: Optional[str] = Field(None, description="The URL path for the streamable HTTP server (e.g., 'example/mcp')")
|
111
|
+
auth_header: Optional[str] = Field(None, description="The name of the authentication header (e.g., 'Authorization')")
|
112
|
+
auth_token: Optional[str] = Field(None, description="The authentication token or API key value")
|
113
|
+
|
114
|
+
|
115
|
+
UpdateMCPServer = Union[UpdateSSEMCPServer, UpdateStdioMCPServer, UpdateStreamableHTTPMCPServer]
|
116
|
+
RegisterMCPServer = Union[RegisterSSEMCPServer, RegisterStdioMCPServer, RegisterStreamableHTTPMCPServer]
|
@@ -123,7 +123,7 @@ class ChatCompletionRequest(BaseModel):
|
|
123
123
|
logit_bias: Optional[Dict[str, int]] = None
|
124
124
|
logprobs: Optional[bool] = False
|
125
125
|
top_logprobs: Optional[int] = None
|
126
|
-
|
126
|
+
max_completion_tokens: Optional[int] = None
|
127
127
|
n: Optional[int] = 1
|
128
128
|
presence_penalty: Optional[float] = 0
|
129
129
|
response_format: Optional[ResponseFormat] = None
|
@@ -62,7 +62,7 @@ class Message(BaseModel):
|
|
62
62
|
reasoning_content: Optional[str] = None # Used in newer reasoning APIs, e.g. DeepSeek
|
63
63
|
reasoning_content_signature: Optional[str] = None # NOTE: for Anthropic
|
64
64
|
redacted_reasoning_content: Optional[str] = None # NOTE: for Anthropic
|
65
|
-
|
65
|
+
omitted_reasoning_content: bool = False # NOTE: for OpenAI o1/o3
|
66
66
|
|
67
67
|
|
68
68
|
class Choice(BaseModel):
|
@@ -0,0 +1,14 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
|
5
|
+
|
6
|
+
class PipRequirement(BaseModel):
|
7
|
+
name: str = Field(..., min_length=1, description="Name of the pip package.")
|
8
|
+
version: Optional[str] = Field(None, description="Optional version of the package, following semantic versioning.")
|
9
|
+
|
10
|
+
def __str__(self) -> str:
|
11
|
+
"""Return a pip-installable string format."""
|
12
|
+
if self.version:
|
13
|
+
return f"{self.name}=={self.version}"
|
14
|
+
return self.name
|
letta/schemas/sandbox_config.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
import hashlib
|
2
2
|
import json
|
3
|
-
import re
|
4
3
|
from enum import Enum
|
5
4
|
from typing import Any, Dict, List, Literal, Optional, Union
|
6
5
|
|
@@ -9,6 +8,7 @@ from pydantic import BaseModel, Field, model_validator
|
|
9
8
|
from letta.constants import LETTA_TOOL_EXECUTION_DIR
|
10
9
|
from letta.schemas.agent import AgentState
|
11
10
|
from letta.schemas.letta_base import LettaBase, OrmMetadataBase
|
11
|
+
from letta.schemas.pip_requirement import PipRequirement
|
12
12
|
from letta.settings import tool_settings
|
13
13
|
|
14
14
|
|
@@ -27,24 +27,6 @@ class SandboxRunResult(BaseModel):
|
|
27
27
|
sandbox_config_fingerprint: str = Field(None, description="The fingerprint of the config for the sandbox")
|
28
28
|
|
29
29
|
|
30
|
-
class PipRequirement(BaseModel):
|
31
|
-
name: str = Field(..., min_length=1, description="Name of the pip package.")
|
32
|
-
version: Optional[str] = Field(None, description="Optional version of the package, following semantic versioning.")
|
33
|
-
|
34
|
-
@classmethod
|
35
|
-
def validate_version(cls, version: Optional[str]) -> Optional[str]:
|
36
|
-
if version is None:
|
37
|
-
return None
|
38
|
-
semver_pattern = re.compile(r"^\d+(\.\d+){0,2}(-[a-zA-Z0-9.]+)?$")
|
39
|
-
if not semver_pattern.match(version):
|
40
|
-
raise ValueError(f"Invalid version format: {version}. Must follow semantic versioning (e.g., 1.2.3, 2.0, 1.5.0-alpha).")
|
41
|
-
return version
|
42
|
-
|
43
|
-
def __init__(self, **data):
|
44
|
-
super().__init__(**data)
|
45
|
-
self.version = self.validate_version(self.version)
|
46
|
-
|
47
|
-
|
48
30
|
class LocalSandboxConfig(BaseModel):
|
49
31
|
sandbox_dir: Optional[str] = Field(None, description="Directory for the sandbox environment.")
|
50
32
|
use_venv: bool = Field(False, description="Whether or not to use the venv, or run directly in the same run loop.")
|
letta/schemas/tool.py
CHANGED
@@ -24,6 +24,7 @@ from letta.functions.schema_generator import (
|
|
24
24
|
from letta.log import get_logger
|
25
25
|
from letta.orm.enums import ToolType
|
26
26
|
from letta.schemas.letta_base import LettaBase
|
27
|
+
from letta.schemas.pip_requirement import PipRequirement
|
27
28
|
|
28
29
|
logger = get_logger(__name__)
|
29
30
|
|
@@ -60,6 +61,7 @@ class Tool(BaseTool):
|
|
60
61
|
|
61
62
|
# tool configuration
|
62
63
|
return_char_limit: int = Field(FUNCTION_RETURN_CHAR_LIMIT, description="The maximum number of characters in the response.")
|
64
|
+
pip_requirements: Optional[List[PipRequirement]] = Field(None, description="Optional list of pip packages required by this tool.")
|
63
65
|
|
64
66
|
# metadata fields
|
65
67
|
created_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
|
@@ -145,6 +147,7 @@ class ToolCreate(LettaBase):
|
|
145
147
|
)
|
146
148
|
args_json_schema: Optional[Dict] = Field(None, description="The args JSON schema of the function.")
|
147
149
|
return_char_limit: int = Field(FUNCTION_RETURN_CHAR_LIMIT, description="The maximum number of characters in the response.")
|
150
|
+
pip_requirements: Optional[List[PipRequirement]] = Field(None, description="Optional list of pip packages required by this tool.")
|
148
151
|
|
149
152
|
# TODO should we put the HTTP / API fetch inside from_mcp?
|
150
153
|
# async def from_mcp(cls, mcp_server: str, mcp_tool_name: str) -> "ToolCreate":
|
@@ -253,6 +256,7 @@ class ToolUpdate(LettaBase):
|
|
253
256
|
)
|
254
257
|
args_json_schema: Optional[Dict] = Field(None, description="The args JSON schema of the function.")
|
255
258
|
return_char_limit: Optional[int] = Field(None, description="The maximum number of characters in the response.")
|
259
|
+
pip_requirements: Optional[List[PipRequirement]] = Field(None, description="Optional list of pip packages required by this tool.")
|
256
260
|
|
257
261
|
class Config:
|
258
262
|
extra = "ignore" # Allows extra fields without validation errors
|
@@ -269,3 +273,4 @@ class ToolRunFromSource(LettaBase):
|
|
269
273
|
json_schema: Optional[Dict] = Field(
|
270
274
|
None, description="The JSON schema of the function (auto-generated from source_code if not provided)"
|
271
275
|
)
|
276
|
+
pip_requirements: Optional[List[PipRequirement]] = Field(None, description="Optional list of pip packages required by this tool.")
|
@@ -32,9 +32,14 @@ class PydanticJSONParser(JSONParser):
|
|
32
32
|
return {}
|
33
33
|
try:
|
34
34
|
return from_json(input_str, allow_partial="trailing-strings" if not self.strict else False)
|
35
|
-
except
|
36
|
-
logger.
|
37
|
-
|
35
|
+
except Exception as e:
|
36
|
+
logger.warning(f"PydanticJSONParser failed: {e} | input_str={input_str!r}, falling back to OptimisticJSONParser")
|
37
|
+
try:
|
38
|
+
fallback_parser = OptimisticJSONParser(strict=self.strict)
|
39
|
+
return fallback_parser.parse(input_str)
|
40
|
+
except Exception as fallback_e:
|
41
|
+
logger.error(f"Both parsers failed. Pydantic: {e}, Optimistic: {fallback_e} | input_str={input_str!r}")
|
42
|
+
raise fallback_e
|
38
43
|
|
39
44
|
|
40
45
|
class OptimisticJSONParser(JSONParser):
|
@@ -219,3 +224,34 @@ class OptimisticJSONParser(JSONParser):
|
|
219
224
|
if input_str.startswith("n"):
|
220
225
|
return None, input_str[4:]
|
221
226
|
raise decode_error
|
227
|
+
|
228
|
+
|
229
|
+
# TODO: Keeping this around for posterity
|
230
|
+
# def main():
|
231
|
+
# test_string = '{"inner_thoughts":}'
|
232
|
+
#
|
233
|
+
# print(f"Testing string: {test_string!r}")
|
234
|
+
# print("=" * 50)
|
235
|
+
#
|
236
|
+
# print("OptimisticJSONParser (strict=False):")
|
237
|
+
# try:
|
238
|
+
# optimistic_parser = OptimisticJSONParser(strict=False)
|
239
|
+
# result = optimistic_parser.parse(test_string)
|
240
|
+
# print(f" Result: {result}")
|
241
|
+
# print(f" Remaining: {optimistic_parser.last_parse_reminding!r}")
|
242
|
+
# except Exception as e:
|
243
|
+
# print(f" Error: {e}")
|
244
|
+
#
|
245
|
+
# print()
|
246
|
+
#
|
247
|
+
# print("PydanticJSONParser (strict=False):")
|
248
|
+
# try:
|
249
|
+
# pydantic_parser = PydanticJSONParser(strict=False)
|
250
|
+
# result = pydantic_parser.parse(test_string)
|
251
|
+
# print(f" Result: {result}")
|
252
|
+
# except Exception as e:
|
253
|
+
# print(f" Error: {e}")
|
254
|
+
#
|
255
|
+
#
|
256
|
+
# if __name__ == "__main__":
|
257
|
+
# main()
|
@@ -504,7 +504,9 @@ async def add_mcp_server_to_config(
|
|
504
504
|
if tool_settings.mcp_disable_stdio: # protected server
|
505
505
|
raise HTTPException(status_code=400, detail="StdioServerConfig is not supported")
|
506
506
|
elif isinstance(request, SSEServerConfig):
|
507
|
-
mapped_request = MCPServer(
|
507
|
+
mapped_request = MCPServer(
|
508
|
+
server_name=request.server_name, server_type=request.type, server_url=request.server_url, token=request.resolve_token()
|
509
|
+
)
|
508
510
|
# TODO: add HTTP streaming
|
509
511
|
mcp_server = await server.mcp_manager.create_or_update_mcp_server(mapped_request, actor=actor)
|
510
512
|
|
@@ -1,9 +1,8 @@
|
|
1
|
-
from typing import TYPE_CHECKING, Optional
|
1
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional
|
2
2
|
|
3
3
|
import openai
|
4
4
|
from fastapi import APIRouter, Body, Depends, Header
|
5
5
|
from fastapi.responses import StreamingResponse
|
6
|
-
from openai.types.chat.completion_create_params import CompletionCreateParams
|
7
6
|
|
8
7
|
from letta.agents.voice_agent import VoiceAgent
|
9
8
|
from letta.log import get_logger
|
@@ -32,7 +31,7 @@ logger = get_logger(__name__)
|
|
32
31
|
)
|
33
32
|
async def create_voice_chat_completions(
|
34
33
|
agent_id: str,
|
35
|
-
completion_request:
|
34
|
+
completion_request: Dict[str, Any] = Body(...), # The validation is soft in case providers like VAPI send extra params
|
36
35
|
server: "SyncServer" = Depends(get_letta_server),
|
37
36
|
user_id: Optional[str] = Header(None, alias="user_id"),
|
38
37
|
):
|
letta/server/rest_api/utils.py
CHANGED
@@ -88,7 +88,7 @@ async def sse_async_generator(
|
|
88
88
|
metric_attributes = get_ctx_attributes()
|
89
89
|
if llm_config:
|
90
90
|
metric_attributes["model.name"] = llm_config.model
|
91
|
-
|
91
|
+
MetricRegistry().ttft_ms_histogram.record(ns_to_ms(ttft_ns), metric_attributes)
|
92
92
|
first_chunk = False
|
93
93
|
|
94
94
|
# yield f"data: {json.dumps(chunk)}\n\n"
|
letta/server/server.py
CHANGED
@@ -48,6 +48,7 @@ from letta.schemas.job import Job, JobUpdate
|
|
48
48
|
from letta.schemas.letta_message import LegacyLettaMessage, LettaMessage, MessageType, ToolReturnMessage
|
49
49
|
from letta.schemas.letta_message_content import TextContent
|
50
50
|
from letta.schemas.letta_response import LettaResponse
|
51
|
+
from letta.schemas.letta_stop_reason import LettaStopReason, StopReasonType
|
51
52
|
from letta.schemas.llm_config import LLMConfig
|
52
53
|
from letta.schemas.memory import ArchivalMemorySummary, Memory, RecallMemorySummary
|
53
54
|
from letta.schemas.message import Message, MessageCreate, MessageUpdate
|
@@ -2359,7 +2360,11 @@ class SyncServer(Server):
|
|
2359
2360
|
# If we want to convert these to Message, we can use the attached IDs
|
2360
2361
|
# NOTE: we will need to de-duplicate the Messsage IDs though (since Assistant->Inner+Func_Call)
|
2361
2362
|
# TODO: eventually update the interface to use `Message` and `MessageChunk` (new) inside the deque instead
|
2362
|
-
return LettaResponse(
|
2363
|
+
return LettaResponse(
|
2364
|
+
messages=filtered_stream,
|
2365
|
+
stop_reason=LettaStopReason(stop_reason=StopReasonType.end_turn.value),
|
2366
|
+
usage=usage,
|
2367
|
+
)
|
2363
2368
|
|
2364
2369
|
except HTTPException:
|
2365
2370
|
raise
|
@@ -2461,4 +2466,8 @@ class SyncServer(Server):
|
|
2461
2466
|
# If we want to convert these to Message, we can use the attached IDs
|
2462
2467
|
# NOTE: we will need to de-duplicate the Messsage IDs though (since Assistant->Inner+Func_Call)
|
2463
2468
|
# TODO: eventually update the interface to use `Message` and `MessageChunk` (new) inside the deque instead
|
2464
|
-
return LettaResponse(
|
2469
|
+
return LettaResponse(
|
2470
|
+
messages=filtered_stream,
|
2471
|
+
stop_reason=LettaStopReason(stop_reason=StopReasonType.end_turn.value),
|
2472
|
+
usage=usage,
|
2473
|
+
)
|
letta/services/agent_manager.py
CHANGED
@@ -15,7 +15,6 @@ from letta.constants import (
|
|
15
15
|
BASE_TOOLS,
|
16
16
|
BASE_VOICE_SLEEPTIME_CHAT_TOOLS,
|
17
17
|
BASE_VOICE_SLEEPTIME_TOOLS,
|
18
|
-
DATA_SOURCE_ATTACH_ALERT,
|
19
18
|
FILES_TOOLS,
|
20
19
|
MULTI_AGENT_TOOLS,
|
21
20
|
)
|
@@ -1419,7 +1418,7 @@ class AgentManager:
|
|
1419
1418
|
system_prompt=agent_state.system,
|
1420
1419
|
in_context_memory=agent_state.memory,
|
1421
1420
|
in_context_memory_last_edit=memory_edit_timestamp,
|
1422
|
-
previous_message_count=num_messages,
|
1421
|
+
previous_message_count=num_messages - len(agent_state.message_ids),
|
1423
1422
|
archival_memory_size=num_archival_memories,
|
1424
1423
|
)
|
1425
1424
|
|
@@ -1493,7 +1492,7 @@ class AgentManager:
|
|
1493
1492
|
system_prompt=agent_state.system,
|
1494
1493
|
in_context_memory=agent_state.memory,
|
1495
1494
|
in_context_memory_last_edit=memory_edit_timestamp,
|
1496
|
-
previous_message_count=num_messages,
|
1495
|
+
previous_message_count=num_messages - len(agent_state.message_ids),
|
1497
1496
|
archival_memory_size=num_archival_memories,
|
1498
1497
|
tool_rules_solver=tool_rules_solver,
|
1499
1498
|
)
|
@@ -1575,10 +1574,11 @@ class AgentManager:
|
|
1575
1574
|
self, agent_id: str, actor: PydanticUser, add_default_initial_messages: bool = False
|
1576
1575
|
) -> PydanticAgentState:
|
1577
1576
|
"""
|
1578
|
-
Removes all in-context messages for the specified agent by:
|
1579
|
-
1)
|
1580
|
-
2)
|
1581
|
-
3)
|
1577
|
+
Removes all in-context messages for the specified agent except the original system message by:
|
1578
|
+
1) Preserving the first message ID (original system message).
|
1579
|
+
2) Deleting all other messages for the agent.
|
1580
|
+
3) Updating the agent's message_ids to only contain the system message.
|
1581
|
+
4) Optionally adding default initial messages after the system message.
|
1582
1582
|
|
1583
1583
|
This action is destructive and cannot be undone once committed.
|
1584
1584
|
|
@@ -1588,35 +1588,49 @@ class AgentManager:
|
|
1588
1588
|
actor (PydanticUser): The user performing this action.
|
1589
1589
|
|
1590
1590
|
Returns:
|
1591
|
-
PydanticAgentState: The updated agent state with
|
1591
|
+
PydanticAgentState: The updated agent state with only the original system message preserved.
|
1592
1592
|
"""
|
1593
1593
|
async with db_registry.async_session() as session:
|
1594
1594
|
# Retrieve the existing agent (will raise NoResultFound if invalid)
|
1595
1595
|
agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
|
1596
1596
|
|
1597
|
-
#
|
1598
|
-
agent.message_ids
|
1597
|
+
# Ensure agent has message_ids with at least one message
|
1598
|
+
if not agent.message_ids or len(agent.message_ids) == 0:
|
1599
|
+
logger.error(
|
1600
|
+
f"Agent {agent_id} has no message_ids. Agent details: "
|
1601
|
+
f"name={agent.name}, created_at={agent.created_at}, "
|
1602
|
+
f"message_ids={agent.message_ids}, organization_id={agent.organization_id}"
|
1603
|
+
)
|
1604
|
+
raise ValueError(f"Agent {agent_id} has no message_ids - cannot preserve system message")
|
1599
1605
|
|
1600
|
-
#
|
1601
|
-
|
1606
|
+
# Get the system message ID (first message)
|
1607
|
+
system_message_id = agent.message_ids[0]
|
1602
1608
|
|
1603
|
-
|
1609
|
+
# Delete all messages for the agent except the system message
|
1610
|
+
await self.message_manager.delete_all_messages_for_agent_async(agent_id=agent_id, actor=actor, exclude_ids=[system_message_id])
|
1604
1611
|
|
1605
|
-
|
1612
|
+
# Update agent to only keep the system message
|
1613
|
+
agent.message_ids = [system_message_id]
|
1614
|
+
await agent.update_async(db_session=session, actor=actor)
|
1615
|
+
agent_state = await agent.to_pydantic_async()
|
1606
1616
|
|
1617
|
+
# Optionally add default initial messages after the system message
|
1607
1618
|
if add_default_initial_messages:
|
1608
|
-
return await self.append_initial_message_sequence_to_in_context_messages_async(actor, agent_state)
|
1609
|
-
else:
|
1610
|
-
# We still want to always have a system message
|
1611
1619
|
init_messages = initialize_message_sequence(
|
1612
1620
|
agent_state=agent_state, memory_edit_timestamp=get_utc_time(), include_initial_boot_message=True
|
1613
1621
|
)
|
1614
|
-
|
1615
|
-
|
1616
|
-
|
1617
|
-
|
1618
|
-
|
1619
|
-
|
1622
|
+
# Skip index 0 (system message) since we preserved the original
|
1623
|
+
non_system_messages = [
|
1624
|
+
PydanticMessage.dict_to_message(
|
1625
|
+
agent_id=agent_state.id,
|
1626
|
+
model=agent_state.llm_config.model,
|
1627
|
+
openai_message_dict=msg,
|
1628
|
+
)
|
1629
|
+
for msg in init_messages[1:]
|
1630
|
+
]
|
1631
|
+
return await self.append_to_in_context_messages_async(non_system_messages, agent_id=agent_state.id, actor=actor)
|
1632
|
+
else:
|
1633
|
+
return agent_state
|
1620
1634
|
|
1621
1635
|
@trace_method
|
1622
1636
|
@enforce_types
|
@@ -1717,13 +1731,7 @@ class AgentManager:
|
|
1717
1731
|
await agent.update_async(session, actor=actor)
|
1718
1732
|
|
1719
1733
|
# Force rebuild of system prompt so that the agent is updated with passage count
|
1720
|
-
# and recent passages and add system message alert to agent
|
1721
1734
|
pydantic_agent = await self.rebuild_system_prompt_async(agent_id=agent_id, actor=actor, force=True)
|
1722
|
-
await self.append_system_message_async(
|
1723
|
-
agent_id=agent_id,
|
1724
|
-
content=DATA_SOURCE_ATTACH_ALERT,
|
1725
|
-
actor=actor,
|
1726
|
-
)
|
1727
1735
|
|
1728
1736
|
return pydantic_agent
|
1729
1737
|
|
@@ -2,7 +2,7 @@ import os
|
|
2
2
|
import platform
|
3
3
|
import subprocess
|
4
4
|
import venv
|
5
|
-
from typing import Dict, Optional
|
5
|
+
from typing import TYPE_CHECKING, Dict, Optional
|
6
6
|
|
7
7
|
from datamodel_code_generator import DataModelType, PythonVersion
|
8
8
|
from datamodel_code_generator.model import get_data_model_types
|
@@ -11,6 +11,9 @@ from datamodel_code_generator.parser.jsonschema import JsonSchemaParser
|
|
11
11
|
from letta.log import get_logger
|
12
12
|
from letta.schemas.sandbox_config import LocalSandboxConfig
|
13
13
|
|
14
|
+
if TYPE_CHECKING:
|
15
|
+
from letta.schemas.tool import Tool
|
16
|
+
|
14
17
|
logger = get_logger(__name__)
|
15
18
|
|
16
19
|
|
@@ -85,14 +88,12 @@ def install_pip_requirements_for_sandbox(
|
|
85
88
|
upgrade: bool = True,
|
86
89
|
user_install_if_no_venv: bool = False,
|
87
90
|
env: Optional[Dict[str, str]] = None,
|
91
|
+
tool: Optional["Tool"] = None,
|
88
92
|
):
|
89
93
|
"""
|
90
94
|
Installs the specified pip requirements inside the correct environment (venv or system).
|
95
|
+
Installs both sandbox-level and tool-specific pip requirements.
|
91
96
|
"""
|
92
|
-
if not local_configs.pip_requirements:
|
93
|
-
logger.debug("No pip requirements specified; skipping installation.")
|
94
|
-
return
|
95
|
-
|
96
97
|
sandbox_dir = os.path.expanduser(local_configs.sandbox_dir) # Expand tilde
|
97
98
|
local_configs.sandbox_dir = sandbox_dir # Update the object to store the absolute path
|
98
99
|
|
@@ -102,19 +103,48 @@ def install_pip_requirements_for_sandbox(
|
|
102
103
|
if local_configs.use_venv:
|
103
104
|
ensure_pip_is_up_to_date(python_exec, env=env)
|
104
105
|
|
105
|
-
#
|
106
|
-
|
106
|
+
# Collect all pip requirements
|
107
|
+
all_packages = []
|
108
|
+
|
109
|
+
# Add sandbox-level pip requirements
|
110
|
+
if local_configs.pip_requirements:
|
111
|
+
packages = [f"{req.name}=={req.version}" if req.version else req.name for req in local_configs.pip_requirements]
|
112
|
+
all_packages.extend(packages)
|
113
|
+
logger.debug(f"Added sandbox pip requirements: {packages}")
|
114
|
+
|
115
|
+
# Add tool-specific pip requirements
|
116
|
+
if tool and tool.pip_requirements:
|
117
|
+
tool_packages = [str(req) for req in tool.pip_requirements]
|
118
|
+
all_packages.extend(tool_packages)
|
119
|
+
logger.debug(f"Added tool pip requirements for {tool.name}: {tool_packages}")
|
120
|
+
|
121
|
+
if not all_packages:
|
122
|
+
logger.debug("No pip requirements specified; skipping installation.")
|
123
|
+
return
|
107
124
|
|
108
125
|
# Construct pip install command
|
109
126
|
pip_cmd = [python_exec, "-m", "pip", "install"]
|
110
127
|
if upgrade:
|
111
128
|
pip_cmd.append("--upgrade")
|
112
|
-
pip_cmd +=
|
129
|
+
pip_cmd += all_packages
|
113
130
|
|
114
131
|
if user_install_if_no_venv and not local_configs.use_venv:
|
115
132
|
pip_cmd.append("--user")
|
116
133
|
|
117
|
-
|
134
|
+
# Enhanced error message for better debugging
|
135
|
+
sandbox_packages = [f"{req.name}=={req.version}" if req.version else req.name for req in (local_configs.pip_requirements or [])]
|
136
|
+
tool_packages = [str(req) for req in (tool.pip_requirements if tool and tool.pip_requirements else [])]
|
137
|
+
|
138
|
+
error_details = []
|
139
|
+
if sandbox_packages:
|
140
|
+
error_details.append(f"sandbox requirements: {', '.join(sandbox_packages)}")
|
141
|
+
if tool_packages:
|
142
|
+
error_details.append(f"tool requirements: {', '.join(tool_packages)}")
|
143
|
+
|
144
|
+
context = f" ({'; '.join(error_details)})" if error_details else ""
|
145
|
+
fail_msg = f"Failed to install pip packages{context}. This may be due to package version incompatibility. Consider updating package versions or removing version constraints."
|
146
|
+
|
147
|
+
run_subprocess(pip_cmd, env=env, fail_msg=fail_msg)
|
118
148
|
|
119
149
|
|
120
150
|
def create_venv_for_local_sandbox(sandbox_dir_path: str, venv_path: str, env: Dict[str, str], force_recreate: bool):
|