letta-nightly 0.10.0.dev20250806104523__py3-none-any.whl → 0.11.0.dev20250807000848__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 (66) hide show
  1. letta/__init__.py +1 -4
  2. letta/agent.py +1 -2
  3. letta/agents/base_agent.py +4 -7
  4. letta/agents/letta_agent.py +59 -51
  5. letta/agents/letta_agent_batch.py +1 -2
  6. letta/agents/voice_agent.py +1 -2
  7. letta/agents/voice_sleeptime_agent.py +1 -3
  8. letta/constants.py +4 -1
  9. letta/embeddings.py +1 -1
  10. letta/functions/function_sets/base.py +0 -1
  11. letta/functions/mcp_client/types.py +4 -0
  12. letta/groups/supervisor_multi_agent.py +1 -1
  13. letta/interfaces/anthropic_streaming_interface.py +16 -24
  14. letta/interfaces/openai_streaming_interface.py +16 -28
  15. letta/llm_api/llm_api_tools.py +3 -3
  16. letta/local_llm/vllm/api.py +3 -0
  17. letta/orm/__init__.py +3 -1
  18. letta/orm/agent.py +8 -0
  19. letta/orm/archive.py +86 -0
  20. letta/orm/archives_agents.py +27 -0
  21. letta/orm/job.py +5 -1
  22. letta/orm/mixins.py +8 -0
  23. letta/orm/organization.py +7 -8
  24. letta/orm/passage.py +12 -10
  25. letta/orm/sqlite_functions.py +2 -2
  26. letta/orm/tool.py +5 -4
  27. letta/schemas/agent.py +4 -2
  28. letta/schemas/agent_file.py +18 -1
  29. letta/schemas/archive.py +44 -0
  30. letta/schemas/embedding_config.py +2 -16
  31. letta/schemas/enums.py +2 -1
  32. letta/schemas/group.py +28 -3
  33. letta/schemas/job.py +4 -0
  34. letta/schemas/llm_config.py +29 -14
  35. letta/schemas/memory.py +9 -3
  36. letta/schemas/npm_requirement.py +12 -0
  37. letta/schemas/passage.py +3 -3
  38. letta/schemas/providers/letta.py +1 -1
  39. letta/schemas/providers/vllm.py +4 -4
  40. letta/schemas/sandbox_config.py +3 -1
  41. letta/schemas/tool.py +10 -38
  42. letta/schemas/tool_rule.py +2 -2
  43. letta/server/db.py +8 -2
  44. letta/server/rest_api/routers/v1/agents.py +9 -8
  45. letta/server/server.py +6 -40
  46. letta/server/startup.sh +3 -0
  47. letta/services/agent_manager.py +92 -31
  48. letta/services/agent_serialization_manager.py +62 -3
  49. letta/services/archive_manager.py +269 -0
  50. letta/services/helpers/agent_manager_helper.py +111 -37
  51. letta/services/job_manager.py +24 -0
  52. letta/services/passage_manager.py +98 -54
  53. letta/services/tool_executor/core_tool_executor.py +0 -1
  54. letta/services/tool_executor/sandbox_tool_executor.py +2 -2
  55. letta/services/tool_executor/tool_execution_manager.py +1 -1
  56. letta/services/tool_manager.py +70 -26
  57. letta/services/tool_sandbox/base.py +2 -2
  58. letta/services/tool_sandbox/local_sandbox.py +5 -1
  59. letta/templates/template_helper.py +8 -0
  60. {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807000848.dist-info}/METADATA +5 -6
  61. {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807000848.dist-info}/RECORD +64 -61
  62. letta/client/client.py +0 -2207
  63. letta/orm/enums.py +0 -21
  64. {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807000848.dist-info}/LICENSE +0 -0
  65. {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807000848.dist-info}/WHEEL +0 -0
  66. {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807000848.dist-info}/entry_points.txt +0 -0
@@ -27,12 +27,12 @@ class VLLMProvider(Provider):
27
27
  async def list_llm_models_async(self) -> list[LLMConfig]:
28
28
  from letta.llm_api.openai import openai_get_model_list_async
29
29
 
30
- # TODO (cliandy): previously unsupported with vLLM; confirm if this is still the case or not
31
- response = await openai_get_model_list_async(self.base_url, api_key=self.api_key)
32
-
30
+ base_url = self.base_url.rstrip("/") + "/v1" if not self.base_url.endswith("/v1") else self.base_url
31
+ response = await openai_get_model_list_async(base_url, api_key=self.api_key)
33
32
  data = response.get("data", response)
34
33
 
35
34
  configs = []
35
+
36
36
  for model in data:
37
37
  model_name = model["id"]
38
38
 
@@ -40,7 +40,7 @@ class VLLMProvider(Provider):
40
40
  LLMConfig(
41
41
  model=model_name,
42
42
  model_endpoint_type="openai", # TODO (cliandy): this was previous vllm for the completions provider, why?
43
- model_endpoint=self.base_url,
43
+ model_endpoint=base_url,
44
44
  model_wrapper=self.default_prompt_formatter,
45
45
  context_window=model["max_model_len"],
46
46
  handle=self.get_handle(model_name),
@@ -81,7 +81,9 @@ class E2BSandboxConfig(BaseModel):
81
81
 
82
82
  class ModalSandboxConfig(BaseModel):
83
83
  timeout: int = Field(5 * 60, description="Time limit for the sandbox (in seconds).")
84
- pip_requirements: Optional[List[str]] = Field(None, description="A list of pip packages to install in the Modal sandbox")
84
+ pip_requirements: list[str] | None = Field(None, description="A list of pip packages to install in the Modal sandbox")
85
+ npm_requirements: list[str] | None = Field(None, description="A list of npm packages to install in the Modal sandbox")
86
+ language: Literal["python", "typescript"] = "python"
85
87
 
86
88
  @property
87
89
  def type(self) -> "SandboxType":
letta/schemas/tool.py CHANGED
@@ -22,8 +22,9 @@ from letta.functions.schema_generator import (
22
22
  generate_tool_schema_for_mcp,
23
23
  )
24
24
  from letta.log import get_logger
25
- from letta.orm.enums import ToolType
25
+ from letta.schemas.enums import ToolType
26
26
  from letta.schemas.letta_base import LettaBase
27
+ from letta.schemas.npm_requirement import NpmRequirement
27
28
  from letta.schemas.pip_requirement import PipRequirement
28
29
 
29
30
  logger = get_logger(__name__)
@@ -60,7 +61,8 @@ 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.")
63
- pip_requirements: Optional[List[PipRequirement]] = Field(None, description="Optional list of pip packages required by this tool.")
64
+ pip_requirements: list[PipRequirement] | None = Field(None, description="Optional list of pip packages required by this tool.")
65
+ npm_requirements: list[NpmRequirement] | None = Field(None, description="Optional list of npm packages required by this tool.")
64
66
 
65
67
  # metadata fields
66
68
  created_by_id: Optional[str] = Field(None, description="The id of the user that made this Tool.")
@@ -144,7 +146,8 @@ class ToolCreate(LettaBase):
144
146
  )
145
147
  args_json_schema: Optional[Dict] = Field(None, description="The args JSON schema of the function.")
146
148
  return_char_limit: int = Field(FUNCTION_RETURN_CHAR_LIMIT, description="The maximum number of characters in the response.")
147
- pip_requirements: Optional[List[PipRequirement]] = Field(None, description="Optional list of pip packages required by this tool.")
149
+ pip_requirements: list[PipRequirement] | None = Field(None, description="Optional list of pip packages required by this tool.")
150
+ npm_requirements: list[NpmRequirement] | None = Field(None, description="Optional list of npm packages required by this tool.")
148
151
 
149
152
  @classmethod
150
153
  def from_mcp(cls, mcp_server_name: str, mcp_tool: MCPTool) -> "ToolCreate":
@@ -206,39 +209,6 @@ class ToolCreate(LettaBase):
206
209
  json_schema=json_schema,
207
210
  )
208
211
 
209
- @classmethod
210
- def from_langchain(
211
- cls,
212
- langchain_tool: "LangChainBaseTool",
213
- additional_imports_module_attr_map: dict[str, str] = None,
214
- ) -> "ToolCreate":
215
- """
216
- Class method to create an instance of Tool from a Langchain tool (must be from langchain_community.tools).
217
-
218
- Args:
219
- langchain_tool (LangChainBaseTool): An instance of a LangChain BaseTool (BaseTool from LangChain)
220
- additional_imports_module_attr_map (dict[str, str]): A mapping of module names to attribute name. This is used internally to import all the required classes for the langchain tool. For example, you would pass in `{"langchain_community.utilities": "WikipediaAPIWrapper"}` for `from langchain_community.tools import WikipediaQueryRun`. NOTE: You do NOT need to specify the tool import here, that is done automatically for you.
221
-
222
- Returns:
223
- Tool: A Letta Tool initialized with attributes derived from the provided LangChain BaseTool object.
224
- """
225
- from letta.functions.helpers import generate_langchain_tool_wrapper
226
-
227
- description = langchain_tool.description
228
- source_type = "python"
229
- tags = ["langchain"]
230
- # NOTE: langchain tools may come from different packages
231
- wrapper_func_name, wrapper_function_str = generate_langchain_tool_wrapper(langchain_tool, additional_imports_module_attr_map)
232
- json_schema = generate_schema_from_args_schema_v2(langchain_tool.args_schema, name=wrapper_func_name, description=description)
233
-
234
- return cls(
235
- description=description,
236
- source_type=source_type,
237
- tags=tags,
238
- source_code=wrapper_function_str,
239
- json_schema=json_schema,
240
- )
241
-
242
212
 
243
213
  class ToolUpdate(LettaBase):
244
214
  description: Optional[str] = Field(None, description="The description of the tool.")
@@ -250,7 +220,8 @@ class ToolUpdate(LettaBase):
250
220
  )
251
221
  args_json_schema: Optional[Dict] = Field(None, description="The args JSON schema of the function.")
252
222
  return_char_limit: Optional[int] = Field(None, description="The maximum number of characters in the response.")
253
- pip_requirements: Optional[List[PipRequirement]] = Field(None, description="Optional list of pip packages required by this tool.")
223
+ pip_requirements: list[PipRequirement] | None = Field(None, description="Optional list of pip packages required by this tool.")
224
+ npm_requirements: list[NpmRequirement] | None = Field(None, description="Optional list of npm packages required by this tool.")
254
225
 
255
226
  class Config:
256
227
  extra = "ignore" # Allows extra fields without validation errors
@@ -267,4 +238,5 @@ class ToolRunFromSource(LettaBase):
267
238
  json_schema: Optional[Dict] = Field(
268
239
  None, description="The JSON schema of the function (auto-generated from source_code if not provided)"
269
240
  )
270
- pip_requirements: Optional[List[PipRequirement]] = Field(None, description="Optional list of pip packages required by this tool.")
241
+ pip_requirements: list[PipRequirement] | None = Field(None, description="Optional list of pip packages required by this tool.")
242
+ npm_requirements: list[NpmRequirement] | None = Field(None, description="Optional list of npm packages required by this tool.")
@@ -208,7 +208,7 @@ class MaxCountPerStepToolRule(BaseToolRule):
208
208
  type: Literal[ToolRuleType.max_count_per_step] = ToolRuleType.max_count_per_step
209
209
  max_count_limit: int = Field(..., description="The max limit for the total number of times this tool can be invoked in a single step.")
210
210
  prompt_template: Optional[str] = Field(
211
- default="<tool_rule>\n{{ tool_name }}: max {{ max_count_limit }} use(s) per response\n</tool_rule>",
211
+ default="<tool_rule>\n{{ tool_name }}: at most {{ max_count_limit }} use(s) per response\n</tool_rule>",
212
212
  description="Optional Jinja2 template for generating agent prompt about this tool rule.",
213
213
  )
214
214
 
@@ -223,7 +223,7 @@ class MaxCountPerStepToolRule(BaseToolRule):
223
223
  return available_tools
224
224
 
225
225
  def _get_default_template(self) -> Optional[str]:
226
- return "<tool_rule>\n{{ tool_name }}: max {{ max_count_limit }} use(s) per response\n</tool_rule>"
226
+ return "<tool_rule>\n{{ tool_name }}: at most {{ max_count_limit }} use(s) per response\n</tool_rule>"
227
227
 
228
228
 
229
229
  ToolRule = Annotated[
letta/server/db.py CHANGED
@@ -226,7 +226,12 @@ class DatabaseRegistry:
226
226
 
227
227
  def _build_sqlalchemy_engine_args(self, *, is_async: bool) -> dict:
228
228
  """Prepare keyword arguments for create_engine / create_async_engine."""
229
- use_null_pool = settings.disable_sqlalchemy_pooling
229
+ # For async SQLite, always use NullPool to avoid cleanup issues during cancellation
230
+ if is_async and settings.database_engine is DatabaseChoice.SQLITE:
231
+ use_null_pool = True
232
+ logger.info("Forcing NullPool for async SQLite to avoid cancellation cleanup issues")
233
+ else:
234
+ use_null_pool = settings.disable_sqlalchemy_pooling
230
235
 
231
236
  if use_null_pool:
232
237
  logger.info("Disabling pooling on SqlAlchemy")
@@ -262,7 +267,8 @@ class DatabaseRegistry:
262
267
  }
263
268
  )
264
269
 
265
- elif is_async:
270
+ elif is_async and settings.database_engine is DatabaseChoice.POSTGRES:
271
+ # Invalid for SQLite, results in [0] TypeError: 'prepared_statement_name_func' is an invalid keyword argument for Connection()
266
272
  # For asyncpg, statement_cache_size should be in connect_args
267
273
  base_args.update(
268
274
  {
@@ -4,7 +4,7 @@ import traceback
4
4
  from datetime import datetime, timezone
5
5
  from typing import Annotated, Any, Dict, List, Optional, Union
6
6
 
7
- from fastapi import APIRouter, Body, Depends, File, Header, HTTPException, Query, Request, UploadFile, status
7
+ from fastapi import APIRouter, Body, Depends, File, Form, Header, HTTPException, Query, Request, UploadFile, status
8
8
  from fastapi.responses import JSONResponse
9
9
  from marshmallow import ValidationError
10
10
  from orjson import orjson
@@ -13,7 +13,7 @@ from sqlalchemy.exc import IntegrityError, OperationalError
13
13
  from starlette.responses import Response, StreamingResponse
14
14
 
15
15
  from letta.agents.letta_agent import LettaAgent
16
- from letta.constants import DEFAULT_MAX_STEPS, DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, LETTA_MODEL_ENDPOINT, REDIS_RUN_ID_PREFIX
16
+ from letta.constants import DEFAULT_MAX_STEPS, DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, REDIS_RUN_ID_PREFIX
17
17
  from letta.data_sources.redis_client import get_redis_client
18
18
  from letta.groups.sleeptime_multi_agent_v2 import SleeptimeMultiAgentV2
19
19
  from letta.helpers.datetime_helpers import get_utc_timestamp_ns
@@ -169,16 +169,17 @@ def import_agent_serialized(
169
169
  file: UploadFile = File(...),
170
170
  server: "SyncServer" = Depends(get_letta_server),
171
171
  actor_id: str | None = Header(None, alias="user_id"),
172
- append_copy_suffix: bool = Query(True, description='If set to True, appends "_copy" to the end of the agent name.'),
173
- override_existing_tools: bool = Query(
172
+ append_copy_suffix: bool = Form(True, description='If set to True, appends "_copy" to the end of the agent name.'),
173
+ override_existing_tools: bool = Form(
174
174
  True,
175
175
  description="If set to True, existing tools can get their source code overwritten by the uploaded tool definitions. Note that Letta core tools can never be updated externally.",
176
176
  ),
177
- project_id: str | None = Query(None, description="The project ID to associate the uploaded agent with."),
178
- strip_messages: bool = Query(
177
+ project_id: str | None = Form(None, description="The project ID to associate the uploaded agent with."),
178
+ strip_messages: bool = Form(
179
179
  False,
180
180
  description="If set to True, strips all messages from the agent before importing.",
181
181
  ),
182
+ env_vars: Optional[Dict[str, Any]] = Form(None, description="Environment variables to pass to the agent for tool execution."),
182
183
  ):
183
184
  """
184
185
  Import a serialized agent file and recreate the agent in the system.
@@ -199,6 +200,7 @@ def import_agent_serialized(
199
200
  override_existing_tools=override_existing_tools,
200
201
  project_id=project_id,
201
202
  strip_messages=strip_messages,
203
+ env_vars=env_vars,
202
204
  )
203
205
  return new_agent
204
206
 
@@ -1017,7 +1019,6 @@ async def send_message_streaming(
1017
1019
  "ollama",
1018
1020
  ]
1019
1021
  model_compatible_token_streaming = agent.llm_config.model_endpoint_type in ["anthropic", "openai", "bedrock"]
1020
- not_letta_endpoint = agent.llm_config.model_endpoint != LETTA_MODEL_ENDPOINT
1021
1022
 
1022
1023
  # Create a new job for execution tracking
1023
1024
  if settings.track_agent_run:
@@ -1085,7 +1086,7 @@ async def send_message_streaming(
1085
1086
  )
1086
1087
  from letta.server.rest_api.streaming_response import StreamingResponseWithStatusCode, add_keepalive_to_stream
1087
1088
 
1088
- if request.stream_tokens and model_compatible_token_streaming and not_letta_endpoint:
1089
+ if request.stream_tokens and model_compatible_token_streaming:
1089
1090
  raw_stream = agent_loop.step_stream(
1090
1091
  input_messages=request.messages,
1091
1092
  max_steps=request.max_steps,
letta/server/server.py CHANGED
@@ -80,6 +80,7 @@ from letta.server.rest_api.interface import StreamingServerInterface
80
80
  from letta.server.rest_api.utils import sse_async_generator
81
81
  from letta.services.agent_manager import AgentManager
82
82
  from letta.services.agent_serialization_manager import AgentSerializationManager
83
+ from letta.services.archive_manager import ArchiveManager
83
84
  from letta.services.block_manager import BlockManager
84
85
  from letta.services.file_manager import FileManager
85
86
  from letta.services.files_agents_manager import FileAgentManager
@@ -215,6 +216,7 @@ class SyncServer(Server):
215
216
  self.message_manager = MessageManager()
216
217
  self.job_manager = JobManager()
217
218
  self.agent_manager = AgentManager()
219
+ self.archive_manager = ArchiveManager()
218
220
  self.provider_manager = ProviderManager()
219
221
  self.step_manager = StepManager()
220
222
  self.identity_manager = IdentityManager()
@@ -1146,29 +1148,12 @@ class SyncServer(Server):
1146
1148
  )
1147
1149
  return records
1148
1150
 
1149
- def insert_archival_memory(self, agent_id: str, memory_contents: str, actor: User) -> List[Passage]:
1150
- # Get the agent object (loaded in memory)
1151
- agent_state = self.agent_manager.get_agent_by_id(agent_id=agent_id, actor=actor)
1152
- # Insert into archival memory
1153
- # TODO: @mindy look at moving this to agent_manager to avoid above extra call
1154
- passages = self.passage_manager.insert_passage(agent_state=agent_state, agent_id=agent_id, text=memory_contents, actor=actor)
1155
-
1156
- # rebuild agent system prompt - force since no archival change
1157
- self.agent_manager.rebuild_system_prompt(agent_id=agent_id, actor=actor, force=True)
1158
-
1159
- return passages
1160
-
1161
1151
  async def insert_archival_memory_async(self, agent_id: str, memory_contents: str, actor: User) -> List[Passage]:
1162
1152
  # Get the agent object (loaded in memory)
1163
1153
  agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=agent_id, actor=actor)
1164
- # Insert into archival memory
1165
- # TODO: @mindy look at moving this to agent_manager to avoid above extra call
1166
- passages = await self.passage_manager.insert_passage_async(
1167
- agent_state=agent_state, agent_id=agent_id, text=memory_contents, actor=actor
1168
- )
1169
1154
 
1170
- # rebuild agent system prompt - force since no archival change
1171
- await self.agent_manager.rebuild_system_prompt_async(agent_id=agent_id, actor=actor, force=True)
1155
+ # Insert passages into the archive
1156
+ passages = await self.passage_manager.insert_passage_async(agent_state=agent_state, text=memory_contents, actor=actor)
1172
1157
 
1173
1158
  return passages
1174
1159
 
@@ -1177,17 +1162,6 @@ class SyncServer(Server):
1177
1162
  passages = self.passage_manager.update_passage_by_id(passage_id=memory_id, passage=passage, actor=actor)
1178
1163
  return passages
1179
1164
 
1180
- def delete_archival_memory(self, memory_id: str, actor: User):
1181
- # TODO check if it exists first, and throw error if not
1182
- # TODO: need to also rebuild the prompt here
1183
- passage = self.passage_manager.get_passage_by_id(passage_id=memory_id, actor=actor)
1184
-
1185
- # delete the passage
1186
- self.passage_manager.delete_passage_by_id(passage_id=memory_id, actor=actor)
1187
-
1188
- # rebuild system prompt and force
1189
- self.agent_manager.rebuild_system_prompt(agent_id=passage.agent_id, actor=actor, force=True)
1190
-
1191
1165
  async def delete_archival_memory_async(self, memory_id: str, actor: User):
1192
1166
  # TODO check if it exists first, and throw error if not
1193
1167
  # TODO: need to also rebuild the prompt here
@@ -1196,9 +1170,6 @@ class SyncServer(Server):
1196
1170
  # delete the passage
1197
1171
  await self.passage_manager.delete_passage_by_id_async(passage_id=memory_id, actor=actor)
1198
1172
 
1199
- # rebuild system prompt and force
1200
- await self.agent_manager.rebuild_system_prompt_async(agent_id=passage.agent_id, actor=actor, force=True)
1201
-
1202
1173
  def get_agent_recall(
1203
1174
  self,
1204
1175
  user_id: str,
@@ -2258,10 +2229,7 @@ class SyncServer(Server):
2258
2229
  llm_config = letta_agent.agent_state.llm_config
2259
2230
  # supports_token_streaming = ["openai", "anthropic", "xai", "deepseek"]
2260
2231
  supports_token_streaming = ["openai", "anthropic", "deepseek"] # TODO re-enable xAI once streaming is patched
2261
- if stream_tokens and (
2262
- llm_config.model_endpoint_type not in supports_token_streaming
2263
- or llm_config.model_endpoint == constants.LETTA_MODEL_ENDPOINT
2264
- ):
2232
+ if stream_tokens and (llm_config.model_endpoint_type not in supports_token_streaming):
2265
2233
  warnings.warn(
2266
2234
  f"Token streaming is only supported for models with type {' or '.join(supports_token_streaming)} in the model_endpoint: agent has endpoint type {llm_config.model_endpoint_type} and {llm_config.model_endpoint}. Setting stream_tokens to False."
2267
2235
  )
@@ -2393,9 +2361,7 @@ class SyncServer(Server):
2393
2361
 
2394
2362
  llm_config = letta_multi_agent.agent_state.llm_config
2395
2363
  supports_token_streaming = ["openai", "anthropic", "deepseek"]
2396
- if stream_tokens and (
2397
- llm_config.model_endpoint_type not in supports_token_streaming or llm_config.model_endpoint == constants.LETTA_MODEL_ENDPOINT
2398
- ):
2364
+ if stream_tokens and (llm_config.model_endpoint_type not in supports_token_streaming):
2399
2365
  warnings.warn(
2400
2366
  f"Token streaming is only supported for models with type {' or '.join(supports_token_streaming)} in the model_endpoint: agent has endpoint type {llm_config.model_endpoint_type} and {llm_config.model_endpoint}. Setting stream_tokens to False."
2401
2367
  )
letta/server/startup.sh CHANGED
@@ -57,6 +57,9 @@ fi
57
57
  if [ -n "$CLICKHOUSE_ENDPOINT" ] && [ -n "$CLICKHOUSE_PASSWORD" ]; then
58
58
  echo "Starting OpenTelemetry Collector with Clickhouse export..."
59
59
  CONFIG_FILE="/etc/otel/config-clickhouse.yaml"
60
+ elif [ -n "$SIGNOZ_ENDPOINT" ] && [ -n "$SIGNOZ_INGESTION_KEY" ]; then
61
+ echo "Starting OpenTelemetry Collector with Signoz export..."
62
+ CONFIG_FILE="/etc/otel/config-signoz.yaml"
60
63
  else
61
64
  echo "Starting OpenTelemetry Collector with file export only..."
62
65
  CONFIG_FILE="/etc/otel/config-file.yaml"
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  import os
3
3
  from datetime import datetime, timezone
4
- from typing import Dict, List, Optional, Set, Tuple
4
+ from typing import Any, Dict, List, Optional, Set, Tuple
5
5
 
6
6
  import sqlalchemy as sa
7
7
  from sqlalchemy import delete, func, insert, literal, or_, select, tuple_
@@ -19,6 +19,7 @@ from letta.constants import (
19
19
  DEFAULT_MAX_FILES_OPEN,
20
20
  DEFAULT_TIMEZONE,
21
21
  DEPRECATED_LETTA_TOOLS,
22
+ EXCLUDED_PROVIDERS_FROM_BASE_TOOL_RULES,
22
23
  FILES_TOOLS,
23
24
  )
24
25
  from letta.helpers import ToolRulesSolver
@@ -26,7 +27,7 @@ from letta.helpers.datetime_helpers import get_utc_time
26
27
  from letta.llm_api.llm_client import LLMClient
27
28
  from letta.log import get_logger
28
29
  from letta.orm import Agent as AgentModel
29
- from letta.orm import AgentPassage, AgentsTags
30
+ from letta.orm import AgentsTags, ArchivalPassage
30
31
  from letta.orm import Block as BlockModel
31
32
  from letta.orm import BlocksAgents
32
33
  from letta.orm import Group as GroupModel
@@ -35,7 +36,6 @@ from letta.orm import Source as SourceModel
35
36
  from letta.orm import SourcePassage, SourcesAgents
36
37
  from letta.orm import Tool as ToolModel
37
38
  from letta.orm import ToolsAgents
38
- from letta.orm.enums import ToolType
39
39
  from letta.orm.errors import NoResultFound
40
40
  from letta.orm.sandbox_config import AgentEnvironmentVariable
41
41
  from letta.orm.sandbox_config import AgentEnvironmentVariable as AgentEnvironmentVariableModel
@@ -47,10 +47,11 @@ from letta.schemas.block import DEFAULT_BLOCKS
47
47
  from letta.schemas.block import Block as PydanticBlock
48
48
  from letta.schemas.block import BlockUpdate
49
49
  from letta.schemas.embedding_config import EmbeddingConfig
50
- from letta.schemas.enums import ProviderType
50
+ from letta.schemas.enums import ProviderType, ToolType
51
51
  from letta.schemas.file import FileMetadata as PydanticFileMetadata
52
52
  from letta.schemas.group import Group as PydanticGroup
53
53
  from letta.schemas.group import ManagerType
54
+ from letta.schemas.llm_config import LLMConfig
54
55
  from letta.schemas.memory import ContextWindowOverview, Memory
55
56
  from letta.schemas.message import Message
56
57
  from letta.schemas.message import Message as PydanticMessage
@@ -86,8 +87,8 @@ from letta.services.helpers.agent_manager_helper import (
86
87
  calculate_multi_agent_tools,
87
88
  check_supports_structured_output,
88
89
  compile_system_message,
89
- compile_system_message_async,
90
90
  derive_system_message,
91
+ get_system_message_from_compiled_memory,
91
92
  initialize_message_sequence,
92
93
  initialize_message_sequence_async,
93
94
  package_initial_message_sequence,
@@ -332,11 +333,26 @@ class AgentManager:
332
333
  tool_names = set(name_to_id.keys()) # now canonical
333
334
 
334
335
  tool_rules = list(agent_create.tool_rules or [])
335
- if agent_create.include_base_tool_rules:
336
+
337
+ # Override include_base_tool_rules to False if provider is not in excluded set and include_base_tool_rules is not explicitly set to True
338
+ if (
339
+ (
340
+ agent_create.llm_config.model_endpoint_type in EXCLUDED_PROVIDERS_FROM_BASE_TOOL_RULES
341
+ and agent_create.include_base_tool_rules is None
342
+ )
343
+ and agent_create.agent_type != AgentType.sleeptime_agent
344
+ ) or agent_create.include_base_tool_rules is False:
345
+ agent_create.include_base_tool_rules = False
346
+ logger.info(f"Overriding include_base_tool_rules to False for provider: {agent_create.llm_config.model_endpoint_type}")
347
+ else:
348
+ agent_create.include_base_tool_rules = True
349
+
350
+ should_add_base_tool_rules = agent_create.include_base_tool_rules
351
+ if should_add_base_tool_rules:
336
352
  for tn in tool_names:
337
353
  if tn in {"send_message", "send_message_to_agent_async", "memory_finish_edits"}:
338
354
  tool_rules.append(TerminalToolRule(tool_name=tn))
339
- elif tn in (BASE_TOOLS + BASE_MEMORY_TOOLS + BASE_SLEEPTIME_TOOLS):
355
+ elif tn in (BASE_TOOLS + BASE_MEMORY_TOOLS + BASE_MEMORY_TOOLS_V2 + BASE_SLEEPTIME_TOOLS):
340
356
  tool_rules.append(ContinueToolRule(tool_name=tn))
341
357
 
342
358
  if tool_rules:
@@ -349,6 +365,7 @@ class AgentManager:
349
365
  enable_sleeptime=agent_create.enable_sleeptime,
350
366
  system=agent_create.system,
351
367
  ),
368
+ hidden=agent_create.hidden,
352
369
  agent_type=agent_create.agent_type,
353
370
  llm_config=agent_create.llm_config,
354
371
  embedding_config=agent_create.embedding_config,
@@ -443,6 +460,9 @@ class AgentManager:
443
460
  if not agent_create.llm_config or not agent_create.embedding_config:
444
461
  raise ValueError("llm_config and embedding_config are required")
445
462
 
463
+ if agent_create.reasoning is not None:
464
+ agent_create.llm_config = LLMConfig.apply_reasoning_setting_to_config(agent_create.llm_config, agent_create.reasoning)
465
+
446
466
  # blocks
447
467
  block_ids = list(agent_create.block_ids or [])
448
468
  if agent_create.memory_blocks:
@@ -521,9 +541,23 @@ class AgentManager:
521
541
 
522
542
  tool_ids = set(name_to_id.values()) | set(id_to_name.keys())
523
543
  tool_names = set(name_to_id.keys()) # now canonical
524
-
525
544
  tool_rules = list(agent_create.tool_rules or [])
526
- if agent_create.include_base_tool_rules:
545
+
546
+ # Override include_base_tool_rules to False if provider is not in excluded set and include_base_tool_rules is not explicitly set to True
547
+ if (
548
+ (
549
+ agent_create.llm_config.model_endpoint_type in EXCLUDED_PROVIDERS_FROM_BASE_TOOL_RULES
550
+ and agent_create.include_base_tool_rules is None
551
+ )
552
+ and agent_create.agent_type != AgentType.sleeptime_agent
553
+ ) or agent_create.include_base_tool_rules is False:
554
+ agent_create.include_base_tool_rules = False
555
+ logger.info(f"Overriding include_base_tool_rules to False for provider: {agent_create.llm_config.model_endpoint_type}")
556
+ else:
557
+ agent_create.include_base_tool_rules = True
558
+
559
+ should_add_base_tool_rules = agent_create.include_base_tool_rules
560
+ if should_add_base_tool_rules:
527
561
  for tn in tool_names:
528
562
  if tn in {"send_message", "send_message_to_agent_async", "memory_finish_edits"}:
529
563
  tool_rules.append(TerminalToolRule(tool_name=tn))
@@ -547,6 +581,7 @@ class AgentManager:
547
581
  description=agent_create.description,
548
582
  metadata_=agent_create.metadata,
549
583
  tool_rules=tool_rules,
584
+ hidden=agent_create.hidden,
550
585
  project_id=agent_create.project_id,
551
586
  template_id=agent_create.template_id,
552
587
  base_template_id=agent_create.base_template_id,
@@ -859,6 +894,10 @@ class AgentManager:
859
894
  agent.updated_at = datetime.now(timezone.utc)
860
895
  agent.last_updated_by_id = actor.id
861
896
 
897
+ if agent_update.reasoning is not None:
898
+ llm_config = agent_update.llm_config or agent.llm_config
899
+ agent_update.llm_config = LLMConfig.apply_reasoning_setting_to_config(llm_config, agent_update.reasoning)
900
+
862
901
  scalar_updates = {
863
902
  "name": agent_update.name,
864
903
  "system": agent_update.system,
@@ -1294,6 +1333,19 @@ class AgentManager:
1294
1333
  agent = AgentModel.read(db_session=session, name=agent_name, actor=actor)
1295
1334
  return agent.to_pydantic()
1296
1335
 
1336
+ @enforce_types
1337
+ @trace_method
1338
+ async def get_agent_archive_ids_async(self, agent_id: str, actor: PydanticUser) -> List[str]:
1339
+ """Get all archive IDs associated with an agent."""
1340
+ from letta.orm import ArchivesAgents
1341
+
1342
+ async with db_registry.async_session() as session:
1343
+ # Direct query to archives_agents table for performance
1344
+ query = select(ArchivesAgents.archive_id).where(ArchivesAgents.agent_id == agent_id)
1345
+ result = await session.execute(query)
1346
+ archive_ids = [row[0] for row in result.fetchall()]
1347
+ return archive_ids
1348
+
1297
1349
  @enforce_types
1298
1350
  @trace_method
1299
1351
  def delete_agent(self, agent_id: str, actor: PydanticUser) -> None:
@@ -1411,6 +1463,7 @@ class AgentManager:
1411
1463
  override_existing_tools: bool = True,
1412
1464
  project_id: Optional[str] = None,
1413
1465
  strip_messages: Optional[bool] = False,
1466
+ env_vars: Optional[dict[str, Any]] = None,
1414
1467
  ) -> PydanticAgentState:
1415
1468
  serialized_agent_dict = serialized_agent.model_dump()
1416
1469
  tool_data_list = serialized_agent_dict.pop("tools", [])
@@ -1441,6 +1494,11 @@ class AgentManager:
1441
1494
  if strip_messages:
1442
1495
  # we want to strip all but the first (system) message
1443
1496
  agent.message_ids = [agent.message_ids[0]]
1497
+
1498
+ if env_vars:
1499
+ for var in agent.tool_exec_environment_variables:
1500
+ var.value = env_vars.get(var.key, "")
1501
+
1444
1502
  agent = agent.create(session, actor=actor)
1445
1503
 
1446
1504
  pydantic_agent = agent.to_pydantic()
@@ -1465,7 +1523,7 @@ class AgentManager:
1465
1523
  ):
1466
1524
  pydantic_tool = existing_pydantic_tool
1467
1525
  else:
1468
- pydantic_tool = self.tool_manager.create_or_update_tool(pydantic_tool, actor=actor)
1526
+ pydantic_tool = self.tool_manager.create_or_update_tool(pydantic_tool, actor=actor, bypass_name_check=True)
1469
1527
 
1470
1528
  pydantic_agent = self.attach_tool(agent_id=pydantic_agent.id, tool_id=pydantic_tool.id, actor=actor)
1471
1529
 
@@ -1683,7 +1741,7 @@ class AgentManager:
1683
1741
 
1684
1742
  # note: we only update the system prompt if the core memory is changed
1685
1743
  # this means that the archival/recall memory statistics may be someout out of date
1686
- curr_memory_str = await agent_state.memory.compile_async(
1744
+ curr_memory_str = await agent_state.memory.compile_in_thread_async(
1687
1745
  sources=agent_state.sources,
1688
1746
  tool_usage_rules=tool_rules_solver.compile_tool_rule_prompts(),
1689
1747
  max_files_open=agent_state.max_files_open,
@@ -1705,16 +1763,13 @@ class AgentManager:
1705
1763
 
1706
1764
  # update memory (TODO: potentially update recall/archival stats separately)
1707
1765
 
1708
- new_system_message_str = await compile_system_message_async(
1766
+ new_system_message_str = get_system_message_from_compiled_memory(
1709
1767
  system_prompt=agent_state.system,
1710
- in_context_memory=agent_state.memory,
1768
+ memory_with_sources=curr_memory_str,
1711
1769
  in_context_memory_last_edit=memory_edit_timestamp,
1712
1770
  timezone=agent_state.timezone,
1713
1771
  previous_message_count=num_messages - len(agent_state.message_ids),
1714
1772
  archival_memory_size=num_archival_memories,
1715
- tool_rules_solver=tool_rules_solver,
1716
- sources=agent_state.sources,
1717
- max_files_open=agent_state.max_files_open,
1718
1773
  )
1719
1774
 
1720
1775
  diff = united_diff(curr_system_message_openai["content"], new_system_message_str)
@@ -1873,7 +1928,7 @@ class AgentManager:
1873
1928
  agent_state = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor, include_relationships=["memory", "sources"])
1874
1929
  system_message = await self.message_manager.get_message_by_id_async(message_id=agent_state.message_ids[0], actor=actor)
1875
1930
  temp_tool_rules_solver = ToolRulesSolver(agent_state.tool_rules)
1876
- new_memory_str = await new_memory.compile_async(
1931
+ new_memory_str = await new_memory.compile_in_thread_async(
1877
1932
  sources=agent_state.sources,
1878
1933
  tool_usage_rules=temp_tool_rules_solver.compile_tool_rule_prompts(),
1879
1934
  max_files_open=agent_state.max_files_open,
@@ -2340,21 +2395,24 @@ class AgentManager:
2340
2395
  main_query = main_query.limit(limit)
2341
2396
 
2342
2397
  # Execute query
2343
- results = list(session.execute(main_query))
2398
+ result = session.execute(main_query)
2344
2399
 
2345
2400
  passages = []
2346
- for row in results:
2401
+ for row in result:
2347
2402
  data = dict(row._mapping)
2348
- if data["agent_id"] is not None:
2349
- # This is an AgentPassage - remove source fields
2403
+ if data.get("archive_id", None):
2404
+ # This is an ArchivalPassage - remove source fields
2350
2405
  data.pop("source_id", None)
2351
2406
  data.pop("file_id", None)
2352
2407
  data.pop("file_name", None)
2353
- passage = AgentPassage(**data)
2354
- else:
2355
- # This is a SourcePassage - remove agent field
2356
- data.pop("agent_id", None)
2408
+ passage = ArchivalPassage(**data)
2409
+ elif data.get("source_id", None):
2410
+ # This is a SourcePassage - remove archive field
2411
+ data.pop("archive_id", None)
2412
+ data.pop("agent_id", None) # For backward compatibility
2357
2413
  passage = SourcePassage(**data)
2414
+ else:
2415
+ raise ValueError(f"Passage data is malformed, is neither ArchivalPassage nor SourcePassage {data}")
2358
2416
  passages.append(passage)
2359
2417
 
2360
2418
  return [p.to_pydantic() for p in passages]
@@ -2406,16 +2464,19 @@ class AgentManager:
2406
2464
  passages = []
2407
2465
  for row in result:
2408
2466
  data = dict(row._mapping)
2409
- if data["agent_id"] is not None:
2410
- # This is an AgentPassage - remove source fields
2467
+ if data.get("archive_id", None):
2468
+ # This is an ArchivalPassage - remove source fields
2411
2469
  data.pop("source_id", None)
2412
2470
  data.pop("file_id", None)
2413
2471
  data.pop("file_name", None)
2414
- passage = AgentPassage(**data)
2415
- else:
2416
- # This is a SourcePassage - remove agent field
2417
- data.pop("agent_id", None)
2472
+ passage = ArchivalPassage(**data)
2473
+ elif data.get("source_id", None):
2474
+ # This is a SourcePassage - remove archive field
2475
+ data.pop("archive_id", None)
2476
+ data.pop("agent_id", None) # For backward compatibility
2418
2477
  passage = SourcePassage(**data)
2478
+ else:
2479
+ raise ValueError(f"Passage data is malformed, is neither ArchivalPassage nor SourcePassage {data}")
2419
2480
  passages.append(passage)
2420
2481
 
2421
2482
  return [p.to_pydantic() for p in passages]