letta-nightly 0.11.6.dev20250903104037__py3-none-any.whl → 0.11.7.dev20250904104046__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- letta/__init__.py +1 -1
- letta/agent.py +10 -14
- letta/agents/base_agent.py +18 -0
- letta/agents/helpers.py +32 -7
- letta/agents/letta_agent.py +953 -762
- letta/agents/voice_agent.py +1 -1
- letta/client/streaming.py +0 -1
- letta/constants.py +11 -8
- letta/errors.py +9 -0
- letta/functions/function_sets/base.py +77 -69
- letta/functions/function_sets/builtin.py +41 -22
- letta/functions/function_sets/multi_agent.py +1 -2
- letta/functions/schema_generator.py +0 -1
- letta/helpers/converters.py +8 -3
- letta/helpers/datetime_helpers.py +5 -4
- letta/helpers/message_helper.py +1 -2
- letta/helpers/pinecone_utils.py +0 -1
- letta/helpers/tool_rule_solver.py +10 -0
- letta/helpers/tpuf_client.py +848 -0
- letta/interface.py +8 -8
- letta/interfaces/anthropic_streaming_interface.py +7 -0
- letta/interfaces/openai_streaming_interface.py +29 -6
- letta/llm_api/anthropic_client.py +188 -18
- letta/llm_api/azure_client.py +0 -1
- letta/llm_api/bedrock_client.py +1 -2
- letta/llm_api/deepseek_client.py +319 -5
- letta/llm_api/google_vertex_client.py +75 -17
- letta/llm_api/groq_client.py +0 -1
- letta/llm_api/helpers.py +2 -2
- letta/llm_api/llm_api_tools.py +1 -50
- letta/llm_api/llm_client.py +6 -8
- letta/llm_api/mistral.py +1 -1
- letta/llm_api/openai.py +16 -13
- letta/llm_api/openai_client.py +31 -16
- letta/llm_api/together_client.py +0 -1
- letta/llm_api/xai_client.py +0 -1
- letta/local_llm/chat_completion_proxy.py +7 -6
- letta/local_llm/settings/settings.py +1 -1
- letta/orm/__init__.py +1 -0
- letta/orm/agent.py +8 -6
- letta/orm/archive.py +9 -1
- letta/orm/block.py +3 -4
- letta/orm/block_history.py +3 -1
- letta/orm/group.py +2 -3
- letta/orm/identity.py +1 -2
- letta/orm/job.py +1 -2
- letta/orm/llm_batch_items.py +1 -2
- letta/orm/message.py +8 -4
- letta/orm/mixins.py +18 -0
- letta/orm/organization.py +2 -0
- letta/orm/passage.py +8 -1
- letta/orm/passage_tag.py +55 -0
- letta/orm/sandbox_config.py +1 -3
- letta/orm/step.py +1 -2
- letta/orm/tool.py +1 -0
- letta/otel/resource.py +2 -2
- letta/plugins/plugins.py +1 -1
- letta/prompts/prompt_generator.py +10 -2
- letta/schemas/agent.py +11 -0
- letta/schemas/archive.py +4 -0
- letta/schemas/block.py +13 -0
- letta/schemas/embedding_config.py +0 -1
- letta/schemas/enums.py +24 -7
- letta/schemas/group.py +12 -0
- letta/schemas/letta_message.py +55 -1
- letta/schemas/letta_message_content.py +28 -0
- letta/schemas/letta_request.py +21 -4
- letta/schemas/letta_stop_reason.py +9 -1
- letta/schemas/llm_config.py +24 -8
- letta/schemas/mcp.py +0 -3
- letta/schemas/memory.py +14 -0
- letta/schemas/message.py +245 -141
- letta/schemas/openai/chat_completion_request.py +2 -1
- letta/schemas/passage.py +1 -0
- letta/schemas/providers/bedrock.py +1 -1
- letta/schemas/providers/openai.py +2 -2
- letta/schemas/tool.py +11 -5
- letta/schemas/tool_execution_result.py +0 -1
- letta/schemas/tool_rule.py +71 -0
- letta/serialize_schemas/marshmallow_agent.py +1 -2
- letta/server/rest_api/app.py +3 -3
- letta/server/rest_api/auth/index.py +0 -1
- letta/server/rest_api/interface.py +3 -11
- letta/server/rest_api/redis_stream_manager.py +3 -4
- letta/server/rest_api/routers/v1/agents.py +143 -84
- letta/server/rest_api/routers/v1/blocks.py +1 -1
- letta/server/rest_api/routers/v1/folders.py +1 -1
- letta/server/rest_api/routers/v1/groups.py +23 -22
- letta/server/rest_api/routers/v1/internal_templates.py +68 -0
- letta/server/rest_api/routers/v1/sandbox_configs.py +11 -5
- letta/server/rest_api/routers/v1/sources.py +1 -1
- letta/server/rest_api/routers/v1/tools.py +167 -15
- letta/server/rest_api/streaming_response.py +4 -3
- letta/server/rest_api/utils.py +75 -18
- letta/server/server.py +24 -35
- letta/services/agent_manager.py +359 -45
- letta/services/agent_serialization_manager.py +23 -3
- letta/services/archive_manager.py +72 -3
- letta/services/block_manager.py +1 -2
- letta/services/context_window_calculator/token_counter.py +11 -6
- letta/services/file_manager.py +1 -3
- letta/services/files_agents_manager.py +2 -4
- letta/services/group_manager.py +73 -12
- letta/services/helpers/agent_manager_helper.py +5 -5
- letta/services/identity_manager.py +8 -3
- letta/services/job_manager.py +2 -14
- letta/services/llm_batch_manager.py +1 -3
- letta/services/mcp/base_client.py +1 -2
- letta/services/mcp_manager.py +5 -6
- letta/services/message_manager.py +536 -15
- letta/services/organization_manager.py +1 -2
- letta/services/passage_manager.py +287 -12
- letta/services/provider_manager.py +1 -3
- letta/services/sandbox_config_manager.py +12 -7
- letta/services/source_manager.py +1 -2
- letta/services/step_manager.py +0 -1
- letta/services/summarizer/summarizer.py +4 -2
- letta/services/telemetry_manager.py +1 -3
- letta/services/tool_executor/builtin_tool_executor.py +136 -316
- letta/services/tool_executor/core_tool_executor.py +231 -74
- letta/services/tool_executor/files_tool_executor.py +2 -2
- letta/services/tool_executor/mcp_tool_executor.py +0 -1
- letta/services/tool_executor/multi_agent_tool_executor.py +2 -2
- letta/services/tool_executor/sandbox_tool_executor.py +0 -1
- letta/services/tool_executor/tool_execution_sandbox.py +2 -3
- letta/services/tool_manager.py +181 -64
- letta/services/tool_sandbox/modal_deployment_manager.py +2 -2
- letta/services/user_manager.py +1 -2
- letta/settings.py +5 -3
- letta/streaming_interface.py +3 -3
- letta/system.py +1 -1
- letta/utils.py +0 -1
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/METADATA +11 -7
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/RECORD +137 -135
- letta/llm_api/deepseek.py +0 -303
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.6.dev20250903104037.dist-info → letta_nightly-0.11.7.dev20250904104046.dist-info}/licenses/LICENSE +0 -0
letta/llm_api/openai.py
CHANGED
@@ -21,11 +21,15 @@ from letta.local_llm.utils import num_tokens_from_functions, num_tokens_from_mes
|
|
21
21
|
from letta.log import get_logger
|
22
22
|
from letta.otel.tracing import log_event
|
23
23
|
from letta.schemas.llm_config import LLMConfig
|
24
|
-
from letta.schemas.message import Message as
|
25
|
-
from letta.schemas.
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
from letta.schemas.message import Message as PydanticMessage, MessageRole as _MessageRole
|
25
|
+
from letta.schemas.openai.chat_completion_request import (
|
26
|
+
ChatCompletionRequest,
|
27
|
+
FunctionCall as ToolFunctionChoiceFunctionCall,
|
28
|
+
FunctionSchema,
|
29
|
+
Tool,
|
30
|
+
ToolFunctionChoice,
|
31
|
+
cast_message_to_subtype,
|
32
|
+
)
|
29
33
|
from letta.schemas.openai.chat_completion_response import (
|
30
34
|
ChatCompletionChunkResponse,
|
31
35
|
ChatCompletionResponse,
|
@@ -173,7 +177,7 @@ async def openai_get_model_list_async(
|
|
173
177
|
|
174
178
|
def build_openai_chat_completions_request(
|
175
179
|
llm_config: LLMConfig,
|
176
|
-
messages: List[
|
180
|
+
messages: List[PydanticMessage],
|
177
181
|
user_id: Optional[str],
|
178
182
|
functions: Optional[list],
|
179
183
|
function_call: Optional[str],
|
@@ -197,13 +201,12 @@ def build_openai_chat_completions_request(
|
|
197
201
|
use_developer_message = accepts_developer_role(llm_config.model)
|
198
202
|
|
199
203
|
openai_message_list = [
|
200
|
-
cast_message_to_subtype(
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
204
|
+
cast_message_to_subtype(m)
|
205
|
+
for m in PydanticMessage.to_openai_dicts_from_list(
|
206
|
+
messages,
|
207
|
+
put_inner_thoughts_in_kwargs=llm_config.put_inner_thoughts_in_kwargs,
|
208
|
+
use_developer_message=use_developer_message,
|
205
209
|
)
|
206
|
-
for m in messages
|
207
210
|
]
|
208
211
|
|
209
212
|
if llm_config.model:
|
@@ -322,7 +325,7 @@ def openai_chat_completions_process_stream(
|
|
322
325
|
|
323
326
|
# Create a dummy Message object to get an ID and date
|
324
327
|
# TODO(sarah): add message ID generation function
|
325
|
-
dummy_message =
|
328
|
+
dummy_message = PydanticMessage(
|
326
329
|
role=_MessageRole.assistant,
|
327
330
|
content=[],
|
328
331
|
agent_id="",
|
letta/llm_api/openai_client.py
CHANGED
@@ -29,11 +29,14 @@ from letta.schemas.embedding_config import EmbeddingConfig
|
|
29
29
|
from letta.schemas.letta_message_content import MessageContentType
|
30
30
|
from letta.schemas.llm_config import LLMConfig
|
31
31
|
from letta.schemas.message import Message as PydanticMessage
|
32
|
-
from letta.schemas.openai.chat_completion_request import
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
from letta.schemas.openai.chat_completion_request import (
|
33
|
+
ChatCompletionRequest,
|
34
|
+
FunctionCall as ToolFunctionChoiceFunctionCall,
|
35
|
+
FunctionSchema,
|
36
|
+
Tool as OpenAITool,
|
37
|
+
ToolFunctionChoice,
|
38
|
+
cast_message_to_subtype,
|
39
|
+
)
|
37
40
|
from letta.schemas.openai.chat_completion_response import ChatCompletionResponse
|
38
41
|
from letta.settings import model_settings
|
39
42
|
|
@@ -44,7 +47,7 @@ def is_openai_reasoning_model(model: str) -> bool:
|
|
44
47
|
"""Utility function to check if the model is a 'reasoner'"""
|
45
48
|
|
46
49
|
# NOTE: needs to be updated with new model releases
|
47
|
-
is_reasoning = model.startswith("o1") or model.startswith("o3") or model.startswith("o4")
|
50
|
+
is_reasoning = model.startswith("o1") or model.startswith("o3") or model.startswith("o4") or model.startswith("gpt-5")
|
48
51
|
return is_reasoning
|
49
52
|
|
50
53
|
|
@@ -176,13 +179,12 @@ class OpenAIClient(LLMClientBase):
|
|
176
179
|
use_developer_message = accepts_developer_role(llm_config.model)
|
177
180
|
|
178
181
|
openai_message_list = [
|
179
|
-
cast_message_to_subtype(
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
182
|
+
cast_message_to_subtype(m)
|
183
|
+
for m in PydanticMessage.to_openai_dicts_from_list(
|
184
|
+
messages,
|
185
|
+
put_inner_thoughts_in_kwargs=llm_config.put_inner_thoughts_in_kwargs,
|
186
|
+
use_developer_message=use_developer_message,
|
184
187
|
)
|
185
|
-
for m in messages
|
186
188
|
]
|
187
189
|
|
188
190
|
if llm_config.model:
|
@@ -219,6 +221,10 @@ class OpenAIClient(LLMClientBase):
|
|
219
221
|
if supports_verbosity_control(model) and llm_config.verbosity:
|
220
222
|
data.verbosity = llm_config.verbosity
|
221
223
|
|
224
|
+
# Add reasoning effort control for reasoning models
|
225
|
+
if is_openai_reasoning_model(model) and llm_config.reasoning_effort:
|
226
|
+
data.reasoning_effort = llm_config.reasoning_effort
|
227
|
+
|
222
228
|
if llm_config.frequency_penalty is not None:
|
223
229
|
data.frequency_penalty = llm_config.frequency_penalty
|
224
230
|
|
@@ -357,10 +363,19 @@ class OpenAIClient(LLMClientBase):
|
|
357
363
|
if isinstance(e, openai.BadRequestError):
|
358
364
|
logger.warning(f"[OpenAI] Bad request (400): {str(e)}")
|
359
365
|
# BadRequestError can signify different issues (e.g., invalid args, context length)
|
360
|
-
# Check
|
361
|
-
|
362
|
-
|
363
|
-
|
366
|
+
# Check for context_length_exceeded error code in the error body
|
367
|
+
error_code = None
|
368
|
+
if e.body and isinstance(e.body, dict):
|
369
|
+
error_details = e.body.get("error", {})
|
370
|
+
if isinstance(error_details, dict):
|
371
|
+
error_code = error_details.get("code")
|
372
|
+
|
373
|
+
# Check both the error code and message content for context length issues
|
374
|
+
if (
|
375
|
+
error_code == "context_length_exceeded"
|
376
|
+
or "This model's maximum context length is" in str(e)
|
377
|
+
or "Input tokens exceed the configured limit" in str(e)
|
378
|
+
):
|
364
379
|
return ContextWindowExceededError(
|
365
380
|
message=f"Bad request to OpenAI (context window exceeded): {str(e)}",
|
366
381
|
)
|
letta/llm_api/together_client.py
CHANGED
letta/llm_api/xai_client.py
CHANGED
@@ -22,6 +22,7 @@ from letta.local_llm.webui.api import get_webui_completion
|
|
22
22
|
from letta.local_llm.webui.legacy_api import get_webui_completion as get_webui_completion_legacy
|
23
23
|
from letta.otel.tracing import log_event
|
24
24
|
from letta.prompts.gpt_summarize import SYSTEM as SUMMARIZE_SYSTEM_MESSAGE
|
25
|
+
from letta.schemas.message import Message as PydanticMessage
|
25
26
|
from letta.schemas.openai.chat_completion_response import ChatCompletionResponse, Choice, Message, ToolCall, UsageStatistics
|
26
27
|
from letta.utils import get_tool_call_id
|
27
28
|
|
@@ -61,7 +62,7 @@ def get_chat_completion(
|
|
61
62
|
|
62
63
|
# TODO: eventually just process Message object
|
63
64
|
if not isinstance(messages[0], dict):
|
64
|
-
messages =
|
65
|
+
messages = PydanticMessage.to_openai_dicts_from_list(messages)
|
65
66
|
|
66
67
|
if function_call is not None and function_call != "auto":
|
67
68
|
raise ValueError(f"function_call == {function_call} not supported (auto or None only)")
|
@@ -205,7 +206,7 @@ def get_chat_completion(
|
|
205
206
|
raise LocalLLMError(f"usage dict in response was missing fields ({usage})")
|
206
207
|
|
207
208
|
if usage["prompt_tokens"] is None:
|
208
|
-
printd(
|
209
|
+
printd("usage dict was missing prompt_tokens, computing on-the-fly...")
|
209
210
|
usage["prompt_tokens"] = count_tokens(prompt)
|
210
211
|
|
211
212
|
# NOTE: we should compute on-the-fly anyways since we might have to correct for errors during JSON parsing
|
@@ -220,7 +221,7 @@ def get_chat_completion(
|
|
220
221
|
|
221
222
|
# NOTE: this is the token count that matters most
|
222
223
|
if usage["total_tokens"] is None:
|
223
|
-
printd(
|
224
|
+
printd("usage dict was missing total_tokens, computing on-the-fly...")
|
224
225
|
usage["total_tokens"] = usage["prompt_tokens"] + usage["completion_tokens"]
|
225
226
|
|
226
227
|
# unpack with response.choices[0].message.content
|
@@ -261,9 +262,9 @@ def generate_grammar_and_documentation(
|
|
261
262
|
):
|
262
263
|
from letta.utils import printd
|
263
264
|
|
264
|
-
assert not (
|
265
|
-
|
266
|
-
)
|
265
|
+
assert not (add_inner_thoughts_top_level and add_inner_thoughts_param_level), (
|
266
|
+
"Can only place inner thoughts in one location in the grammar generator"
|
267
|
+
)
|
267
268
|
|
268
269
|
grammar_function_models = []
|
269
270
|
# create_dynamic_model_from_function will add inner thoughts to the function parameters if add_inner_thoughts is True.
|
@@ -46,7 +46,7 @@ def get_completions_settings(defaults="simple") -> dict:
|
|
46
46
|
with open(settings_file, "r", encoding="utf-8") as file:
|
47
47
|
user_settings = json.load(file)
|
48
48
|
if len(user_settings) > 0:
|
49
|
-
printd(f"Updating base settings with the following user settings:\n{json_dumps(user_settings,indent=2)}")
|
49
|
+
printd(f"Updating base settings with the following user settings:\n{json_dumps(user_settings, indent=2)}")
|
50
50
|
settings.update(user_settings)
|
51
51
|
else:
|
52
52
|
printd(f"'{settings_file}' was empty, ignoring...")
|
letta/orm/__init__.py
CHANGED
@@ -22,6 +22,7 @@ from letta.orm.mcp_server import MCPServer
|
|
22
22
|
from letta.orm.message import Message
|
23
23
|
from letta.orm.organization import Organization
|
24
24
|
from letta.orm.passage import ArchivalPassage, BasePassage, SourcePassage
|
25
|
+
from letta.orm.passage_tag import PassageTag
|
25
26
|
from letta.orm.prompt import Prompt
|
26
27
|
from letta.orm.provider import Provider
|
27
28
|
from letta.orm.provider_trace import ProviderTrace
|
letta/orm/agent.py
CHANGED
@@ -10,11 +10,10 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
10
10
|
from letta.orm.block import Block
|
11
11
|
from letta.orm.custom_columns import EmbeddingConfigColumn, LLMConfigColumn, ResponseFormatColumn, ToolRulesColumn
|
12
12
|
from letta.orm.identity import Identity
|
13
|
-
from letta.orm.mixins import OrganizationMixin, ProjectMixin
|
13
|
+
from letta.orm.mixins import OrganizationMixin, ProjectMixin, TemplateEntityMixin, TemplateMixin
|
14
14
|
from letta.orm.organization import Organization
|
15
15
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
16
|
-
from letta.schemas.agent import AgentState as PydanticAgentState
|
17
|
-
from letta.schemas.agent import AgentType, get_prompt_template_for_agent_type
|
16
|
+
from letta.schemas.agent import AgentState as PydanticAgentState, AgentType, get_prompt_template_for_agent_type
|
18
17
|
from letta.schemas.embedding_config import EmbeddingConfig
|
19
18
|
from letta.schemas.llm_config import LLMConfig
|
20
19
|
from letta.schemas.memory import Memory
|
@@ -32,7 +31,7 @@ if TYPE_CHECKING:
|
|
32
31
|
from letta.orm.tool import Tool
|
33
32
|
|
34
33
|
|
35
|
-
class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, AsyncAttrs):
|
34
|
+
class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin, TemplateMixin, AsyncAttrs):
|
36
35
|
__tablename__ = "agents"
|
37
36
|
__pydantic_model__ = PydanticAgentState
|
38
37
|
__table_args__ = (Index("ix_agents_created_at", "created_at", "id"),)
|
@@ -68,8 +67,6 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, AsyncAttrs):
|
|
68
67
|
embedding_config: Mapped[Optional[EmbeddingConfig]] = mapped_column(
|
69
68
|
EmbeddingConfigColumn, doc="the embedding configuration object for this agent."
|
70
69
|
)
|
71
|
-
template_id: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The id of the template the agent belongs to.")
|
72
|
-
base_template_id: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="The base template id of the agent.")
|
73
70
|
|
74
71
|
# Tool rules
|
75
72
|
tool_rules: Mapped[Optional[List[ToolRule]]] = mapped_column(ToolRulesColumn, doc="the tool rules for this agent.")
|
@@ -103,6 +100,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, AsyncAttrs):
|
|
103
100
|
|
104
101
|
# indexing controls
|
105
102
|
hidden: Mapped[Optional[bool]] = mapped_column(Boolean, nullable=True, default=None, doc="If set to True, the agent will be hidden.")
|
103
|
+
_vector_db_namespace: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="Private field for vector database namespace")
|
106
104
|
|
107
105
|
# relationships
|
108
106
|
organization: Mapped["Organization"] = relationship("Organization", back_populates="agents", lazy="raise")
|
@@ -208,6 +206,8 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, AsyncAttrs):
|
|
208
206
|
"project_id": self.project_id,
|
209
207
|
"template_id": self.template_id,
|
210
208
|
"base_template_id": self.base_template_id,
|
209
|
+
"deployment_id": self.deployment_id,
|
210
|
+
"entity_id": self.entity_id,
|
211
211
|
"tool_rules": self.tool_rules,
|
212
212
|
"message_buffer_autoclear": self.message_buffer_autoclear,
|
213
213
|
"created_by_id": self.created_by_id,
|
@@ -296,6 +296,8 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, AsyncAttrs):
|
|
296
296
|
"project_id": self.project_id,
|
297
297
|
"template_id": self.template_id,
|
298
298
|
"base_template_id": self.base_template_id,
|
299
|
+
"deployment_id": self.deployment_id,
|
300
|
+
"entity_id": self.entity_id,
|
299
301
|
"tool_rules": self.tool_rules,
|
300
302
|
"message_buffer_autoclear": self.message_buffer_autoclear,
|
301
303
|
"created_by_id": self.created_by_id,
|
letta/orm/archive.py
CHANGED
@@ -2,12 +2,13 @@ import uuid
|
|
2
2
|
from datetime import datetime, timezone
|
3
3
|
from typing import TYPE_CHECKING, List, Optional
|
4
4
|
|
5
|
-
from sqlalchemy import JSON, Index, String
|
5
|
+
from sqlalchemy import JSON, Enum, Index, String
|
6
6
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
7
7
|
|
8
8
|
from letta.orm.mixins import OrganizationMixin
|
9
9
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
10
10
|
from letta.schemas.archive import Archive as PydanticArchive
|
11
|
+
from letta.schemas.enums import VectorDBProvider
|
11
12
|
from letta.settings import DatabaseChoice, settings
|
12
13
|
|
13
14
|
if TYPE_CHECKING:
|
@@ -38,7 +39,14 @@ class Archive(SqlalchemyBase, OrganizationMixin):
|
|
38
39
|
# archive-specific fields
|
39
40
|
name: Mapped[str] = mapped_column(String, nullable=False, doc="The name of the archive")
|
40
41
|
description: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="A description of the archive")
|
42
|
+
vector_db_provider: Mapped[VectorDBProvider] = mapped_column(
|
43
|
+
Enum(VectorDBProvider),
|
44
|
+
nullable=False,
|
45
|
+
default=VectorDBProvider.NATIVE,
|
46
|
+
doc="The vector database provider used for this archive's passages",
|
47
|
+
)
|
41
48
|
metadata_: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True, doc="Additional metadata for the archive")
|
49
|
+
_vector_db_namespace: Mapped[Optional[str]] = mapped_column(String, nullable=True, doc="Private field for vector database namespace")
|
42
50
|
|
43
51
|
# relationships
|
44
52
|
archives_agents: Mapped[List["ArchivesAgents"]] = relationship(
|
letta/orm/block.py
CHANGED
@@ -6,17 +6,16 @@ from sqlalchemy.orm import Mapped, attributes, declared_attr, mapped_column, rel
|
|
6
6
|
from letta.constants import CORE_MEMORY_BLOCK_CHAR_LIMIT
|
7
7
|
from letta.orm.block_history import BlockHistory
|
8
8
|
from letta.orm.blocks_agents import BlocksAgents
|
9
|
-
from letta.orm.mixins import OrganizationMixin, ProjectMixin
|
9
|
+
from letta.orm.mixins import OrganizationMixin, ProjectMixin, TemplateEntityMixin, TemplateMixin
|
10
10
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
11
|
-
from letta.schemas.block import Block as PydanticBlock
|
12
|
-
from letta.schemas.block import Human, Persona
|
11
|
+
from letta.schemas.block import Block as PydanticBlock, Human, Persona
|
13
12
|
|
14
13
|
if TYPE_CHECKING:
|
15
14
|
from letta.orm import Organization
|
16
15
|
from letta.orm.identity import Identity
|
17
16
|
|
18
17
|
|
19
|
-
class Block(OrganizationMixin, SqlalchemyBase, ProjectMixin):
|
18
|
+
class Block(OrganizationMixin, SqlalchemyBase, ProjectMixin, TemplateEntityMixin, TemplateMixin):
|
20
19
|
"""Blocks are sections of the LLM context, representing a specific part of the total Memory"""
|
21
20
|
|
22
21
|
__tablename__ = "block"
|
letta/orm/block_history.py
CHANGED
@@ -38,7 +38,9 @@ class BlockHistory(OrganizationMixin, SqlalchemyBase):
|
|
38
38
|
|
39
39
|
# Relationships
|
40
40
|
block_id: Mapped[str] = mapped_column(
|
41
|
-
String,
|
41
|
+
String,
|
42
|
+
ForeignKey("block.id", ondelete="CASCADE"),
|
43
|
+
nullable=False, # History deleted if Block is deleted
|
42
44
|
)
|
43
45
|
|
44
46
|
sequence_number: Mapped[int] = mapped_column(
|
letta/orm/group.py
CHANGED
@@ -4,13 +4,12 @@ from typing import List, Optional
|
|
4
4
|
from sqlalchemy import JSON, ForeignKey, String
|
5
5
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
6
6
|
|
7
|
-
from letta.orm.mixins import OrganizationMixin, ProjectMixin
|
7
|
+
from letta.orm.mixins import OrganizationMixin, ProjectMixin, TemplateMixin
|
8
8
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
9
9
|
from letta.schemas.group import Group as PydanticGroup
|
10
10
|
|
11
11
|
|
12
|
-
class Group(SqlalchemyBase, OrganizationMixin, ProjectMixin):
|
13
|
-
|
12
|
+
class Group(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateMixin):
|
14
13
|
__tablename__ = "groups"
|
15
14
|
__pydantic_model__ = PydanticGroup
|
16
15
|
|
letta/orm/identity.py
CHANGED
@@ -7,8 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
7
7
|
|
8
8
|
from letta.orm.mixins import OrganizationMixin, ProjectMixin
|
9
9
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
10
|
-
from letta.schemas.identity import Identity as PydanticIdentity
|
11
|
-
from letta.schemas.identity import IdentityProperty
|
10
|
+
from letta.schemas.identity import Identity as PydanticIdentity, IdentityProperty
|
12
11
|
|
13
12
|
|
14
13
|
class Identity(SqlalchemyBase, OrganizationMixin, ProjectMixin):
|
letta/orm/job.py
CHANGED
@@ -7,8 +7,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
7
7
|
from letta.orm.mixins import UserMixin
|
8
8
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
9
9
|
from letta.schemas.enums import JobStatus, JobType
|
10
|
-
from letta.schemas.job import Job as PydanticJob
|
11
|
-
from letta.schemas.job import LettaRequestConfig
|
10
|
+
from letta.schemas.job import Job as PydanticJob, LettaRequestConfig
|
12
11
|
|
13
12
|
if TYPE_CHECKING:
|
14
13
|
from letta.orm.job_messages import JobMessage
|
letta/orm/llm_batch_items.py
CHANGED
@@ -9,8 +9,7 @@ from letta.orm.custom_columns import AgentStepStateColumn, BatchRequestResultCol
|
|
9
9
|
from letta.orm.mixins import AgentMixin, OrganizationMixin
|
10
10
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
11
11
|
from letta.schemas.enums import AgentStepStatus, JobStatus
|
12
|
-
from letta.schemas.llm_batch_job import AgentStepState
|
13
|
-
from letta.schemas.llm_batch_job import LLMBatchItem as PydanticLLMBatchItem
|
12
|
+
from letta.schemas.llm_batch_job import AgentStepState, LLMBatchItem as PydanticLLMBatchItem
|
14
13
|
from letta.schemas.llm_config import LLMConfig
|
15
14
|
|
16
15
|
|
letta/orm/message.py
CHANGED
@@ -7,10 +7,8 @@ from sqlalchemy.orm import Mapped, Session, mapped_column, relationship
|
|
7
7
|
from letta.orm.custom_columns import MessageContentColumn, ToolCallColumn, ToolReturnColumn
|
8
8
|
from letta.orm.mixins import AgentMixin, OrganizationMixin
|
9
9
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
10
|
-
from letta.schemas.letta_message_content import MessageContent
|
11
|
-
from letta.schemas.
|
12
|
-
from letta.schemas.message import Message as PydanticMessage
|
13
|
-
from letta.schemas.message import ToolReturn
|
10
|
+
from letta.schemas.letta_message_content import MessageContent, TextContent as PydanticTextContent
|
11
|
+
from letta.schemas.message import Message as PydanticMessage, ToolReturn
|
14
12
|
from letta.settings import DatabaseChoice, settings
|
15
13
|
|
16
14
|
|
@@ -52,6 +50,12 @@ class Message(SqlalchemyBase, OrganizationMixin, AgentMixin):
|
|
52
50
|
is_err: Mapped[Optional[bool]] = mapped_column(
|
53
51
|
nullable=True, doc="Whether this message is part of an error step. Used only for debugging purposes."
|
54
52
|
)
|
53
|
+
approval_request_id: Mapped[Optional[str]] = mapped_column(
|
54
|
+
nullable=True,
|
55
|
+
doc="The id of the approval request if this message is associated with a tool call request.",
|
56
|
+
)
|
57
|
+
approve: Mapped[Optional[bool]] = mapped_column(nullable=True, doc="Whether tool call is approved.")
|
58
|
+
denial_reason: Mapped[Optional[str]] = mapped_column(nullable=True, doc="The reason the tool call request was denied.")
|
55
59
|
|
56
60
|
# Monotonically increasing sequence for efficient/correct listing
|
57
61
|
sequence_id: Mapped[int] = mapped_column(
|
letta/orm/mixins.py
CHANGED
@@ -78,3 +78,21 @@ class ArchiveMixin(Base):
|
|
78
78
|
__abstract__ = True
|
79
79
|
|
80
80
|
archive_id: Mapped[str] = mapped_column(String, ForeignKey("archives.id", ondelete="CASCADE"))
|
81
|
+
|
82
|
+
|
83
|
+
class TemplateMixin(Base):
|
84
|
+
"""TemplateMixin for models that belong to a template."""
|
85
|
+
|
86
|
+
__abstract__ = True
|
87
|
+
|
88
|
+
base_template_id: Mapped[str] = mapped_column(nullable=True, doc="The id of the base template.")
|
89
|
+
template_id: Mapped[str] = mapped_column(nullable=True, doc="The id of the template.")
|
90
|
+
deployment_id: Mapped[str] = mapped_column(nullable=True, doc="The id of the deployment.")
|
91
|
+
|
92
|
+
|
93
|
+
class TemplateEntityMixin(Base):
|
94
|
+
"""Mixin for models that belong to an entity (only used for templates)."""
|
95
|
+
|
96
|
+
__abstract__ = True
|
97
|
+
|
98
|
+
entity_id: Mapped[str] = mapped_column(nullable=True, doc="The id of the entity within the template.")
|
letta/orm/organization.py
CHANGED
@@ -16,6 +16,7 @@ if TYPE_CHECKING:
|
|
16
16
|
from letta.orm.llm_batch_job import LLMBatchJob
|
17
17
|
from letta.orm.message import Message
|
18
18
|
from letta.orm.passage import ArchivalPassage, SourcePassage
|
19
|
+
from letta.orm.passage_tag import PassageTag
|
19
20
|
from letta.orm.provider import Provider
|
20
21
|
from letta.orm.sandbox_config import AgentEnvironmentVariable, SandboxConfig, SandboxEnvironmentVariable
|
21
22
|
from letta.orm.tool import Tool
|
@@ -56,6 +57,7 @@ class Organization(SqlalchemyBase):
|
|
56
57
|
archival_passages: Mapped[List["ArchivalPassage"]] = relationship(
|
57
58
|
"ArchivalPassage", back_populates="organization", cascade="all, delete-orphan"
|
58
59
|
)
|
60
|
+
passage_tags: Mapped[List["PassageTag"]] = relationship("PassageTag", back_populates="organization", cascade="all, delete-orphan")
|
59
61
|
archives: Mapped[List["Archive"]] = relationship("Archive", back_populates="organization", cascade="all, delete-orphan")
|
60
62
|
providers: Mapped[List["Provider"]] = relationship("Provider", back_populates="organization", cascade="all, delete-orphan")
|
61
63
|
identities: Mapped[List["Identity"]] = relationship("Identity", back_populates="organization", cascade="all, delete-orphan")
|
letta/orm/passage.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import TYPE_CHECKING
|
1
|
+
from typing import TYPE_CHECKING, List, Optional
|
2
2
|
|
3
3
|
from sqlalchemy import JSON, Column, Index
|
4
4
|
from sqlalchemy.orm import Mapped, declared_attr, mapped_column, relationship
|
@@ -27,6 +27,8 @@ class BasePassage(SqlalchemyBase, OrganizationMixin):
|
|
27
27
|
text: Mapped[str] = mapped_column(doc="Passage text content")
|
28
28
|
embedding_config: Mapped[dict] = mapped_column(EmbeddingConfigColumn, doc="Embedding configuration")
|
29
29
|
metadata_: Mapped[dict] = mapped_column(JSON, doc="Additional metadata")
|
30
|
+
# dual storage: json column for fast retrieval, junction table for efficient queries
|
31
|
+
tags: Mapped[Optional[List[str]]] = mapped_column(JSON, nullable=True, doc="Tags associated with this passage")
|
30
32
|
|
31
33
|
# Vector embedding field based on database type
|
32
34
|
if settings.database_engine is DatabaseChoice.POSTGRES:
|
@@ -75,6 +77,11 @@ class ArchivalPassage(BasePassage, ArchiveMixin):
|
|
75
77
|
|
76
78
|
__tablename__ = "archival_passages"
|
77
79
|
|
80
|
+
# junction table for efficient tag queries (complements json column above)
|
81
|
+
passage_tags: Mapped[List["PassageTag"]] = relationship(
|
82
|
+
"PassageTag", back_populates="passage", cascade="all, delete-orphan", lazy="noload"
|
83
|
+
)
|
84
|
+
|
78
85
|
@declared_attr
|
79
86
|
def organization(cls) -> Mapped["Organization"]:
|
80
87
|
return relationship("Organization", back_populates="archival_passages", lazy="selectin")
|
letta/orm/passage_tag.py
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
from typing import TYPE_CHECKING
|
2
|
+
|
3
|
+
from sqlalchemy import ForeignKey, Index, String, UniqueConstraint
|
4
|
+
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
5
|
+
|
6
|
+
from letta.orm.mixins import OrganizationMixin
|
7
|
+
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from letta.orm.organization import Organization
|
11
|
+
from letta.orm.passage import ArchivalPassage
|
12
|
+
|
13
|
+
|
14
|
+
class PassageTag(SqlalchemyBase, OrganizationMixin):
|
15
|
+
"""Junction table for tags associated with passages.
|
16
|
+
|
17
|
+
Design: dual storage approach where tags are stored both in:
|
18
|
+
1. JSON column in passages table (fast retrieval with passage data)
|
19
|
+
2. This junction table (efficient DISTINCT/COUNT queries and filtering)
|
20
|
+
"""
|
21
|
+
|
22
|
+
__tablename__ = "passage_tags"
|
23
|
+
|
24
|
+
__table_args__ = (
|
25
|
+
# ensure uniqueness of tag per passage
|
26
|
+
UniqueConstraint("passage_id", "tag", name="uq_passage_tag"),
|
27
|
+
# indexes for efficient queries
|
28
|
+
Index("ix_passage_tags_archive_id", "archive_id"),
|
29
|
+
Index("ix_passage_tags_tag", "tag"),
|
30
|
+
Index("ix_passage_tags_archive_tag", "archive_id", "tag"),
|
31
|
+
Index("ix_passage_tags_org_archive", "organization_id", "archive_id"),
|
32
|
+
)
|
33
|
+
|
34
|
+
# primary key
|
35
|
+
id: Mapped[str] = mapped_column(String, primary_key=True, doc="Unique identifier for the tag entry")
|
36
|
+
|
37
|
+
# tag value
|
38
|
+
tag: Mapped[str] = mapped_column(String, nullable=False, doc="The tag value")
|
39
|
+
|
40
|
+
# foreign keys
|
41
|
+
passage_id: Mapped[str] = mapped_column(
|
42
|
+
String, ForeignKey("archival_passages.id", ondelete="CASCADE"), nullable=False, doc="ID of the passage this tag belongs to"
|
43
|
+
)
|
44
|
+
|
45
|
+
archive_id: Mapped[str] = mapped_column(
|
46
|
+
String,
|
47
|
+
ForeignKey("archives.id", ondelete="CASCADE"),
|
48
|
+
nullable=False,
|
49
|
+
doc="ID of the archive this passage belongs to (denormalized for efficient queries)",
|
50
|
+
)
|
51
|
+
|
52
|
+
# relationships
|
53
|
+
passage: Mapped["ArchivalPassage"] = relationship("ArchivalPassage", back_populates="passage_tags", lazy="noload")
|
54
|
+
|
55
|
+
organization: Mapped["Organization"] = relationship("Organization", back_populates="passage_tags", lazy="selectin")
|
letta/orm/sandbox_config.py
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
import uuid
|
2
2
|
from typing import TYPE_CHECKING, Dict, List, Optional
|
3
3
|
|
4
|
-
from sqlalchemy import JSON
|
5
|
-
from sqlalchemy import Enum as SqlEnum
|
6
|
-
from sqlalchemy import Index, String, UniqueConstraint
|
4
|
+
from sqlalchemy import JSON, Enum as SqlEnum, Index, String, UniqueConstraint
|
7
5
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
8
6
|
|
9
7
|
from letta.orm.mixins import AgentMixin, OrganizationMixin, SandboxConfigMixin
|
letta/orm/step.py
CHANGED
@@ -7,7 +7,6 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
7
7
|
from letta.orm.mixins import ProjectMixin
|
8
8
|
from letta.orm.sqlalchemy_base import SqlalchemyBase
|
9
9
|
from letta.schemas.enums import StepStatus
|
10
|
-
from letta.schemas.letta_stop_reason import StopReasonType
|
11
10
|
from letta.schemas.step import Step as PydanticStep
|
12
11
|
|
13
12
|
if TYPE_CHECKING:
|
@@ -51,7 +50,7 @@ class Step(SqlalchemyBase, ProjectMixin):
|
|
51
50
|
prompt_tokens: Mapped[int] = mapped_column(default=0, doc="Number of tokens in the prompt")
|
52
51
|
total_tokens: Mapped[int] = mapped_column(default=0, doc="Total number of tokens processed by the agent")
|
53
52
|
completion_tokens_details: Mapped[Optional[Dict]] = mapped_column(JSON, nullable=True, doc="metadata for the agent.")
|
54
|
-
stop_reason: Mapped[Optional[
|
53
|
+
stop_reason: Mapped[Optional[str]] = mapped_column(None, nullable=True, doc="The stop reason associated with this step.")
|
55
54
|
tags: Mapped[Optional[List]] = mapped_column(JSON, doc="Metadata tags.")
|
56
55
|
tid: Mapped[Optional[str]] = mapped_column(None, nullable=True, doc="Transaction ID that processed the step.")
|
57
56
|
trace_id: Mapped[Optional[str]] = mapped_column(None, nullable=True, doc="The trace id of the agent step.")
|
letta/orm/tool.py
CHANGED
@@ -49,6 +49,7 @@ class Tool(SqlalchemyBase, OrganizationMixin):
|
|
49
49
|
JSON, nullable=True, doc="Optional list of pip packages required by this tool."
|
50
50
|
)
|
51
51
|
npm_requirements: Mapped[list | None] = mapped_column(JSON, doc="Optional list of npm packages required by this tool.")
|
52
|
+
default_requires_approval: Mapped[bool] = mapped_column(nullable=True, doc="Whether or not to require approval.")
|
52
53
|
metadata_: Mapped[Optional[dict]] = mapped_column(JSON, default=lambda: {}, doc="A dictionary of additional metadata for the tool.")
|
53
54
|
# relationships
|
54
55
|
organization: Mapped["Organization"] = relationship("Organization", back_populates="tools", lazy="selectin")
|
letta/otel/resource.py
CHANGED
@@ -1,16 +1,16 @@
|
|
1
|
-
import os
|
2
1
|
import sys
|
3
2
|
import uuid
|
4
3
|
|
5
4
|
from opentelemetry.sdk.resources import Resource
|
6
5
|
|
7
6
|
from letta import __version__ as letta_version
|
7
|
+
from letta.settings import settings
|
8
8
|
|
9
9
|
_resources = {}
|
10
10
|
|
11
11
|
|
12
12
|
def get_resource(service_name: str) -> Resource:
|
13
|
-
_env =
|
13
|
+
_env = settings.environment
|
14
14
|
if service_name not in _resources:
|
15
15
|
resource_dict = {
|
16
16
|
"service.name": service_name,
|
letta/plugins/plugins.py
CHANGED
@@ -37,7 +37,7 @@ def get_plugin(plugin_type: str):
|
|
37
37
|
return plugin
|
38
38
|
elif type(plugin).__name__ == "class":
|
39
39
|
if plugin_register["protocol"] and not isinstance(plugin, type(plugin_register["protocol"])):
|
40
|
-
raise TypeError(f
|
40
|
+
raise TypeError(f"{plugin} does not implement {type(plugin_register['protocol']).__name__}")
|
41
41
|
return plugin()
|
42
42
|
raise TypeError("Unknown plugin type")
|
43
43
|
|