letta-nightly 0.12.1.dev20251023104211__py3-none-any.whl → 0.13.0.dev20251024223017__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.
Potentially problematic release.
This version of letta-nightly might be problematic. Click here for more details.
- letta/__init__.py +2 -3
- letta/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/simple_llm_request_adapter.py +8 -5
- letta/adapters/simple_llm_stream_adapter.py +22 -6
- letta/agents/agent_loop.py +10 -3
- letta/agents/base_agent.py +4 -1
- letta/agents/helpers.py +41 -9
- letta/agents/letta_agent.py +11 -10
- letta/agents/letta_agent_v2.py +47 -37
- letta/agents/letta_agent_v3.py +395 -300
- letta/agents/voice_agent.py +8 -6
- letta/agents/voice_sleeptime_agent.py +3 -3
- letta/constants.py +30 -7
- letta/errors.py +20 -0
- letta/functions/function_sets/base.py +55 -3
- letta/functions/mcp_client/types.py +33 -57
- letta/functions/schema_generator.py +135 -23
- letta/groups/sleeptime_multi_agent_v3.py +6 -11
- letta/groups/sleeptime_multi_agent_v4.py +227 -0
- letta/helpers/converters.py +78 -4
- letta/helpers/crypto_utils.py +6 -2
- letta/interfaces/anthropic_parallel_tool_call_streaming_interface.py +9 -11
- letta/interfaces/anthropic_streaming_interface.py +3 -4
- letta/interfaces/gemini_streaming_interface.py +4 -6
- letta/interfaces/openai_streaming_interface.py +63 -28
- letta/llm_api/anthropic_client.py +7 -4
- letta/llm_api/deepseek_client.py +6 -4
- letta/llm_api/google_ai_client.py +3 -12
- letta/llm_api/google_vertex_client.py +1 -1
- letta/llm_api/helpers.py +90 -61
- letta/llm_api/llm_api_tools.py +4 -1
- letta/llm_api/openai.py +12 -12
- letta/llm_api/openai_client.py +53 -16
- letta/local_llm/constants.py +4 -3
- letta/local_llm/json_parser.py +5 -2
- letta/local_llm/utils.py +2 -3
- letta/log.py +171 -7
- letta/orm/agent.py +43 -9
- letta/orm/archive.py +4 -0
- letta/orm/custom_columns.py +15 -0
- letta/orm/identity.py +11 -11
- letta/orm/mcp_server.py +9 -0
- letta/orm/message.py +6 -1
- letta/orm/run_metrics.py +7 -2
- letta/orm/sqlalchemy_base.py +2 -2
- letta/orm/tool.py +3 -0
- letta/otel/tracing.py +2 -0
- letta/prompts/prompt_generator.py +7 -2
- letta/schemas/agent.py +41 -10
- letta/schemas/agent_file.py +3 -0
- letta/schemas/archive.py +4 -2
- letta/schemas/block.py +2 -1
- letta/schemas/enums.py +36 -3
- letta/schemas/file.py +3 -3
- letta/schemas/folder.py +2 -1
- letta/schemas/group.py +2 -1
- letta/schemas/identity.py +18 -9
- letta/schemas/job.py +3 -1
- letta/schemas/letta_message.py +71 -12
- letta/schemas/letta_request.py +7 -3
- letta/schemas/letta_stop_reason.py +0 -25
- letta/schemas/llm_config.py +8 -2
- letta/schemas/mcp.py +80 -83
- letta/schemas/mcp_server.py +349 -0
- letta/schemas/memory.py +20 -8
- letta/schemas/message.py +212 -67
- letta/schemas/providers/anthropic.py +13 -6
- letta/schemas/providers/azure.py +6 -4
- letta/schemas/providers/base.py +8 -4
- letta/schemas/providers/bedrock.py +6 -2
- letta/schemas/providers/cerebras.py +7 -3
- letta/schemas/providers/deepseek.py +2 -1
- letta/schemas/providers/google_gemini.py +15 -6
- letta/schemas/providers/groq.py +2 -1
- letta/schemas/providers/lmstudio.py +9 -6
- letta/schemas/providers/mistral.py +2 -1
- letta/schemas/providers/openai.py +7 -2
- letta/schemas/providers/together.py +9 -3
- letta/schemas/providers/xai.py +7 -3
- letta/schemas/run.py +7 -2
- letta/schemas/run_metrics.py +2 -1
- letta/schemas/sandbox_config.py +2 -2
- letta/schemas/secret.py +3 -158
- letta/schemas/source.py +2 -2
- letta/schemas/step.py +2 -2
- letta/schemas/tool.py +24 -1
- letta/schemas/usage.py +0 -1
- letta/server/rest_api/app.py +123 -7
- letta/server/rest_api/dependencies.py +3 -0
- letta/server/rest_api/interface.py +7 -4
- letta/server/rest_api/redis_stream_manager.py +16 -1
- letta/server/rest_api/routers/v1/__init__.py +7 -0
- letta/server/rest_api/routers/v1/agents.py +332 -322
- letta/server/rest_api/routers/v1/archives.py +127 -40
- letta/server/rest_api/routers/v1/blocks.py +54 -6
- letta/server/rest_api/routers/v1/chat_completions.py +146 -0
- letta/server/rest_api/routers/v1/folders.py +27 -35
- letta/server/rest_api/routers/v1/groups.py +23 -35
- letta/server/rest_api/routers/v1/identities.py +24 -10
- letta/server/rest_api/routers/v1/internal_runs.py +107 -0
- letta/server/rest_api/routers/v1/internal_templates.py +162 -179
- letta/server/rest_api/routers/v1/jobs.py +15 -27
- letta/server/rest_api/routers/v1/mcp_servers.py +309 -0
- letta/server/rest_api/routers/v1/messages.py +23 -34
- letta/server/rest_api/routers/v1/organizations.py +6 -27
- letta/server/rest_api/routers/v1/providers.py +35 -62
- letta/server/rest_api/routers/v1/runs.py +30 -43
- letta/server/rest_api/routers/v1/sandbox_configs.py +6 -4
- letta/server/rest_api/routers/v1/sources.py +26 -42
- letta/server/rest_api/routers/v1/steps.py +16 -29
- letta/server/rest_api/routers/v1/tools.py +17 -13
- letta/server/rest_api/routers/v1/users.py +5 -17
- letta/server/rest_api/routers/v1/voice.py +18 -27
- letta/server/rest_api/streaming_response.py +5 -2
- letta/server/rest_api/utils.py +187 -25
- letta/server/server.py +27 -22
- letta/server/ws_api/server.py +5 -4
- letta/services/agent_manager.py +148 -26
- letta/services/agent_serialization_manager.py +6 -1
- letta/services/archive_manager.py +168 -15
- letta/services/block_manager.py +14 -4
- letta/services/file_manager.py +33 -29
- letta/services/group_manager.py +10 -0
- letta/services/helpers/agent_manager_helper.py +65 -11
- letta/services/identity_manager.py +105 -4
- letta/services/job_manager.py +11 -1
- letta/services/mcp/base_client.py +2 -2
- letta/services/mcp/oauth_utils.py +33 -8
- letta/services/mcp_manager.py +174 -78
- letta/services/mcp_server_manager.py +1331 -0
- letta/services/message_manager.py +109 -4
- letta/services/organization_manager.py +4 -4
- letta/services/passage_manager.py +9 -25
- letta/services/provider_manager.py +91 -15
- letta/services/run_manager.py +72 -15
- letta/services/sandbox_config_manager.py +45 -3
- letta/services/source_manager.py +15 -8
- letta/services/step_manager.py +24 -1
- letta/services/streaming_service.py +581 -0
- letta/services/summarizer/summarizer.py +1 -1
- letta/services/tool_executor/core_tool_executor.py +111 -0
- letta/services/tool_executor/files_tool_executor.py +5 -3
- 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 +10 -3
- letta/services/tool_sandbox/base.py +61 -1
- letta/services/tool_sandbox/local_sandbox.py +1 -3
- letta/services/user_manager.py +2 -2
- letta/settings.py +49 -5
- letta/system.py +14 -5
- letta/utils.py +73 -1
- letta/validators.py +105 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/METADATA +4 -2
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/RECORD +157 -151
- letta/schemas/letta_ping.py +0 -28
- letta/server/rest_api/routers/openai/chat_completions/__init__.py +0 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/WHEEL +0 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.12.1.dev20251023104211.dist-info → letta_nightly-0.13.0.dev20251024223017.dist-info}/licenses/LICENSE +0 -0
letta/services/agent_manager.py
CHANGED
|
@@ -25,6 +25,7 @@ from letta.constants import (
|
|
|
25
25
|
INCLUDE_MODEL_KEYWORDS_BASE_TOOL_RULES,
|
|
26
26
|
RETRIEVAL_QUERY_DEFAULT_PAGE_SIZE,
|
|
27
27
|
)
|
|
28
|
+
from letta.errors import LettaAgentNotFoundError
|
|
28
29
|
from letta.helpers import ToolRulesSolver
|
|
29
30
|
from letta.helpers.datetime_helpers import get_utc_time
|
|
30
31
|
from letta.llm_api.llm_client import LLMClient
|
|
@@ -50,6 +51,7 @@ from letta.orm.sqlalchemy_base import AccessType
|
|
|
50
51
|
from letta.otel.tracing import trace_method
|
|
51
52
|
from letta.prompts.prompt_generator import PromptGenerator
|
|
52
53
|
from letta.schemas.agent import (
|
|
54
|
+
AgentRelationships,
|
|
53
55
|
AgentState as PydanticAgentState,
|
|
54
56
|
CreateAgent,
|
|
55
57
|
InternalTemplateAgentCreate,
|
|
@@ -57,13 +59,14 @@ from letta.schemas.agent import (
|
|
|
57
59
|
)
|
|
58
60
|
from letta.schemas.block import DEFAULT_BLOCKS, Block as PydanticBlock, BlockUpdate
|
|
59
61
|
from letta.schemas.embedding_config import EmbeddingConfig
|
|
60
|
-
from letta.schemas.enums import AgentType, ProviderType, TagMatchMode, ToolType, VectorDBProvider
|
|
62
|
+
from letta.schemas.enums import AgentType, PrimitiveType, ProviderType, TagMatchMode, ToolType, VectorDBProvider
|
|
61
63
|
from letta.schemas.file import FileMetadata as PydanticFileMetadata
|
|
62
64
|
from letta.schemas.group import Group as PydanticGroup, ManagerType
|
|
63
65
|
from letta.schemas.llm_config import LLMConfig
|
|
64
66
|
from letta.schemas.memory import ContextWindowOverview, Memory
|
|
65
67
|
from letta.schemas.message import Message, Message as PydanticMessage, MessageCreate, MessageUpdate
|
|
66
68
|
from letta.schemas.passage import Passage as PydanticPassage
|
|
69
|
+
from letta.schemas.secret import Secret
|
|
67
70
|
from letta.schemas.source import Source as PydanticSource
|
|
68
71
|
from letta.schemas.tool import Tool as PydanticTool
|
|
69
72
|
from letta.schemas.tool_rule import ContinueToolRule, RequiresApprovalToolRule, TerminalToolRule
|
|
@@ -106,8 +109,9 @@ from letta.services.message_manager import MessageManager
|
|
|
106
109
|
from letta.services.passage_manager import PassageManager
|
|
107
110
|
from letta.services.source_manager import SourceManager
|
|
108
111
|
from letta.services.tool_manager import ToolManager
|
|
109
|
-
from letta.settings import DatabaseChoice, settings
|
|
112
|
+
from letta.settings import DatabaseChoice, model_settings, settings
|
|
110
113
|
from letta.utils import calculate_file_defaults_based_on_context_window, enforce_types, united_diff
|
|
114
|
+
from letta.validators import raise_on_invalid_id
|
|
111
115
|
|
|
112
116
|
logger = get_logger(__name__)
|
|
113
117
|
|
|
@@ -170,13 +174,20 @@ class AgentManager:
|
|
|
170
174
|
|
|
171
175
|
@staticmethod
|
|
172
176
|
async def _resolve_tools_async(
|
|
173
|
-
session, names: Set[str], ids: Set[str], org_id: str
|
|
177
|
+
session, names: Set[str], ids: Set[str], org_id: str, ignore_invalid_tools: bool = False
|
|
174
178
|
) -> Tuple[Dict[str, str], Dict[str, str], List[str]]:
|
|
175
179
|
"""
|
|
176
180
|
Bulk‑fetch all ToolModel rows matching either name ∈ names or id ∈ ids
|
|
177
181
|
(and scoped to this organization), and return two maps:
|
|
178
182
|
name_to_id, id_to_name.
|
|
179
|
-
Raises if any requested name or id was not found.
|
|
183
|
+
Raises if any requested name or id was not found (unless ignore_invalid_tools is True).
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
session: Database session
|
|
187
|
+
names: Set of tool names to resolve
|
|
188
|
+
ids: Set of tool IDs to resolve
|
|
189
|
+
org_id: Organization ID for scoping
|
|
190
|
+
ignore_invalid_tools: If True, silently filters out missing tools instead of raising an error
|
|
180
191
|
"""
|
|
181
192
|
stmt = select(ToolModel.id, ToolModel.name, ToolModel.default_requires_approval).where(
|
|
182
193
|
ToolModel.organization_id == org_id,
|
|
@@ -193,10 +204,21 @@ class AgentManager:
|
|
|
193
204
|
|
|
194
205
|
missing_names = names - set(name_to_id.keys())
|
|
195
206
|
missing_ids = ids - set(id_to_name.keys())
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
207
|
+
|
|
208
|
+
if not ignore_invalid_tools:
|
|
209
|
+
# Original behavior: raise errors for missing tools
|
|
210
|
+
if missing_names:
|
|
211
|
+
raise ValueError(f"Tools not found by name: {missing_names}")
|
|
212
|
+
if missing_ids:
|
|
213
|
+
raise ValueError(f"Tools not found by id: {missing_ids}")
|
|
214
|
+
else:
|
|
215
|
+
# New behavior: log missing tools but don't raise errors
|
|
216
|
+
if missing_names or missing_ids:
|
|
217
|
+
logger = get_logger(__name__)
|
|
218
|
+
if missing_names:
|
|
219
|
+
logger.warning(f"Ignoring tools not found by name: {missing_names}")
|
|
220
|
+
if missing_ids:
|
|
221
|
+
logger.warning(f"Ignoring tools not found by id: {missing_ids}")
|
|
200
222
|
|
|
201
223
|
return name_to_id, id_to_name, requires_approval
|
|
202
224
|
|
|
@@ -310,6 +332,7 @@ class AgentManager:
|
|
|
310
332
|
actor: PydanticUser,
|
|
311
333
|
_test_only_force_id: Optional[str] = None,
|
|
312
334
|
_init_with_no_messages: bool = False,
|
|
335
|
+
ignore_invalid_tools: bool = False,
|
|
313
336
|
) -> PydanticAgentState:
|
|
314
337
|
# validate required configs
|
|
315
338
|
if not agent_create.llm_config or not agent_create.embedding_config:
|
|
@@ -378,6 +401,9 @@ class AgentManager:
|
|
|
378
401
|
# NOTE: also overwrite initial message sequence to empty by default
|
|
379
402
|
if agent_create.initial_message_sequence is None:
|
|
380
403
|
agent_create.initial_message_sequence = []
|
|
404
|
+
# NOTE: default to no base tool rules unless explicitly provided
|
|
405
|
+
if not agent_create.tool_rules and agent_create.include_base_tool_rules is None:
|
|
406
|
+
agent_create.include_base_tool_rules = False
|
|
381
407
|
elif agent_create.agent_type == AgentType.workflow_agent:
|
|
382
408
|
pass # no default tools
|
|
383
409
|
else:
|
|
@@ -416,6 +442,7 @@ class AgentManager:
|
|
|
416
442
|
tool_names,
|
|
417
443
|
supplied_ids,
|
|
418
444
|
actor.organization_id,
|
|
445
|
+
ignore_invalid_tools=ignore_invalid_tools,
|
|
419
446
|
)
|
|
420
447
|
|
|
421
448
|
tool_ids = set(name_to_id.values()) | set(id_to_name.keys())
|
|
@@ -521,16 +548,22 @@ class AgentManager:
|
|
|
521
548
|
|
|
522
549
|
env_rows = []
|
|
523
550
|
agent_secrets = agent_create.secrets or agent_create.tool_exec_environment_variables
|
|
551
|
+
|
|
524
552
|
if agent_secrets:
|
|
525
|
-
|
|
526
|
-
|
|
553
|
+
# Encrypt environment variable values
|
|
554
|
+
env_rows = []
|
|
555
|
+
for key, val in agent_secrets.items():
|
|
556
|
+
row = {
|
|
527
557
|
"agent_id": aid,
|
|
528
558
|
"key": key,
|
|
529
559
|
"value": val,
|
|
530
560
|
"organization_id": actor.organization_id,
|
|
531
561
|
}
|
|
532
|
-
|
|
533
|
-
|
|
562
|
+
# Encrypt value (Secret.from_plaintext handles missing encryption key internally)
|
|
563
|
+
value_secret = Secret.from_plaintext(val)
|
|
564
|
+
row["value_enc"] = value_secret.get_encrypted()
|
|
565
|
+
env_rows.append(row)
|
|
566
|
+
|
|
534
567
|
result = await session.execute(insert(AgentEnvironmentVariable).values(env_rows).returning(AgentEnvironmentVariable.id))
|
|
535
568
|
env_rows = [{**row, "id": env_var_id} for row, env_var_id in zip(env_rows, result.scalars().all())]
|
|
536
569
|
|
|
@@ -640,6 +673,7 @@ class AgentManager:
|
|
|
640
673
|
|
|
641
674
|
@enforce_types
|
|
642
675
|
@trace_method
|
|
676
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
643
677
|
async def update_agent_async(
|
|
644
678
|
self,
|
|
645
679
|
agent_id: str,
|
|
@@ -742,16 +776,44 @@ class AgentManager:
|
|
|
742
776
|
|
|
743
777
|
agent_secrets = agent_update.secrets or agent_update.tool_exec_environment_variables
|
|
744
778
|
if agent_secrets is not None:
|
|
779
|
+
# Fetch existing environment variables to check if values changed
|
|
780
|
+
result = await session.execute(select(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
|
|
781
|
+
existing_env_vars = {env.key: env for env in result.scalars().all()}
|
|
782
|
+
|
|
783
|
+
# TODO: do we need to delete each time or can we just upsert?
|
|
745
784
|
await session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
|
|
746
|
-
|
|
747
|
-
|
|
785
|
+
# Encrypt environment variable values
|
|
786
|
+
# Only re-encrypt if the value has actually changed
|
|
787
|
+
env_rows = []
|
|
788
|
+
for k, v in agent_secrets.items():
|
|
789
|
+
row = {
|
|
748
790
|
"agent_id": aid,
|
|
749
791
|
"key": k,
|
|
750
792
|
"value": v,
|
|
751
793
|
"organization_id": agent.organization_id,
|
|
752
794
|
}
|
|
753
|
-
|
|
754
|
-
|
|
795
|
+
|
|
796
|
+
# Check if value changed to avoid unnecessary re-encryption
|
|
797
|
+
existing_env = existing_env_vars.get(k)
|
|
798
|
+
existing_value = None
|
|
799
|
+
if existing_env:
|
|
800
|
+
if existing_env.value_enc:
|
|
801
|
+
existing_secret = Secret.from_encrypted(existing_env.value_enc)
|
|
802
|
+
existing_value = existing_secret.get_plaintext()
|
|
803
|
+
elif existing_env.value:
|
|
804
|
+
existing_value = existing_env.value
|
|
805
|
+
|
|
806
|
+
# Encrypt value (reuse existing encrypted value if unchanged)
|
|
807
|
+
if existing_value == v and existing_env and existing_env.value_enc:
|
|
808
|
+
# Value unchanged, reuse existing encrypted value
|
|
809
|
+
row["value_enc"] = existing_env.value_enc
|
|
810
|
+
else:
|
|
811
|
+
# Value changed or new, encrypt
|
|
812
|
+
value_secret = Secret.from_plaintext(v)
|
|
813
|
+
row["value_enc"] = value_secret.get_encrypted()
|
|
814
|
+
|
|
815
|
+
env_rows.append(row)
|
|
816
|
+
|
|
755
817
|
if env_rows:
|
|
756
818
|
await self._bulk_insert_pivot_async(session, AgentEnvironmentVariable.__table__, env_rows)
|
|
757
819
|
session.expire(agent, ["tool_exec_environment_variables"])
|
|
@@ -809,6 +871,7 @@ class AgentManager:
|
|
|
809
871
|
identity_id: Optional[str] = None,
|
|
810
872
|
identifier_keys: Optional[List[str]] = None,
|
|
811
873
|
include_relationships: Optional[List[str]] = None,
|
|
874
|
+
include: List[str] = [],
|
|
812
875
|
ascending: bool = True,
|
|
813
876
|
sort_by: Optional[str] = "created_at",
|
|
814
877
|
show_hidden_agents: Optional[bool] = None,
|
|
@@ -846,7 +909,7 @@ class AgentManager:
|
|
|
846
909
|
query = _apply_filters(query, name, query_text, project_id, template_id, base_template_id)
|
|
847
910
|
query = _apply_identity_filters(query, identity_id, identifier_keys)
|
|
848
911
|
query = _apply_tag_filter(query, tags, match_all_tags)
|
|
849
|
-
query = _apply_relationship_filters(query, include_relationships)
|
|
912
|
+
query = _apply_relationship_filters(query, include_relationships, include)
|
|
850
913
|
|
|
851
914
|
# Apply hidden filter
|
|
852
915
|
if not show_hidden_agents:
|
|
@@ -857,7 +920,9 @@ class AgentManager:
|
|
|
857
920
|
query = query.limit(limit)
|
|
858
921
|
result = await session.execute(query)
|
|
859
922
|
agents = result.scalars().all()
|
|
860
|
-
return await asyncio.gather(
|
|
923
|
+
return await asyncio.gather(
|
|
924
|
+
*[agent.to_pydantic_async(include_relationships=include_relationships, include=include) for agent in agents]
|
|
925
|
+
)
|
|
861
926
|
|
|
862
927
|
@enforce_types
|
|
863
928
|
@trace_method
|
|
@@ -916,19 +981,22 @@ class AgentManager:
|
|
|
916
981
|
|
|
917
982
|
@enforce_types
|
|
918
983
|
@trace_method
|
|
984
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
919
985
|
async def get_agent_by_id_async(
|
|
920
986
|
self,
|
|
921
987
|
agent_id: str,
|
|
922
988
|
actor: PydanticUser,
|
|
923
989
|
include_relationships: Optional[List[str]] = None,
|
|
990
|
+
include: List[str] = [],
|
|
924
991
|
) -> PydanticAgentState:
|
|
925
992
|
"""Fetch an agent by its ID."""
|
|
993
|
+
|
|
926
994
|
async with db_registry.async_session() as session:
|
|
927
995
|
try:
|
|
928
996
|
query = select(AgentModel)
|
|
929
997
|
query = AgentModel.apply_access_predicate(query, actor, ["read"], AccessType.ORGANIZATION)
|
|
930
998
|
query = query.where(AgentModel.id == agent_id)
|
|
931
|
-
query = _apply_relationship_filters(query, include_relationships)
|
|
999
|
+
query = _apply_relationship_filters(query, include_relationships, include)
|
|
932
1000
|
|
|
933
1001
|
result = await session.execute(query)
|
|
934
1002
|
agent = result.scalar_one_or_none()
|
|
@@ -936,7 +1004,7 @@ class AgentManager:
|
|
|
936
1004
|
if agent is None:
|
|
937
1005
|
raise NoResultFound(f"Agent with ID {agent_id} not found")
|
|
938
1006
|
|
|
939
|
-
return await agent.to_pydantic_async(include_relationships=include_relationships)
|
|
1007
|
+
return await agent.to_pydantic_async(include_relationships=include_relationships, include=include)
|
|
940
1008
|
except NoResultFound:
|
|
941
1009
|
# Re-raise NoResultFound without logging to preserve 404 handling
|
|
942
1010
|
raise
|
|
@@ -974,6 +1042,7 @@ class AgentManager:
|
|
|
974
1042
|
|
|
975
1043
|
@enforce_types
|
|
976
1044
|
@trace_method
|
|
1045
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
977
1046
|
async def get_agent_archive_ids_async(self, agent_id: str, actor: PydanticUser) -> List[str]:
|
|
978
1047
|
"""Get all archive IDs associated with an agent."""
|
|
979
1048
|
from letta.orm import ArchivesAgents
|
|
@@ -987,6 +1056,25 @@ class AgentManager:
|
|
|
987
1056
|
|
|
988
1057
|
@enforce_types
|
|
989
1058
|
@trace_method
|
|
1059
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
1060
|
+
async def validate_agent_exists_async(self, agent_id: str, actor: PydanticUser) -> None:
|
|
1061
|
+
"""
|
|
1062
|
+
Validate that an agent exists and user has access to it.
|
|
1063
|
+
Lightweight method that doesn't load the full agent object.
|
|
1064
|
+
|
|
1065
|
+
Args:
|
|
1066
|
+
agent_id: ID of the agent to validate
|
|
1067
|
+
actor: User performing the action
|
|
1068
|
+
|
|
1069
|
+
Raises:
|
|
1070
|
+
LettaAgentNotFoundError: If agent doesn't exist or user doesn't have access
|
|
1071
|
+
"""
|
|
1072
|
+
async with db_registry.async_session() as session:
|
|
1073
|
+
await validate_agent_exists_async(session, agent_id, actor)
|
|
1074
|
+
|
|
1075
|
+
@enforce_types
|
|
1076
|
+
@trace_method
|
|
1077
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
990
1078
|
async def delete_agent_async(self, agent_id: str, actor: PydanticUser) -> None:
|
|
991
1079
|
"""
|
|
992
1080
|
Deletes an agent and its associated relationships.
|
|
@@ -1049,6 +1137,7 @@ class AgentManager:
|
|
|
1049
1137
|
# TODO: This can also be made more efficient, instead of getting, setting, we can do it all in one db session for one query.
|
|
1050
1138
|
@enforce_types
|
|
1051
1139
|
@trace_method
|
|
1140
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
1052
1141
|
async def get_in_context_messages(self, agent_id: str, actor: PydanticUser) -> List[PydanticMessage]:
|
|
1053
1142
|
agent_state = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor)
|
|
1054
1143
|
return await self.message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=actor)
|
|
@@ -1061,6 +1150,7 @@ class AgentManager:
|
|
|
1061
1150
|
|
|
1062
1151
|
@enforce_types
|
|
1063
1152
|
@trace_method
|
|
1153
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
1064
1154
|
async def get_system_message_async(self, agent_id: str, actor: PydanticUser) -> PydanticMessage:
|
|
1065
1155
|
agent = await self.get_agent_by_id_async(agent_id=agent_id, include_relationships=[], actor=actor)
|
|
1066
1156
|
return await self.message_manager.get_message_by_id_async(message_id=agent.message_ids[0], actor=actor)
|
|
@@ -1091,7 +1181,7 @@ class AgentManager:
|
|
|
1091
1181
|
|
|
1092
1182
|
# note: we only update the system prompt if the core memory is changed
|
|
1093
1183
|
# this means that the archival/recall memory statistics may be someout out of date
|
|
1094
|
-
curr_memory_str = agent_state.memory.compile(sources=agent_state.sources)
|
|
1184
|
+
curr_memory_str = agent_state.memory.compile(sources=agent_state.sources, llm_config=agent_state.llm_config)
|
|
1095
1185
|
if curr_memory_str in curr_system_message_openai["content"] and not force:
|
|
1096
1186
|
# NOTE: could this cause issues if a block is removed? (substring match would still work)
|
|
1097
1187
|
logger.debug(
|
|
@@ -1120,6 +1210,7 @@ class AgentManager:
|
|
|
1120
1210
|
archival_memory_size=num_archival_memories,
|
|
1121
1211
|
sources=agent_state.sources,
|
|
1122
1212
|
max_files_open=agent_state.max_files_open,
|
|
1213
|
+
llm_config=agent_state.llm_config,
|
|
1123
1214
|
)
|
|
1124
1215
|
|
|
1125
1216
|
diff = united_diff(curr_system_message_openai["content"], new_system_message_str)
|
|
@@ -1165,7 +1256,10 @@ class AgentManager:
|
|
|
1165
1256
|
|
|
1166
1257
|
tool_rules_solver = ToolRulesSolver(agent_state.tool_rules)
|
|
1167
1258
|
|
|
1168
|
-
|
|
1259
|
+
if agent_state.message_ids == []:
|
|
1260
|
+
curr_system_message = None
|
|
1261
|
+
else:
|
|
1262
|
+
curr_system_message = await self.message_manager.get_message_by_id_async(message_id=agent_state.message_ids[0], actor=actor)
|
|
1169
1263
|
|
|
1170
1264
|
if curr_system_message is None:
|
|
1171
1265
|
logger.warning(f"No system message found for agent {agent_state.id} and user {actor}")
|
|
@@ -1179,6 +1273,7 @@ class AgentManager:
|
|
|
1179
1273
|
sources=agent_state.sources,
|
|
1180
1274
|
tool_usage_rules=tool_rules_solver.compile_tool_rule_prompts(),
|
|
1181
1275
|
max_files_open=agent_state.max_files_open,
|
|
1276
|
+
llm_config=agent_state.llm_config,
|
|
1182
1277
|
)
|
|
1183
1278
|
if curr_memory_str in curr_system_message_openai["content"] and not force:
|
|
1184
1279
|
# NOTE: could this cause issues if a block is removed? (substring match would still work)
|
|
@@ -1237,6 +1332,7 @@ class AgentManager:
|
|
|
1237
1332
|
|
|
1238
1333
|
@enforce_types
|
|
1239
1334
|
@trace_method
|
|
1335
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
1240
1336
|
async def set_in_context_messages_async(self, agent_id: str, message_ids: List[str], actor: PydanticUser) -> PydanticAgentState:
|
|
1241
1337
|
return await self.update_agent_async(agent_id=agent_id, agent_update=UpdateAgent(message_ids=message_ids), actor=actor)
|
|
1242
1338
|
|
|
@@ -1347,6 +1443,7 @@ class AgentManager:
|
|
|
1347
1443
|
|
|
1348
1444
|
@enforce_types
|
|
1349
1445
|
@trace_method
|
|
1446
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
1350
1447
|
async def update_memory_if_changed_async(self, agent_id: str, new_memory: Memory, actor: PydanticUser) -> PydanticAgentState:
|
|
1351
1448
|
"""
|
|
1352
1449
|
Update internal memory object and system prompt if there have been modifications.
|
|
@@ -1366,6 +1463,7 @@ class AgentManager:
|
|
|
1366
1463
|
sources=agent_state.sources,
|
|
1367
1464
|
tool_usage_rules=temp_tool_rules_solver.compile_tool_rule_prompts(),
|
|
1368
1465
|
max_files_open=agent_state.max_files_open,
|
|
1466
|
+
llm_config=agent_state.llm_config,
|
|
1369
1467
|
)
|
|
1370
1468
|
if new_memory_str not in system_message.content[0].text:
|
|
1371
1469
|
# update the blocks (LRW) in the DB
|
|
@@ -1458,6 +1556,8 @@ class AgentManager:
|
|
|
1458
1556
|
# ======================================================================================================================
|
|
1459
1557
|
@enforce_types
|
|
1460
1558
|
@trace_method
|
|
1559
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
1560
|
+
@raise_on_invalid_id(param_name="source_id", expected_prefix=PrimitiveType.SOURCE)
|
|
1461
1561
|
async def attach_source_async(self, agent_id: str, source_id: str, actor: PydanticUser) -> PydanticAgentState:
|
|
1462
1562
|
"""
|
|
1463
1563
|
Attaches a source to an agent.
|
|
@@ -1527,6 +1627,7 @@ class AgentManager:
|
|
|
1527
1627
|
|
|
1528
1628
|
@enforce_types
|
|
1529
1629
|
@trace_method
|
|
1630
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
1530
1631
|
async def append_system_message_async(self, agent_id: str, content: str, actor: PydanticUser):
|
|
1531
1632
|
"""
|
|
1532
1633
|
Async version of append_system_message.
|
|
@@ -1614,6 +1715,8 @@ class AgentManager:
|
|
|
1614
1715
|
|
|
1615
1716
|
@enforce_types
|
|
1616
1717
|
@trace_method
|
|
1718
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
1719
|
+
@raise_on_invalid_id(param_name="source_id", expected_prefix=PrimitiveType.SOURCE)
|
|
1617
1720
|
async def detach_source_async(self, agent_id: str, source_id: str, actor: PydanticUser) -> PydanticAgentState:
|
|
1618
1721
|
"""
|
|
1619
1722
|
Detaches a source from an agent.
|
|
@@ -1699,6 +1802,8 @@ class AgentManager:
|
|
|
1699
1802
|
|
|
1700
1803
|
@enforce_types
|
|
1701
1804
|
@trace_method
|
|
1805
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
1806
|
+
@raise_on_invalid_id(param_name="block_id", expected_prefix=PrimitiveType.BLOCK)
|
|
1702
1807
|
async def attach_block_async(self, agent_id: str, block_id: str, actor: PydanticUser) -> PydanticAgentState:
|
|
1703
1808
|
"""Attaches a block to an agent. For sleeptime agents, also attaches to paired agents in the same group."""
|
|
1704
1809
|
async with db_registry.async_session() as session:
|
|
@@ -1849,9 +1954,8 @@ class AgentManager:
|
|
|
1849
1954
|
"""
|
|
1850
1955
|
import warnings
|
|
1851
1956
|
|
|
1852
|
-
|
|
1957
|
+
logger.warning(
|
|
1853
1958
|
"list_passages_async is deprecated. Use query_source_passages_async or query_agent_passages_async instead.",
|
|
1854
|
-
DeprecationWarning,
|
|
1855
1959
|
stacklevel=2,
|
|
1856
1960
|
)
|
|
1857
1961
|
|
|
@@ -2285,8 +2389,9 @@ class AgentManager:
|
|
|
2285
2389
|
# Tool Management
|
|
2286
2390
|
# ======================================================================================================================
|
|
2287
2391
|
@enforce_types
|
|
2288
|
-
@enforce_types
|
|
2289
2392
|
@trace_method
|
|
2393
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
2394
|
+
@raise_on_invalid_id(param_name="tool_id", expected_prefix=PrimitiveType.TOOL)
|
|
2290
2395
|
async def attach_tool_async(self, agent_id: str, tool_id: str, actor: PydanticUser) -> None:
|
|
2291
2396
|
"""
|
|
2292
2397
|
Attaches a tool to an agent.
|
|
@@ -2355,6 +2460,7 @@ class AgentManager:
|
|
|
2355
2460
|
|
|
2356
2461
|
@enforce_types
|
|
2357
2462
|
@trace_method
|
|
2463
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
2358
2464
|
async def bulk_attach_tools_async(self, agent_id: str, tool_ids: List[str], actor: PydanticUser) -> None:
|
|
2359
2465
|
"""
|
|
2360
2466
|
Efficiently attaches multiple tools to an agent in a single operation.
|
|
@@ -2520,6 +2626,8 @@ class AgentManager:
|
|
|
2520
2626
|
|
|
2521
2627
|
@enforce_types
|
|
2522
2628
|
@trace_method
|
|
2629
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
2630
|
+
@raise_on_invalid_id(param_name="tool_id", expected_prefix=PrimitiveType.TOOL)
|
|
2523
2631
|
async def detach_tool_async(self, agent_id: str, tool_id: str, actor: PydanticUser) -> None:
|
|
2524
2632
|
"""
|
|
2525
2633
|
Detaches a tool from an agent.
|
|
@@ -2549,6 +2657,7 @@ class AgentManager:
|
|
|
2549
2657
|
|
|
2550
2658
|
@enforce_types
|
|
2551
2659
|
@trace_method
|
|
2660
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
2552
2661
|
async def bulk_detach_tools_async(self, agent_id: str, tool_ids: List[str], actor: PydanticUser) -> None:
|
|
2553
2662
|
"""
|
|
2554
2663
|
Efficiently detaches multiple tools from an agent in a single operation.
|
|
@@ -2585,6 +2694,7 @@ class AgentManager:
|
|
|
2585
2694
|
|
|
2586
2695
|
@enforce_types
|
|
2587
2696
|
@trace_method
|
|
2697
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
2588
2698
|
async def modify_approvals_async(self, agent_id: str, tool_name: str, requires_approval: bool, actor: PydanticUser) -> None:
|
|
2589
2699
|
def is_target_rule(rule):
|
|
2590
2700
|
return rule.tool_name == tool_name and rule.type == "requires_approval"
|
|
@@ -2933,6 +3043,7 @@ class AgentManager:
|
|
|
2933
3043
|
|
|
2934
3044
|
@enforce_types
|
|
2935
3045
|
@trace_method
|
|
3046
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
2936
3047
|
async def get_agent_files_config_async(self, agent_id: str, actor: PydanticUser) -> Tuple[int, int]:
|
|
2937
3048
|
"""Get per_file_view_window_char_limit and max_files_open for an agent.
|
|
2938
3049
|
|
|
@@ -2989,6 +3100,7 @@ class AgentManager:
|
|
|
2989
3100
|
|
|
2990
3101
|
@enforce_types
|
|
2991
3102
|
@trace_method
|
|
3103
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
2992
3104
|
async def get_agent_max_files_open_async(self, agent_id: str, actor: PydanticUser) -> int:
|
|
2993
3105
|
"""Get max_files_open for an agent.
|
|
2994
3106
|
|
|
@@ -3017,6 +3129,7 @@ class AgentManager:
|
|
|
3017
3129
|
|
|
3018
3130
|
@enforce_types
|
|
3019
3131
|
@trace_method
|
|
3132
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
3020
3133
|
async def get_agent_per_file_view_window_char_limit_async(self, agent_id: str, actor: PydanticUser) -> int:
|
|
3021
3134
|
"""Get per_file_view_window_char_limit for an agent.
|
|
3022
3135
|
|
|
@@ -3043,14 +3156,23 @@ class AgentManager:
|
|
|
3043
3156
|
|
|
3044
3157
|
return row
|
|
3045
3158
|
|
|
3159
|
+
@enforce_types
|
|
3046
3160
|
@trace_method
|
|
3161
|
+
@raise_on_invalid_id(param_name="agent_id", expected_prefix=PrimitiveType.AGENT)
|
|
3047
3162
|
async def get_context_window(self, agent_id: str, actor: PydanticUser) -> ContextWindowOverview:
|
|
3048
3163
|
agent_state, system_message, num_messages, num_archival_memories = await self.rebuild_system_prompt_async(
|
|
3049
3164
|
agent_id=agent_id, actor=actor, force=True, dry_run=True
|
|
3050
3165
|
)
|
|
3051
3166
|
calculator = ContextWindowCalculator()
|
|
3052
3167
|
|
|
3053
|
-
|
|
3168
|
+
# Use Anthropic token counter if:
|
|
3169
|
+
# 1. The model endpoint type is anthropic, OR
|
|
3170
|
+
# 2. We're in PRODUCTION and anthropic_api_key is available
|
|
3171
|
+
use_anthropic = agent_state.llm_config.model_endpoint_type == "anthropic" or (
|
|
3172
|
+
settings.environment == "PRODUCTION" and model_settings.anthropic_api_key is not None
|
|
3173
|
+
)
|
|
3174
|
+
|
|
3175
|
+
if use_anthropic:
|
|
3054
3176
|
anthropic_client = LLMClient.create(provider_type=ProviderType.anthropic, actor=actor)
|
|
3055
3177
|
model = agent_state.llm_config.model if agent_state.llm_config.model_endpoint_type == "anthropic" else None
|
|
3056
3178
|
|
|
@@ -452,6 +452,7 @@ class AgentSerializationManager:
|
|
|
452
452
|
schema: AgentFileSchema,
|
|
453
453
|
actor: User,
|
|
454
454
|
append_copy_suffix: bool = False,
|
|
455
|
+
override_name: Optional[str] = None,
|
|
455
456
|
override_existing_tools: bool = True,
|
|
456
457
|
dry_run: bool = False,
|
|
457
458
|
env_vars: Optional[Dict[str, Any]] = None,
|
|
@@ -658,7 +659,11 @@ class AgentSerializationManager:
|
|
|
658
659
|
|
|
659
660
|
# Convert AgentSchema back to CreateAgent, remapping tool/block IDs
|
|
660
661
|
agent_data = agent_schema.model_dump(exclude={"id", "in_context_message_ids", "messages"})
|
|
661
|
-
|
|
662
|
+
|
|
663
|
+
# Handle agent name override: override_name takes precedence over append_copy_suffix
|
|
664
|
+
if override_name:
|
|
665
|
+
agent_data["name"] = override_name
|
|
666
|
+
elif append_copy_suffix:
|
|
662
667
|
agent_data["name"] = agent_data.get("name") + "_copy"
|
|
663
668
|
|
|
664
669
|
# Remap tool_ids from file IDs to database IDs
|