letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.12.0.dev20251009104148__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- letta/__init__.py +1 -1
- letta/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 +927 -0
- letta/agents/voice_agent.py +2 -6
- letta/constants.py +8 -4
- letta/database_utils.py +161 -0
- 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 +127 -16
- letta/llm_api/bedrock_client.py +4 -2
- letta/llm_api/deepseek_client.py +4 -1
- letta/llm_api/google_vertex_client.py +124 -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 +2 -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/run_metrics.py +82 -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 +564 -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/run_metrics.py +21 -0
- 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 +79 -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 +149 -99
- 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/context_window_calculator/token_counter.py +1 -1
- 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 +69 -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 +364 -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/system.py +5 -1
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +3 -5
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +146 -135
- 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.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/licenses/LICENSE +0 -0
@@ -11,6 +11,7 @@ from letta.constants import (
|
|
11
11
|
from letta.helpers.json_helpers import json_dumps
|
12
12
|
from letta.log import get_logger
|
13
13
|
from letta.schemas.agent import AgentState
|
14
|
+
from letta.schemas.block import BlockUpdate
|
14
15
|
from letta.schemas.enums import MessageRole, TagMatchMode
|
15
16
|
from letta.schemas.sandbox_config import SandboxConfig
|
16
17
|
from letta.schemas.tool import Tool
|
@@ -46,8 +47,11 @@ class LettaCoreToolExecutor(ToolExecutor):
|
|
46
47
|
"core_memory_replace": self.core_memory_replace,
|
47
48
|
"memory_replace": self.memory_replace,
|
48
49
|
"memory_insert": self.memory_insert,
|
50
|
+
"memory_str_replace": self.memory_str_replace,
|
51
|
+
"memory_str_insert": self.memory_str_insert,
|
49
52
|
"memory_rethink": self.memory_rethink,
|
50
53
|
"memory_finish_edits": self.memory_finish_edits,
|
54
|
+
"memory": self.memory,
|
51
55
|
}
|
52
56
|
|
53
57
|
if function_name not in function_map:
|
@@ -479,8 +483,14 @@ class LettaCoreToolExecutor(ToolExecutor):
|
|
479
483
|
"are for display purposes only)."
|
480
484
|
)
|
481
485
|
|
482
|
-
|
483
|
-
agent_state.memory.
|
486
|
+
try:
|
487
|
+
agent_state.memory.get_block(label)
|
488
|
+
except KeyError:
|
489
|
+
# Block doesn't exist, create it
|
490
|
+
from letta.schemas.block import Block
|
491
|
+
|
492
|
+
new_block = Block(label=label, value=new_memory)
|
493
|
+
agent_state.memory.set_block(new_block)
|
484
494
|
|
485
495
|
agent_state.memory.update_block_value(label=label, value=new_memory)
|
486
496
|
|
@@ -502,3 +512,388 @@ class LettaCoreToolExecutor(ToolExecutor):
|
|
502
512
|
|
503
513
|
async def memory_finish_edits(self, agent_state: AgentState, actor: User) -> None:
|
504
514
|
return None
|
515
|
+
|
516
|
+
async def memory_delete(self, agent_state: AgentState, actor: User, path: str) -> str:
|
517
|
+
"""Delete a memory block by detaching it from the agent."""
|
518
|
+
# Extract memory block label from path
|
519
|
+
label = path.removeprefix("/memories/").replace("/", "_")
|
520
|
+
|
521
|
+
try:
|
522
|
+
# Check if memory block exists
|
523
|
+
memory_block = agent_state.memory.get_block(label)
|
524
|
+
if memory_block is None:
|
525
|
+
raise ValueError(f"Error: Memory block '{label}' does not exist")
|
526
|
+
|
527
|
+
# Detach the block from the agent
|
528
|
+
updated_agent_state = await self.agent_manager.detach_block_async(
|
529
|
+
agent_id=agent_state.id, block_id=memory_block.id, actor=actor
|
530
|
+
)
|
531
|
+
|
532
|
+
# Update the agent state with the updated memory from the database
|
533
|
+
agent_state.memory = updated_agent_state.memory
|
534
|
+
|
535
|
+
return f"Successfully deleted memory block '{label}'"
|
536
|
+
|
537
|
+
except Exception as e:
|
538
|
+
return f"Error performing delete: {str(e)}"
|
539
|
+
|
540
|
+
async def memory_update_description(self, agent_state: AgentState, actor: User, path: str, description: str) -> str:
|
541
|
+
"""Update the description of a memory block."""
|
542
|
+
label = path.removeprefix("/memories/").replace("/", "_")
|
543
|
+
|
544
|
+
try:
|
545
|
+
# Check if old memory block exists
|
546
|
+
memory_block = agent_state.memory.get_block(label)
|
547
|
+
if memory_block is None:
|
548
|
+
raise ValueError(f"Error: Memory block '{label}' does not exist")
|
549
|
+
|
550
|
+
await self.block_manager.update_block_async(
|
551
|
+
block_id=memory_block.id, block_update=BlockUpdate(description=description), actor=actor
|
552
|
+
)
|
553
|
+
await self.agent_manager.rebuild_system_prompt_async(agent_id=agent_state.id, actor=actor, force=True)
|
554
|
+
|
555
|
+
return f"Successfully updated description of memory block '{label}'"
|
556
|
+
|
557
|
+
except Exception as e:
|
558
|
+
raise Exception(f"Error performing update_description: {str(e)}")
|
559
|
+
|
560
|
+
async def memory_rename(self, agent_state: AgentState, actor: User, old_path: str, new_path: str) -> str:
|
561
|
+
"""Rename a memory block by copying content to new label and detaching old one."""
|
562
|
+
# Extract memory block labels from paths
|
563
|
+
old_label = old_path.removeprefix("/memories/").replace("/", "_")
|
564
|
+
new_label = new_path.removeprefix("/memories/").replace("/", "_")
|
565
|
+
|
566
|
+
try:
|
567
|
+
# Check if old memory block exists
|
568
|
+
memory_block = agent_state.memory.get_block(old_label)
|
569
|
+
if memory_block is None:
|
570
|
+
raise ValueError(f"Error: Memory block '{old_label}' does not exist")
|
571
|
+
|
572
|
+
await self.block_manager.update_block_async(block_id=memory_block.id, block_update=BlockUpdate(label=new_label), actor=actor)
|
573
|
+
await self.agent_manager.rebuild_system_prompt_async(agent_id=agent_state.id, actor=actor, force=True)
|
574
|
+
|
575
|
+
return f"Successfully renamed memory block '{old_label}' to '{new_label}'"
|
576
|
+
|
577
|
+
except Exception as e:
|
578
|
+
raise Exception(f"Error performing rename: {str(e)}")
|
579
|
+
|
580
|
+
async def memory_view(self, agent_state: AgentState, actor: User, path: str, view_range: Optional[int] = None) -> str:
|
581
|
+
"""View the content of a memory block with optional line range."""
|
582
|
+
try:
|
583
|
+
# Special case: if path is "/memories", list all blocks
|
584
|
+
if path == "/memories":
|
585
|
+
blocks = agent_state.memory.get_blocks()
|
586
|
+
|
587
|
+
if not blocks:
|
588
|
+
raise ValueError("No memory blocks found.")
|
589
|
+
|
590
|
+
result_lines = [f"Found {len(blocks)} memory block(s):\n"]
|
591
|
+
|
592
|
+
for i, block in enumerate(blocks, 1):
|
593
|
+
content = str(block.value)
|
594
|
+
content_length = len(content)
|
595
|
+
line_count = len(content.split("\n")) if content else 0
|
596
|
+
|
597
|
+
# Basic info
|
598
|
+
block_info = [f"{i}. {block.label}"]
|
599
|
+
|
600
|
+
# Add description if available
|
601
|
+
if block.description:
|
602
|
+
block_info.append(f" Description: {block.description}")
|
603
|
+
|
604
|
+
# Add read-only status
|
605
|
+
if block.read_only:
|
606
|
+
block_info.append(" Read-only: true")
|
607
|
+
|
608
|
+
# Add content stats
|
609
|
+
block_info.append(f" Character limit: {block.limit}")
|
610
|
+
block_info.append(f" Current length: {content_length} characters")
|
611
|
+
block_info.append(f" Lines: {line_count}")
|
612
|
+
|
613
|
+
# Add content preview (first 100 characters)
|
614
|
+
if content:
|
615
|
+
preview = content[:100].replace("\n", "\\n")
|
616
|
+
if len(content) > 100:
|
617
|
+
preview += "..."
|
618
|
+
block_info.append(f" Preview: {preview}")
|
619
|
+
else:
|
620
|
+
block_info.append(" Preview: (empty)")
|
621
|
+
|
622
|
+
result_lines.append("\n".join(block_info))
|
623
|
+
if i < len(blocks): # Add separator between blocks
|
624
|
+
result_lines.append("")
|
625
|
+
|
626
|
+
return "\n".join(result_lines)
|
627
|
+
|
628
|
+
# Extract memory block label from path (e.g., "/memories/preferences.txt" -> "preferences.txt")
|
629
|
+
if path.startswith("/memories/"):
|
630
|
+
label = path[10:] # Remove "/memories/" prefix
|
631
|
+
else:
|
632
|
+
label = path
|
633
|
+
|
634
|
+
# Get the memory block
|
635
|
+
memory_block = agent_state.memory.get_block(label)
|
636
|
+
if memory_block is None:
|
637
|
+
raise ValueError(f"Error: Memory block '{label}' does not exist")
|
638
|
+
|
639
|
+
# Get the content
|
640
|
+
content = str(memory_block.value)
|
641
|
+
if not content:
|
642
|
+
raise ValueError(f"Memory block '{label}' is empty")
|
643
|
+
|
644
|
+
# Split content into lines
|
645
|
+
lines = content.split("\n")
|
646
|
+
total_lines = len(lines)
|
647
|
+
|
648
|
+
# Handle view_range parameter
|
649
|
+
if view_range is not None:
|
650
|
+
if view_range <= 0:
|
651
|
+
raise ValueError(f"Error: view_range must be positive, got {view_range}")
|
652
|
+
|
653
|
+
# Show only the first view_range lines
|
654
|
+
lines_to_show = lines[:view_range]
|
655
|
+
range_info = f" (showing first {view_range} of {total_lines} lines)"
|
656
|
+
else:
|
657
|
+
lines_to_show = lines
|
658
|
+
range_info = f" ({total_lines} lines total)"
|
659
|
+
|
660
|
+
# Format output with line numbers
|
661
|
+
numbered_lines = []
|
662
|
+
for i, line in enumerate(lines_to_show, start=1):
|
663
|
+
numbered_lines.append(f"Line {i}: {line}")
|
664
|
+
|
665
|
+
numbered_content = "\n".join(numbered_lines)
|
666
|
+
|
667
|
+
# Add metadata information
|
668
|
+
metadata_info = []
|
669
|
+
if memory_block.description:
|
670
|
+
metadata_info.append(f"Description: {memory_block.description}")
|
671
|
+
if memory_block.read_only:
|
672
|
+
metadata_info.append("Read-only: true")
|
673
|
+
metadata_info.append(f"Character limit: {memory_block.limit}")
|
674
|
+
metadata_info.append(f"Current length: {len(content)} characters")
|
675
|
+
|
676
|
+
metadata_str = "\n".join(metadata_info)
|
677
|
+
|
678
|
+
result = f"Memory block: {label}{range_info}\n"
|
679
|
+
result += f"Metadata:\n{metadata_str}\n\n"
|
680
|
+
result += f"Content:\n{numbered_content}"
|
681
|
+
|
682
|
+
return result
|
683
|
+
|
684
|
+
except KeyError:
|
685
|
+
raise ValueError(f"Error: Memory block '{label}' does not exist")
|
686
|
+
except Exception as e:
|
687
|
+
raise Exception(f"Error viewing memory block: {str(e)}")
|
688
|
+
|
689
|
+
async def memory_create(
|
690
|
+
self, agent_state: AgentState, actor: User, path: str, description: str, file_text: Optional[str] = None
|
691
|
+
) -> str:
|
692
|
+
"""Create a memory block by setting its value to an empty string."""
|
693
|
+
from letta.schemas.block import Block
|
694
|
+
|
695
|
+
label = path.removeprefix("/memories/").replace("/", "_")
|
696
|
+
|
697
|
+
# Create a new block and persist it to the database
|
698
|
+
new_block = Block(label=label, value=file_text if file_text else "", description=description)
|
699
|
+
persisted_block = await self.block_manager.create_or_update_block_async(new_block, actor)
|
700
|
+
|
701
|
+
# Attach the block to the agent
|
702
|
+
await self.agent_manager.attach_block_async(agent_id=agent_state.id, block_id=persisted_block.id, actor=actor)
|
703
|
+
|
704
|
+
# Add the persisted block to memory
|
705
|
+
agent_state.memory.set_block(persisted_block)
|
706
|
+
|
707
|
+
await self.agent_manager.update_memory_if_changed_async(agent_id=agent_state.id, new_memory=agent_state.memory, actor=actor)
|
708
|
+
return f"Successfully created memory block '{label}'"
|
709
|
+
|
710
|
+
async def memory_str_replace(self, agent_state: AgentState, actor: User, path: str, old_str: str, new_str: str) -> str:
|
711
|
+
"""Replace text in a memory block."""
|
712
|
+
label = path.removeprefix("/memories/").replace("/", "_")
|
713
|
+
|
714
|
+
memory_block = agent_state.memory.get_block(label)
|
715
|
+
if memory_block is None:
|
716
|
+
raise ValueError(f"Error: Memory block '{label}' does not exist")
|
717
|
+
|
718
|
+
if memory_block.read_only:
|
719
|
+
raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
|
720
|
+
|
721
|
+
if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(old_str)):
|
722
|
+
raise ValueError(
|
723
|
+
"old_str contains a line number prefix, which is not allowed. "
|
724
|
+
"Do not include line numbers when calling memory tools (line "
|
725
|
+
"numbers are for display purposes only)."
|
726
|
+
)
|
727
|
+
if CORE_MEMORY_LINE_NUMBER_WARNING in old_str:
|
728
|
+
raise ValueError(
|
729
|
+
"old_str contains a line number warning, which is not allowed. "
|
730
|
+
"Do not include line number information when calling memory tools "
|
731
|
+
"(line numbers are for display purposes only)."
|
732
|
+
)
|
733
|
+
if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(new_str)):
|
734
|
+
raise ValueError(
|
735
|
+
"new_str contains a line number prefix, which is not allowed. "
|
736
|
+
"Do not include line numbers when calling memory tools (line "
|
737
|
+
"numbers are for display purposes only)."
|
738
|
+
)
|
739
|
+
|
740
|
+
old_str = str(old_str).expandtabs()
|
741
|
+
new_str = str(new_str).expandtabs()
|
742
|
+
current_value = str(memory_block.value).expandtabs()
|
743
|
+
|
744
|
+
# Check if old_str is unique in the block
|
745
|
+
occurences = current_value.count(old_str)
|
746
|
+
if occurences == 0:
|
747
|
+
raise ValueError(
|
748
|
+
f"No replacement was performed, old_str `{old_str}` did not appear verbatim in memory block with label `{label}`."
|
749
|
+
)
|
750
|
+
elif occurences > 1:
|
751
|
+
content_value_lines = current_value.split("\n")
|
752
|
+
lines = [idx + 1 for idx, line in enumerate(content_value_lines) if old_str in line]
|
753
|
+
raise ValueError(
|
754
|
+
f"No replacement was performed. Multiple occurrences of old_str `{old_str}` in lines {lines}. Please ensure it is unique."
|
755
|
+
)
|
756
|
+
|
757
|
+
# Replace old_str with new_str
|
758
|
+
new_value = current_value.replace(str(old_str), str(new_str))
|
759
|
+
|
760
|
+
# Write the new content to the block
|
761
|
+
await self.block_manager.update_block_async(block_id=memory_block.id, block_update=BlockUpdate(value=new_value), actor=actor)
|
762
|
+
await self.agent_manager.rebuild_system_prompt_async(agent_id=agent_state.id, actor=actor, force=True)
|
763
|
+
|
764
|
+
# Prepare the success message
|
765
|
+
success_msg = f"The core memory block with label `{label}` has been edited. "
|
766
|
+
success_msg += (
|
767
|
+
"Review the changes and make sure they are as expected (correct indentation, "
|
768
|
+
"no duplicate lines, etc). Edit the memory block again if necessary."
|
769
|
+
)
|
770
|
+
|
771
|
+
return success_msg
|
772
|
+
|
773
|
+
async def memory_str_insert(self, agent_state: AgentState, actor: User, path: str, insert_text: str, insert_line: int = -1) -> str:
|
774
|
+
"""Insert text into a memory block at a specific line."""
|
775
|
+
label = path.removeprefix("/memories/").replace("/", "_")
|
776
|
+
|
777
|
+
memory_block = agent_state.memory.get_block(label)
|
778
|
+
if memory_block is None:
|
779
|
+
raise ValueError(f"Error: Memory block '{label}' does not exist")
|
780
|
+
|
781
|
+
if memory_block.read_only:
|
782
|
+
raise ValueError(f"{READ_ONLY_BLOCK_EDIT_ERROR}")
|
783
|
+
|
784
|
+
if bool(MEMORY_TOOLS_LINE_NUMBER_PREFIX_REGEX.search(insert_text)):
|
785
|
+
raise ValueError(
|
786
|
+
"insert_text contains a line number prefix, which is not allowed. "
|
787
|
+
"Do not include line numbers when calling memory tools (line "
|
788
|
+
"numbers are for display purposes only)."
|
789
|
+
)
|
790
|
+
if CORE_MEMORY_LINE_NUMBER_WARNING in insert_text:
|
791
|
+
raise ValueError(
|
792
|
+
"insert_text contains a line number warning, which is not allowed. "
|
793
|
+
"Do not include line number information when calling memory tools "
|
794
|
+
"(line numbers are for display purposes only)."
|
795
|
+
)
|
796
|
+
|
797
|
+
current_value = str(memory_block.value).expandtabs()
|
798
|
+
insert_text = str(insert_text).expandtabs()
|
799
|
+
current_value_lines = current_value.split("\n")
|
800
|
+
n_lines = len(current_value_lines)
|
801
|
+
|
802
|
+
# Check if we're in range, from 0 (pre-line), to 1 (first line), to n_lines (last line)
|
803
|
+
if insert_line == -1:
|
804
|
+
insert_line = n_lines
|
805
|
+
elif insert_line < 0 or insert_line > n_lines:
|
806
|
+
raise ValueError(
|
807
|
+
f"Invalid `insert_line` parameter: {insert_line}. It should be within "
|
808
|
+
f"the range of lines of the memory block: {[0, n_lines]}, or -1 to "
|
809
|
+
f"append to the end of the memory block."
|
810
|
+
)
|
811
|
+
|
812
|
+
# Insert the new text as a line
|
813
|
+
SNIPPET_LINES = 3
|
814
|
+
insert_text_lines = insert_text.split("\n")
|
815
|
+
new_value_lines = current_value_lines[:insert_line] + insert_text_lines + current_value_lines[insert_line:]
|
816
|
+
snippet_lines = (
|
817
|
+
current_value_lines[max(0, insert_line - SNIPPET_LINES) : insert_line]
|
818
|
+
+ insert_text_lines
|
819
|
+
+ current_value_lines[insert_line : insert_line + SNIPPET_LINES]
|
820
|
+
)
|
821
|
+
|
822
|
+
# Collate into the new value to update
|
823
|
+
new_value = "\n".join(new_value_lines)
|
824
|
+
snippet = "\n".join(snippet_lines)
|
825
|
+
|
826
|
+
# Write into the block
|
827
|
+
await self.block_manager.update_block_async(block_id=memory_block.id, block_update=BlockUpdate(value=new_value), actor=actor)
|
828
|
+
await self.agent_manager.rebuild_system_prompt_async(agent_id=agent_state.id, actor=actor, force=True)
|
829
|
+
|
830
|
+
# Prepare the success message
|
831
|
+
success_msg = f"The core memory block with label `{label}` has been edited. "
|
832
|
+
success_msg += (
|
833
|
+
"Review the changes and make sure they are as expected (correct indentation, "
|
834
|
+
"no duplicate lines, etc). Edit the memory block again if necessary."
|
835
|
+
)
|
836
|
+
|
837
|
+
return success_msg
|
838
|
+
|
839
|
+
async def memory(
|
840
|
+
self,
|
841
|
+
agent_state: AgentState,
|
842
|
+
actor: User,
|
843
|
+
command: str,
|
844
|
+
file_text: Optional[str] = None,
|
845
|
+
description: Optional[str] = None,
|
846
|
+
path: Optional[str] = None,
|
847
|
+
old_str: Optional[str] = None,
|
848
|
+
new_str: Optional[str] = None,
|
849
|
+
insert_line: Optional[int] = None,
|
850
|
+
insert_text: Optional[str] = None,
|
851
|
+
old_path: Optional[str] = None,
|
852
|
+
new_path: Optional[str] = None,
|
853
|
+
view_range: Optional[int] = None,
|
854
|
+
) -> Optional[str]:
|
855
|
+
if command == "view":
|
856
|
+
if path is None:
|
857
|
+
raise ValueError("Error: path is required for view command")
|
858
|
+
return await self.memory_view(agent_state, actor, path, view_range)
|
859
|
+
|
860
|
+
elif command == "create":
|
861
|
+
if path is None:
|
862
|
+
raise ValueError("Error: path is required for create command")
|
863
|
+
if description is None:
|
864
|
+
raise ValueError("Error: description is required for create command")
|
865
|
+
return await self.memory_create(agent_state, actor, path, description, file_text)
|
866
|
+
|
867
|
+
elif command == "str_replace":
|
868
|
+
if path is None:
|
869
|
+
raise ValueError("Error: path is required for str_replace command")
|
870
|
+
if old_str is None:
|
871
|
+
raise ValueError("Error: old_str is required for str_replace command")
|
872
|
+
if new_str is None:
|
873
|
+
raise ValueError("Error: new_str is required for str_replace command")
|
874
|
+
return await self.memory_str_replace(agent_state, actor, path, old_str, new_str)
|
875
|
+
|
876
|
+
elif command == "insert":
|
877
|
+
if path is None:
|
878
|
+
raise ValueError("Error: path is required for insert command")
|
879
|
+
if insert_text is None:
|
880
|
+
raise ValueError("Error: insert_text is required for insert command")
|
881
|
+
return await self.memory_str_insert(agent_state, actor, path, insert_text, insert_line)
|
882
|
+
|
883
|
+
elif command == "delete":
|
884
|
+
if path is None:
|
885
|
+
raise ValueError("Error: path is required for delete command")
|
886
|
+
return await self.memory_delete(agent_state, actor, path)
|
887
|
+
|
888
|
+
elif command == "rename":
|
889
|
+
if path and description:
|
890
|
+
return await self.memory_update_description(agent_state, actor, path, description)
|
891
|
+
elif old_path and new_path:
|
892
|
+
return await self.memory_rename(agent_state, actor, old_path, new_path)
|
893
|
+
else:
|
894
|
+
raise ValueError(
|
895
|
+
"Error: path and description are required for update_description command, or old_path and new_path are required for rename command"
|
896
|
+
)
|
897
|
+
|
898
|
+
else:
|
899
|
+
raise ValueError(f"Error: Unknown command '{command}'. Supported commands: str_replace, str_insert, insert, delete, rename")
|
@@ -20,9 +20,9 @@ from letta.services.block_manager import BlockManager
|
|
20
20
|
from letta.services.file_manager import FileManager
|
21
21
|
from letta.services.file_processor.chunker.line_chunker import LineChunker
|
22
22
|
from letta.services.files_agents_manager import FileAgentManager
|
23
|
-
from letta.services.job_manager import JobManager
|
24
23
|
from letta.services.message_manager import MessageManager
|
25
24
|
from letta.services.passage_manager import PassageManager
|
25
|
+
from letta.services.run_manager import RunManager
|
26
26
|
from letta.services.source_manager import SourceManager
|
27
27
|
from letta.services.tool_executor.tool_executor_base import ToolExecutor
|
28
28
|
from letta.utils import get_friendly_error_msg
|
@@ -47,7 +47,7 @@ class LettaFileToolExecutor(ToolExecutor):
|
|
47
47
|
message_manager: MessageManager,
|
48
48
|
agent_manager: AgentManager,
|
49
49
|
block_manager: BlockManager,
|
50
|
-
|
50
|
+
run_manager: RunManager,
|
51
51
|
passage_manager: PassageManager,
|
52
52
|
actor: User,
|
53
53
|
):
|
@@ -55,7 +55,7 @@ class LettaFileToolExecutor(ToolExecutor):
|
|
55
55
|
message_manager=message_manager,
|
56
56
|
agent_manager=agent_manager,
|
57
57
|
block_manager=block_manager,
|
58
|
-
|
58
|
+
run_manager=run_manager,
|
59
59
|
passage_manager=passage_manager,
|
60
60
|
actor=actor,
|
61
61
|
)
|
@@ -7,10 +7,12 @@ from letta.schemas.enums import MessageRole
|
|
7
7
|
from letta.schemas.letta_message import AssistantMessage
|
8
8
|
from letta.schemas.letta_message_content import TextContent
|
9
9
|
from letta.schemas.message import MessageCreate
|
10
|
+
from letta.schemas.run import Run
|
10
11
|
from letta.schemas.sandbox_config import SandboxConfig
|
11
12
|
from letta.schemas.tool import Tool
|
12
13
|
from letta.schemas.tool_execution_result import ToolExecutionResult
|
13
14
|
from letta.schemas.user import User
|
15
|
+
from letta.services.run_manager import RunManager
|
14
16
|
from letta.services.tool_executor.tool_executor_base import ToolExecutor
|
15
17
|
from letta.settings import settings
|
16
18
|
from letta.utils import safe_create_task
|
@@ -43,13 +45,15 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
|
|
43
45
|
|
44
46
|
# Execute the appropriate function
|
45
47
|
function_args_copy = function_args.copy() # Make a copy to avoid modifying the original
|
46
|
-
function_response = await function_map[function_name](agent_state, **function_args_copy)
|
48
|
+
function_response = await function_map[function_name](agent_state, actor, **function_args_copy)
|
47
49
|
return ToolExecutionResult(
|
48
50
|
status="success",
|
49
51
|
func_return=function_response,
|
50
52
|
)
|
51
53
|
|
52
|
-
async def send_message_to_agent_and_wait_for_reply(
|
54
|
+
async def send_message_to_agent_and_wait_for_reply(
|
55
|
+
self, agent_state: AgentState, actor: User, message: str, other_agent_id: str
|
56
|
+
) -> str:
|
53
57
|
augmented_message = (
|
54
58
|
f"[Incoming message from agent with ID '{agent_state.id}' - to reply to this message, "
|
55
59
|
f"make sure to use the 'send_message' at the end, and the system will notify the sender of your response] "
|
@@ -57,10 +61,10 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
|
|
57
61
|
)
|
58
62
|
|
59
63
|
other_agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=other_agent_id, actor=self.actor)
|
60
|
-
return str(await self._process_agent(agent_state=other_agent_state, message=augmented_message))
|
64
|
+
return str(await self._process_agent(agent_state=other_agent_state, message=augmented_message, actor=actor))
|
61
65
|
|
62
66
|
async def send_message_to_agents_matching_tags_async(
|
63
|
-
self, agent_state: AgentState, message: str, match_all: List[str], match_some: List[str]
|
67
|
+
self, agent_state: AgentState, actor: User, message: str, match_all: List[str], match_some: List[str]
|
64
68
|
) -> str:
|
65
69
|
# Find matching agents
|
66
70
|
matching_agents = await self.agent_manager.list_agents_matching_tags_async(
|
@@ -76,25 +80,36 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
|
|
76
80
|
f"{message}"
|
77
81
|
)
|
78
82
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
for agent_state in matching_agents
|
84
|
-
]
|
85
|
-
results = await asyncio.gather(*tasks)
|
83
|
+
# Run concurrent requests and collect their return values.
|
84
|
+
# Note: Do not wrap with safe_create_task here — it swallows return values (returns None).
|
85
|
+
coros = [self._process_agent(agent_state=a_state, message=augmented_message, actor=actor) for a_state in matching_agents]
|
86
|
+
results = await asyncio.gather(*coros)
|
86
87
|
return str(results)
|
87
88
|
|
88
|
-
async def _process_agent(self, agent_state: AgentState, message: str) -> Dict[str, Any]:
|
89
|
+
async def _process_agent(self, agent_state: AgentState, message: str, actor: User) -> Dict[str, Any]:
|
89
90
|
from letta.agents.letta_agent_v2 import LettaAgentV2
|
90
91
|
|
91
92
|
try:
|
93
|
+
runs_manager = RunManager()
|
94
|
+
run = await runs_manager.create_run(
|
95
|
+
pydantic_run=Run(
|
96
|
+
agent_id=agent_state.id,
|
97
|
+
background=False,
|
98
|
+
metadata={
|
99
|
+
"run_type": "agent_send_message_to_agent", # TODO: Make this a constant
|
100
|
+
},
|
101
|
+
),
|
102
|
+
actor=actor,
|
103
|
+
)
|
104
|
+
|
92
105
|
letta_agent = LettaAgentV2(
|
93
106
|
agent_state=agent_state,
|
94
107
|
actor=self.actor,
|
95
108
|
)
|
96
109
|
|
97
|
-
letta_response = await letta_agent.step(
|
110
|
+
letta_response = await letta_agent.step(
|
111
|
+
[MessageCreate(role=MessageRole.system, content=[TextContent(text=message)])], run_id=run.id
|
112
|
+
)
|
98
113
|
messages = letta_response.messages
|
99
114
|
|
100
115
|
send_message_content = [message.content for message in messages if isinstance(message, AssistantMessage)]
|
@@ -111,7 +126,7 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
|
|
111
126
|
"type": type(e).__name__,
|
112
127
|
}
|
113
128
|
|
114
|
-
async def send_message_to_agent_async(self, agent_state: AgentState, message: str, other_agent_id: str) -> str:
|
129
|
+
async def send_message_to_agent_async(self, agent_state: AgentState, actor: User, message: str, other_agent_id: str) -> str:
|
115
130
|
if settings.environment == "PRODUCTION":
|
116
131
|
raise RuntimeError("This tool is not allowed to be run on Letta Cloud.")
|
117
132
|
|
@@ -125,7 +140,7 @@ class LettaMultiAgentToolExecutor(ToolExecutor):
|
|
125
140
|
|
126
141
|
other_agent_state = await self.agent_manager.get_agent_by_id_async(agent_id=other_agent_id, actor=self.actor)
|
127
142
|
task = safe_create_task(
|
128
|
-
self._process_agent(agent_state=other_agent_state, message=prefixed), label=f"send_message_to_{other_agent_id}"
|
143
|
+
self._process_agent(agent_state=other_agent_state, message=prefixed, actor=actor), label=f"send_message_to_{other_agent_id}"
|
129
144
|
)
|
130
145
|
|
131
146
|
task.add_done_callback(lambda t: (logger.error(f"Async send_message task failed: {t.exception()}") if t.exception() else None))
|
@@ -16,11 +16,10 @@ from letta.schemas.tool_execution_result import ToolExecutionResult
|
|
16
16
|
from letta.schemas.user import User
|
17
17
|
from letta.services.agent_manager import AgentManager
|
18
18
|
from letta.services.block_manager import BlockManager
|
19
|
-
from letta.services.job_manager import JobManager
|
20
19
|
from letta.services.message_manager import MessageManager
|
21
20
|
from letta.services.passage_manager import PassageManager
|
21
|
+
from letta.services.run_manager import RunManager
|
22
22
|
from letta.services.tool_executor.builtin_tool_executor import LettaBuiltinToolExecutor
|
23
|
-
from letta.services.tool_executor.composio_tool_executor import ExternalComposioToolExecutor
|
24
23
|
from letta.services.tool_executor.core_tool_executor import LettaCoreToolExecutor
|
25
24
|
from letta.services.tool_executor.files_tool_executor import LettaFileToolExecutor
|
26
25
|
from letta.services.tool_executor.mcp_tool_executor import ExternalMCPToolExecutor
|
@@ -40,7 +39,6 @@ class ToolExecutorFactory:
|
|
40
39
|
ToolType.LETTA_MULTI_AGENT_CORE: LettaMultiAgentToolExecutor,
|
41
40
|
ToolType.LETTA_BUILTIN: LettaBuiltinToolExecutor,
|
42
41
|
ToolType.LETTA_FILES_CORE: LettaFileToolExecutor,
|
43
|
-
ToolType.EXTERNAL_COMPOSIO: ExternalComposioToolExecutor,
|
44
42
|
ToolType.EXTERNAL_MCP: ExternalMCPToolExecutor,
|
45
43
|
}
|
46
44
|
|
@@ -51,7 +49,7 @@ class ToolExecutorFactory:
|
|
51
49
|
message_manager: MessageManager,
|
52
50
|
agent_manager: AgentManager,
|
53
51
|
block_manager: BlockManager,
|
54
|
-
|
52
|
+
run_manager: RunManager,
|
55
53
|
passage_manager: PassageManager,
|
56
54
|
actor: User,
|
57
55
|
) -> ToolExecutor:
|
@@ -61,7 +59,7 @@ class ToolExecutorFactory:
|
|
61
59
|
message_manager=message_manager,
|
62
60
|
agent_manager=agent_manager,
|
63
61
|
block_manager=block_manager,
|
64
|
-
|
62
|
+
run_manager=run_manager,
|
65
63
|
passage_manager=passage_manager,
|
66
64
|
actor=actor,
|
67
65
|
)
|
@@ -75,7 +73,7 @@ class ToolExecutionManager:
|
|
75
73
|
message_manager: MessageManager,
|
76
74
|
agent_manager: AgentManager,
|
77
75
|
block_manager: BlockManager,
|
78
|
-
|
76
|
+
run_manager: RunManager,
|
79
77
|
passage_manager: PassageManager,
|
80
78
|
actor: User,
|
81
79
|
agent_state: Optional[AgentState] = None,
|
@@ -85,7 +83,7 @@ class ToolExecutionManager:
|
|
85
83
|
self.message_manager = message_manager
|
86
84
|
self.agent_manager = agent_manager
|
87
85
|
self.block_manager = block_manager
|
88
|
-
self.
|
86
|
+
self.run_manager = run_manager
|
89
87
|
self.passage_manager = passage_manager
|
90
88
|
self.agent_state = agent_state
|
91
89
|
self.logger = get_logger(__name__)
|
@@ -107,7 +105,7 @@ class ToolExecutionManager:
|
|
107
105
|
message_manager=self.message_manager,
|
108
106
|
agent_manager=self.agent_manager,
|
109
107
|
block_manager=self.block_manager,
|
110
|
-
|
108
|
+
run_manager=self.run_manager,
|
111
109
|
passage_manager=self.passage_manager,
|
112
110
|
actor=self.actor,
|
113
111
|
)
|
@@ -8,9 +8,9 @@ from letta.schemas.tool_execution_result import ToolExecutionResult
|
|
8
8
|
from letta.schemas.user import User
|
9
9
|
from letta.services.agent_manager import AgentManager
|
10
10
|
from letta.services.block_manager import BlockManager
|
11
|
-
from letta.services.job_manager import JobManager
|
12
11
|
from letta.services.message_manager import MessageManager
|
13
12
|
from letta.services.passage_manager import PassageManager
|
13
|
+
from letta.services.run_manager import RunManager
|
14
14
|
|
15
15
|
|
16
16
|
class ToolExecutor(ABC):
|
@@ -21,14 +21,14 @@ class ToolExecutor(ABC):
|
|
21
21
|
message_manager: MessageManager,
|
22
22
|
agent_manager: AgentManager,
|
23
23
|
block_manager: BlockManager,
|
24
|
-
|
24
|
+
run_manager: RunManager,
|
25
25
|
passage_manager: PassageManager,
|
26
26
|
actor: User,
|
27
27
|
):
|
28
28
|
self.message_manager = message_manager
|
29
29
|
self.agent_manager = agent_manager
|
30
30
|
self.block_manager = block_manager
|
31
|
-
self.
|
31
|
+
self.run_manager = run_manager
|
32
32
|
self.passage_manager = passage_manager
|
33
33
|
self.actor = actor
|
34
34
|
|