From acd8dd7bcf286b3d9de5af01ba45c66b8ba54fde Mon Sep 17 00:00:00 2001 From: Sarah Wooders Date: Fri, 19 Dec 2025 16:35:44 -0800 Subject: [PATCH] feat: make embedding_config optional on agent creation (#7553) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: make embedding_config optional on agent creation - Remove requirement for embedding_config in agent creation - Add EmbeddingConfigRequiredError for operations that need embeddings - Add null checks in sleeptime agent creation, passage insert, archive creation - Register new error in app.py exception handlers 🐾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta * chore: update API schemas for optional embedding_config 🐾 Generated with [Letta Code](https://letta.com) Co-Authored-By: Letta --------- Co-authored-by: Letta --- fern/openapi.json | 10 +++++++-- letta/errors.py | 13 +++++++++++ letta/orm/agent.py | 2 +- letta/schemas/agent.py | 4 ++-- letta/server/rest_api/app.py | 2 ++ letta/server/server.py | 36 ++++++++++++++++++++----------- letta/services/agent_manager.py | 4 ++-- letta/services/archive_manager.py | 3 +++ letta/services/passage_manager.py | 3 +++ 9 files changed, 57 insertions(+), 20 deletions(-) diff --git a/fern/openapi.json b/fern/openapi.json index 412806e4..c1ca0b5f 100644 --- a/fern/openapi.json +++ b/fern/openapi.json @@ -21037,7 +21037,14 @@ "deprecated": true }, "embedding_config": { - "$ref": "#/components/schemas/EmbeddingConfig", + "anyOf": [ + { + "$ref": "#/components/schemas/EmbeddingConfig" + }, + { + "type": "null" + } + ], "description": "Deprecated: Use `embedding` field instead. The embedding configuration used by the agent.", "deprecated": true }, @@ -21461,7 +21468,6 @@ "system", "agent_type", "llm_config", - "embedding_config", "memory", "blocks", "tools", diff --git a/letta/errors.py b/letta/errors.py index 30ca0ad2..1328f069 100644 --- a/letta/errors.py +++ b/letta/errors.py @@ -90,6 +90,19 @@ class LettaConfigurationError(LettaError): super().__init__(message=message, details={"missing_fields": self.missing_fields}) +class EmbeddingConfigRequiredError(LettaError): + """Error raised when an operation requires embedding_config but the agent doesn't have one configured.""" + + def __init__(self, agent_id: Optional[str] = None, operation: Optional[str] = None): + self.agent_id = agent_id + self.operation = operation + message = "This operation requires an embedding configuration, but the agent does not have one configured." + if operation: + message = f"Operation '{operation}' requires an embedding configuration, but the agent does not have one configured." + details = {"agent_id": agent_id, "operation": operation} + super().__init__(message=message, code=ErrorCode.INVALID_ARGUMENT, details=details) + + class LettaAgentNotFoundError(LettaError): """Error raised when an agent is not found.""" diff --git a/letta/orm/agent.py b/letta/orm/agent.py index 7a431a93..216968df 100644 --- a/letta/orm/agent.py +++ b/letta/orm/agent.py @@ -74,7 +74,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, ProjectMixin, TemplateEntityMixin LLMConfigColumn, nullable=True, doc="the LLM backend configuration object for this agent." ) embedding_config: Mapped[Optional[EmbeddingConfig]] = mapped_column( - EmbeddingConfigColumn, doc="the embedding configuration object for this agent." + EmbeddingConfigColumn, nullable=True, doc="the embedding configuration object for this agent." ) compaction_settings: Mapped[Optional[dict]] = mapped_column( CompactionSettingsColumn, nullable=True, doc="the compaction settings configuration object for compaction." diff --git a/letta/schemas/agent.py b/letta/schemas/agent.py index 57d8e0b8..b6a22ea8 100644 --- a/letta/schemas/agent.py +++ b/letta/schemas/agent.py @@ -81,8 +81,8 @@ class AgentState(OrmMetadataBase, validate_assignment=True): llm_config: LLMConfig = Field( ..., description="Deprecated: Use `model` field instead. The LLM configuration used by the agent.", deprecated=True ) - embedding_config: EmbeddingConfig = Field( - ..., description="Deprecated: Use `embedding` field instead. The embedding configuration used by the agent.", deprecated=True + embedding_config: Optional[EmbeddingConfig] = Field( + None, description="Deprecated: Use `embedding` field instead. The embedding configuration used by the agent.", deprecated=True ) model: Optional[str] = Field(None, description="The model handle used by the agent (format: provider/model-name).") embedding: Optional[str] = Field(None, description="The embedding model handle used by the agent (format: provider/model-name).") diff --git a/letta/server/rest_api/app.py b/letta/server/rest_api/app.py index 17089dbf..f1441ea5 100644 --- a/letta/server/rest_api/app.py +++ b/letta/server/rest_api/app.py @@ -32,6 +32,7 @@ from letta.errors import ( AgentFileImportError, AgentNotFoundForExportError, BedrockPermissionError, + EmbeddingConfigRequiredError, HandleNotFoundError, LettaAgentNotFoundError, LettaExpiredError, @@ -470,6 +471,7 @@ def create_application() -> "FastAPI": app.add_exception_handler(LettaToolCreateError, _error_handler_400) app.add_exception_handler(LettaToolNameConflictError, _error_handler_400) app.add_exception_handler(AgentFileImportError, _error_handler_400) + app.add_exception_handler(EmbeddingConfigRequiredError, _error_handler_400) app.add_exception_handler(ValueError, _error_handler_400) # 404 Not Found errors diff --git a/letta/server/server.py b/letta/server/server.py index df31a139..bfa41bc0 100644 --- a/letta/server/server.py +++ b/letta/server/server.py @@ -18,7 +18,13 @@ import letta.system as system from letta.config import LettaConfig from letta.constants import LETTA_TOOL_EXECUTION_DIR from letta.data_sources.connectors import DataConnector, load_data -from letta.errors import HandleNotFoundError, LettaInvalidArgumentError, LettaMCPConnectionError, LettaMCPTimeoutError +from letta.errors import ( + EmbeddingConfigRequiredError, + HandleNotFoundError, + LettaInvalidArgumentError, + LettaMCPConnectionError, + LettaMCPTimeoutError, +) from letta.functions.mcp_client.types import MCPServerType, MCPTool, MCPToolHealth, SSEServerConfig, StdioServerConfig from letta.functions.schema_validator import validate_complete_json_schema from letta.groups.helpers import load_multi_agent @@ -493,19 +499,17 @@ class SyncServer(object): if request.embedding_config is None: if request.embedding is None: - if settings.default_embedding_handle is None: - raise LettaInvalidArgumentError( - "Must specify either embedding or embedding_config in request", argument_name="embedding" - ) - else: + if settings.default_embedding_handle is not None: request.embedding = settings.default_embedding_handle - embedding_config_params = { - "handle": request.embedding, - "embedding_chunk_size": request.embedding_chunk_size or constants.DEFAULT_EMBEDDING_CHUNK_SIZE, - } - log_event(name="start get_cached_embedding_config", attributes=embedding_config_params) - request.embedding_config = await self.get_cached_embedding_config_async(actor=actor, **embedding_config_params) - log_event(name="end get_cached_embedding_config", attributes=embedding_config_params) + # Only resolve embedding config if we have an embedding handle + if request.embedding is not None: + embedding_config_params = { + "handle": request.embedding, + "embedding_chunk_size": request.embedding_chunk_size or constants.DEFAULT_EMBEDDING_CHUNK_SIZE, + } + log_event(name="start get_cached_embedding_config", attributes=embedding_config_params) + request.embedding_config = await self.get_cached_embedding_config_async(actor=actor, **embedding_config_params) + log_event(name="end get_cached_embedding_config", attributes=embedding_config_params) log_event(name="start create_agent db") main_agent = await self.agent_manager.create_agent_async( @@ -593,6 +597,8 @@ class SyncServer(object): ) async def create_sleeptime_agent_async(self, main_agent: AgentState, actor: User) -> AgentState: + if main_agent.embedding_config is None: + raise EmbeddingConfigRequiredError(agent_id=main_agent.id, operation="create_sleeptime_agent") request = CreateAgent( name=main_agent.name + "-sleeptime", agent_type=AgentType.sleeptime_agent, @@ -625,6 +631,8 @@ class SyncServer(object): return await self.agent_manager.get_agent_by_id_async(agent_id=main_agent.id, actor=actor) async def create_voice_sleeptime_agent_async(self, main_agent: AgentState, actor: User) -> AgentState: + if main_agent.embedding_config is None: + raise EmbeddingConfigRequiredError(agent_id=main_agent.id, operation="create_voice_sleeptime_agent") # TODO: Inject system request = CreateAgent( name=main_agent.name + "-sleeptime", @@ -998,6 +1006,8 @@ class SyncServer(object): async def create_document_sleeptime_agent_async( self, main_agent: AgentState, source: Source, actor: User, clear_history: bool = False ) -> AgentState: + if main_agent.embedding_config is None: + raise EmbeddingConfigRequiredError(agent_id=main_agent.id, operation="create_document_sleeptime_agent") try: block = await self.agent_manager.get_block_with_label_async(agent_id=main_agent.id, block_label=source.name, actor=actor) except: diff --git a/letta/services/agent_manager.py b/letta/services/agent_manager.py index 58da6915..1be471b4 100644 --- a/letta/services/agent_manager.py +++ b/letta/services/agent_manager.py @@ -336,8 +336,8 @@ class AgentManager: ignore_invalid_tools: bool = False, ) -> PydanticAgentState: # validate required configs - if not agent_create.llm_config or not agent_create.embedding_config: - raise ValueError("llm_config and embedding_config are required") + if not agent_create.llm_config: + raise ValueError("llm_config is required") # For v1 agents, enforce sane defaults even when reasoning is omitted if agent_create.agent_type == AgentType.letta_v1_agent: diff --git a/letta/services/archive_manager.py b/letta/services/archive_manager.py index 6e1bf0ec..83903b93 100644 --- a/letta/services/archive_manager.py +++ b/letta/services/archive_manager.py @@ -4,6 +4,7 @@ from typing import Dict, List, Optional from sqlalchemy import delete, or_, select +from letta.errors import EmbeddingConfigRequiredError from letta.helpers.tpuf_client import should_use_tpuf from letta.log import get_logger from letta.orm import ArchivalPassage, Archive as ArchiveModel, ArchivesAgents @@ -433,6 +434,8 @@ class ArchiveManager: return archive # Create a default archive for this agent + if agent_state.embedding_config is None: + raise EmbeddingConfigRequiredError(agent_id=agent_state.id, operation="create_default_archive") archive_name = f"{agent_state.name}'s Archive" archive = await self.create_archive_async( name=archive_name, diff --git a/letta/services/passage_manager.py b/letta/services/passage_manager.py index 55f628ed..6616671c 100644 --- a/letta/services/passage_manager.py +++ b/letta/services/passage_manager.py @@ -8,6 +8,7 @@ from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from letta.constants import MAX_EMBEDDING_DIM +from letta.errors import EmbeddingConfigRequiredError from letta.helpers.decorators import async_redis_cache from letta.llm_api.llm_client import LLMClient from letta.log import get_logger @@ -471,6 +472,8 @@ class PassageManager: Returns: List of created passage objects """ + if agent_state.embedding_config is None: + raise EmbeddingConfigRequiredError(agent_id=agent_state.id, operation="insert_passage") embedding_chunk_size = agent_state.embedding_config.embedding_chunk_size embedding_client = LLMClient.create(