letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.11.7.dev20251008104128__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/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/letta_llm_request_adapter.py +0 -1
- letta/adapters/letta_llm_stream_adapter.py +7 -2
- letta/adapters/simple_llm_request_adapter.py +88 -0
- letta/adapters/simple_llm_stream_adapter.py +192 -0
- letta/agents/agent_loop.py +6 -0
- letta/agents/ephemeral_summary_agent.py +2 -1
- letta/agents/helpers.py +142 -6
- letta/agents/letta_agent.py +13 -33
- letta/agents/letta_agent_batch.py +2 -4
- letta/agents/letta_agent_v2.py +87 -77
- letta/agents/letta_agent_v3.py +899 -0
- letta/agents/voice_agent.py +2 -6
- letta/constants.py +8 -4
- letta/errors.py +40 -0
- letta/functions/function_sets/base.py +84 -4
- letta/functions/function_sets/multi_agent.py +0 -3
- letta/functions/schema_generator.py +113 -71
- letta/groups/dynamic_multi_agent.py +3 -2
- letta/groups/helpers.py +1 -2
- letta/groups/round_robin_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/groups/sleeptime_multi_agent_v3.py +17 -17
- letta/groups/supervisor_multi_agent.py +84 -80
- letta/helpers/converters.py +3 -0
- letta/helpers/message_helper.py +4 -0
- letta/helpers/tool_rule_solver.py +92 -5
- letta/interfaces/anthropic_streaming_interface.py +409 -0
- letta/interfaces/gemini_streaming_interface.py +296 -0
- letta/interfaces/openai_streaming_interface.py +752 -1
- letta/llm_api/anthropic_client.py +126 -16
- letta/llm_api/bedrock_client.py +4 -2
- letta/llm_api/deepseek_client.py +4 -1
- letta/llm_api/google_vertex_client.py +123 -42
- letta/llm_api/groq_client.py +4 -1
- letta/llm_api/llm_api_tools.py +11 -4
- letta/llm_api/llm_client_base.py +6 -2
- letta/llm_api/openai.py +32 -2
- letta/llm_api/openai_client.py +423 -18
- letta/llm_api/xai_client.py +4 -1
- letta/main.py +9 -5
- letta/memory.py +1 -0
- letta/orm/__init__.py +1 -1
- letta/orm/agent.py +10 -0
- letta/orm/block.py +7 -16
- letta/orm/blocks_agents.py +8 -2
- letta/orm/files_agents.py +2 -0
- letta/orm/job.py +7 -5
- letta/orm/mcp_oauth.py +1 -0
- letta/orm/message.py +21 -6
- letta/orm/organization.py +2 -0
- letta/orm/provider.py +6 -2
- letta/orm/run.py +71 -0
- letta/orm/sandbox_config.py +7 -1
- letta/orm/sqlalchemy_base.py +0 -306
- letta/orm/step.py +6 -5
- letta/orm/step_metrics.py +5 -5
- letta/otel/tracing.py +28 -3
- letta/plugins/defaults.py +4 -4
- letta/prompts/system_prompts/__init__.py +2 -0
- letta/prompts/system_prompts/letta_v1.py +25 -0
- letta/schemas/agent.py +3 -2
- letta/schemas/agent_file.py +9 -3
- letta/schemas/block.py +23 -10
- letta/schemas/enums.py +21 -2
- letta/schemas/job.py +17 -4
- letta/schemas/letta_message_content.py +71 -2
- letta/schemas/letta_stop_reason.py +5 -5
- letta/schemas/llm_config.py +53 -3
- letta/schemas/memory.py +1 -1
- letta/schemas/message.py +504 -117
- letta/schemas/openai/responses_request.py +64 -0
- letta/schemas/providers/__init__.py +2 -0
- letta/schemas/providers/anthropic.py +16 -0
- letta/schemas/providers/ollama.py +115 -33
- letta/schemas/providers/openrouter.py +52 -0
- letta/schemas/providers/vllm.py +2 -1
- letta/schemas/run.py +48 -42
- letta/schemas/step.py +2 -2
- letta/schemas/step_metrics.py +1 -1
- letta/schemas/tool.py +15 -107
- letta/schemas/tool_rule.py +88 -5
- letta/serialize_schemas/marshmallow_agent.py +1 -0
- letta/server/db.py +86 -408
- letta/server/rest_api/app.py +61 -10
- letta/server/rest_api/dependencies.py +14 -0
- letta/server/rest_api/redis_stream_manager.py +19 -8
- letta/server/rest_api/routers/v1/agents.py +364 -292
- letta/server/rest_api/routers/v1/blocks.py +14 -20
- letta/server/rest_api/routers/v1/identities.py +45 -110
- letta/server/rest_api/routers/v1/internal_templates.py +21 -0
- letta/server/rest_api/routers/v1/jobs.py +23 -6
- letta/server/rest_api/routers/v1/messages.py +1 -1
- letta/server/rest_api/routers/v1/runs.py +126 -85
- letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
- letta/server/rest_api/routers/v1/tools.py +281 -594
- letta/server/rest_api/routers/v1/voice.py +1 -1
- letta/server/rest_api/streaming_response.py +29 -29
- letta/server/rest_api/utils.py +122 -64
- letta/server/server.py +160 -887
- letta/services/agent_manager.py +236 -919
- letta/services/agent_serialization_manager.py +16 -0
- letta/services/archive_manager.py +0 -100
- letta/services/block_manager.py +211 -168
- letta/services/file_manager.py +1 -1
- letta/services/files_agents_manager.py +24 -33
- letta/services/group_manager.py +0 -142
- letta/services/helpers/agent_manager_helper.py +7 -2
- letta/services/helpers/run_manager_helper.py +85 -0
- letta/services/job_manager.py +96 -411
- letta/services/lettuce/__init__.py +6 -0
- letta/services/lettuce/lettuce_client_base.py +86 -0
- letta/services/mcp_manager.py +38 -6
- letta/services/message_manager.py +165 -362
- letta/services/organization_manager.py +0 -36
- letta/services/passage_manager.py +0 -345
- letta/services/provider_manager.py +0 -80
- letta/services/run_manager.py +301 -0
- letta/services/sandbox_config_manager.py +0 -234
- letta/services/step_manager.py +62 -39
- letta/services/summarizer/summarizer.py +9 -7
- letta/services/telemetry_manager.py +0 -16
- letta/services/tool_executor/builtin_tool_executor.py +35 -0
- letta/services/tool_executor/core_tool_executor.py +397 -2
- letta/services/tool_executor/files_tool_executor.py +3 -3
- letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
- letta/services/tool_executor/tool_execution_manager.py +6 -8
- letta/services/tool_executor/tool_executor_base.py +3 -3
- letta/services/tool_manager.py +85 -339
- letta/services/tool_sandbox/base.py +24 -13
- letta/services/tool_sandbox/e2b_sandbox.py +16 -1
- letta/services/tool_schema_generator.py +123 -0
- letta/services/user_manager.py +0 -99
- letta/settings.py +20 -4
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/METADATA +3 -5
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/RECORD +140 -132
- letta/agents/temporal/activities/__init__.py +0 -4
- letta/agents/temporal/activities/example_activity.py +0 -7
- letta/agents/temporal/activities/prepare_messages.py +0 -10
- letta/agents/temporal/temporal_agent_workflow.py +0 -56
- letta/agents/temporal/types.py +0 -25
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.11.7.dev20251008104128.dist-info}/licenses/LICENSE +0 -0
letta/services/agent_manager.py
CHANGED
@@ -10,6 +10,7 @@ from sqlalchemy.dialects.postgresql import insert as pg_insert
|
|
10
10
|
from letta.constants import (
|
11
11
|
BASE_MEMORY_TOOLS,
|
12
12
|
BASE_MEMORY_TOOLS_V2,
|
13
|
+
BASE_MEMORY_TOOLS_V3,
|
13
14
|
BASE_SLEEPTIME_CHAT_TOOLS,
|
14
15
|
BASE_SLEEPTIME_TOOLS,
|
15
16
|
BASE_TOOLS,
|
@@ -73,7 +74,7 @@ from letta.serialize_schemas.marshmallow_tool import SerializedToolSchema
|
|
73
74
|
from letta.serialize_schemas.pydantic_agent_schema import AgentSchema
|
74
75
|
from letta.server.db import db_registry
|
75
76
|
from letta.services.archive_manager import ArchiveManager
|
76
|
-
from letta.services.block_manager import BlockManager
|
77
|
+
from letta.services.block_manager import BlockManager, validate_block_limit_constraint
|
77
78
|
from letta.services.context_window_calculator.context_window_calculator import ContextWindowCalculator
|
78
79
|
from letta.services.context_window_calculator.token_counter import AnthropicTokenCounter, TiktokenCounter
|
79
80
|
from letta.services.file_processor.chunker.line_chunker import LineChunker
|
@@ -301,185 +302,6 @@ class AgentManager:
|
|
301
302
|
# ======================================================================================================================
|
302
303
|
# Basic CRUD operations
|
303
304
|
# ======================================================================================================================
|
304
|
-
@trace_method
|
305
|
-
def create_agent(self, agent_create: CreateAgent, actor: PydanticUser, _test_only_force_id: Optional[str] = None) -> PydanticAgentState:
|
306
|
-
# validate required configs
|
307
|
-
if not agent_create.llm_config or not agent_create.embedding_config:
|
308
|
-
raise ValueError("llm_config and embedding_config are required")
|
309
|
-
|
310
|
-
# blocks
|
311
|
-
block_ids = list(agent_create.block_ids or [])
|
312
|
-
if agent_create.memory_blocks:
|
313
|
-
pydantic_blocks = [PydanticBlock(**b.model_dump(to_orm=True)) for b in agent_create.memory_blocks]
|
314
|
-
created_blocks = self.block_manager.batch_create_blocks(
|
315
|
-
pydantic_blocks,
|
316
|
-
actor=actor,
|
317
|
-
)
|
318
|
-
block_ids.extend([blk.id for blk in created_blocks])
|
319
|
-
|
320
|
-
# tools
|
321
|
-
tool_names = set(agent_create.tools or [])
|
322
|
-
if agent_create.include_base_tools:
|
323
|
-
if agent_create.agent_type == AgentType.voice_sleeptime_agent:
|
324
|
-
tool_names |= set(BASE_VOICE_SLEEPTIME_TOOLS)
|
325
|
-
elif agent_create.agent_type == AgentType.voice_convo_agent:
|
326
|
-
tool_names |= set(BASE_VOICE_SLEEPTIME_CHAT_TOOLS)
|
327
|
-
elif agent_create.agent_type == AgentType.sleeptime_agent:
|
328
|
-
tool_names |= set(BASE_SLEEPTIME_TOOLS)
|
329
|
-
elif agent_create.enable_sleeptime:
|
330
|
-
tool_names |= set(BASE_SLEEPTIME_CHAT_TOOLS)
|
331
|
-
elif agent_create.agent_type == AgentType.memgpt_v2_agent:
|
332
|
-
tool_names |= calculate_base_tools(is_v2=True)
|
333
|
-
elif agent_create.agent_type == AgentType.react_agent:
|
334
|
-
pass # no default tools
|
335
|
-
elif agent_create.agent_type == AgentType.workflow_agent:
|
336
|
-
pass # no default tools
|
337
|
-
else:
|
338
|
-
tool_names |= calculate_base_tools(is_v2=False)
|
339
|
-
if agent_create.include_multi_agent_tools:
|
340
|
-
tool_names |= calculate_multi_agent_tools()
|
341
|
-
|
342
|
-
supplied_ids = set(agent_create.tool_ids or [])
|
343
|
-
|
344
|
-
source_ids = agent_create.source_ids or []
|
345
|
-
identity_ids = agent_create.identity_ids or []
|
346
|
-
tag_values = agent_create.tags or []
|
347
|
-
|
348
|
-
with db_registry.session() as session:
|
349
|
-
with session.begin():
|
350
|
-
name_to_id, id_to_name = self._resolve_tools(
|
351
|
-
session,
|
352
|
-
tool_names,
|
353
|
-
supplied_ids,
|
354
|
-
actor.organization_id,
|
355
|
-
)
|
356
|
-
|
357
|
-
tool_ids = set(name_to_id.values()) | set(id_to_name.keys())
|
358
|
-
tool_names = set(name_to_id.keys()) # now canonical
|
359
|
-
|
360
|
-
tool_rules = list(agent_create.tool_rules or [])
|
361
|
-
|
362
|
-
# Override include_base_tool_rules to False if model matches exclusion keywords and include_base_tool_rules is not explicitly set to True
|
363
|
-
if (
|
364
|
-
(
|
365
|
-
self._should_exclude_model_from_base_tool_rules(agent_create.llm_config.model)
|
366
|
-
and agent_create.include_base_tool_rules is None
|
367
|
-
)
|
368
|
-
and agent_create.agent_type != AgentType.sleeptime_agent
|
369
|
-
) or agent_create.include_base_tool_rules is False:
|
370
|
-
agent_create.include_base_tool_rules = False
|
371
|
-
logger.info(f"Overriding include_base_tool_rules to False for model: {agent_create.llm_config.model}")
|
372
|
-
else:
|
373
|
-
agent_create.include_base_tool_rules = True
|
374
|
-
|
375
|
-
should_add_base_tool_rules = agent_create.include_base_tool_rules
|
376
|
-
if should_add_base_tool_rules:
|
377
|
-
for tn in tool_names:
|
378
|
-
if tn in {"send_message", "send_message_to_agent_async", "memory_finish_edits"}:
|
379
|
-
tool_rules.append(TerminalToolRule(tool_name=tn))
|
380
|
-
elif tn in (BASE_TOOLS + BASE_MEMORY_TOOLS + BASE_MEMORY_TOOLS_V2 + BASE_SLEEPTIME_TOOLS):
|
381
|
-
tool_rules.append(ContinueToolRule(tool_name=tn))
|
382
|
-
|
383
|
-
if tool_rules:
|
384
|
-
check_supports_structured_output(model=agent_create.llm_config.model, tool_rules=tool_rules)
|
385
|
-
|
386
|
-
new_agent = AgentModel(
|
387
|
-
name=agent_create.name,
|
388
|
-
system=derive_system_message(
|
389
|
-
agent_type=agent_create.agent_type,
|
390
|
-
enable_sleeptime=agent_create.enable_sleeptime,
|
391
|
-
system=agent_create.system,
|
392
|
-
),
|
393
|
-
hidden=agent_create.hidden,
|
394
|
-
agent_type=agent_create.agent_type,
|
395
|
-
llm_config=agent_create.llm_config,
|
396
|
-
embedding_config=agent_create.embedding_config,
|
397
|
-
organization_id=actor.organization_id,
|
398
|
-
description=agent_create.description,
|
399
|
-
metadata_=agent_create.metadata,
|
400
|
-
tool_rules=tool_rules,
|
401
|
-
project_id=agent_create.project_id,
|
402
|
-
template_id=agent_create.template_id,
|
403
|
-
base_template_id=agent_create.base_template_id,
|
404
|
-
message_buffer_autoclear=agent_create.message_buffer_autoclear,
|
405
|
-
enable_sleeptime=agent_create.enable_sleeptime,
|
406
|
-
response_format=agent_create.response_format,
|
407
|
-
created_by_id=actor.id,
|
408
|
-
last_updated_by_id=actor.id,
|
409
|
-
timezone=agent_create.timezone,
|
410
|
-
max_files_open=agent_create.max_files_open,
|
411
|
-
per_file_view_window_char_limit=agent_create.per_file_view_window_char_limit,
|
412
|
-
)
|
413
|
-
|
414
|
-
# Set template fields for InternalTemplateAgentCreate (similar to group creation)
|
415
|
-
if isinstance(agent_create, InternalTemplateAgentCreate):
|
416
|
-
new_agent.base_template_id = agent_create.base_template_id
|
417
|
-
new_agent.template_id = agent_create.template_id
|
418
|
-
new_agent.deployment_id = agent_create.deployment_id
|
419
|
-
new_agent.entity_id = agent_create.entity_id
|
420
|
-
|
421
|
-
if _test_only_force_id:
|
422
|
-
new_agent.id = _test_only_force_id
|
423
|
-
|
424
|
-
session.add(new_agent)
|
425
|
-
session.flush()
|
426
|
-
aid = new_agent.id
|
427
|
-
|
428
|
-
# Note: These methods may need async versions if they perform database operations
|
429
|
-
self._bulk_insert_pivot(
|
430
|
-
session,
|
431
|
-
ToolsAgents.__table__,
|
432
|
-
[{"agent_id": aid, "tool_id": tid} for tid in tool_ids],
|
433
|
-
)
|
434
|
-
|
435
|
-
if block_ids:
|
436
|
-
result = session.execute(select(BlockModel.id, BlockModel.label).where(BlockModel.id.in_(block_ids)))
|
437
|
-
rows = [{"agent_id": aid, "block_id": bid, "block_label": lbl} for bid, lbl in result.all()]
|
438
|
-
self._bulk_insert_pivot(session, BlocksAgents.__table__, rows)
|
439
|
-
|
440
|
-
self._bulk_insert_pivot(
|
441
|
-
session,
|
442
|
-
SourcesAgents.__table__,
|
443
|
-
[{"agent_id": aid, "source_id": sid} for sid in source_ids],
|
444
|
-
)
|
445
|
-
self._bulk_insert_pivot(
|
446
|
-
session,
|
447
|
-
AgentsTags.__table__,
|
448
|
-
[{"agent_id": aid, "tag": tag} for tag in tag_values],
|
449
|
-
)
|
450
|
-
self._bulk_insert_pivot(
|
451
|
-
session,
|
452
|
-
IdentitiesAgents.__table__,
|
453
|
-
[{"agent_id": aid, "identity_id": iid} for iid in identity_ids],
|
454
|
-
)
|
455
|
-
|
456
|
-
agent_secrets = agent_create.secrets or agent_create.tool_exec_environment_variables
|
457
|
-
if agent_secrets:
|
458
|
-
env_rows = [
|
459
|
-
{
|
460
|
-
"agent_id": aid,
|
461
|
-
"key": key,
|
462
|
-
"value": val,
|
463
|
-
"organization_id": actor.organization_id,
|
464
|
-
}
|
465
|
-
for key, val in agent_secrets.items()
|
466
|
-
]
|
467
|
-
session.execute(insert(AgentEnvironmentVariable).values(env_rows))
|
468
|
-
|
469
|
-
# initial message sequence
|
470
|
-
init_messages = self._generate_initial_message_sequence(
|
471
|
-
actor,
|
472
|
-
agent_state=new_agent.to_pydantic(include_relationships={"memory"}),
|
473
|
-
supplied_initial_message_sequence=agent_create.initial_message_sequence,
|
474
|
-
)
|
475
|
-
new_agent.message_ids = [msg.id for msg in init_messages]
|
476
|
-
|
477
|
-
session.refresh(new_agent)
|
478
|
-
|
479
|
-
# Using the synchronous version since we don't have an async version yet
|
480
|
-
# If you implement an async version of create_many_messages, you can switch to that
|
481
|
-
self.message_manager.create_many_messages(pydantic_msgs=init_messages, actor=actor)
|
482
|
-
return new_agent.to_pydantic()
|
483
305
|
|
484
306
|
@trace_method
|
485
307
|
async def create_agent_async(
|
@@ -493,8 +315,24 @@ class AgentManager:
|
|
493
315
|
if not agent_create.llm_config or not agent_create.embedding_config:
|
494
316
|
raise ValueError("llm_config and embedding_config are required")
|
495
317
|
|
496
|
-
|
497
|
-
|
318
|
+
# For v1 agents, enforce sane defaults even when reasoning is omitted
|
319
|
+
if agent_create.agent_type == AgentType.letta_v1_agent:
|
320
|
+
# Claude 3.7/4 or OpenAI o1/o3/o4/gpt-5
|
321
|
+
default_reasoning = LLMConfig.is_anthropic_reasoning_model(agent_create.llm_config) or LLMConfig.is_openai_reasoning_model(
|
322
|
+
agent_create.llm_config
|
323
|
+
)
|
324
|
+
agent_create.llm_config = LLMConfig.apply_reasoning_setting_to_config(
|
325
|
+
agent_create.llm_config,
|
326
|
+
agent_create.reasoning if agent_create.reasoning is not None else default_reasoning,
|
327
|
+
agent_create.agent_type,
|
328
|
+
)
|
329
|
+
else:
|
330
|
+
if agent_create.reasoning is not None:
|
331
|
+
agent_create.llm_config = LLMConfig.apply_reasoning_setting_to_config(
|
332
|
+
agent_create.llm_config,
|
333
|
+
agent_create.reasoning,
|
334
|
+
agent_create.agent_type,
|
335
|
+
)
|
498
336
|
|
499
337
|
# blocks
|
500
338
|
block_ids = list(agent_create.block_ids or [])
|
@@ -531,6 +369,15 @@ class AgentManager:
|
|
531
369
|
tool_names |= calculate_base_tools(is_v2=True)
|
532
370
|
elif agent_create.agent_type == AgentType.react_agent:
|
533
371
|
pass # no default tools
|
372
|
+
elif agent_create.agent_type == AgentType.letta_v1_agent:
|
373
|
+
tool_names |= calculate_base_tools(is_v2=True)
|
374
|
+
# Remove `send_message` if it exists
|
375
|
+
tool_names.discard("send_message")
|
376
|
+
# NOTE: also overwriting inner_thoughts_in_kwargs to force False
|
377
|
+
agent_create.llm_config.put_inner_thoughts_in_kwargs = False
|
378
|
+
# NOTE: also overwrite initial message sequence to empty by default
|
379
|
+
if agent_create.initial_message_sequence is None:
|
380
|
+
agent_create.initial_message_sequence = []
|
534
381
|
elif agent_create.agent_type == AgentType.workflow_agent:
|
535
382
|
pass # no default tools
|
536
383
|
else:
|
@@ -593,7 +440,7 @@ class AgentManager:
|
|
593
440
|
for tn in tool_names:
|
594
441
|
if tn in {"send_message", "send_message_to_agent_async", "memory_finish_edits"}:
|
595
442
|
tool_rules.append(TerminalToolRule(tool_name=tn))
|
596
|
-
elif tn in (BASE_TOOLS + BASE_MEMORY_TOOLS + BASE_MEMORY_TOOLS_V2 + BASE_SLEEPTIME_TOOLS):
|
443
|
+
elif tn in (BASE_TOOLS + BASE_MEMORY_TOOLS + BASE_MEMORY_TOOLS_V2 + BASE_MEMORY_TOOLS_V3 + BASE_SLEEPTIME_TOOLS):
|
597
444
|
tool_rules.append(ContinueToolRule(tool_name=tn))
|
598
445
|
|
599
446
|
for tool_with_requires_approval in requires_approval:
|
@@ -783,14 +630,6 @@ class AgentManager:
|
|
783
630
|
|
784
631
|
return init_messages
|
785
632
|
|
786
|
-
@enforce_types
|
787
|
-
@trace_method
|
788
|
-
def append_initial_message_sequence_to_in_context_messages(
|
789
|
-
self, actor: PydanticUser, agent_state: PydanticAgentState, initial_message_sequence: Optional[List[MessageCreate]] = None
|
790
|
-
) -> PydanticAgentState:
|
791
|
-
init_messages = self._generate_initial_message_sequence(actor, agent_state, initial_message_sequence)
|
792
|
-
return self.append_to_in_context_messages(init_messages, agent_id=agent_state.id, actor=actor)
|
793
|
-
|
794
633
|
@enforce_types
|
795
634
|
@trace_method
|
796
635
|
async def append_initial_message_sequence_to_in_context_messages_async(
|
@@ -799,130 +638,6 @@ class AgentManager:
|
|
799
638
|
init_messages = await self._generate_initial_message_sequence_async(actor, agent_state, initial_message_sequence)
|
800
639
|
return await self.append_to_in_context_messages_async(init_messages, agent_id=agent_state.id, actor=actor)
|
801
640
|
|
802
|
-
@enforce_types
|
803
|
-
@trace_method
|
804
|
-
def update_agent(
|
805
|
-
self,
|
806
|
-
agent_id: str,
|
807
|
-
agent_update: UpdateAgent,
|
808
|
-
actor: PydanticUser,
|
809
|
-
) -> PydanticAgentState:
|
810
|
-
new_tools = set(agent_update.tool_ids or [])
|
811
|
-
new_sources = set(agent_update.source_ids or [])
|
812
|
-
new_blocks = set(agent_update.block_ids or [])
|
813
|
-
new_idents = set(agent_update.identity_ids or [])
|
814
|
-
new_tags = set(agent_update.tags or [])
|
815
|
-
|
816
|
-
with db_registry.session() as session, session.begin():
|
817
|
-
agent: AgentModel = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
818
|
-
agent.updated_at = datetime.now(timezone.utc)
|
819
|
-
agent.last_updated_by_id = actor.id
|
820
|
-
|
821
|
-
scalar_updates = {
|
822
|
-
"name": agent_update.name,
|
823
|
-
"system": agent_update.system,
|
824
|
-
"llm_config": agent_update.llm_config,
|
825
|
-
"embedding_config": agent_update.embedding_config,
|
826
|
-
"message_ids": agent_update.message_ids,
|
827
|
-
"tool_rules": agent_update.tool_rules,
|
828
|
-
"description": agent_update.description,
|
829
|
-
"project_id": agent_update.project_id,
|
830
|
-
"template_id": agent_update.template_id,
|
831
|
-
"base_template_id": agent_update.base_template_id,
|
832
|
-
"message_buffer_autoclear": agent_update.message_buffer_autoclear,
|
833
|
-
"enable_sleeptime": agent_update.enable_sleeptime,
|
834
|
-
"response_format": agent_update.response_format,
|
835
|
-
"last_run_completion": agent_update.last_run_completion,
|
836
|
-
"last_run_duration_ms": agent_update.last_run_duration_ms,
|
837
|
-
"max_files_open": agent_update.max_files_open,
|
838
|
-
"per_file_view_window_char_limit": agent_update.per_file_view_window_char_limit,
|
839
|
-
"timezone": agent_update.timezone,
|
840
|
-
}
|
841
|
-
for col, val in scalar_updates.items():
|
842
|
-
if val is not None:
|
843
|
-
setattr(agent, col, val)
|
844
|
-
|
845
|
-
if agent_update.metadata is not None:
|
846
|
-
agent.metadata_ = agent_update.metadata
|
847
|
-
|
848
|
-
aid = agent.id
|
849
|
-
|
850
|
-
if agent_update.tool_ids is not None:
|
851
|
-
self._replace_pivot_rows(
|
852
|
-
session,
|
853
|
-
ToolsAgents.__table__,
|
854
|
-
aid,
|
855
|
-
[{"agent_id": aid, "tool_id": tid} for tid in new_tools],
|
856
|
-
)
|
857
|
-
session.expire(agent, ["tools"])
|
858
|
-
|
859
|
-
if agent_update.source_ids is not None:
|
860
|
-
self._replace_pivot_rows(
|
861
|
-
session,
|
862
|
-
SourcesAgents.__table__,
|
863
|
-
aid,
|
864
|
-
[{"agent_id": aid, "source_id": sid} for sid in new_sources],
|
865
|
-
)
|
866
|
-
session.expire(agent, ["sources"])
|
867
|
-
|
868
|
-
if agent_update.block_ids is not None:
|
869
|
-
rows = []
|
870
|
-
if new_blocks:
|
871
|
-
label_map = {
|
872
|
-
bid: lbl
|
873
|
-
for bid, lbl in session.execute(select(BlockModel.id, BlockModel.label).where(BlockModel.id.in_(new_blocks)))
|
874
|
-
}
|
875
|
-
rows = [{"agent_id": aid, "block_id": bid, "block_label": label_map[bid]} for bid in new_blocks]
|
876
|
-
|
877
|
-
self._replace_pivot_rows(session, BlocksAgents.__table__, aid, rows)
|
878
|
-
session.expire(agent, ["core_memory"])
|
879
|
-
|
880
|
-
if agent_update.identity_ids is not None:
|
881
|
-
self._replace_pivot_rows(
|
882
|
-
session,
|
883
|
-
IdentitiesAgents.__table__,
|
884
|
-
aid,
|
885
|
-
[{"agent_id": aid, "identity_id": iid} for iid in new_idents],
|
886
|
-
)
|
887
|
-
session.expire(agent, ["identities"])
|
888
|
-
|
889
|
-
if agent_update.tags is not None:
|
890
|
-
self._replace_pivot_rows(
|
891
|
-
session,
|
892
|
-
AgentsTags.__table__,
|
893
|
-
aid,
|
894
|
-
[{"agent_id": aid, "tag": tag} for tag in new_tags],
|
895
|
-
)
|
896
|
-
session.expire(agent, ["tags"])
|
897
|
-
|
898
|
-
agent_secrets = agent_update.secrets or agent_update.tool_exec_environment_variables
|
899
|
-
if agent_secrets is not None:
|
900
|
-
session.execute(delete(AgentEnvironmentVariable).where(AgentEnvironmentVariable.agent_id == aid))
|
901
|
-
env_rows = [
|
902
|
-
{
|
903
|
-
"agent_id": aid,
|
904
|
-
"key": k,
|
905
|
-
"value": v,
|
906
|
-
"organization_id": agent.organization_id,
|
907
|
-
}
|
908
|
-
for k, v in agent_secrets.items()
|
909
|
-
]
|
910
|
-
if env_rows:
|
911
|
-
self._bulk_insert_pivot(session, AgentEnvironmentVariable.__table__, env_rows)
|
912
|
-
session.expire(agent, ["tool_exec_environment_variables"])
|
913
|
-
|
914
|
-
if agent_update.enable_sleeptime and agent_update.system is None:
|
915
|
-
agent.system = derive_system_message(
|
916
|
-
agent_type=agent.agent_type,
|
917
|
-
enable_sleeptime=agent_update.enable_sleeptime,
|
918
|
-
system=agent.system,
|
919
|
-
)
|
920
|
-
|
921
|
-
session.flush()
|
922
|
-
session.refresh(agent)
|
923
|
-
|
924
|
-
return agent.to_pydantic()
|
925
|
-
|
926
641
|
@enforce_types
|
927
642
|
@trace_method
|
928
643
|
async def update_agent_async(
|
@@ -944,7 +659,11 @@ class AgentManager:
|
|
944
659
|
|
945
660
|
if agent_update.reasoning is not None:
|
946
661
|
llm_config = agent_update.llm_config or agent.llm_config
|
947
|
-
agent_update.llm_config = LLMConfig.apply_reasoning_setting_to_config(
|
662
|
+
agent_update.llm_config = LLMConfig.apply_reasoning_setting_to_config(
|
663
|
+
llm_config,
|
664
|
+
agent_update.reasoning,
|
665
|
+
agent.agent_type,
|
666
|
+
)
|
948
667
|
|
949
668
|
scalar_updates = {
|
950
669
|
"name": agent_update.name,
|
@@ -1073,67 +792,6 @@ class AgentManager:
|
|
1073
792
|
await agent.update_async(db_session=session, actor=actor, no_commit=True, no_refresh=True)
|
1074
793
|
await session.commit()
|
1075
794
|
|
1076
|
-
# TODO: Make this general and think about how to roll this into sqlalchemybase
|
1077
|
-
@trace_method
|
1078
|
-
def list_agents(
|
1079
|
-
self,
|
1080
|
-
actor: PydanticUser,
|
1081
|
-
name: Optional[str] = None,
|
1082
|
-
tags: Optional[List[str]] = None,
|
1083
|
-
match_all_tags: bool = False,
|
1084
|
-
before: Optional[str] = None,
|
1085
|
-
after: Optional[str] = None,
|
1086
|
-
limit: Optional[int] = 50,
|
1087
|
-
query_text: Optional[str] = None,
|
1088
|
-
project_id: Optional[str] = None,
|
1089
|
-
template_id: Optional[str] = None,
|
1090
|
-
base_template_id: Optional[str] = None,
|
1091
|
-
identity_id: Optional[str] = None,
|
1092
|
-
identifier_keys: Optional[List[str]] = None,
|
1093
|
-
include_relationships: Optional[List[str]] = None,
|
1094
|
-
ascending: bool = True,
|
1095
|
-
sort_by: Optional[str] = "created_at",
|
1096
|
-
) -> List[PydanticAgentState]:
|
1097
|
-
"""
|
1098
|
-
Retrieves agents with optimized filtering and optional field selection.
|
1099
|
-
|
1100
|
-
Args:
|
1101
|
-
actor: The User requesting the list
|
1102
|
-
name (Optional[str]): Filter by agent name.
|
1103
|
-
tags (Optional[List[str]]): Filter agents by tags.
|
1104
|
-
match_all_tags (bool): If True, only return agents that match ALL given tags.
|
1105
|
-
before (Optional[str]): Cursor for pagination.
|
1106
|
-
after (Optional[str]): Cursor for pagination.
|
1107
|
-
limit (Optional[int]): Maximum number of agents to return.
|
1108
|
-
query_text (Optional[str]): Search agents by name.
|
1109
|
-
project_id (Optional[str]): Filter by project ID.
|
1110
|
-
template_id (Optional[str]): Filter by template ID.
|
1111
|
-
base_template_id (Optional[str]): Filter by base template ID.
|
1112
|
-
identity_id (Optional[str]): Filter by identifier ID.
|
1113
|
-
identifier_keys (Optional[List[str]]): Search agents by identifier keys.
|
1114
|
-
include_relationships (Optional[List[str]]): List of fields to load for performance optimization.
|
1115
|
-
ascending
|
1116
|
-
|
1117
|
-
Returns:
|
1118
|
-
List[PydanticAgentState]: The filtered list of matching agents.
|
1119
|
-
"""
|
1120
|
-
with db_registry.session() as session:
|
1121
|
-
query = select(AgentModel).distinct(AgentModel.created_at, AgentModel.id)
|
1122
|
-
query = AgentModel.apply_access_predicate(query, actor, ["read"], AccessType.ORGANIZATION)
|
1123
|
-
|
1124
|
-
# Apply filters
|
1125
|
-
query = _apply_filters(query, name, query_text, project_id, template_id, base_template_id)
|
1126
|
-
query = _apply_identity_filters(query, identity_id, identifier_keys)
|
1127
|
-
query = _apply_tag_filter(query, tags, match_all_tags)
|
1128
|
-
query = _apply_pagination(query, before, after, session, ascending=ascending, sort_by=sort_by)
|
1129
|
-
|
1130
|
-
if limit:
|
1131
|
-
query = query.limit(limit)
|
1132
|
-
|
1133
|
-
result = session.execute(query)
|
1134
|
-
agents = result.scalars().all()
|
1135
|
-
return [agent.to_pydantic(include_relationships=include_relationships) for agent in agents]
|
1136
|
-
|
1137
795
|
@trace_method
|
1138
796
|
async def list_agents_async(
|
1139
797
|
self,
|
@@ -1201,50 +859,6 @@ class AgentManager:
|
|
1201
859
|
agents = result.scalars().all()
|
1202
860
|
return await asyncio.gather(*[agent.to_pydantic_async(include_relationships=include_relationships) for agent in agents])
|
1203
861
|
|
1204
|
-
@enforce_types
|
1205
|
-
@trace_method
|
1206
|
-
def list_agents_matching_tags(
|
1207
|
-
self,
|
1208
|
-
actor: PydanticUser,
|
1209
|
-
match_all: List[str],
|
1210
|
-
match_some: List[str],
|
1211
|
-
limit: Optional[int] = 50,
|
1212
|
-
) -> List[PydanticAgentState]:
|
1213
|
-
"""
|
1214
|
-
Retrieves agents in the same organization that match all specified `match_all` tags
|
1215
|
-
and at least one tag from `match_some`. The query is optimized for efficiency by
|
1216
|
-
leveraging indexed filtering and aggregation.
|
1217
|
-
|
1218
|
-
Args:
|
1219
|
-
actor (PydanticUser): The user requesting the agent list.
|
1220
|
-
match_all (List[str]): Agents must have all these tags.
|
1221
|
-
match_some (List[str]): Agents must have at least one of these tags.
|
1222
|
-
limit (Optional[int]): Maximum number of agents to return.
|
1223
|
-
|
1224
|
-
Returns:
|
1225
|
-
List[PydanticAgentState: The filtered list of matching agents.
|
1226
|
-
"""
|
1227
|
-
with db_registry.session() as session:
|
1228
|
-
query = select(AgentModel).where(AgentModel.organization_id == actor.organization_id)
|
1229
|
-
|
1230
|
-
if match_all:
|
1231
|
-
# Subquery to find agent IDs that contain all match_all tags
|
1232
|
-
subquery = (
|
1233
|
-
select(AgentsTags.agent_id)
|
1234
|
-
.where(AgentsTags.tag.in_(match_all))
|
1235
|
-
.group_by(AgentsTags.agent_id)
|
1236
|
-
.having(func.count(AgentsTags.tag) == literal(len(match_all)))
|
1237
|
-
)
|
1238
|
-
query = query.where(AgentModel.id.in_(subquery))
|
1239
|
-
|
1240
|
-
if match_some:
|
1241
|
-
# Ensures agents match at least one tag in match_some
|
1242
|
-
query = query.join(AgentsTags).where(AgentsTags.tag.in_(match_some))
|
1243
|
-
|
1244
|
-
query = query.distinct(AgentModel.id).order_by(AgentModel.id).limit(limit)
|
1245
|
-
|
1246
|
-
return list(session.execute(query).scalars())
|
1247
|
-
|
1248
862
|
@enforce_types
|
1249
863
|
@trace_method
|
1250
864
|
async def list_agents_matching_tags_async(
|
@@ -1289,17 +903,6 @@ class AgentManager:
|
|
1289
903
|
result = await session.execute(query)
|
1290
904
|
return await asyncio.gather(*[agent.to_pydantic_async() for agent in result.scalars()])
|
1291
905
|
|
1292
|
-
@trace_method
|
1293
|
-
def size(
|
1294
|
-
self,
|
1295
|
-
actor: PydanticUser,
|
1296
|
-
) -> int:
|
1297
|
-
"""
|
1298
|
-
Get the total count of agents for the given user.
|
1299
|
-
"""
|
1300
|
-
with db_registry.session() as session:
|
1301
|
-
return AgentModel.size(db_session=session, actor=actor)
|
1302
|
-
|
1303
906
|
@trace_method
|
1304
907
|
async def size_async(
|
1305
908
|
self,
|
@@ -1311,14 +914,6 @@ class AgentManager:
|
|
1311
914
|
async with db_registry.async_session() as session:
|
1312
915
|
return await AgentModel.size_async(db_session=session, actor=actor)
|
1313
916
|
|
1314
|
-
@enforce_types
|
1315
|
-
@trace_method
|
1316
|
-
def get_agent_by_id(self, agent_id: str, actor: PydanticUser) -> PydanticAgentState:
|
1317
|
-
"""Fetch an agent by its ID."""
|
1318
|
-
with db_registry.session() as session:
|
1319
|
-
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
1320
|
-
return agent.to_pydantic()
|
1321
|
-
|
1322
917
|
@enforce_types
|
1323
918
|
@trace_method
|
1324
919
|
async def get_agent_by_id_async(
|
@@ -1342,6 +937,9 @@ class AgentManager:
|
|
1342
937
|
raise NoResultFound(f"Agent with ID {agent_id} not found")
|
1343
938
|
|
1344
939
|
return await agent.to_pydantic_async(include_relationships=include_relationships)
|
940
|
+
except NoResultFound:
|
941
|
+
# Re-raise NoResultFound without logging to preserve 404 handling
|
942
|
+
raise
|
1345
943
|
except Exception as e:
|
1346
944
|
logger.error(f"Error fetching agent {agent_id}: {str(e)}")
|
1347
945
|
raise
|
@@ -1374,14 +972,6 @@ class AgentManager:
|
|
1374
972
|
logger.error(f"Error fetching agents with IDs {agent_ids}: {str(e)}")
|
1375
973
|
raise
|
1376
974
|
|
1377
|
-
@enforce_types
|
1378
|
-
@trace_method
|
1379
|
-
def get_agent_by_name(self, agent_name: str, actor: PydanticUser) -> PydanticAgentState:
|
1380
|
-
"""Fetch an agent by its ID."""
|
1381
|
-
with db_registry.session() as session:
|
1382
|
-
agent = AgentModel.read(db_session=session, name=agent_name, actor=actor)
|
1383
|
-
return agent.to_pydantic()
|
1384
|
-
|
1385
975
|
@enforce_types
|
1386
976
|
@trace_method
|
1387
977
|
async def get_agent_archive_ids_async(self, agent_id: str, actor: PydanticUser) -> List[str]:
|
@@ -1395,54 +985,6 @@ class AgentManager:
|
|
1395
985
|
archive_ids = [row[0] for row in result.fetchall()]
|
1396
986
|
return archive_ids
|
1397
987
|
|
1398
|
-
@enforce_types
|
1399
|
-
@trace_method
|
1400
|
-
def delete_agent(self, agent_id: str, actor: PydanticUser) -> None:
|
1401
|
-
"""
|
1402
|
-
Deletes an agent and its associated relationships.
|
1403
|
-
Ensures proper permission checks and cascades where applicable.
|
1404
|
-
|
1405
|
-
Args:
|
1406
|
-
agent_id: ID of the agent to be deleted.
|
1407
|
-
actor: User performing the action.
|
1408
|
-
|
1409
|
-
Raises:
|
1410
|
-
NoResultFound: If agent doesn't exist
|
1411
|
-
"""
|
1412
|
-
with db_registry.session() as session:
|
1413
|
-
# Retrieve the agent
|
1414
|
-
logger.debug(f"Hard deleting Agent with ID: {agent_id} with actor={actor}")
|
1415
|
-
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
1416
|
-
agents_to_delete = [agent]
|
1417
|
-
sleeptime_group_to_delete = None
|
1418
|
-
|
1419
|
-
# Delete sleeptime agent and group (TODO this is flimsy pls fix)
|
1420
|
-
if agent.multi_agent_group:
|
1421
|
-
participant_agent_ids = agent.multi_agent_group.agent_ids
|
1422
|
-
if agent.multi_agent_group.manager_type in {ManagerType.sleeptime, ManagerType.voice_sleeptime} and participant_agent_ids:
|
1423
|
-
for participant_agent_id in participant_agent_ids:
|
1424
|
-
try:
|
1425
|
-
sleeptime_agent = AgentModel.read(db_session=session, identifier=participant_agent_id, actor=actor)
|
1426
|
-
agents_to_delete.append(sleeptime_agent)
|
1427
|
-
except NoResultFound:
|
1428
|
-
pass # agent already deleted
|
1429
|
-
sleeptime_agent_group = GroupModel.read(db_session=session, identifier=agent.multi_agent_group.id, actor=actor)
|
1430
|
-
sleeptime_group_to_delete = sleeptime_agent_group
|
1431
|
-
|
1432
|
-
try:
|
1433
|
-
if sleeptime_group_to_delete is not None:
|
1434
|
-
session.delete(sleeptime_group_to_delete)
|
1435
|
-
session.commit()
|
1436
|
-
for agent in agents_to_delete:
|
1437
|
-
session.delete(agent)
|
1438
|
-
session.commit()
|
1439
|
-
except Exception as e:
|
1440
|
-
session.rollback()
|
1441
|
-
logger.exception(f"Failed to hard delete Agent with ID {agent_id}")
|
1442
|
-
raise ValueError(f"Failed to hard delete Agent with ID {agent_id}: {e}")
|
1443
|
-
else:
|
1444
|
-
logger.debug(f"Agent with ID {agent_id} successfully hard deleted")
|
1445
|
-
|
1446
988
|
@enforce_types
|
1447
989
|
@trace_method
|
1448
990
|
async def delete_agent_async(self, agent_id: str, actor: PydanticUser) -> None:
|
@@ -1493,168 +1035,9 @@ class AgentManager:
|
|
1493
1035
|
else:
|
1494
1036
|
logger.debug(f"Agent with ID {agent_id} successfully hard deleted")
|
1495
1037
|
|
1496
|
-
@enforce_types
|
1497
|
-
@trace_method
|
1498
|
-
def serialize(self, agent_id: str, actor: PydanticUser, max_steps: Optional[int] = None) -> AgentSchema:
|
1499
|
-
with db_registry.session() as session:
|
1500
|
-
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
1501
|
-
schema = MarshmallowAgentSchema(session=session, actor=actor, max_steps=max_steps)
|
1502
|
-
data = schema.dump(agent)
|
1503
|
-
return AgentSchema(**data)
|
1504
|
-
|
1505
|
-
@enforce_types
|
1506
|
-
@trace_method
|
1507
|
-
def deserialize(
|
1508
|
-
self,
|
1509
|
-
serialized_agent: AgentSchema,
|
1510
|
-
actor: PydanticUser,
|
1511
|
-
append_copy_suffix: bool = True,
|
1512
|
-
override_existing_tools: bool = True,
|
1513
|
-
project_id: Optional[str] = None,
|
1514
|
-
strip_messages: Optional[bool] = False,
|
1515
|
-
env_vars: Optional[dict[str, Any]] = None,
|
1516
|
-
) -> PydanticAgentState:
|
1517
|
-
serialized_agent_dict = serialized_agent.model_dump()
|
1518
|
-
tool_data_list = serialized_agent_dict.pop("tools", [])
|
1519
|
-
messages = serialized_agent_dict.pop(MarshmallowAgentSchema.FIELD_MESSAGES, [])
|
1520
|
-
|
1521
|
-
for msg in messages:
|
1522
|
-
msg[MarshmallowAgentSchema.FIELD_ID] = SerializedMessageSchema.generate_id() # Generate new ID
|
1523
|
-
|
1524
|
-
message_ids = []
|
1525
|
-
in_context_message_indices = serialized_agent_dict.pop(MarshmallowAgentSchema.FIELD_IN_CONTEXT_INDICES)
|
1526
|
-
for idx in in_context_message_indices:
|
1527
|
-
message_ids.append(messages[idx][MarshmallowAgentSchema.FIELD_ID])
|
1528
|
-
|
1529
|
-
serialized_agent_dict[MarshmallowAgentSchema.FIELD_MESSAGE_IDS] = message_ids
|
1530
|
-
|
1531
|
-
with db_registry.session() as session:
|
1532
|
-
schema = MarshmallowAgentSchema(session=session, actor=actor)
|
1533
|
-
agent = schema.load(serialized_agent_dict, session=session)
|
1534
|
-
|
1535
|
-
agent.organization_id = actor.organization_id
|
1536
|
-
for block in agent.core_memory:
|
1537
|
-
block.organization_id = actor.organization_id
|
1538
|
-
if append_copy_suffix:
|
1539
|
-
agent.name += "_copy"
|
1540
|
-
if project_id:
|
1541
|
-
agent.project_id = project_id
|
1542
|
-
|
1543
|
-
if strip_messages:
|
1544
|
-
# we want to strip all but the first (system) message
|
1545
|
-
agent.message_ids = [agent.message_ids[0]]
|
1546
|
-
|
1547
|
-
if env_vars:
|
1548
|
-
for var in agent.tool_exec_environment_variables:
|
1549
|
-
var.value = env_vars.get(var.key, "")
|
1550
|
-
for var in agent.secrets:
|
1551
|
-
var.value = env_vars.get(var.key, "")
|
1552
|
-
|
1553
|
-
agent = agent.create(session, actor=actor)
|
1554
|
-
|
1555
|
-
pydantic_agent = agent.to_pydantic()
|
1556
|
-
|
1557
|
-
pyd_msgs = []
|
1558
|
-
message_schema = SerializedMessageSchema(session=session, actor=actor)
|
1559
|
-
|
1560
|
-
for serialized_message in messages:
|
1561
|
-
pydantic_message = message_schema.load(serialized_message, session=session).to_pydantic()
|
1562
|
-
pydantic_message.agent_id = agent.id
|
1563
|
-
pyd_msgs.append(pydantic_message)
|
1564
|
-
self.message_manager.create_many_messages(pyd_msgs, actor=actor)
|
1565
|
-
|
1566
|
-
# Need to do this separately as there's some fancy upsert logic that SqlAlchemy cannot handle
|
1567
|
-
for tool_data in tool_data_list:
|
1568
|
-
pydantic_tool = SerializedToolSchema(actor=actor).load(tool_data, transient=True).to_pydantic()
|
1569
|
-
|
1570
|
-
existing_pydantic_tool = self.tool_manager.get_tool_by_name(pydantic_tool.name, actor=actor)
|
1571
|
-
if existing_pydantic_tool and (
|
1572
|
-
existing_pydantic_tool.tool_type in {ToolType.LETTA_CORE, ToolType.LETTA_MULTI_AGENT_CORE, ToolType.LETTA_MEMORY_CORE}
|
1573
|
-
or not override_existing_tools
|
1574
|
-
):
|
1575
|
-
pydantic_tool = existing_pydantic_tool
|
1576
|
-
else:
|
1577
|
-
pydantic_tool = self.tool_manager.create_or_update_tool(pydantic_tool, actor=actor, bypass_name_check=True)
|
1578
|
-
|
1579
|
-
pydantic_agent = self.attach_tool(agent_id=pydantic_agent.id, tool_id=pydantic_tool.id, actor=actor)
|
1580
|
-
|
1581
|
-
return pydantic_agent
|
1582
|
-
|
1583
1038
|
# ======================================================================================================================
|
1584
1039
|
# Per Agent Environment Variable Management
|
1585
1040
|
# ======================================================================================================================
|
1586
|
-
@enforce_types
|
1587
|
-
@trace_method
|
1588
|
-
def _set_environment_variables(
|
1589
|
-
self,
|
1590
|
-
agent_id: str,
|
1591
|
-
env_vars: Dict[str, str],
|
1592
|
-
actor: PydanticUser,
|
1593
|
-
) -> PydanticAgentState:
|
1594
|
-
"""
|
1595
|
-
Adds or replaces the environment variables for the specified agent.
|
1596
|
-
|
1597
|
-
Args:
|
1598
|
-
agent_id: The agent id.
|
1599
|
-
env_vars: A dictionary of environment variable key-value pairs.
|
1600
|
-
actor: The user performing the action.
|
1601
|
-
|
1602
|
-
Returns:
|
1603
|
-
PydanticAgentState: The updated agent as a Pydantic model.
|
1604
|
-
"""
|
1605
|
-
with db_registry.session() as session:
|
1606
|
-
# Retrieve the agent
|
1607
|
-
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
1608
|
-
|
1609
|
-
# Fetch existing environment variables as a dictionary
|
1610
|
-
existing_vars = {var.key: var for var in agent.tool_exec_environment_variables}
|
1611
|
-
|
1612
|
-
# Update or create environment variables
|
1613
|
-
updated_vars = []
|
1614
|
-
for key, value in env_vars.items():
|
1615
|
-
if key in existing_vars:
|
1616
|
-
# Update existing variable
|
1617
|
-
existing_vars[key].value = value
|
1618
|
-
updated_vars.append(existing_vars[key])
|
1619
|
-
else:
|
1620
|
-
# Create new variable
|
1621
|
-
updated_vars.append(
|
1622
|
-
AgentEnvironmentVariableModel(
|
1623
|
-
key=key,
|
1624
|
-
value=value,
|
1625
|
-
agent_id=agent_id,
|
1626
|
-
organization_id=actor.organization_id,
|
1627
|
-
created_by_id=actor.id,
|
1628
|
-
last_updated_by_id=actor.id,
|
1629
|
-
)
|
1630
|
-
)
|
1631
|
-
|
1632
|
-
# Remove stale variables
|
1633
|
-
stale_keys = set(existing_vars) - set(env_vars)
|
1634
|
-
agent.tool_exec_environment_variables = [var for var in updated_vars if var.key not in stale_keys]
|
1635
|
-
agent.secrets = [var for var in updated_vars if var.key not in stale_keys]
|
1636
|
-
|
1637
|
-
# Update the agent in the database
|
1638
|
-
agent.update(session, actor=actor)
|
1639
|
-
|
1640
|
-
# Return the updated agent state
|
1641
|
-
return agent.to_pydantic()
|
1642
|
-
|
1643
|
-
@enforce_types
|
1644
|
-
@trace_method
|
1645
|
-
def list_groups(self, agent_id: str, actor: PydanticUser, manager_type: Optional[str] = None) -> List[PydanticGroup]:
|
1646
|
-
with db_registry.session() as session:
|
1647
|
-
query = (
|
1648
|
-
select(GroupModel)
|
1649
|
-
.join(GroupsAgents, GroupModel.id == GroupsAgents.group_id)
|
1650
|
-
.where(GroupsAgents.agent_id == agent_id, GroupModel.organization_id == actor.organization_id)
|
1651
|
-
)
|
1652
|
-
|
1653
|
-
if manager_type:
|
1654
|
-
query = query.where(GroupModel.manager_type == manager_type)
|
1655
|
-
|
1656
|
-
result = session.execute(query)
|
1657
|
-
return [group.to_pydantic() for group in result.scalars()]
|
1658
1041
|
|
1659
1042
|
# ======================================================================================================================
|
1660
1043
|
# In Context Messages Management
|
@@ -1666,9 +1049,9 @@ class AgentManager:
|
|
1666
1049
|
# TODO: This can also be made more efficient, instead of getting, setting, we can do it all in one db session for one query.
|
1667
1050
|
@enforce_types
|
1668
1051
|
@trace_method
|
1669
|
-
def get_in_context_messages(self, agent_id: str, actor: PydanticUser) -> List[PydanticMessage]:
|
1670
|
-
|
1671
|
-
return self.message_manager.
|
1052
|
+
async def get_in_context_messages(self, agent_id: str, actor: PydanticUser) -> List[PydanticMessage]:
|
1053
|
+
agent_state = await self.get_agent_by_id_async(agent_id=agent_id, actor=actor)
|
1054
|
+
return await self.message_manager.get_messages_by_ids_async(message_ids=agent_state.message_ids, actor=actor)
|
1672
1055
|
|
1673
1056
|
@enforce_types
|
1674
1057
|
@trace_method
|
@@ -1924,10 +1307,8 @@ class AgentManager:
|
|
1924
1307
|
PydanticAgentState: The updated agent state with only the original system message preserved.
|
1925
1308
|
"""
|
1926
1309
|
async with db_registry.async_session() as session:
|
1927
|
-
# Retrieve the existing agent (will raise NoResultFound if invalid)
|
1928
1310
|
agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
|
1929
1311
|
|
1930
|
-
# Ensure agent has message_ids with at least one message
|
1931
1312
|
if not agent.message_ids or len(agent.message_ids) == 0:
|
1932
1313
|
logger.error(
|
1933
1314
|
f"Agent {agent_id} has no message_ids. Agent details: "
|
@@ -1936,13 +1317,12 @@ class AgentManager:
|
|
1936
1317
|
)
|
1937
1318
|
raise ValueError(f"Agent {agent_id} has no message_ids - cannot preserve system message")
|
1938
1319
|
|
1939
|
-
# Get the system message ID (first message)
|
1940
1320
|
system_message_id = agent.message_ids[0]
|
1941
1321
|
|
1942
|
-
|
1943
|
-
await self.message_manager.delete_all_messages_for_agent_async(agent_id=agent_id, actor=actor, exclude_ids=[system_message_id])
|
1322
|
+
await self.message_manager.delete_all_messages_for_agent_async(agent_id=agent_id, actor=actor, exclude_ids=[system_message_id])
|
1944
1323
|
|
1945
|
-
|
1324
|
+
async with db_registry.async_session() as session:
|
1325
|
+
agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
|
1946
1326
|
agent.message_ids = [system_message_id]
|
1947
1327
|
await agent.update_async(db_session=session, actor=actor)
|
1948
1328
|
agent_state = await agent.to_pydantic_async(include_relationships=["sources"])
|
@@ -1989,19 +1369,22 @@ class AgentManager:
|
|
1989
1369
|
)
|
1990
1370
|
if new_memory_str not in system_message.content[0].text:
|
1991
1371
|
# update the blocks (LRW) in the DB
|
1992
|
-
for label in
|
1993
|
-
|
1994
|
-
|
1995
|
-
|
1996
|
-
|
1997
|
-
|
1998
|
-
block_id=
|
1999
|
-
|
1372
|
+
for label in new_memory.list_block_labels():
|
1373
|
+
if label in agent_state.memory.list_block_labels():
|
1374
|
+
# Block exists in both old and new memory - check if value changed
|
1375
|
+
updated_value = new_memory.get_block(label).value
|
1376
|
+
if updated_value != agent_state.memory.get_block(label).value:
|
1377
|
+
# update the block if it's changed
|
1378
|
+
block_id = agent_state.memory.get_block(label).id
|
1379
|
+
await self.block_manager.update_block_async(
|
1380
|
+
block_id=block_id, block_update=BlockUpdate(value=updated_value), actor=actor
|
1381
|
+
)
|
2000
1382
|
|
2001
|
-
|
2002
|
-
|
2003
|
-
|
2004
|
-
)
|
1383
|
+
# Note: New blocks are already persisted in the creation methods,
|
1384
|
+
# so we don't need to handle them here
|
1385
|
+
|
1386
|
+
# refresh memory from DB (using block ids from the new memory)
|
1387
|
+
blocks = await self.block_manager.get_all_blocks_by_ids_async(block_ids=[b.id for b in new_memory.get_blocks()], actor=actor)
|
2005
1388
|
|
2006
1389
|
agent_state.memory = Memory(
|
2007
1390
|
blocks=blocks,
|
@@ -2166,16 +1549,28 @@ class AgentManager:
|
|
2166
1549
|
|
2167
1550
|
@enforce_types
|
2168
1551
|
@trace_method
|
2169
|
-
async def list_attached_sources_async(
|
1552
|
+
async def list_attached_sources_async(
|
1553
|
+
self,
|
1554
|
+
agent_id: str,
|
1555
|
+
actor: PydanticUser,
|
1556
|
+
before: Optional[str] = None,
|
1557
|
+
after: Optional[str] = None,
|
1558
|
+
limit: Optional[int] = None,
|
1559
|
+
ascending: bool = False,
|
1560
|
+
) -> List[PydanticSource]:
|
2170
1561
|
"""
|
2171
|
-
Lists all sources attached to an agent.
|
1562
|
+
Lists all sources attached to an agent with pagination.
|
2172
1563
|
|
2173
1564
|
Args:
|
2174
1565
|
agent_id: ID of the agent to list sources for
|
2175
1566
|
actor: User performing the action
|
1567
|
+
before: Source ID cursor for pagination. Returns sources that come before this source ID.
|
1568
|
+
after: Source ID cursor for pagination. Returns sources that come after this source ID.
|
1569
|
+
limit: Maximum number of sources to return.
|
1570
|
+
ascending: Sort order by creation time.
|
2176
1571
|
|
2177
1572
|
Returns:
|
2178
|
-
List[
|
1573
|
+
List[PydanticSource]: List of sources attached to the agent
|
2179
1574
|
|
2180
1575
|
Raises:
|
2181
1576
|
NoResultFound: If agent doesn't exist or user doesn't have access
|
@@ -2194,9 +1589,24 @@ class AgentManager:
|
|
2194
1589
|
SourceModel.organization_id == actor.organization_id,
|
2195
1590
|
SourceModel.is_deleted == False,
|
2196
1591
|
)
|
2197
|
-
.order_by(SourceModel.created_at.desc(), SourceModel.id)
|
2198
1592
|
)
|
2199
1593
|
|
1594
|
+
# Apply cursor-based pagination
|
1595
|
+
if before:
|
1596
|
+
query = query.where(SourceModel.id < before)
|
1597
|
+
if after:
|
1598
|
+
query = query.where(SourceModel.id > after)
|
1599
|
+
|
1600
|
+
# Apply sorting
|
1601
|
+
if ascending:
|
1602
|
+
query = query.order_by(SourceModel.created_at.asc(), SourceModel.id.asc())
|
1603
|
+
else:
|
1604
|
+
query = query.order_by(SourceModel.created_at.desc(), SourceModel.id.desc())
|
1605
|
+
|
1606
|
+
# Apply limit
|
1607
|
+
if limit:
|
1608
|
+
query = query.limit(limit)
|
1609
|
+
|
2200
1610
|
result = await session.execute(query)
|
2201
1611
|
sources = result.scalars().all()
|
2202
1612
|
|
@@ -2240,22 +1650,6 @@ class AgentManager:
|
|
2240
1650
|
# ======================================================================================================================
|
2241
1651
|
# Block management
|
2242
1652
|
# ======================================================================================================================
|
2243
|
-
@enforce_types
|
2244
|
-
@trace_method
|
2245
|
-
def get_block_with_label(
|
2246
|
-
self,
|
2247
|
-
agent_id: str,
|
2248
|
-
block_label: str,
|
2249
|
-
actor: PydanticUser,
|
2250
|
-
) -> PydanticBlock:
|
2251
|
-
"""Gets a block attached to an agent by its label."""
|
2252
|
-
with db_registry.session() as session:
|
2253
|
-
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
2254
|
-
for block in agent.core_memory:
|
2255
|
-
if block.label == block_label:
|
2256
|
-
return block.to_pydantic()
|
2257
|
-
raise NoResultFound(f"No block with label '{block_label}' found for agent '{agent_id}'")
|
2258
|
-
|
2259
1653
|
@enforce_types
|
2260
1654
|
@trace_method
|
2261
1655
|
async def get_block_with_label_async(
|
@@ -2294,67 +1688,15 @@ class AgentManager:
|
|
2294
1688
|
|
2295
1689
|
update_data = block_update.model_dump(to_orm=True, exclude_unset=True, exclude_none=True)
|
2296
1690
|
|
1691
|
+
# Validate limit constraints before updating
|
1692
|
+
validate_block_limit_constraint(update_data, block)
|
1693
|
+
|
2297
1694
|
for key, value in update_data.items():
|
2298
1695
|
setattr(block, key, value)
|
2299
1696
|
|
2300
1697
|
await block.update_async(session, actor=actor)
|
2301
1698
|
return block.to_pydantic()
|
2302
1699
|
|
2303
|
-
@enforce_types
|
2304
|
-
@trace_method
|
2305
|
-
def update_block_with_label(
|
2306
|
-
self,
|
2307
|
-
agent_id: str,
|
2308
|
-
block_label: str,
|
2309
|
-
new_block_id: str,
|
2310
|
-
actor: PydanticUser,
|
2311
|
-
) -> PydanticAgentState:
|
2312
|
-
"""Updates which block is assigned to a specific label for an agent."""
|
2313
|
-
with db_registry.session() as session:
|
2314
|
-
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
2315
|
-
new_block = BlockModel.read(db_session=session, identifier=new_block_id, actor=actor)
|
2316
|
-
|
2317
|
-
if new_block.label != block_label:
|
2318
|
-
raise ValueError(f"New block label '{new_block.label}' doesn't match required label '{block_label}'")
|
2319
|
-
|
2320
|
-
# Remove old block with this label if it exists
|
2321
|
-
agent.core_memory = [b for b in agent.core_memory if b.label != block_label]
|
2322
|
-
|
2323
|
-
# Add new block
|
2324
|
-
agent.core_memory.append(new_block)
|
2325
|
-
agent.update(session, actor=actor)
|
2326
|
-
return agent.to_pydantic()
|
2327
|
-
|
2328
|
-
@enforce_types
|
2329
|
-
@trace_method
|
2330
|
-
def attach_block(self, agent_id: str, block_id: str, actor: PydanticUser) -> PydanticAgentState:
|
2331
|
-
"""Attaches a block to an agent. For sleeptime agents, also attaches to paired agents in the same group."""
|
2332
|
-
with db_registry.session() as session:
|
2333
|
-
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
2334
|
-
block = BlockModel.read(db_session=session, identifier=block_id, actor=actor)
|
2335
|
-
|
2336
|
-
# Attach block to the main agent
|
2337
|
-
agent.core_memory.append(block)
|
2338
|
-
agent.update(session, actor=actor, no_commit=True)
|
2339
|
-
|
2340
|
-
# If agent is part of a sleeptime group, attach block to the sleeptime_agent
|
2341
|
-
if agent.multi_agent_group and agent.multi_agent_group.manager_type == ManagerType.sleeptime:
|
2342
|
-
group = agent.multi_agent_group
|
2343
|
-
# Find the sleeptime_agent in the group
|
2344
|
-
for other_agent_id in group.agent_ids or []:
|
2345
|
-
if other_agent_id != agent_id:
|
2346
|
-
try:
|
2347
|
-
other_agent = AgentModel.read(db_session=session, identifier=other_agent_id, actor=actor)
|
2348
|
-
if other_agent.agent_type == AgentType.sleeptime_agent and block not in other_agent.core_memory:
|
2349
|
-
other_agent.core_memory.append(block)
|
2350
|
-
other_agent.update(session, actor=actor, no_commit=True)
|
2351
|
-
except NoResultFound:
|
2352
|
-
# Agent might not exist anymore, skip
|
2353
|
-
continue
|
2354
|
-
session.commit()
|
2355
|
-
|
2356
|
-
return agent.to_pydantic()
|
2357
|
-
|
2358
1700
|
@enforce_types
|
2359
1701
|
@trace_method
|
2360
1702
|
async def attach_block_async(self, agent_id: str, block_id: str, actor: PydanticUser) -> PydanticAgentState:
|
@@ -2391,27 +1733,6 @@ class AgentManager:
|
|
2391
1733
|
|
2392
1734
|
return await agent.to_pydantic_async()
|
2393
1735
|
|
2394
|
-
@enforce_types
|
2395
|
-
@trace_method
|
2396
|
-
def detach_block(
|
2397
|
-
self,
|
2398
|
-
agent_id: str,
|
2399
|
-
block_id: str,
|
2400
|
-
actor: PydanticUser,
|
2401
|
-
) -> PydanticAgentState:
|
2402
|
-
"""Detaches a block from an agent."""
|
2403
|
-
with db_registry.session() as session:
|
2404
|
-
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
2405
|
-
original_length = len(agent.core_memory)
|
2406
|
-
|
2407
|
-
agent.core_memory = [b for b in agent.core_memory if b.id != block_id]
|
2408
|
-
|
2409
|
-
if len(agent.core_memory) == original_length:
|
2410
|
-
raise NoResultFound(f"No block with id '{block_id}' found for agent '{agent_id}' with actor id: '{actor.id}'")
|
2411
|
-
|
2412
|
-
agent.update(session, actor=actor)
|
2413
|
-
return agent.to_pydantic()
|
2414
|
-
|
2415
1736
|
@enforce_types
|
2416
1737
|
@trace_method
|
2417
1738
|
async def detach_block_async(
|
@@ -2433,27 +1754,6 @@ class AgentManager:
|
|
2433
1754
|
await agent.update_async(session, actor=actor)
|
2434
1755
|
return await agent.to_pydantic_async()
|
2435
1756
|
|
2436
|
-
@enforce_types
|
2437
|
-
@trace_method
|
2438
|
-
def detach_block_with_label(
|
2439
|
-
self,
|
2440
|
-
agent_id: str,
|
2441
|
-
block_label: str,
|
2442
|
-
actor: PydanticUser,
|
2443
|
-
) -> PydanticAgentState:
|
2444
|
-
"""Detaches a block with the specified label from an agent."""
|
2445
|
-
with db_registry.session() as session:
|
2446
|
-
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
2447
|
-
original_length = len(agent.core_memory)
|
2448
|
-
|
2449
|
-
agent.core_memory = [b for b in agent.core_memory if b.label != block_label]
|
2450
|
-
|
2451
|
-
if len(agent.core_memory) == original_length:
|
2452
|
-
raise NoResultFound(f"No block with label '{block_label}' found for agent '{agent_id}' with actor id: '{actor.id}'")
|
2453
|
-
|
2454
|
-
agent.update(session, actor=actor)
|
2455
|
-
return agent.to_pydantic()
|
2456
|
-
|
2457
1757
|
# ======================================================================================================================
|
2458
1758
|
# Passage Management
|
2459
1759
|
# ======================================================================================================================
|
@@ -2985,41 +2285,6 @@ class AgentManager:
|
|
2985
2285
|
# Tool Management
|
2986
2286
|
# ======================================================================================================================
|
2987
2287
|
@enforce_types
|
2988
|
-
@trace_method
|
2989
|
-
def attach_tool(self, agent_id: str, tool_id: str, actor: PydanticUser) -> PydanticAgentState:
|
2990
|
-
"""
|
2991
|
-
Attaches a tool to an agent.
|
2992
|
-
|
2993
|
-
Args:
|
2994
|
-
agent_id: ID of the agent to attach the tool to.
|
2995
|
-
tool_id: ID of the tool to attach.
|
2996
|
-
actor: User performing the action.
|
2997
|
-
|
2998
|
-
Raises:
|
2999
|
-
NoResultFound: If the agent or tool is not found.
|
3000
|
-
|
3001
|
-
Returns:
|
3002
|
-
PydanticAgentState: The updated agent state.
|
3003
|
-
"""
|
3004
|
-
with db_registry.session() as session:
|
3005
|
-
# Verify the agent exists and user has permission to access it
|
3006
|
-
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
3007
|
-
|
3008
|
-
# Use the _process_relationship helper to attach the tool
|
3009
|
-
_process_relationship(
|
3010
|
-
session=session,
|
3011
|
-
agent=agent,
|
3012
|
-
relationship_name="tools",
|
3013
|
-
model_class=ToolModel,
|
3014
|
-
item_ids=[tool_id],
|
3015
|
-
allow_partial=False, # Ensure the tool exists
|
3016
|
-
replace=False, # Extend the existing tools
|
3017
|
-
)
|
3018
|
-
|
3019
|
-
# Commit and refresh the agent
|
3020
|
-
agent.update(session, actor=actor)
|
3021
|
-
return agent.to_pydantic()
|
3022
|
-
|
3023
2288
|
@enforce_types
|
3024
2289
|
@trace_method
|
3025
2290
|
async def attach_tool_async(self, agent_id: str, tool_id: str, actor: PydanticUser) -> None:
|
@@ -3253,40 +2518,6 @@ class AgentManager:
|
|
3253
2518
|
|
3254
2519
|
return PydanticAgentState(**agent_state_dict)
|
3255
2520
|
|
3256
|
-
@enforce_types
|
3257
|
-
@trace_method
|
3258
|
-
def detach_tool(self, agent_id: str, tool_id: str, actor: PydanticUser) -> PydanticAgentState:
|
3259
|
-
"""
|
3260
|
-
Detaches a tool from an agent.
|
3261
|
-
|
3262
|
-
Args:
|
3263
|
-
agent_id: ID of the agent to detach the tool from.
|
3264
|
-
tool_id: ID of the tool to detach.
|
3265
|
-
actor: User performing the action.
|
3266
|
-
|
3267
|
-
Raises:
|
3268
|
-
NoResultFound: If the agent or tool is not found.
|
3269
|
-
|
3270
|
-
Returns:
|
3271
|
-
PydanticAgentState: The updated agent state.
|
3272
|
-
"""
|
3273
|
-
with db_registry.session() as session:
|
3274
|
-
# Verify the agent exists and user has permission to access it
|
3275
|
-
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
3276
|
-
|
3277
|
-
# Filter out the tool to be detached
|
3278
|
-
remaining_tools = [tool for tool in agent.tools if tool.id != tool_id]
|
3279
|
-
|
3280
|
-
if len(remaining_tools) == len(agent.tools): # Tool ID was not in the relationship
|
3281
|
-
logger.warning(f"Attempted to remove unattached tool id={tool_id} from agent id={agent_id} by actor={actor}")
|
3282
|
-
|
3283
|
-
# Update the tools relationship
|
3284
|
-
agent.tools = remaining_tools
|
3285
|
-
|
3286
|
-
# Commit and refresh the agent
|
3287
|
-
agent.update(session, actor=actor)
|
3288
|
-
return agent.to_pydantic()
|
3289
|
-
|
3290
2521
|
@enforce_types
|
3291
2522
|
@trace_method
|
3292
2523
|
async def detach_tool_async(self, agent_id: str, tool_id: str, actor: PydanticUser) -> None:
|
@@ -3381,24 +2612,15 @@ class AgentManager:
|
|
3381
2612
|
|
3382
2613
|
@enforce_types
|
3383
2614
|
@trace_method
|
3384
|
-
def
|
3385
|
-
|
3386
|
-
|
3387
|
-
|
3388
|
-
|
3389
|
-
|
3390
|
-
|
3391
|
-
|
3392
|
-
|
3393
|
-
List[PydanticTool]: List of tools attached to the agent.
|
3394
|
-
"""
|
3395
|
-
with db_registry.session() as session:
|
3396
|
-
agent = AgentModel.read(db_session=session, identifier=agent_id, actor=actor)
|
3397
|
-
return [tool.to_pydantic() for tool in agent.tools]
|
3398
|
-
|
3399
|
-
@enforce_types
|
3400
|
-
@trace_method
|
3401
|
-
async def list_attached_tools_async(self, agent_id: str, actor: PydanticUser) -> List[PydanticTool]:
|
2615
|
+
async def list_attached_tools_async(
|
2616
|
+
self,
|
2617
|
+
agent_id: str,
|
2618
|
+
actor: PydanticUser,
|
2619
|
+
before: Optional[str] = None,
|
2620
|
+
after: Optional[str] = None,
|
2621
|
+
limit: Optional[int] = None,
|
2622
|
+
ascending: bool = False,
|
2623
|
+
) -> List[PydanticTool]:
|
3402
2624
|
"""
|
3403
2625
|
List all tools attached to an agent (async version with optimized performance).
|
3404
2626
|
Uses direct SQL queries to avoid SqlAlchemyBase overhead.
|
@@ -3406,6 +2628,10 @@ class AgentManager:
|
|
3406
2628
|
Args:
|
3407
2629
|
agent_id: ID of the agent to list tools for.
|
3408
2630
|
actor: User performing the action.
|
2631
|
+
before: Tool ID cursor for pagination. Returns tools that come before this tool ID.
|
2632
|
+
after: Tool ID cursor for pagination. Returns tools that come after this tool ID.
|
2633
|
+
limit: Maximum number of tools to return.
|
2634
|
+
ascending: Sort order by creation time.
|
3409
2635
|
|
3410
2636
|
Returns:
|
3411
2637
|
List[PydanticTool]: List of tools attached to the agent.
|
@@ -3421,10 +2647,140 @@ class AgentManager:
|
|
3421
2647
|
.where(ToolsAgents.agent_id == agent_id, ToolModel.organization_id == actor.organization_id)
|
3422
2648
|
)
|
3423
2649
|
|
2650
|
+
# Apply cursor-based pagination
|
2651
|
+
if before:
|
2652
|
+
query = query.where(ToolModel.id < before)
|
2653
|
+
if after:
|
2654
|
+
query = query.where(ToolModel.id > after)
|
2655
|
+
|
2656
|
+
# Apply sorting
|
2657
|
+
if ascending:
|
2658
|
+
query = query.order_by(ToolModel.created_at.asc())
|
2659
|
+
else:
|
2660
|
+
query = query.order_by(ToolModel.created_at.desc())
|
2661
|
+
|
2662
|
+
# Apply limit
|
2663
|
+
if limit:
|
2664
|
+
query = query.limit(limit)
|
2665
|
+
|
3424
2666
|
result = await session.execute(query)
|
3425
2667
|
tools = result.scalars().all()
|
3426
2668
|
return [tool.to_pydantic() for tool in tools]
|
3427
2669
|
|
2670
|
+
@enforce_types
|
2671
|
+
@trace_method
|
2672
|
+
async def list_agent_blocks_async(
|
2673
|
+
self,
|
2674
|
+
agent_id: str,
|
2675
|
+
actor: PydanticUser,
|
2676
|
+
before: Optional[str] = None,
|
2677
|
+
after: Optional[str] = None,
|
2678
|
+
limit: Optional[int] = None,
|
2679
|
+
ascending: bool = False,
|
2680
|
+
) -> List[PydanticBlock]:
|
2681
|
+
"""
|
2682
|
+
List all blocks for a specific agent with pagination.
|
2683
|
+
|
2684
|
+
Args:
|
2685
|
+
agent_id: ID of the agent to find blocks for.
|
2686
|
+
actor: User performing the action.
|
2687
|
+
before: Block ID cursor for pagination. Returns blocks that come before this block ID.
|
2688
|
+
after: Block ID cursor for pagination. Returns blocks that come after this block ID.
|
2689
|
+
limit: Maximum number of blocks to return.
|
2690
|
+
ascending: Sort order by creation time.
|
2691
|
+
|
2692
|
+
Returns:
|
2693
|
+
List[PydanticBlock]: List of blocks for the agent.
|
2694
|
+
"""
|
2695
|
+
async with db_registry.async_session() as session:
|
2696
|
+
# First verify agent exists and user has access
|
2697
|
+
await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
|
2698
|
+
|
2699
|
+
# Build query to get blocks for this agent with pagination
|
2700
|
+
query = (
|
2701
|
+
select(BlockModel)
|
2702
|
+
.join(BlocksAgents, BlockModel.id == BlocksAgents.block_id)
|
2703
|
+
.where(BlocksAgents.agent_id == agent_id, BlockModel.organization_id == actor.organization_id)
|
2704
|
+
)
|
2705
|
+
|
2706
|
+
# Apply cursor-based pagination
|
2707
|
+
if before:
|
2708
|
+
query = query.where(BlockModel.id < before)
|
2709
|
+
if after:
|
2710
|
+
query = query.where(BlockModel.id > after)
|
2711
|
+
|
2712
|
+
# Apply sorting - use id instead of created_at for core memory blocks
|
2713
|
+
if ascending:
|
2714
|
+
query = query.order_by(BlockModel.id.asc())
|
2715
|
+
else:
|
2716
|
+
query = query.order_by(BlockModel.id.desc())
|
2717
|
+
|
2718
|
+
# Apply limit
|
2719
|
+
if limit:
|
2720
|
+
query = query.limit(limit)
|
2721
|
+
|
2722
|
+
result = await session.execute(query)
|
2723
|
+
blocks = result.scalars().all()
|
2724
|
+
|
2725
|
+
return [block.to_pydantic() for block in blocks]
|
2726
|
+
|
2727
|
+
@enforce_types
|
2728
|
+
@trace_method
|
2729
|
+
async def list_groups_async(
|
2730
|
+
self,
|
2731
|
+
agent_id: str,
|
2732
|
+
actor: PydanticUser,
|
2733
|
+
manager_type: Optional[str] = None,
|
2734
|
+
before: Optional[str] = None,
|
2735
|
+
after: Optional[str] = None,
|
2736
|
+
limit: Optional[int] = None,
|
2737
|
+
ascending: bool = False,
|
2738
|
+
) -> List[PydanticGroup]:
|
2739
|
+
"""
|
2740
|
+
List all groups that contain the specified agent.
|
2741
|
+
|
2742
|
+
Args:
|
2743
|
+
agent_id: ID of the agent to find groups for.
|
2744
|
+
actor: User performing the action.
|
2745
|
+
manager_type: Optional manager type to filter by.
|
2746
|
+
before: Group ID cursor for pagination. Returns groups that come before this group ID.
|
2747
|
+
after: Group ID cursor for pagination. Returns groups that come after this group ID.
|
2748
|
+
limit: Maximum number of groups to return.
|
2749
|
+
ascending: Sort order by creation time.
|
2750
|
+
|
2751
|
+
Returns:
|
2752
|
+
List[PydanticGroup]: List of groups containing the agent.
|
2753
|
+
"""
|
2754
|
+
async with db_registry.async_session() as session:
|
2755
|
+
query = (
|
2756
|
+
select(GroupModel)
|
2757
|
+
.join(GroupsAgents, GroupModel.id == GroupsAgents.group_id)
|
2758
|
+
.where(GroupsAgents.agent_id == agent_id, GroupModel.organization_id == actor.organization_id)
|
2759
|
+
)
|
2760
|
+
|
2761
|
+
if manager_type:
|
2762
|
+
query = query.where(GroupModel.manager_type == manager_type)
|
2763
|
+
|
2764
|
+
# Apply cursor-based pagination
|
2765
|
+
if before:
|
2766
|
+
query = query.where(GroupModel.id < before)
|
2767
|
+
if after:
|
2768
|
+
query = query.where(GroupModel.id > after)
|
2769
|
+
|
2770
|
+
# Apply sorting
|
2771
|
+
if ascending:
|
2772
|
+
query = query.order_by(GroupModel.created_at.asc())
|
2773
|
+
else:
|
2774
|
+
query = query.order_by(GroupModel.created_at.desc())
|
2775
|
+
|
2776
|
+
# Apply limit
|
2777
|
+
if limit:
|
2778
|
+
query = query.limit(limit)
|
2779
|
+
|
2780
|
+
result = await session.execute(query)
|
2781
|
+
groups = result.scalars().all()
|
2782
|
+
return [group.to_pydantic() for group in groups]
|
2783
|
+
|
3428
2784
|
# ======================================================================================================================
|
3429
2785
|
# File Management
|
3430
2786
|
# ======================================================================================================================
|
@@ -3505,45 +2861,6 @@ class AgentManager:
|
|
3505
2861
|
# ======================================================================================================================
|
3506
2862
|
# Tag Management
|
3507
2863
|
# ======================================================================================================================
|
3508
|
-
@enforce_types
|
3509
|
-
@trace_method
|
3510
|
-
def list_tags(
|
3511
|
-
self, actor: PydanticUser, after: Optional[str] = None, limit: Optional[int] = 50, query_text: Optional[str] = None
|
3512
|
-
) -> List[str]:
|
3513
|
-
"""
|
3514
|
-
Get all tags a user has created, ordered alphabetically.
|
3515
|
-
|
3516
|
-
Args:
|
3517
|
-
actor: User performing the action.
|
3518
|
-
after: Cursor for forward pagination.
|
3519
|
-
limit: Maximum number of tags to return.
|
3520
|
-
query_text: Query text to filter tags by.
|
3521
|
-
|
3522
|
-
Returns:
|
3523
|
-
List[str]: List of all tags.
|
3524
|
-
"""
|
3525
|
-
with db_registry.session() as session:
|
3526
|
-
query = (
|
3527
|
-
session.query(AgentsTags.tag)
|
3528
|
-
.join(AgentModel, AgentModel.id == AgentsTags.agent_id)
|
3529
|
-
.filter(AgentModel.organization_id == actor.organization_id)
|
3530
|
-
.distinct()
|
3531
|
-
)
|
3532
|
-
|
3533
|
-
if query_text:
|
3534
|
-
if settings.database_engine is DatabaseChoice.POSTGRES:
|
3535
|
-
# PostgreSQL: Use ILIKE for case-insensitive search
|
3536
|
-
query = query.filter(AgentsTags.tag.ilike(f"%{query_text}%"))
|
3537
|
-
else:
|
3538
|
-
# SQLite: Use LIKE with LOWER for case-insensitive search
|
3539
|
-
query = query.filter(func.lower(AgentsTags.tag).like(func.lower(f"%{query_text}%")))
|
3540
|
-
|
3541
|
-
if after:
|
3542
|
-
query = query.filter(AgentsTags.tag > after)
|
3543
|
-
|
3544
|
-
query = query.order_by(AgentsTags.tag).limit(limit)
|
3545
|
-
results = [tag[0] for tag in query.all()]
|
3546
|
-
return results
|
3547
2864
|
|
3548
2865
|
@enforce_types
|
3549
2866
|
@trace_method
|