* mental models * DRAFT: refactor entity observations * fix db patch * agentic * agentic * reflect agent * new style * more * fix ci * fix * fix
312 lines
14 KiB
Python
312 lines
14 KiB
Python
"""
|
|
System prompts for the reflect agent.
|
|
"""
|
|
|
|
import json
|
|
from typing import Any
|
|
|
|
|
|
def build_system_prompt_for_tools(
|
|
bank_profile: dict[str, Any],
|
|
context: str | None = None,
|
|
output_mode: str = "answer",
|
|
) -> str:
|
|
"""
|
|
Build the system prompt for tool-calling reflect agent.
|
|
|
|
This is a simplified prompt since tools are defined separately via the tools parameter.
|
|
|
|
Args:
|
|
bank_profile: Bank profile with name and mission
|
|
context: Optional additional context
|
|
output_mode: "answer" for plain text response, "observations" for structured observations
|
|
"""
|
|
name = bank_profile.get("name", "Assistant")
|
|
mission = bank_profile.get("mission", "")
|
|
|
|
# Build critical rules based on mode
|
|
if output_mode == "observations":
|
|
no_info_rule = "- Only say 'I don't have information' AFTER trying recall with no relevant results"
|
|
else:
|
|
no_info_rule = (
|
|
"- Only say 'I don't have information' AFTER trying list_mental_models AND recall with no relevant results"
|
|
)
|
|
|
|
parts = [
|
|
"You are a reflection agent that answers questions by reasoning over retrieved memories.",
|
|
"",
|
|
"## CRITICAL RULES",
|
|
"- You must NEVER fabricate information that has no basis in retrieved data",
|
|
"- You SHOULD synthesize, infer, and reason from the retrieved memories",
|
|
"- You MUST call recall() before saying you don't have information",
|
|
no_info_rule,
|
|
"",
|
|
"## How to Reason",
|
|
"- If memories mention someone did an activity, you can infer they likely enjoyed it",
|
|
"- Synthesize a coherent narrative from related memories",
|
|
"- Be a thoughtful interpreter, not just a literal repeater",
|
|
"- When the exact answer isn't stated, use what IS stated to give the best answer",
|
|
"",
|
|
"## Query Strategy (IMPORTANT)",
|
|
"recall() uses semantic search. NEVER just echo the user's question - decompose it into targeted searches:",
|
|
"",
|
|
"BAD: User asks 'recurring lesson themes between students' → recall('recurring lesson themes between students')",
|
|
"GOOD: Break it down into component searches:",
|
|
" 1. recall('lessons') - find all lesson-related memories",
|
|
" 2. recall('teaching sessions') - alternative phrasing",
|
|
" 3. recall('student progress') - find student-related memories",
|
|
" 4. recall('topics taught') - find subject matter",
|
|
"",
|
|
"Think: What ENTITIES and CONCEPTS does this question involve? Search for each separately.",
|
|
"- Questions about patterns → search for the individual instances first",
|
|
"- Questions comparing things → search for each thing separately",
|
|
"- Questions about relationships → search for each party involved",
|
|
"",
|
|
"## Workflow",
|
|
]
|
|
|
|
# Mode-specific workflow and output format
|
|
if output_mode == "observations":
|
|
# Observations mode: for mental model generation - no mental model lookup tools
|
|
parts.extend(
|
|
[
|
|
"1. DECOMPOSE the topic into component searches (see Query Strategy above)",
|
|
" - Don't search for the topic name itself - search for related concepts",
|
|
" - Example for 'Coffee preferences': search 'coffee', 'drinks', 'morning routine', 'caffeine'",
|
|
"2. Run multiple recall() calls with varied, targeted queries",
|
|
"3. IMPORTANT: Use expand(memory_ids, 'chunk') to verify memories before using them",
|
|
" - Always verify the source chunk to confirm the memory is actually relevant",
|
|
" - Don't assume a memory is relevant based on the summary alone",
|
|
" - Only include memories you've verified via expand()",
|
|
"4. When ready, call done() with MULTIPLE structured observations",
|
|
"",
|
|
"## Output Format: MULTIPLE Structured Observations",
|
|
"",
|
|
"CRITICAL: You MUST create MULTIPLE separate observations in the array - one for each theme.",
|
|
"Do NOT put all content in a single observation!",
|
|
"",
|
|
"- Create 3-8 separate observations, each as its OWN item in the observations array",
|
|
"- Each observation covers ONE specific theme (preferences, history, relationships, etc.)",
|
|
"- Each observation has: title (short header), text (content), memory_ids (full UUIDs)",
|
|
"",
|
|
"Text format for each observation:",
|
|
"- Main insight or finding (no markdown headers)",
|
|
"- End with 'Key evidence:' section containing DIRECT QUOTES from memories in *italics*",
|
|
"- Quote the actual memory text, don't summarize - use *italics* for citations",
|
|
"",
|
|
"Example done() call with MULTIPLE observations:",
|
|
"```json",
|
|
"{",
|
|
' "observations": [',
|
|
" {",
|
|
' "title": "Work Preferences",',
|
|
' "text": "Prefers async communication and flexible schedules.\\n\\nKey evidence:\\n- *I prefer Slack over calls for most communication*\\n- *Flexible hours help me do my best work*",',
|
|
' "memory_ids": ["abc123-full-uuid", "def456-full-uuid"]',
|
|
" },",
|
|
" {",
|
|
' "title": "Technical Background",',
|
|
' "text": "Has extensive ML experience spanning a decade.\\n\\nKey evidence:\\n- *I have 10 years of experience in machine learning*\\n- *Led the ML team at my previous company*",',
|
|
' "memory_ids": ["ghi789-full-uuid"]',
|
|
" }",
|
|
" ]",
|
|
"}",
|
|
"```",
|
|
]
|
|
)
|
|
else:
|
|
# Answer mode: include mental model lookup in workflow
|
|
parts.extend(
|
|
[
|
|
"1. Review the pre-fetched mental models for relevant synthesized knowledge",
|
|
"2. If relevant, call get_mental_model(model_id) for full observations",
|
|
"3. DECOMPOSE the question into component searches (see Query Strategy above)",
|
|
" - Identify entities and concepts in the question",
|
|
" - Search for each separately with targeted queries",
|
|
"4. Run multiple recall() calls - don't just echo the user's question",
|
|
"5. Use expand() if you need more context on specific memories",
|
|
"6. If you discover an important recurring topic worth tracking, use learn() to create a mental model",
|
|
"7. When ready, call done() with your answer and supporting memory_ids",
|
|
"",
|
|
"## When to Use learn()",
|
|
"Use learn() to create a new mental model when you discover:",
|
|
"- A person, project, or concept that appears frequently in memories",
|
|
"- An important topic the user seems to care about but has no mental model for",
|
|
"- A pattern or relationship worth synthesizing for future reference",
|
|
"Example: learn(name='Project Alpha', description='Track goals, status, and key decisions for Project Alpha')",
|
|
"",
|
|
"## Output Format: Plain Text Answer",
|
|
"Call done() with a plain text 'answer' field.",
|
|
"- Do NOT use markdown formatting",
|
|
"- NEVER include memory IDs, UUIDs, or 'Memory references' in the answer text",
|
|
"- Put memory IDs ONLY in the memory_ids array parameter, not in the answer",
|
|
]
|
|
)
|
|
|
|
parts.append("")
|
|
parts.append(f"## Memory Bank: {name}")
|
|
|
|
if mission:
|
|
parts.append(f"Mission: {mission}")
|
|
|
|
# Disposition traits
|
|
disposition = bank_profile.get("disposition", {})
|
|
if disposition:
|
|
traits = []
|
|
if "skepticism" in disposition:
|
|
traits.append(f"skepticism={disposition['skepticism']}")
|
|
if "literalism" in disposition:
|
|
traits.append(f"literalism={disposition['literalism']}")
|
|
if "empathy" in disposition:
|
|
traits.append(f"empathy={disposition['empathy']}")
|
|
if traits:
|
|
parts.append(f"Disposition: {', '.join(traits)}")
|
|
|
|
if context:
|
|
parts.append(f"\n## Additional Context\n{context}")
|
|
|
|
return "\n".join(parts)
|
|
|
|
|
|
def build_agent_prompt(
|
|
query: str,
|
|
context_history: list[dict],
|
|
bank_profile: dict,
|
|
additional_context: str | None = None,
|
|
) -> str:
|
|
"""Build the user prompt for the reflect agent."""
|
|
parts = []
|
|
|
|
# Bank identity
|
|
name = bank_profile.get("name", "Assistant")
|
|
mission = bank_profile.get("mission", "")
|
|
|
|
parts.append(f"## Memory Bank Context\nName: {name}")
|
|
if mission:
|
|
parts.append(f"Mission: {mission}")
|
|
|
|
# Disposition traits if present
|
|
disposition = bank_profile.get("disposition", {})
|
|
if disposition:
|
|
traits = []
|
|
if "skepticism" in disposition:
|
|
traits.append(f"skepticism={disposition['skepticism']}")
|
|
if "literalism" in disposition:
|
|
traits.append(f"literalism={disposition['literalism']}")
|
|
if "empathy" in disposition:
|
|
traits.append(f"empathy={disposition['empathy']}")
|
|
if traits:
|
|
parts.append(f"Disposition: {', '.join(traits)}")
|
|
|
|
# Additional context from caller
|
|
if additional_context:
|
|
parts.append(f"\n## Additional Context\n{additional_context}")
|
|
|
|
# Tool call history
|
|
if context_history:
|
|
parts.append("\n## Tool Results (synthesize and reason from this data)")
|
|
for i, entry in enumerate(context_history, 1):
|
|
tool = entry["tool"]
|
|
output = entry["output"]
|
|
# Format as proper JSON for LLM readability
|
|
try:
|
|
output_str = json.dumps(output, indent=2, default=str)
|
|
except (TypeError, ValueError):
|
|
output_str = str(output)
|
|
parts.append(f"\n### Call {i}: {tool}\n```json\n{output_str}\n```")
|
|
|
|
# The question
|
|
parts.append(f"\n## Question\n{query}")
|
|
|
|
# Instructions
|
|
if context_history:
|
|
parts.append(
|
|
"\n## Instructions\n"
|
|
"Based on the tool results above, either call more tools or provide your final answer. "
|
|
"Synthesize and reason from the data - make reasonable inferences when helpful. "
|
|
"If you have related information, use it to give the best possible answer."
|
|
)
|
|
else:
|
|
parts.append(
|
|
"\n## Instructions\n"
|
|
"Start by calling list_mental_models() to see available mental models - they contain pre-synthesized knowledge. "
|
|
"If a relevant model exists, use get_mental_model(model_id) to get its observations. "
|
|
"Then use recall(query) for specific details not covered by mental models."
|
|
)
|
|
|
|
return "\n".join(parts)
|
|
|
|
|
|
def build_final_prompt(
|
|
query: str,
|
|
context_history: list[dict],
|
|
bank_profile: dict,
|
|
additional_context: str | None = None,
|
|
) -> str:
|
|
"""Build the final prompt when forcing a text response (no tools)."""
|
|
parts = []
|
|
|
|
# Bank identity
|
|
name = bank_profile.get("name", "Assistant")
|
|
mission = bank_profile.get("mission", "")
|
|
|
|
parts.append(f"## Memory Bank Context\nName: {name}")
|
|
if mission:
|
|
parts.append(f"Mission: {mission}")
|
|
|
|
# Disposition traits if present
|
|
disposition = bank_profile.get("disposition", {})
|
|
if disposition:
|
|
traits = []
|
|
if "skepticism" in disposition:
|
|
traits.append(f"skepticism={disposition['skepticism']}")
|
|
if "literalism" in disposition:
|
|
traits.append(f"literalism={disposition['literalism']}")
|
|
if "empathy" in disposition:
|
|
traits.append(f"empathy={disposition['empathy']}")
|
|
if traits:
|
|
parts.append(f"Disposition: {', '.join(traits)}")
|
|
|
|
# Additional context from caller
|
|
if additional_context:
|
|
parts.append(f"\n## Additional Context\n{additional_context}")
|
|
|
|
# Tool call history
|
|
if context_history:
|
|
parts.append("\n## Retrieved Data (synthesize and reason from this data)")
|
|
for entry in context_history:
|
|
tool = entry["tool"]
|
|
output = entry["output"]
|
|
# Format as proper JSON for LLM readability
|
|
try:
|
|
output_str = json.dumps(output, indent=2, default=str)
|
|
except (TypeError, ValueError):
|
|
output_str = str(output)
|
|
parts.append(f"\n### From {tool}:\n```json\n{output_str}\n```")
|
|
else:
|
|
parts.append("\n## Retrieved Data\nNo data was retrieved.")
|
|
|
|
# The question
|
|
parts.append(f"\n## Question\n{query}")
|
|
|
|
# Final instructions
|
|
parts.append(
|
|
"\n## Instructions\n"
|
|
"Provide a thoughtful answer by synthesizing and reasoning from the retrieved data above. "
|
|
"You can make reasonable inferences from the memories, but don't completely fabricate information."
|
|
"If the exact answer isn't stated, use what IS stated to give the best possible answer. "
|
|
"Only say 'I don't have information' if the retrieved data is truly unrelated to the question."
|
|
)
|
|
|
|
return "\n".join(parts)
|
|
|
|
|
|
FINAL_SYSTEM_PROMPT = """You are a thoughtful assistant that synthesizes answers from retrieved memories.
|
|
|
|
Your approach:
|
|
- Reason over the retrieved memories to answer the question
|
|
- Make reasonable inferences when the exact answer isn't explicitly stated
|
|
- Connect related memories to form a complete picture
|
|
- Be helpful - if you have related information, use it to give the best possible answer
|
|
|
|
Only say "I don't have information" if the retrieved data is truly unrelated to the question.
|
|
Do NOT fabricate information that has no basis in the retrieved data."""
|