letta-nightly 0.10.0.dev20250806104523__py3-none-any.whl → 0.11.0.dev20250807104511__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 -4
- letta/agent.py +1 -2
- letta/agents/base_agent.py +4 -7
- letta/agents/letta_agent.py +59 -51
- letta/agents/letta_agent_batch.py +1 -2
- letta/agents/voice_agent.py +1 -2
- letta/agents/voice_sleeptime_agent.py +1 -3
- letta/constants.py +4 -1
- letta/embeddings.py +1 -1
- letta/functions/function_sets/base.py +0 -1
- letta/functions/mcp_client/types.py +4 -0
- letta/groups/supervisor_multi_agent.py +1 -1
- letta/interfaces/anthropic_streaming_interface.py +16 -24
- letta/interfaces/openai_streaming_interface.py +16 -28
- letta/llm_api/llm_api_tools.py +3 -3
- letta/local_llm/vllm/api.py +3 -0
- letta/orm/__init__.py +3 -1
- letta/orm/agent.py +8 -0
- letta/orm/archive.py +86 -0
- letta/orm/archives_agents.py +27 -0
- letta/orm/job.py +5 -1
- letta/orm/mixins.py +8 -0
- letta/orm/organization.py +7 -8
- letta/orm/passage.py +12 -10
- letta/orm/sqlite_functions.py +2 -2
- letta/orm/tool.py +5 -4
- letta/schemas/agent.py +4 -2
- letta/schemas/agent_file.py +18 -1
- letta/schemas/archive.py +44 -0
- letta/schemas/embedding_config.py +2 -16
- letta/schemas/enums.py +2 -1
- letta/schemas/group.py +28 -3
- letta/schemas/job.py +4 -0
- letta/schemas/llm_config.py +29 -14
- letta/schemas/memory.py +9 -3
- letta/schemas/npm_requirement.py +12 -0
- letta/schemas/passage.py +3 -3
- letta/schemas/providers/letta.py +1 -1
- letta/schemas/providers/vllm.py +4 -4
- letta/schemas/sandbox_config.py +3 -1
- letta/schemas/tool.py +10 -38
- letta/schemas/tool_rule.py +2 -2
- letta/server/db.py +8 -2
- letta/server/rest_api/routers/v1/agents.py +9 -8
- letta/server/server.py +6 -40
- letta/server/startup.sh +3 -0
- letta/services/agent_manager.py +92 -31
- letta/services/agent_serialization_manager.py +62 -3
- letta/services/archive_manager.py +269 -0
- letta/services/helpers/agent_manager_helper.py +111 -37
- letta/services/job_manager.py +24 -0
- letta/services/passage_manager.py +98 -54
- letta/services/tool_executor/core_tool_executor.py +0 -1
- letta/services/tool_executor/sandbox_tool_executor.py +2 -2
- letta/services/tool_executor/tool_execution_manager.py +1 -1
- letta/services/tool_manager.py +70 -26
- letta/services/tool_sandbox/base.py +2 -2
- letta/services/tool_sandbox/local_sandbox.py +5 -1
- letta/templates/template_helper.py +8 -0
- {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807104511.dist-info}/METADATA +5 -6
- {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807104511.dist-info}/RECORD +64 -61
- letta/client/client.py +0 -2207
- letta/orm/enums.py +0 -21
- {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807104511.dist-info}/LICENSE +0 -0
- {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807104511.dist-info}/WHEEL +0 -0
- {letta_nightly-0.10.0.dev20250806104523.dist-info → letta_nightly-0.11.0.dev20250807104511.dist-info}/entry_points.txt +0 -0
letta/schemas/providers/vllm.py
CHANGED
@@ -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
|
-
|
31
|
-
response = await openai_get_model_list_async(
|
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=
|
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),
|
letta/schemas/sandbox_config.py
CHANGED
@@ -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:
|
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.
|
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:
|
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:
|
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:
|
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:
|
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.")
|
letta/schemas/tool_rule.py
CHANGED
@@ -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 }}:
|
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 }}:
|
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
|
-
|
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,
|
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 =
|
173
|
-
override_existing_tools: bool =
|
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 =
|
178
|
-
strip_messages: bool =
|
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
|
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
|
-
#
|
1171
|
-
await self.
|
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"
|
letta/services/agent_manager.py
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
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.
|
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 =
|
1766
|
+
new_system_message_str = get_system_message_from_compiled_memory(
|
1709
1767
|
system_prompt=agent_state.system,
|
1710
|
-
|
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.
|
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
|
-
|
2398
|
+
result = session.execute(main_query)
|
2344
2399
|
|
2345
2400
|
passages = []
|
2346
|
-
for row in
|
2401
|
+
for row in result:
|
2347
2402
|
data = dict(row._mapping)
|
2348
|
-
if data
|
2349
|
-
# This is an
|
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 =
|
2354
|
-
|
2355
|
-
# This is a SourcePassage - remove
|
2356
|
-
data.pop("
|
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
|
2410
|
-
# This is an
|
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 =
|
2415
|
-
|
2416
|
-
# This is a SourcePassage - remove
|
2417
|
-
data.pop("
|
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]
|