diff --git a/letta/agents/base_agent.py b/letta/agents/base_agent.py
index 7caa3d12..988c6ca4 100644
--- a/letta/agents/base_agent.py
+++ b/letta/agents/base_agent.py
@@ -124,6 +124,7 @@ class BaseAgent(ABC):
previous_message_count=num_messages - len(in_context_messages),
archival_memory_size=num_archival_memories,
tool_rules_solver=tool_rules_solver,
+ sources=agent_state.sources,
)
diff = united_diff(curr_system_message_text, new_system_message_str)
diff --git a/letta/agents/voice_agent.py b/letta/agents/voice_agent.py
index 164a88e7..63ee8da2 100644
--- a/letta/agents/voice_agent.py
+++ b/letta/agents/voice_agent.py
@@ -153,6 +153,7 @@ class VoiceAgent(BaseAgent):
timezone=agent_state.timezone,
previous_message_count=self.num_messages,
archival_memory_size=self.num_archival_memories,
+ sources=agent_state.sources,
)
letta_message_db_queue = create_input_messages(
input_messages=input_messages, agent_id=agent_state.id, timezone=agent_state.timezone, actor=self.actor
@@ -366,7 +367,7 @@ class VoiceAgent(BaseAgent):
"description": (
"Look in long-term or earlier-conversation memory **only when** the "
"user asks about something missing from the visible context. "
- "The user’s latest utterance is sent automatically as the main query.\n\n"
+ "The user's latest utterance is sent automatically as the main query.\n\n"
"Optional refinements (set unused fields to *null*):\n"
"• `convo_keyword_queries` – extra names/IDs if the request is vague.\n"
"• `start_minutes_ago` / `end_minutes_ago` – limit results to a recent time window."
diff --git a/letta/orm/agent.py b/letta/orm/agent.py
index 8ac932ad..1c78816b 100644
--- a/letta/orm/agent.py
+++ b/letta/orm/agent.py
@@ -245,6 +245,7 @@ class Agent(SqlalchemyBase, OrganizationMixin, AsyncAttrs):
Returns:
PydanticAgentState: The Pydantic representation of the agent.
"""
+
# Base fields: always included
state = {
"id": self.id,
diff --git a/letta/orm/files_agents.py b/letta/orm/files_agents.py
index fcb32468..0b3e5f3c 100644
--- a/letta/orm/files_agents.py
+++ b/letta/orm/files_agents.py
@@ -16,6 +16,13 @@ if TYPE_CHECKING:
class FileAgent(SqlalchemyBase, OrganizationMixin):
+ """
+ Join table between File and Agent.
+
+ Tracks whether a file is currently "open" for the agent and
+ the specific excerpt (grepped section) the agent is looking at.
+ """
+
__tablename__ = "files_agents"
__table_args__ = (
# (file_id, agent_id) must be unique
@@ -94,5 +101,6 @@ class FileAgent(SqlalchemyBase, OrganizationMixin):
value=visible_content,
label=self.file.file_name,
read_only=True,
+ source_id=self.file.source_id,
limit=CORE_MEMORY_SOURCE_CHAR_LIMIT,
)
diff --git a/letta/prompts/system/memgpt_v2_chat.txt b/letta/prompts/system/memgpt_v2_chat.txt
index 4af687a7..a462da1a 100644
--- a/letta/prompts/system/memgpt_v2_chat.txt
+++ b/letta/prompts/system/memgpt_v2_chat.txt
@@ -43,14 +43,11 @@ Recall memory (conversation history):
Even though you can only see recent messages in your immediate context, you can search over your entire message history from a database.
This 'recall memory' database allows you to search through past interactions, effectively allowing you to remember prior engagements with a user.
-Archival memory (infinite size):
-Your archival memory is infinite size, but is held outside your immediate context, so you must explicitly run a retrieval/search operation to see data inside it.
-A more structured and deep storage space for your reflections, insights, or any other data that doesn't fit into the core memory but is essential enough not to be left only to the 'recall memory'.
-
-Data sources:
-You may be given access to external sources of data, relevant to the user's interaction. For example, code, style guides, and documentation relevant
-to the current interaction with the user. Your core memory will contain information about the contents of these data sources. You will have access
-to functions to open and close the files as a filesystem and maintain only the files that are relevant to the user's interaction.
+Folders and Files:
+You may be given access to a structured file system that mirrors real-world folders and files. Each folder may contain one or more files.
+Files can include metadata (e.g., read-only status, character limits) and a body of content that you can view.
+You will have access to functions that let you open and search these files, and your core memory will reflect the contents of any files currently open.
+Maintain only those files relevant to the user’s current interaction.
Base instructions finished.
diff --git a/letta/schemas/agent.py b/letta/schemas/agent.py
index b7efb4e3..12691231 100644
--- a/letta/schemas/agent.py
+++ b/letta/schemas/agent.py
@@ -4,12 +4,7 @@ from typing import Dict, List, Optional
from pydantic import BaseModel, Field, field_validator, model_validator
-from letta.constants import (
- CORE_MEMORY_LINE_NUMBER_WARNING,
- DEFAULT_EMBEDDING_CHUNK_SIZE,
- FILE_MEMORY_EMPTY_MESSAGE,
- FILE_MEMORY_EXISTS_MESSAGE,
-)
+from letta.constants import CORE_MEMORY_LINE_NUMBER_WARNING, DEFAULT_EMBEDDING_CHUNK_SIZE
from letta.schemas.block import CreateBlock
from letta.schemas.embedding_config import EmbeddingConfig
from letta.schemas.environment_variables import AgentEnvironmentVariable
@@ -319,9 +314,21 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
# However, they still allow files to be injected into the context
if agent_type == AgentType.react_agent or agent_type == AgentType.workflow_agent:
return (
- f"\n{{% if file_blocks %}}{FILE_MEMORY_EXISTS_MESSAGE}\n{{% else %}}{FILE_MEMORY_EMPTY_MESSAGE}{{% endif %}}"
+ "{% if sources %}"
+ "\n"
+ "{% for source in sources %}"
+ f'\n'
+ "{% if source.description %}"
+ "{{ source.description }}\n"
+ "{% endif %}"
+ "{% if source.instructions %}"
+ "{{ source.instructions }}\n"
+ "{% endif %}"
+ "{% if file_blocks %}"
"{% for block in file_blocks %}"
- f"\n"
+ "{% if block.source_id == source.id %}"
+ f"\n"
+ "<{{ block.label }}>\n"
"\n"
"{{ block.description }}\n"
"\n"
@@ -334,9 +341,13 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
"{{ block.value }}\n"
"\n"
"\n"
- "{% if not loop.last %}\n{% endif %}"
+ "{% endif %}"
"{% endfor %}"
- "\n"
+ "{% endif %}"
+ "\n"
+ "{% endfor %}"
+ ""
+ "{% endif %}"
)
# Sleeptime agents use the MemGPT v2 memory tools (line numbers)
@@ -370,8 +381,19 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
"{{ tool_usage_rules.value }}\n"
""
"{% endif %}"
- f"\n\n\n{{% if file_blocks %}}{FILE_MEMORY_EXISTS_MESSAGE}\n{{% else %}}{FILE_MEMORY_EMPTY_MESSAGE}{{% endif %}}"
+ "\n\n{% if sources %}"
+ "\n"
+ "{% for source in sources %}"
+ f'\n'
+ "{% if source.description %}"
+ "{{ source.description }}\n"
+ "{% endif %}"
+ "{% if source.instructions %}"
+ "{{ source.instructions }}\n"
+ "{% endif %}"
+ "{% if file_blocks %}"
"{% for block in file_blocks %}"
+ "{% if block.source_id == source.id %}"
f"\n"
"{% if block.description %}"
"\n"
@@ -389,12 +411,16 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
"\n"
"{% endif %}"
"\n"
- "{% if not loop.last %}\n{% endif %}"
+ "{% endif %}"
"{% endfor %}"
- "\n"
+ "{% endif %}"
+ "\n"
+ "{% endfor %}"
+ ""
+ "{% endif %}"
)
- # Default setup (MemGPT), no line numbers
+ # All other agent types use memory blocks
else:
return (
"\nThe following memory blocks are currently engaged in your core memory unit:\n\n"
@@ -421,9 +447,25 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
"{{ tool_usage_rules.value }}\n"
""
"{% endif %}"
- f"\n\n\n{{% if file_blocks %}}{FILE_MEMORY_EXISTS_MESSAGE}\n{{% else %}}{FILE_MEMORY_EMPTY_MESSAGE}{{% endif %}}"
+ "\n\n{% if sources %}"
+ "\n"
+ "{% for source in sources %}"
+ f'\n'
+ "{% if source.description %}"
+ "{{ source.description }}\n"
+ "{% endif %}"
+ "{% if source.instructions %}"
+ "{{ source.instructions }}\n"
+ "{% endif %}"
+ "{% if file_blocks %}"
"{% for block in file_blocks %}"
+ "{% if block.source_id == source.id %}"
f"\n"
+ "{% if block.description %}"
+ "\n"
+ "{{ block.description }}\n"
+ "\n"
+ "{% endif %}"
""
"{% if block.read_only %}\n- read_only=true{% endif %}\n"
"- chars_current={{ block.value|length }}\n"
@@ -435,7 +477,11 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
"\n"
"{% endif %}"
"\n"
- "{% if not loop.last %}\n{% endif %}"
+ "{% endif %}"
"{% endfor %}"
- "\n"
+ "{% endif %}"
+ "\n"
+ "{% endfor %}"
+ ""
+ "{% endif %}"
)
diff --git a/letta/schemas/block.py b/letta/schemas/block.py
index 8b00d5c1..bb53cdad 100644
--- a/letta/schemas/block.py
+++ b/letta/schemas/block.py
@@ -33,6 +33,9 @@ class BaseBlock(LettaBase, validate_assignment=True):
description: Optional[str] = Field(None, description="Description of the block.")
metadata: Optional[dict] = Field({}, description="Metadata of the block.")
+ # source association (for file blocks)
+ source_id: Optional[str] = Field(None, description="The source ID associated with this block (for file blocks).")
+
# def __len__(self):
# return len(self.value)
diff --git a/letta/schemas/memory.py b/letta/schemas/memory.py
index 8437bd04..18105990 100644
--- a/letta/schemas/memory.py
+++ b/letta/schemas/memory.py
@@ -124,7 +124,7 @@ class Memory(BaseModel, validate_assignment=True):
Template(prompt_template)
# Validate compatibility with current memory structure
- Template(prompt_template).render(blocks=self.blocks, file_blocks=self.file_blocks)
+ Template(prompt_template).render(blocks=self.blocks, file_blocks=self.file_blocks, sources=[])
# If we get here, the template is valid and compatible
self.prompt_template = prompt_template
@@ -133,10 +133,10 @@ class Memory(BaseModel, validate_assignment=True):
except Exception as e:
raise ValueError(f"Prompt template is not compatible with current memory structure: {str(e)}")
- def compile(self, tool_usage_rules=None) -> str:
+ def compile(self, tool_usage_rules=None, sources=None) -> str:
"""Generate a string representation of the memory in-context using the Jinja2 template"""
template = Template(self.prompt_template)
- return template.render(blocks=self.blocks, file_blocks=self.file_blocks, tool_usage_rules=tool_usage_rules)
+ return template.render(blocks=self.blocks, file_blocks=self.file_blocks, tool_usage_rules=tool_usage_rules, sources=sources)
def list_block_labels(self) -> List[str]:
"""Return a list of the block names held inside the memory object"""
diff --git a/letta/services/agent_manager.py b/letta/services/agent_manager.py
index c2710a0c..d96aabe3 100644
--- a/letta/services/agent_manager.py
+++ b/letta/services/agent_manager.py
@@ -1111,6 +1111,7 @@ class AgentManager:
include_relationships: Optional[List[str]] = None,
) -> PydanticAgentState:
"""Fetch an agent by its ID."""
+
async with db_registry.async_session() as session:
agent = await AgentModel.read_async(db_session=session, identifier=agent_id, actor=actor)
return await agent.to_pydantic_async(include_relationships=include_relationships)
@@ -1461,6 +1462,7 @@ class AgentManager:
timezone=agent_state.timezone,
previous_message_count=num_messages - len(agent_state.message_ids),
archival_memory_size=num_archival_memories,
+ sources=agent_state.sources,
)
diff = united_diff(curr_system_message_openai["content"], new_system_message_str)
@@ -1493,7 +1495,8 @@ class AgentManager:
Updates to the memory header should *not* trigger a rebuild, since that will simply flood recall storage with excess messages
"""
- agent_state = await self.get_agent_by_id_async(agent_id=agent_id, include_relationships=["memory"], actor=actor)
+ # Get the current agent state
+ agent_state = await self.get_agent_by_id_async(agent_id=agent_id, include_relationships=["memory", "sources"], actor=actor)
if not tool_rules_solver:
tool_rules_solver = ToolRulesSolver(agent_state.tool_rules)
@@ -1529,6 +1532,7 @@ class AgentManager:
num_archival_memories = await self.passage_manager.agent_passage_size_async(actor=actor, agent_id=agent_id)
# update memory (TODO: potentially update recall/archival stats separately)
+
new_system_message_str = compile_system_message(
system_prompt=agent_state.system,
in_context_memory=agent_state.memory,
@@ -1537,6 +1541,7 @@ class AgentManager:
previous_message_count=num_messages - len(agent_state.message_ids),
archival_memory_size=num_archival_memories,
tool_rules_solver=tool_rules_solver,
+ sources=agent_state.sources,
)
diff = united_diff(curr_system_message_openai["content"], new_system_message_str)
@@ -1654,7 +1659,7 @@ class AgentManager:
# Update agent to only keep the system message
agent.message_ids = [system_message_id]
await agent.update_async(db_session=session, actor=actor)
- agent_state = await agent.to_pydantic_async()
+ agent_state = await agent.to_pydantic_async(include_relationships=["sources"])
# Optionally add default initial messages after the system message
if add_default_initial_messages:
@@ -1774,8 +1779,7 @@ class AgentManager:
relationship_name="sources",
model_class=SourceModel,
item_ids=[source_id],
- allow_partial=False,
- replace=False, # Extend existing sources rather than replace
+ allow_partial=False, # Extend existing sources rather than replace
)
# Commit the changes
diff --git a/letta/services/helpers/agent_manager_helper.py b/letta/services/helpers/agent_manager_helper.py
index d22c9cb0..4e262de0 100644
--- a/letta/services/helpers/agent_manager_helper.py
+++ b/letta/services/helpers/agent_manager_helper.py
@@ -251,6 +251,7 @@ def compile_system_message(
previous_message_count: int = 0,
archival_memory_size: int = 0,
tool_rules_solver: Optional[ToolRulesSolver] = None,
+ sources: Optional[List] = None,
) -> str:
"""Prepare the final/full system message that will be fed into the LLM API
@@ -259,6 +260,7 @@ def compile_system_message(
The following are reserved variables:
- CORE_MEMORY: the in-context memory of the LLM
"""
+
# Add tool rule constraints if available
tool_constraint_block = None
if tool_rules_solver is not None:
@@ -281,13 +283,16 @@ def compile_system_message(
archival_memory_size=archival_memory_size,
timezone=timezone,
)
- full_memory_string = in_context_memory.compile(tool_usage_rules=tool_constraint_block) + "\n\n" + memory_metadata_string
+
+ memory_with_sources = in_context_memory.compile(tool_usage_rules=tool_constraint_block, sources=sources)
+ full_memory_string = memory_with_sources + "\n\n" + memory_metadata_string
# Add to the variables list to inject
variables[IN_CONTEXT_MEMORY_KEYWORD] = full_memory_string
if template_format == "f-string":
memory_variable_string = "{" + IN_CONTEXT_MEMORY_KEYWORD + "}"
+
# Catch the special case where the system prompt is unformatted
if append_icm_if_missing:
if memory_variable_string not in system_prompt:
@@ -330,6 +335,7 @@ def initialize_message_sequence(
append_icm_if_missing=True,
previous_message_count=previous_message_count,
archival_memory_size=archival_memory_size,
+ sources=agent_state.sources,
)
first_user_message = get_login_event(agent_state.timezone) # event letting Letta know the user just logged in