letta-nightly 0.11.3.dev20250820104219__py3-none-any.whl → 0.11.4.dev20250820213507__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/agents/helpers.py +4 -0
- letta/agents/letta_agent.py +142 -5
- letta/constants.py +10 -7
- letta/data_sources/connectors.py +70 -53
- letta/embeddings.py +3 -240
- letta/errors.py +28 -0
- letta/functions/function_sets/base.py +4 -4
- letta/functions/functions.py +287 -32
- letta/functions/mcp_client/types.py +11 -0
- letta/functions/schema_validator.py +187 -0
- letta/functions/typescript_parser.py +196 -0
- letta/helpers/datetime_helpers.py +8 -4
- letta/helpers/tool_execution_helper.py +25 -2
- letta/llm_api/anthropic_client.py +23 -18
- letta/llm_api/azure_client.py +73 -0
- letta/llm_api/bedrock_client.py +8 -4
- letta/llm_api/google_vertex_client.py +14 -5
- letta/llm_api/llm_api_tools.py +2 -217
- letta/llm_api/llm_client.py +15 -1
- letta/llm_api/llm_client_base.py +32 -1
- letta/llm_api/openai.py +1 -0
- letta/llm_api/openai_client.py +18 -28
- letta/llm_api/together_client.py +55 -0
- letta/orm/provider.py +1 -0
- letta/orm/step_metrics.py +40 -1
- letta/otel/db_pool_monitoring.py +1 -1
- letta/schemas/agent.py +3 -4
- letta/schemas/agent_file.py +2 -0
- letta/schemas/block.py +11 -5
- letta/schemas/embedding_config.py +4 -5
- letta/schemas/enums.py +1 -1
- letta/schemas/job.py +2 -3
- letta/schemas/llm_config.py +79 -7
- letta/schemas/mcp.py +0 -24
- letta/schemas/message.py +0 -108
- letta/schemas/openai/chat_completion_request.py +1 -0
- letta/schemas/providers/__init__.py +0 -2
- letta/schemas/providers/anthropic.py +106 -8
- letta/schemas/providers/azure.py +102 -8
- letta/schemas/providers/base.py +10 -3
- letta/schemas/providers/bedrock.py +28 -16
- letta/schemas/providers/letta.py +3 -3
- letta/schemas/providers/ollama.py +2 -12
- letta/schemas/providers/openai.py +4 -4
- letta/schemas/providers/together.py +14 -2
- letta/schemas/sandbox_config.py +2 -1
- letta/schemas/tool.py +46 -22
- letta/server/rest_api/routers/v1/agents.py +179 -38
- letta/server/rest_api/routers/v1/folders.py +13 -8
- letta/server/rest_api/routers/v1/providers.py +10 -3
- letta/server/rest_api/routers/v1/sources.py +14 -8
- letta/server/rest_api/routers/v1/steps.py +17 -1
- letta/server/rest_api/routers/v1/tools.py +96 -5
- letta/server/rest_api/streaming_response.py +91 -45
- letta/server/server.py +27 -38
- letta/services/agent_manager.py +92 -20
- letta/services/agent_serialization_manager.py +11 -7
- letta/services/context_window_calculator/context_window_calculator.py +40 -2
- letta/services/helpers/agent_manager_helper.py +73 -12
- letta/services/mcp_manager.py +109 -15
- letta/services/passage_manager.py +28 -109
- letta/services/provider_manager.py +24 -0
- letta/services/step_manager.py +68 -0
- letta/services/summarizer/summarizer.py +1 -4
- letta/services/tool_executor/core_tool_executor.py +1 -1
- letta/services/tool_executor/sandbox_tool_executor.py +26 -9
- letta/services/tool_manager.py +82 -5
- letta/services/tool_sandbox/base.py +3 -11
- letta/services/tool_sandbox/modal_constants.py +17 -0
- letta/services/tool_sandbox/modal_deployment_manager.py +242 -0
- letta/services/tool_sandbox/modal_sandbox.py +218 -3
- letta/services/tool_sandbox/modal_sandbox_v2.py +429 -0
- letta/services/tool_sandbox/modal_version_manager.py +273 -0
- letta/services/tool_sandbox/safe_pickle.py +193 -0
- letta/settings.py +5 -3
- letta/templates/sandbox_code_file.py.j2 +2 -4
- letta/templates/sandbox_code_file_async.py.j2 +2 -4
- letta/utils.py +1 -1
- {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/METADATA +2 -2
- {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/RECORD +84 -81
- letta/llm_api/anthropic.py +0 -1206
- letta/llm_api/aws_bedrock.py +0 -104
- letta/llm_api/azure_openai.py +0 -118
- letta/llm_api/azure_openai_constants.py +0 -11
- letta/llm_api/cohere.py +0 -391
- letta/schemas/providers/cohere.py +0 -18
- {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/LICENSE +0 -0
- {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/entry_points.txt +0 -0
letta/schemas/providers/azure.py
CHANGED
@@ -1,15 +1,29 @@
|
|
1
|
+
from collections import defaultdict
|
1
2
|
from typing import ClassVar, Literal
|
2
3
|
|
4
|
+
import requests
|
5
|
+
from openai import AzureOpenAI
|
3
6
|
from pydantic import Field, field_validator
|
4
7
|
|
5
8
|
from letta.constants import DEFAULT_EMBEDDING_CHUNK_SIZE, LLM_MAX_TOKENS
|
6
|
-
from letta.
|
7
|
-
from letta.llm_api.azure_openai_constants import AZURE_MODEL_TO_CONTEXT_LENGTH
|
9
|
+
from letta.errors import ErrorCode, LLMAuthenticationError
|
8
10
|
from letta.schemas.embedding_config import EmbeddingConfig
|
9
11
|
from letta.schemas.enums import ProviderCategory, ProviderType
|
10
12
|
from letta.schemas.llm_config import LLMConfig
|
11
13
|
from letta.schemas.providers.base import Provider
|
12
14
|
|
15
|
+
AZURE_MODEL_TO_CONTEXT_LENGTH = {
|
16
|
+
"babbage-002": 16384,
|
17
|
+
"davinci-002": 16384,
|
18
|
+
"gpt-35-turbo-0613": 4096,
|
19
|
+
"gpt-35-turbo-1106": 16385,
|
20
|
+
"gpt-35-turbo-0125": 16385,
|
21
|
+
"gpt-4-0613": 8192,
|
22
|
+
"gpt-4o-mini-2024-07-18": 128000,
|
23
|
+
"gpt-4o-mini": 128000,
|
24
|
+
"gpt-4o": 128000,
|
25
|
+
}
|
26
|
+
|
13
27
|
|
14
28
|
class AzureProvider(Provider):
|
15
29
|
LATEST_API_VERSION: ClassVar[str] = "2024-09-01-preview"
|
@@ -29,16 +43,78 @@ class AzureProvider(Provider):
|
|
29
43
|
def replace_none_with_default(cls, v):
|
30
44
|
return v if v is not None else cls.LATEST_API_VERSION
|
31
45
|
|
46
|
+
def get_azure_chat_completions_endpoint(self, model: str):
|
47
|
+
return f"{self.base_url}/openai/deployments/{model}/chat/completions?api-version={self.api_version}"
|
48
|
+
|
49
|
+
def get_azure_embeddings_endpoint(self, model: str):
|
50
|
+
return f"{self.base_url}/openai/deployments/{model}/embeddings?api-version={self.api_version}"
|
51
|
+
|
52
|
+
def get_azure_model_list_endpoint(self):
|
53
|
+
return f"{self.base_url}/openai/models?api-version={self.api_version}"
|
54
|
+
|
55
|
+
def get_azure_deployment_list_endpoint(self):
|
56
|
+
# Please note that it has to be 2023-03-15-preview
|
57
|
+
# That's the only api version that works with this deployments endpoint
|
58
|
+
return f"{self.base_url}/openai/deployments?api-version=2023-03-15-preview"
|
59
|
+
|
60
|
+
def azure_openai_get_deployed_model_list(self) -> list:
|
61
|
+
"""https://learn.microsoft.com/en-us/rest/api/azureopenai/models/list?view=rest-azureopenai-2023-05-15&tabs=HTTP"""
|
62
|
+
|
63
|
+
client = AzureOpenAI(api_key=self.api_key, api_version=self.api_version, azure_endpoint=self.base_url)
|
64
|
+
|
65
|
+
try:
|
66
|
+
models_list = client.models.list()
|
67
|
+
except Exception:
|
68
|
+
return []
|
69
|
+
|
70
|
+
all_available_models = [model.to_dict() for model in models_list.data]
|
71
|
+
|
72
|
+
# https://xxx.openai.azure.com/openai/models?api-version=xxx
|
73
|
+
headers = {"Content-Type": "application/json"}
|
74
|
+
if self.api_key is not None:
|
75
|
+
headers["api-key"] = f"{self.api_key}"
|
76
|
+
|
77
|
+
# 2. Get all the deployed models
|
78
|
+
url = self.get_azure_deployment_list_endpoint()
|
79
|
+
try:
|
80
|
+
response = requests.get(url, headers=headers)
|
81
|
+
response.raise_for_status()
|
82
|
+
except requests.RequestException as e:
|
83
|
+
raise RuntimeError(f"Failed to retrieve model list: {e}")
|
84
|
+
|
85
|
+
deployed_models = response.json().get("data", [])
|
86
|
+
deployed_model_names = set([m["id"] for m in deployed_models])
|
87
|
+
|
88
|
+
# 3. Only return the models in available models if they have been deployed
|
89
|
+
deployed_models = [m for m in all_available_models if m["id"] in deployed_model_names]
|
90
|
+
|
91
|
+
# 4. Remove redundant deployments, only include the ones with the latest deployment
|
92
|
+
# Create a dictionary to store the latest model for each ID
|
93
|
+
latest_models = defaultdict()
|
94
|
+
|
95
|
+
# Iterate through the models and update the dictionary with the most recent model
|
96
|
+
for model in deployed_models:
|
97
|
+
model_id = model["id"]
|
98
|
+
updated_at = model["created_at"]
|
99
|
+
|
100
|
+
# If the model ID is new or the current model has a more recent created_at, update the dictionary
|
101
|
+
if model_id not in latest_models or updated_at > latest_models[model_id]["created_at"]:
|
102
|
+
latest_models[model_id] = model
|
103
|
+
|
104
|
+
# Extract the unique models
|
105
|
+
return list(latest_models.values())
|
106
|
+
|
32
107
|
async def list_llm_models_async(self) -> list[LLMConfig]:
|
33
108
|
# TODO (cliandy): asyncify
|
34
|
-
|
109
|
+
model_list = self.azure_openai_get_deployed_model_list()
|
110
|
+
# Extract models that support text generation
|
111
|
+
model_options = [m for m in model_list if m.get("capabilities").get("chat_completion") == True]
|
35
112
|
|
36
|
-
model_options = azure_openai_get_chat_completion_model_list(self.base_url, api_key=self.api_key, api_version=self.api_version)
|
37
113
|
configs = []
|
38
114
|
for model_option in model_options:
|
39
115
|
model_name = model_option["id"]
|
40
116
|
context_window_size = self.get_model_context_window(model_name)
|
41
|
-
model_endpoint = get_azure_chat_completions_endpoint(
|
117
|
+
model_endpoint = self.get_azure_chat_completions_endpoint(model_name)
|
42
118
|
configs.append(
|
43
119
|
LLMConfig(
|
44
120
|
model=model_name,
|
@@ -54,13 +130,22 @@ class AzureProvider(Provider):
|
|
54
130
|
|
55
131
|
async def list_embedding_models_async(self) -> list[EmbeddingConfig]:
|
56
132
|
# TODO (cliandy): asyncify dependent function calls
|
57
|
-
|
133
|
+
def valid_embedding_model(m: dict, require_embedding_in_name: bool = True):
|
134
|
+
valid_name = True
|
135
|
+
if require_embedding_in_name:
|
136
|
+
valid_name = "embedding" in m["id"]
|
137
|
+
|
138
|
+
return m.get("capabilities").get("embeddings") == True and valid_name
|
139
|
+
|
140
|
+
model_list = self.azure_openai_get_deployed_model_list()
|
141
|
+
# Extract models that support embeddings
|
142
|
+
|
143
|
+
model_options = [m for m in model_list if valid_embedding_model(m)]
|
58
144
|
|
59
|
-
model_options = azure_openai_get_embeddings_model_list(self.base_url, api_key=self.api_key, api_version=self.api_version)
|
60
145
|
configs = []
|
61
146
|
for model_option in model_options:
|
62
147
|
model_name = model_option["id"]
|
63
|
-
model_endpoint = get_azure_embeddings_endpoint(
|
148
|
+
model_endpoint = self.get_azure_embeddings_endpoint(model_name)
|
64
149
|
configs.append(
|
65
150
|
EmbeddingConfig(
|
66
151
|
embedding_model=model_name,
|
@@ -78,3 +163,12 @@ class AzureProvider(Provider):
|
|
78
163
|
# Hard coded as there are no API endpoints for this
|
79
164
|
llm_default = LLM_MAX_TOKENS.get(model_name, 4096)
|
80
165
|
return AZURE_MODEL_TO_CONTEXT_LENGTH.get(model_name, llm_default)
|
166
|
+
|
167
|
+
async def check_api_key(self):
|
168
|
+
if not self.api_key:
|
169
|
+
raise ValueError("No API key provided")
|
170
|
+
|
171
|
+
try:
|
172
|
+
await self.list_llm_models_async()
|
173
|
+
except Exception as e:
|
174
|
+
raise LLMAuthenticationError(message=f"Failed to authenticate with Azure: {e}", code=ErrorCode.UNAUTHENTICATED)
|
letta/schemas/providers/base.py
CHANGED
@@ -24,6 +24,7 @@ class Provider(ProviderBase):
|
|
24
24
|
base_url: str | None = Field(None, description="Base URL for the provider.")
|
25
25
|
access_key: str | None = Field(None, description="Access key used for requests to the provider.")
|
26
26
|
region: str | None = Field(None, description="Region used for requests to the provider.")
|
27
|
+
api_version: str | None = Field(None, description="API version used for requests to the provider.")
|
27
28
|
organization_id: str | None = Field(None, description="The organization id of the user")
|
28
29
|
updated_at: datetime | None = Field(None, description="The last update timestamp of the provider.")
|
29
30
|
|
@@ -126,7 +127,6 @@ class Provider(ProviderBase):
|
|
126
127
|
AzureProvider,
|
127
128
|
BedrockProvider,
|
128
129
|
CerebrasProvider,
|
129
|
-
CohereProvider,
|
130
130
|
DeepSeekProvider,
|
131
131
|
GoogleAIProvider,
|
132
132
|
GoogleVertexProvider,
|
@@ -141,6 +141,9 @@ class Provider(ProviderBase):
|
|
141
141
|
XAIProvider,
|
142
142
|
)
|
143
143
|
|
144
|
+
if self.base_url == "":
|
145
|
+
self.base_url = None
|
146
|
+
|
144
147
|
match self.provider_type:
|
145
148
|
case ProviderType.letta:
|
146
149
|
return LettaProvider(**self.model_dump(exclude_none=True))
|
@@ -174,8 +177,6 @@ class Provider(ProviderBase):
|
|
174
177
|
return LMStudioOpenAIProvider(**self.model_dump(exclude_none=True))
|
175
178
|
case ProviderType.bedrock:
|
176
179
|
return BedrockProvider(**self.model_dump(exclude_none=True))
|
177
|
-
case ProviderType.cohere:
|
178
|
-
return CohereProvider(**self.model_dump(exclude_none=True))
|
179
180
|
case _:
|
180
181
|
raise ValueError(f"Unknown provider type: {self.provider_type}")
|
181
182
|
|
@@ -186,12 +187,16 @@ class ProviderCreate(ProviderBase):
|
|
186
187
|
api_key: str = Field(..., description="API key or secret key used for requests to the provider.")
|
187
188
|
access_key: str | None = Field(None, description="Access key used for requests to the provider.")
|
188
189
|
region: str | None = Field(None, description="Region used for requests to the provider.")
|
190
|
+
base_url: str | None = Field(None, description="Base URL used for requests to the provider.")
|
191
|
+
api_version: str | None = Field(None, description="API version used for requests to the provider.")
|
189
192
|
|
190
193
|
|
191
194
|
class ProviderUpdate(ProviderBase):
|
192
195
|
api_key: str = Field(..., description="API key or secret key used for requests to the provider.")
|
193
196
|
access_key: str | None = Field(None, description="Access key used for requests to the provider.")
|
194
197
|
region: str | None = Field(None, description="Region used for requests to the provider.")
|
198
|
+
base_url: str | None = Field(None, description="Base URL used for requests to the provider.")
|
199
|
+
api_version: str | None = Field(None, description="API version used for requests to the provider.")
|
195
200
|
|
196
201
|
|
197
202
|
class ProviderCheck(BaseModel):
|
@@ -199,3 +204,5 @@ class ProviderCheck(BaseModel):
|
|
199
204
|
api_key: str = Field(..., description="API key or secret key used for requests to the provider.")
|
200
205
|
access_key: str | None = Field(None, description="Access key used for requests to the provider.")
|
201
206
|
region: str | None = Field(None, description="Region used for requests to the provider.")
|
207
|
+
base_url: str | None = Field(None, description="Base URL used for requests to the provider.")
|
208
|
+
api_version: str | None = Field(None, description="API version used for requests to the provider.")
|
@@ -20,20 +20,32 @@ class BedrockProvider(Provider):
|
|
20
20
|
provider_category: ProviderCategory = Field(ProviderCategory.base, description="The category of the provider (base or byok)")
|
21
21
|
region: str = Field(..., description="AWS region for Bedrock")
|
22
22
|
|
23
|
+
async def bedrock_get_model_list_async(self) -> list[dict]:
|
24
|
+
from aioboto3.session import Session
|
25
|
+
|
26
|
+
try:
|
27
|
+
session = Session()
|
28
|
+
async with session.client(
|
29
|
+
"bedrock",
|
30
|
+
aws_access_key_id=self.access_key,
|
31
|
+
aws_secret_access_key=self.api_key,
|
32
|
+
region_name=self.region,
|
33
|
+
) as bedrock:
|
34
|
+
response = await bedrock.list_inference_profiles()
|
35
|
+
return response["inferenceProfileSummaries"]
|
36
|
+
except Exception as e:
|
37
|
+
logger.error(f"Error getting model list for bedrock: %s", e)
|
38
|
+
raise e
|
39
|
+
|
23
40
|
async def check_api_key(self):
|
24
41
|
"""Check if the Bedrock credentials are valid"""
|
25
42
|
from letta.errors import LLMAuthenticationError
|
26
|
-
from letta.llm_api.aws_bedrock import bedrock_get_model_list_async
|
27
43
|
|
28
44
|
try:
|
29
45
|
# For BYOK providers, use the custom credentials
|
30
46
|
if self.provider_category == ProviderCategory.byok:
|
31
47
|
# If we can list models, the credentials are valid
|
32
|
-
await bedrock_get_model_list_async(
|
33
|
-
access_key_id=self.access_key,
|
34
|
-
secret_access_key=self.api_key, # api_key stores the secret access key
|
35
|
-
region_name=self.region,
|
36
|
-
)
|
48
|
+
await self.bedrock_get_model_list_async()
|
37
49
|
else:
|
38
50
|
# For base providers, use default credentials
|
39
51
|
bedrock_get_model_list(region_name=self.region)
|
@@ -41,13 +53,7 @@ class BedrockProvider(Provider):
|
|
41
53
|
raise LLMAuthenticationError(message=f"Failed to authenticate with Bedrock: {e}")
|
42
54
|
|
43
55
|
async def list_llm_models_async(self) -> list[LLMConfig]:
|
44
|
-
|
45
|
-
|
46
|
-
models = await bedrock_get_model_list_async(
|
47
|
-
self.access_key,
|
48
|
-
self.api_key,
|
49
|
-
self.region,
|
50
|
-
)
|
56
|
+
models = await self.bedrock_get_model_list_async()
|
51
57
|
|
52
58
|
configs = []
|
53
59
|
for model_summary in models:
|
@@ -67,10 +73,16 @@ class BedrockProvider(Provider):
|
|
67
73
|
return configs
|
68
74
|
|
69
75
|
def get_model_context_window(self, model_name: str) -> int | None:
|
70
|
-
|
71
|
-
|
76
|
+
"""
|
77
|
+
Get context window size for a specific model.
|
72
78
|
|
73
|
-
|
79
|
+
Bedrock doesn't provide this via API, so we maintain a mapping
|
80
|
+
200k for anthropic: https://aws.amazon.com/bedrock/anthropic/
|
81
|
+
"""
|
82
|
+
if model_name.startswith("anthropic"):
|
83
|
+
return 200_000
|
84
|
+
else:
|
85
|
+
return 100_000 # default to 100k if unknown
|
74
86
|
|
75
87
|
def get_handle(self, model_name: str, is_embedding: bool = False, base_name: str | None = None) -> str:
|
76
88
|
logger.debug("Getting handle for model_name: %s", model_name)
|
letta/schemas/providers/letta.py
CHANGED
@@ -30,9 +30,9 @@ class LettaProvider(Provider):
|
|
30
30
|
return [
|
31
31
|
EmbeddingConfig(
|
32
32
|
embedding_model="letta-free", # NOTE: renamed
|
33
|
-
embedding_endpoint_type="
|
34
|
-
embedding_endpoint="https://
|
35
|
-
embedding_dim=
|
33
|
+
embedding_endpoint_type="openai",
|
34
|
+
embedding_endpoint="https://embeddings.letta.com/",
|
35
|
+
embedding_dim=1536,
|
36
36
|
embedding_chunk_size=DEFAULT_EMBEDDING_CHUNK_SIZE,
|
37
37
|
handle=self.get_handle("letta-free", is_embedding=True),
|
38
38
|
)
|
@@ -82,18 +82,8 @@ class OllamaProvider(OpenAIProvider):
|
|
82
82
|
response_json = await response.json()
|
83
83
|
|
84
84
|
configs = []
|
85
|
-
for model in response_json
|
86
|
-
|
87
|
-
model_details = await self._get_model_details_async(model_name)
|
88
|
-
if not model_details or "embedding" not in model_details.get("capabilities", []):
|
89
|
-
continue
|
90
|
-
|
91
|
-
embedding_dim = None
|
92
|
-
model_info = model_details.get("model_info", {})
|
93
|
-
if architecture := model_info.get("general.architecture"):
|
94
|
-
if embedding_length := model_info.get(f"{architecture}.embedding_length"):
|
95
|
-
embedding_dim = int(embedding_length)
|
96
|
-
|
85
|
+
for model in response_json["models"]:
|
86
|
+
embedding_dim = await self._get_model_embedding_dim(model["name"])
|
97
87
|
if not embedding_dim:
|
98
88
|
logger.warning(f"Ollama model {model_name} has no embedding dimension, using default {DEFAULT_EMBEDDING_DIM}")
|
99
89
|
embedding_dim = DEFAULT_EMBEDDING_DIM
|
@@ -12,7 +12,7 @@ from letta.schemas.providers.base import Provider
|
|
12
12
|
logger = get_logger(__name__)
|
13
13
|
|
14
14
|
ALLOWED_PREFIXES = {"gpt-4", "gpt-5", "o1", "o3", "o4"}
|
15
|
-
DISALLOWED_KEYWORDS = {"transcribe", "search", "realtime", "tts", "audio", "computer", "o1-mini", "o1-preview", "o1-pro"}
|
15
|
+
DISALLOWED_KEYWORDS = {"transcribe", "search", "realtime", "tts", "audio", "computer", "o1-mini", "o1-preview", "o1-pro", "chat"}
|
16
16
|
DEFAULT_EMBEDDING_BATCH_SIZE = 1024
|
17
17
|
|
18
18
|
|
@@ -20,10 +20,10 @@ class OpenAIProvider(Provider):
|
|
20
20
|
provider_type: Literal[ProviderType.openai] = Field(ProviderType.openai, description="The type of the provider.")
|
21
21
|
provider_category: ProviderCategory = Field(ProviderCategory.base, description="The category of the provider (base or byok)")
|
22
22
|
api_key: str = Field(..., description="API key for the OpenAI API.")
|
23
|
-
base_url: str = Field(
|
23
|
+
base_url: str = Field("https://api.openai.com/v1", description="Base URL for the OpenAI API.")
|
24
24
|
|
25
25
|
async def check_api_key(self):
|
26
|
-
from letta.llm_api.openai import openai_check_valid_api_key
|
26
|
+
from letta.llm_api.openai import openai_check_valid_api_key # TODO: DO NOT USE THIS - old code path
|
27
27
|
|
28
28
|
openai_check_valid_api_key(self.base_url, self.api_key)
|
29
29
|
|
@@ -142,7 +142,7 @@ class OpenAIProvider(Provider):
|
|
142
142
|
"""This function is used to tune LLMConfig parameters to improve model performance."""
|
143
143
|
|
144
144
|
# gpt-4o-mini has started to regress with pretty bad emoji spam loops (2025-07)
|
145
|
-
if "gpt-4o
|
145
|
+
if "gpt-4o" in model_name or "gpt-4.1-mini" in model_name or model_name == "letta-free":
|
146
146
|
llm_config.frequency_penalty = 1.0
|
147
147
|
return llm_config
|
148
148
|
|
@@ -2,11 +2,12 @@
|
|
2
2
|
Note: this supports completions (deprecated by openai) and chat completions via the OpenAI API.
|
3
3
|
"""
|
4
4
|
|
5
|
-
from typing import Literal
|
5
|
+
from typing import Literal, Optional
|
6
6
|
|
7
7
|
from pydantic import Field
|
8
8
|
|
9
9
|
from letta.constants import MIN_CONTEXT_WINDOW
|
10
|
+
from letta.errors import ErrorCode, LLMAuthenticationError
|
10
11
|
from letta.schemas.embedding_config import EmbeddingConfig
|
11
12
|
from letta.schemas.enums import ProviderCategory, ProviderType
|
12
13
|
from letta.schemas.llm_config import LLMConfig
|
@@ -18,7 +19,9 @@ class TogetherProvider(OpenAIProvider):
|
|
18
19
|
provider_category: ProviderCategory = Field(ProviderCategory.base, description="The category of the provider (base or byok)")
|
19
20
|
base_url: str = "https://api.together.xyz/v1"
|
20
21
|
api_key: str = Field(..., description="API key for the Together API.")
|
21
|
-
default_prompt_formatter: str = Field(
|
22
|
+
default_prompt_formatter: Optional[str] = Field(
|
23
|
+
None, description="Default prompt formatter (aka model wrapper) to use on vLLM /completions API."
|
24
|
+
)
|
22
25
|
|
23
26
|
async def list_llm_models_async(self) -> list[LLMConfig]:
|
24
27
|
from letta.llm_api.openai import openai_get_model_list_async
|
@@ -83,3 +86,12 @@ class TogetherProvider(OpenAIProvider):
|
|
83
86
|
)
|
84
87
|
|
85
88
|
return configs
|
89
|
+
|
90
|
+
async def check_api_key(self):
|
91
|
+
if not self.api_key:
|
92
|
+
raise ValueError("No API key provided")
|
93
|
+
|
94
|
+
try:
|
95
|
+
await self.list_llm_models_async()
|
96
|
+
except Exception as e:
|
97
|
+
raise LLMAuthenticationError(message=f"Failed to authenticate with Together: {e}", code=ErrorCode.UNAUTHENTICATED)
|
letta/schemas/sandbox_config.py
CHANGED
@@ -9,6 +9,7 @@ from letta.schemas.agent import AgentState
|
|
9
9
|
from letta.schemas.enums import SandboxType
|
10
10
|
from letta.schemas.letta_base import LettaBase, OrmMetadataBase
|
11
11
|
from letta.schemas.pip_requirement import PipRequirement
|
12
|
+
from letta.services.tool_sandbox.modal_constants import DEFAULT_MODAL_TIMEOUT
|
12
13
|
from letta.settings import tool_settings
|
13
14
|
|
14
15
|
# Sandbox Config
|
@@ -80,7 +81,7 @@ class E2BSandboxConfig(BaseModel):
|
|
80
81
|
|
81
82
|
|
82
83
|
class ModalSandboxConfig(BaseModel):
|
83
|
-
timeout: int = Field(
|
84
|
+
timeout: int = Field(DEFAULT_MODAL_TIMEOUT, description="Time limit for the sandbox (in seconds).")
|
84
85
|
pip_requirements: list[str] | None = Field(None, description="A list of pip packages to install in the Modal sandbox")
|
85
86
|
npm_requirements: list[str] | None = Field(None, description="A list of npm packages to install in the Modal sandbox")
|
86
87
|
language: Literal["python", "typescript"] = "python"
|
letta/schemas/tool.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from typing import Any, Dict, List, Optional
|
2
2
|
|
3
|
-
from pydantic import Field, model_validator
|
3
|
+
from pydantic import ConfigDict, Field, model_validator
|
4
4
|
|
5
5
|
from letta.constants import (
|
6
6
|
COMPOSIO_TOOL_TAG_NAME,
|
@@ -12,6 +12,10 @@ from letta.constants import (
|
|
12
12
|
LETTA_VOICE_TOOL_MODULE_NAME,
|
13
13
|
MCP_TOOL_TAG_NAME_PREFIX,
|
14
14
|
)
|
15
|
+
|
16
|
+
# MCP Tool metadata constants for schema health status
|
17
|
+
MCP_TOOL_METADATA_SCHEMA_STATUS = f"{MCP_TOOL_TAG_NAME_PREFIX}:SCHEMA_STATUS"
|
18
|
+
MCP_TOOL_METADATA_SCHEMA_WARNINGS = f"{MCP_TOOL_TAG_NAME_PREFIX}:SCHEMA_WARNINGS"
|
15
19
|
from letta.functions.ast_parsers import get_function_name_and_docstring
|
16
20
|
from letta.functions.composio_helpers import generate_composio_tool_wrapper
|
17
21
|
from letta.functions.functions import derive_openai_json_schema, get_json_schema_from_module
|
@@ -22,7 +26,7 @@ from letta.functions.schema_generator import (
|
|
22
26
|
generate_tool_schema_for_mcp,
|
23
27
|
)
|
24
28
|
from letta.log import get_logger
|
25
|
-
from letta.schemas.enums import ToolType
|
29
|
+
from letta.schemas.enums import ToolSourceType, ToolType
|
26
30
|
from letta.schemas.letta_base import LettaBase
|
27
31
|
from letta.schemas.npm_requirement import NpmRequirement
|
28
32
|
from letta.schemas.pip_requirement import PipRequirement
|
@@ -76,27 +80,42 @@ class Tool(BaseTool):
|
|
76
80
|
"""
|
77
81
|
from letta.functions.helpers import generate_model_from_args_json_schema
|
78
82
|
|
79
|
-
if self.tool_type
|
83
|
+
if self.tool_type == ToolType.CUSTOM:
|
80
84
|
if not self.source_code:
|
81
85
|
logger.error("Custom tool with id=%s is missing source_code field", self.id)
|
82
86
|
raise ValueError(f"Custom tool with id={self.id} is missing source_code field.")
|
83
87
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
88
|
+
if self.source_type == ToolSourceType.typescript:
|
89
|
+
# TypeScript tools don't support args_json_schema, only direct schema generation
|
90
|
+
if not self.json_schema:
|
91
|
+
try:
|
92
|
+
from letta.functions.typescript_parser import derive_typescript_json_schema
|
93
|
+
|
94
|
+
self.json_schema = derive_typescript_json_schema(source_code=self.source_code)
|
95
|
+
except Exception as e:
|
96
|
+
logger.error("Failed to derive TypeScript json schema for tool with id=%s name=%s: %s", self.id, self.name, e)
|
97
|
+
elif (
|
98
|
+
self.source_type == ToolSourceType.python or self.source_type is None
|
99
|
+
): # default to python if not provided for backwards compatability
|
100
|
+
# Python tool handling
|
101
|
+
# Always derive json_schema for freshest possible json_schema
|
102
|
+
if self.args_json_schema is not None:
|
103
|
+
name, description = get_function_name_and_docstring(self.source_code, self.name)
|
104
|
+
args_schema = generate_model_from_args_json_schema(self.args_json_schema)
|
105
|
+
self.json_schema = generate_schema_from_args_schema_v2(
|
106
|
+
args_schema=args_schema,
|
107
|
+
name=name,
|
108
|
+
description=description,
|
109
|
+
append_heartbeat=False,
|
110
|
+
)
|
111
|
+
else: # elif not self.json_schema: # TODO: JSON schema is not being derived correctly the first time?
|
112
|
+
# If there's not a json_schema provided, then we need to re-derive
|
113
|
+
try:
|
114
|
+
self.json_schema = derive_openai_json_schema(source_code=self.source_code)
|
115
|
+
except Exception as e:
|
116
|
+
logger.error("Failed to derive json schema for tool with id=%s name=%s: %s", self.id, self.name, e)
|
117
|
+
else:
|
118
|
+
raise ValueError(f"Unknown tool source type: {self.source_type}")
|
100
119
|
elif self.tool_type in {ToolType.LETTA_CORE, ToolType.LETTA_MEMORY_CORE, ToolType.LETTA_SLEEPTIME_CORE}:
|
101
120
|
# If it's letta core tool, we generate the json_schema on the fly here
|
102
121
|
self.json_schema = get_json_schema_from_module(module_name=LETTA_CORE_TOOL_MODULE_NAME, function_name=self.name)
|
@@ -156,6 +175,11 @@ class ToolCreate(LettaBase):
|
|
156
175
|
# Pass the MCP tool to the schema generator
|
157
176
|
json_schema = generate_tool_schema_for_mcp(mcp_tool=mcp_tool)
|
158
177
|
|
178
|
+
# Store health status in json_schema metadata if available
|
179
|
+
if mcp_tool.health:
|
180
|
+
json_schema[MCP_TOOL_METADATA_SCHEMA_STATUS] = mcp_tool.health.status
|
181
|
+
json_schema[MCP_TOOL_METADATA_SCHEMA_WARNINGS] = mcp_tool.health.reasons
|
182
|
+
|
159
183
|
# Return a ToolCreate instance
|
160
184
|
description = mcp_tool.description
|
161
185
|
source_type = "python"
|
@@ -222,10 +246,10 @@ class ToolUpdate(LettaBase):
|
|
222
246
|
return_char_limit: Optional[int] = Field(None, description="The maximum number of characters in the response.")
|
223
247
|
pip_requirements: list[PipRequirement] | None = Field(None, description="Optional list of pip packages required by this tool.")
|
224
248
|
npm_requirements: list[NpmRequirement] | None = Field(None, description="Optional list of npm packages required by this tool.")
|
249
|
+
metadata_: Optional[Dict[str, Any]] = Field(None, description="A dictionary of additional metadata for the tool.")
|
225
250
|
|
226
|
-
|
227
|
-
|
228
|
-
# TODO: Remove this, and clean usage of ToolUpdate everywhere else
|
251
|
+
model_config = ConfigDict(extra="ignore") # Allows extra fields without validation errors
|
252
|
+
# TODO: Remove this, and clean usage of ToolUpdate everywhere else
|
229
253
|
|
230
254
|
|
231
255
|
class ToolRunFromSource(LettaBase):
|