feat: replace file descriptions with per data source description (#3067)
Co-authored-by: Matt Zhou <mattzh1314@gmail.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"<files>\n{{% if file_blocks %}}{FILE_MEMORY_EXISTS_MESSAGE}\n{{% else %}}{FILE_MEMORY_EMPTY_MESSAGE}{{% endif %}}"
|
||||
"{% if sources %}"
|
||||
"<folders>\n"
|
||||
"{% for source in sources %}"
|
||||
f'<folder name="{{{{ source.name }}}}">\n'
|
||||
"{% if source.description %}"
|
||||
"<description>{{ source.description }}</description>\n"
|
||||
"{% endif %}"
|
||||
"{% if source.instructions %}"
|
||||
"<instructions>{{ source.instructions }}</instructions>\n"
|
||||
"{% endif %}"
|
||||
"{% if file_blocks %}"
|
||||
"{% for block in file_blocks %}"
|
||||
f"<file status=\"{{{{ '{FileStatus.open.value}' if block.value else '{FileStatus.closed.value}' }}}}\" name=\"{{{{ block.label }}}}\">\n"
|
||||
"{% if block.source_id == source.id %}"
|
||||
f"<file status=\"{{{{ '{FileStatus.open.value}' if block.value else '{FileStatus.closed.value}' }}}}\">\n"
|
||||
"<{{ block.label }}>\n"
|
||||
"<description>\n"
|
||||
"{{ block.description }}\n"
|
||||
"</description>\n"
|
||||
@@ -334,9 +341,13 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
|
||||
"{{ block.value }}\n"
|
||||
"</value>\n"
|
||||
"</file>\n"
|
||||
"{% if not loop.last %}\n{% endif %}"
|
||||
"{% endif %}"
|
||||
"{% endfor %}"
|
||||
"\n</files>"
|
||||
"{% endif %}"
|
||||
"</folder>\n"
|
||||
"{% endfor %}"
|
||||
"</folders>"
|
||||
"{% 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"
|
||||
"</tool_usage_rules>"
|
||||
"{% endif %}"
|
||||
f"\n\n<files>\n{{% if file_blocks %}}{FILE_MEMORY_EXISTS_MESSAGE}\n{{% else %}}{FILE_MEMORY_EMPTY_MESSAGE}{{% endif %}}"
|
||||
"\n\n{% if sources %}"
|
||||
"<folders>\n"
|
||||
"{% for source in sources %}"
|
||||
f'<folder name="{{{{ source.name }}}}">\n'
|
||||
"{% if source.description %}"
|
||||
"<description>{{ source.description }}</description>\n"
|
||||
"{% endif %}"
|
||||
"{% if source.instructions %}"
|
||||
"<instructions>{{ source.instructions }}</instructions>\n"
|
||||
"{% endif %}"
|
||||
"{% if file_blocks %}"
|
||||
"{% for block in file_blocks %}"
|
||||
"{% if block.source_id == source.id %}"
|
||||
f"<file status=\"{{{{ '{FileStatus.open.value}' if block.value else '{FileStatus.closed.value}' }}}}\" name=\"{{{{ block.label }}}}\">\n"
|
||||
"{% if block.description %}"
|
||||
"<description>\n"
|
||||
@@ -389,12 +411,16 @@ def get_prompt_template_for_agent_type(agent_type: Optional[AgentType] = None):
|
||||
"</value>\n"
|
||||
"{% endif %}"
|
||||
"</file>\n"
|
||||
"{% if not loop.last %}\n{% endif %}"
|
||||
"{% endif %}"
|
||||
"{% endfor %}"
|
||||
"\n</files>"
|
||||
"{% endif %}"
|
||||
"</folder>\n"
|
||||
"{% endfor %}"
|
||||
"</folders>"
|
||||
"{% endif %}"
|
||||
)
|
||||
|
||||
# Default setup (MemGPT), no line numbers
|
||||
# All other agent types use memory blocks
|
||||
else:
|
||||
return (
|
||||
"<memory_blocks>\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"
|
||||
"</tool_usage_rules>"
|
||||
"{% endif %}"
|
||||
f"\n\n<files>\n{{% if file_blocks %}}{FILE_MEMORY_EXISTS_MESSAGE}\n{{% else %}}{FILE_MEMORY_EMPTY_MESSAGE}{{% endif %}}"
|
||||
"\n\n{% if sources %}"
|
||||
"<folders>\n"
|
||||
"{% for source in sources %}"
|
||||
f'<folder name="{{{{ source.name }}}}">\n'
|
||||
"{% if source.description %}"
|
||||
"<description>{{ source.description }}</description>\n"
|
||||
"{% endif %}"
|
||||
"{% if source.instructions %}"
|
||||
"<instructions>{{ source.instructions }}</instructions>\n"
|
||||
"{% endif %}"
|
||||
"{% if file_blocks %}"
|
||||
"{% for block in file_blocks %}"
|
||||
"{% if block.source_id == source.id %}"
|
||||
f"<file status=\"{{{{ '{FileStatus.open.value}' if block.value else '{FileStatus.closed.value}' }}}}\" name=\"{{{{ block.label }}}}\">\n"
|
||||
"{% if block.description %}"
|
||||
"<description>\n"
|
||||
"{{ block.description }}\n"
|
||||
"</description>\n"
|
||||
"{% endif %}"
|
||||
"<metadata>"
|
||||
"{% 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):
|
||||
"</value>\n"
|
||||
"{% endif %}"
|
||||
"</file>\n"
|
||||
"{% if not loop.last %}\n{% endif %}"
|
||||
"{% endif %}"
|
||||
"{% endfor %}"
|
||||
"\n</files>"
|
||||
"{% endif %}"
|
||||
"</folder>\n"
|
||||
"{% endfor %}"
|
||||
"</folders>"
|
||||
"{% endif %}"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user