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.
Files changed (90) hide show
  1. letta/__init__.py +1 -1
  2. letta/agents/helpers.py +4 -0
  3. letta/agents/letta_agent.py +142 -5
  4. letta/constants.py +10 -7
  5. letta/data_sources/connectors.py +70 -53
  6. letta/embeddings.py +3 -240
  7. letta/errors.py +28 -0
  8. letta/functions/function_sets/base.py +4 -4
  9. letta/functions/functions.py +287 -32
  10. letta/functions/mcp_client/types.py +11 -0
  11. letta/functions/schema_validator.py +187 -0
  12. letta/functions/typescript_parser.py +196 -0
  13. letta/helpers/datetime_helpers.py +8 -4
  14. letta/helpers/tool_execution_helper.py +25 -2
  15. letta/llm_api/anthropic_client.py +23 -18
  16. letta/llm_api/azure_client.py +73 -0
  17. letta/llm_api/bedrock_client.py +8 -4
  18. letta/llm_api/google_vertex_client.py +14 -5
  19. letta/llm_api/llm_api_tools.py +2 -217
  20. letta/llm_api/llm_client.py +15 -1
  21. letta/llm_api/llm_client_base.py +32 -1
  22. letta/llm_api/openai.py +1 -0
  23. letta/llm_api/openai_client.py +18 -28
  24. letta/llm_api/together_client.py +55 -0
  25. letta/orm/provider.py +1 -0
  26. letta/orm/step_metrics.py +40 -1
  27. letta/otel/db_pool_monitoring.py +1 -1
  28. letta/schemas/agent.py +3 -4
  29. letta/schemas/agent_file.py +2 -0
  30. letta/schemas/block.py +11 -5
  31. letta/schemas/embedding_config.py +4 -5
  32. letta/schemas/enums.py +1 -1
  33. letta/schemas/job.py +2 -3
  34. letta/schemas/llm_config.py +79 -7
  35. letta/schemas/mcp.py +0 -24
  36. letta/schemas/message.py +0 -108
  37. letta/schemas/openai/chat_completion_request.py +1 -0
  38. letta/schemas/providers/__init__.py +0 -2
  39. letta/schemas/providers/anthropic.py +106 -8
  40. letta/schemas/providers/azure.py +102 -8
  41. letta/schemas/providers/base.py +10 -3
  42. letta/schemas/providers/bedrock.py +28 -16
  43. letta/schemas/providers/letta.py +3 -3
  44. letta/schemas/providers/ollama.py +2 -12
  45. letta/schemas/providers/openai.py +4 -4
  46. letta/schemas/providers/together.py +14 -2
  47. letta/schemas/sandbox_config.py +2 -1
  48. letta/schemas/tool.py +46 -22
  49. letta/server/rest_api/routers/v1/agents.py +179 -38
  50. letta/server/rest_api/routers/v1/folders.py +13 -8
  51. letta/server/rest_api/routers/v1/providers.py +10 -3
  52. letta/server/rest_api/routers/v1/sources.py +14 -8
  53. letta/server/rest_api/routers/v1/steps.py +17 -1
  54. letta/server/rest_api/routers/v1/tools.py +96 -5
  55. letta/server/rest_api/streaming_response.py +91 -45
  56. letta/server/server.py +27 -38
  57. letta/services/agent_manager.py +92 -20
  58. letta/services/agent_serialization_manager.py +11 -7
  59. letta/services/context_window_calculator/context_window_calculator.py +40 -2
  60. letta/services/helpers/agent_manager_helper.py +73 -12
  61. letta/services/mcp_manager.py +109 -15
  62. letta/services/passage_manager.py +28 -109
  63. letta/services/provider_manager.py +24 -0
  64. letta/services/step_manager.py +68 -0
  65. letta/services/summarizer/summarizer.py +1 -4
  66. letta/services/tool_executor/core_tool_executor.py +1 -1
  67. letta/services/tool_executor/sandbox_tool_executor.py +26 -9
  68. letta/services/tool_manager.py +82 -5
  69. letta/services/tool_sandbox/base.py +3 -11
  70. letta/services/tool_sandbox/modal_constants.py +17 -0
  71. letta/services/tool_sandbox/modal_deployment_manager.py +242 -0
  72. letta/services/tool_sandbox/modal_sandbox.py +218 -3
  73. letta/services/tool_sandbox/modal_sandbox_v2.py +429 -0
  74. letta/services/tool_sandbox/modal_version_manager.py +273 -0
  75. letta/services/tool_sandbox/safe_pickle.py +193 -0
  76. letta/settings.py +5 -3
  77. letta/templates/sandbox_code_file.py.j2 +2 -4
  78. letta/templates/sandbox_code_file_async.py.j2 +2 -4
  79. letta/utils.py +1 -1
  80. {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/METADATA +2 -2
  81. {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/RECORD +84 -81
  82. letta/llm_api/anthropic.py +0 -1206
  83. letta/llm_api/aws_bedrock.py +0 -104
  84. letta/llm_api/azure_openai.py +0 -118
  85. letta/llm_api/azure_openai_constants.py +0 -11
  86. letta/llm_api/cohere.py +0 -391
  87. letta/schemas/providers/cohere.py +0 -18
  88. {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/LICENSE +0 -0
  89. {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/WHEEL +0 -0
  90. {letta_nightly-0.11.3.dev20250820104219.dist-info → letta_nightly-0.11.4.dev20250820213507.dist-info}/entry_points.txt +0 -0
@@ -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.llm_api.azure_openai import get_azure_chat_completions_endpoint, get_azure_embeddings_endpoint
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
- from letta.llm_api.azure_openai import azure_openai_get_chat_completion_model_list
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(self.base_url, model_name, self.api_version)
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
- from letta.llm_api.azure_openai import azure_openai_get_embeddings_model_list
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(self.base_url, model_name, self.api_version)
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)
@@ -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
- from letta.llm_api.aws_bedrock import bedrock_get_model_list_async
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
- # Context windows for Claude models
71
- from letta.llm_api.aws_bedrock import bedrock_get_model_context_window
76
+ """
77
+ Get context window size for a specific model.
72
78
 
73
- return bedrock_get_model_context_window(model_name)
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)
@@ -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="hugging-face",
34
- embedding_endpoint="https://bun-function-production-e310.up.railway.app/v1",
35
- embedding_dim=1024,
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.get("models", []):
86
- model_name = model["name"]
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(..., description="Base URL for the OpenAI API.")
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-mini" in model_name or "gpt-4.1-mini" in model_name:
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(..., description="Default prompt formatter (aka model wrapper) to use on vLLM /completions API.")
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)
@@ -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(5 * 60, description="Time limit for the sandbox (in seconds).")
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 is ToolType.CUSTOM:
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
- # Always derive json_schema for freshest possible json_schema
85
- if self.args_json_schema is not None:
86
- name, description = get_function_name_and_docstring(self.source_code, self.name)
87
- args_schema = generate_model_from_args_json_schema(self.args_json_schema)
88
- self.json_schema = generate_schema_from_args_schema_v2(
89
- args_schema=args_schema,
90
- name=name,
91
- description=description,
92
- append_heartbeat=False,
93
- )
94
- else: # elif not self.json_schema: # TODO: JSON schema is not being derived correctly the first time?
95
- # If there's not a json_schema provided, then we need to re-derive
96
- try:
97
- self.json_schema = derive_openai_json_schema(source_code=self.source_code)
98
- except Exception as e:
99
- logger.error("Failed to derive json schema for tool with id=%s name=%s: %s", self.id, self.name, e)
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
- class Config:
227
- extra = "ignore" # Allows extra fields without validation errors
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):