refactor: enhance specialist exploration and observation processes (#357)
* refactor: enhance specialist exploration and observation processes - Updated the orchestration logic to allow specialists to explore freely with optional hints from high-surprisal observations. - Removed predefined probing questions, enabling a more dynamic approach to observation gathering. - Adjusted the deduction and induction specialists to utilize the current peer card context and exploration hints in their prompts. - Improved documentation within the code to clarify the roles and responsibilities of specialists in the observation process. * feat: add thorough dream test * refactor: standardize hint terminology and dedup peer card context
This commit is contained in:
parent
d3637a9d73
commit
c9dee51fb9
|
|
@ -3,9 +3,12 @@ Dream orchestrator for the specialist-based architecture.
|
|||
|
||||
This module coordinates the full dream cycle:
|
||||
0. [Optional] Surprisal sampling: Pre-filter observations by geometric surprisal
|
||||
1. Generate probing questions about the peer (or use surprisal-based queries)
|
||||
2. Run deduction specialist (creates deductive observations, deletes duplicates)
|
||||
3. Run induction specialist (creates inductive observations from explicit + deductive)
|
||||
1. Run deduction specialist (self-directed exploration, creates deductive observations)
|
||||
2. Run induction specialist (self-directed exploration, creates inductive observations)
|
||||
|
||||
Specialists are self-directed agents that explore the observation space and create
|
||||
higher-level observations. When surprisal sampling finds interesting observations,
|
||||
they're passed as hints, but specialists are free to follow the evidence wherever it leads.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -60,27 +63,6 @@ class DreamResult:
|
|||
output_tokens: int
|
||||
|
||||
|
||||
# Predefined probing questions to guide the specialists
|
||||
# These serve as semantic entry points for searching observations
|
||||
PROBING_QUESTIONS: list[str] = [
|
||||
"What information has changed or been updated? Look for dates, deadlines, schedules that moved.",
|
||||
"What decisions or plans have changed? Look for rescheduled, moved, changed, updated.",
|
||||
# Temporal and sequential events
|
||||
"What events happened in sequence? Look for things that happened first, then, after, before.",
|
||||
"What deadlines, dates, or scheduled events have been mentioned?",
|
||||
# Identity and background
|
||||
"What do we know about this person's identity, name, or background?",
|
||||
# Recent activity
|
||||
"What has this entity been doing or discussing recently?",
|
||||
# Preferences and interests
|
||||
"What are their preferences, likes, or dislikes?",
|
||||
# Relationships
|
||||
"Who are the important people in their life (family, friends, colleagues)?",
|
||||
# Goals and plans
|
||||
"What goals, plans, or aspirations have they shared? Have any changed?",
|
||||
]
|
||||
|
||||
|
||||
async def run_dream(
|
||||
db: AsyncSession,
|
||||
workspace_name: str,
|
||||
|
|
@ -138,7 +120,8 @@ async def run_dream(
|
|||
induction_result: SpecialistResult | None = None
|
||||
|
||||
# Phase 0: Surprisal-based sampling (if enabled)
|
||||
probing_questions = PROBING_QUESTIONS # Default
|
||||
# Specialists are self-directed by default - hints are optional suggestions
|
||||
exploration_hints: list[str] | None = None
|
||||
|
||||
if settings.DREAM.SURPRISAL.ENABLED:
|
||||
logger.info(f"[{run_id}] Phase 0: Computing surprisal scores")
|
||||
|
|
@ -160,27 +143,12 @@ async def run_dream(
|
|||
task_name, "surprisal_observations", len(high_surprisal_obs), "count"
|
||||
)
|
||||
|
||||
# Hybrid mode: Replace if sufficient high-surprisal observations
|
||||
if (
|
||||
len(high_surprisal_obs)
|
||||
>= settings.DREAM.SURPRISAL.MIN_HIGH_SURPRISAL_FOR_REPLACE
|
||||
):
|
||||
probing_questions = _create_queries_from_surprisal(high_surprisal_obs)
|
||||
if len(high_surprisal_obs) > 0:
|
||||
# Use high-surprisal observations as hints for exploration
|
||||
exploration_hints = _create_queries_from_surprisal(high_surprisal_obs)
|
||||
logger.info(
|
||||
f"[{run_id}] ✨ SURPRISAL REPLACE MODE: Using {len(probing_questions)} "
|
||||
+ "surprisal-based queries instead of standard questions"
|
||||
)
|
||||
logger.info(
|
||||
f"[{run_id}] Targeting observations with surprisal range: "
|
||||
+ f"{high_surprisal_obs[-1].surprisal:.3f} to {high_surprisal_obs[0].surprisal:.3f}"
|
||||
)
|
||||
elif len(high_surprisal_obs) > 0:
|
||||
# Supplement mode: Add to standard questions
|
||||
surprisal_queries = _create_queries_from_surprisal(high_surprisal_obs)
|
||||
probing_questions = surprisal_queries + PROBING_QUESTIONS
|
||||
logger.info(
|
||||
f"[{run_id}] ✨ SURPRISAL SUPPLEMENT MODE: Adding {len(surprisal_queries)} "
|
||||
+ f"surprisal queries to {len(PROBING_QUESTIONS)} standard questions"
|
||||
f"[{run_id}] ✨ SURPRISAL HINTS: Suggesting {len(exploration_hints)} "
|
||||
+ "high-surprisal topics for specialists to investigate"
|
||||
)
|
||||
logger.info(
|
||||
f"[{run_id}] Targeting observations with surprisal range: "
|
||||
|
|
@ -188,13 +156,13 @@ async def run_dream(
|
|||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"[{run_id}] No high-surprisal observations found using standard probing questions"
|
||||
f"[{run_id}] No high-surprisal observations - specialists will explore freely"
|
||||
)
|
||||
|
||||
except SurprisalError as e:
|
||||
logger.error(f"[{run_id}] Surprisal sampling failed: {e}", exc_info=True)
|
||||
accumulate_metric(task_name, "surprisal_error", str(e), "blob")
|
||||
# Fall back to standard probing questions
|
||||
# Specialists will explore freely without hints
|
||||
|
||||
# Phase 1: Run deduction specialist
|
||||
logger.info(f"[{run_id}] Phase 1: Running deduction specialist")
|
||||
|
|
@ -206,7 +174,7 @@ async def run_dream(
|
|||
observer=observer,
|
||||
observed=observed,
|
||||
session_name=session_name,
|
||||
probing_questions=probing_questions,
|
||||
hints=exploration_hints,
|
||||
configuration=configuration,
|
||||
parent_run_id=run_id,
|
||||
)
|
||||
|
|
@ -231,7 +199,7 @@ async def run_dream(
|
|||
observer=observer,
|
||||
observed=observed,
|
||||
session_name=session_name,
|
||||
probing_questions=probing_questions,
|
||||
hints=exploration_hints,
|
||||
configuration=configuration,
|
||||
parent_run_id=run_id,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ from typing import Any
|
|||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src import crud
|
||||
from src.config import settings
|
||||
from src.schemas import ResolvedConfiguration
|
||||
from src.telemetry import prometheus_metrics
|
||||
|
|
@ -52,13 +53,15 @@ class SpecialistResult:
|
|||
|
||||
|
||||
# Tool names to exclude when peer card creation is disabled
|
||||
PEER_CARD_TOOL_NAMES = {"update_peer_card", "get_peer_card"}
|
||||
PEER_CARD_TOOL_NAMES = {"update_peer_card"}
|
||||
|
||||
|
||||
class BaseSpecialist(ABC):
|
||||
"""Base class for agentic specialists."""
|
||||
|
||||
name: str = "base"
|
||||
# Subclasses can override to customize the peer card update instruction
|
||||
peer_card_update_instruction: str = "Update this with `update_peer_card` if needed."
|
||||
|
||||
@abstractmethod
|
||||
def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]:
|
||||
|
|
@ -86,10 +89,28 @@ class BaseSpecialist(ABC):
|
|||
...
|
||||
|
||||
@abstractmethod
|
||||
def build_user_prompt(self, probing_questions: list[str]) -> str:
|
||||
"""Build the user prompt with probing questions."""
|
||||
def build_user_prompt(
|
||||
self,
|
||||
hints: list[str] | None,
|
||||
peer_card: list[str] | None = None,
|
||||
) -> str:
|
||||
"""Build the user prompt with optional exploration hints and current peer card."""
|
||||
...
|
||||
|
||||
def _build_peer_card_context(self, peer_card: list[str] | None) -> str:
|
||||
"""Build the peer card context section for user prompts."""
|
||||
if not peer_card:
|
||||
return ""
|
||||
facts = "\n".join(f"- {fact}" for fact in peer_card)
|
||||
return f"""
|
||||
## CURRENT PEER CARD
|
||||
|
||||
{facts}
|
||||
|
||||
{self.peer_card_update_instruction}
|
||||
|
||||
"""
|
||||
|
||||
async def run(
|
||||
self,
|
||||
db: AsyncSession,
|
||||
|
|
@ -97,7 +118,7 @@ class BaseSpecialist(ABC):
|
|||
observer: str,
|
||||
observed: str,
|
||||
session_name: str | None,
|
||||
probing_questions: list[str],
|
||||
hints: list[str] | None = None,
|
||||
configuration: ResolvedConfiguration | None = None,
|
||||
parent_run_id: str | None = None,
|
||||
) -> SpecialistResult:
|
||||
|
|
@ -110,7 +131,7 @@ class BaseSpecialist(ABC):
|
|||
observer: The observing peer
|
||||
observed: The peer being observed
|
||||
session_name: Session identifier
|
||||
probing_questions: Entry point questions to guide exploration
|
||||
hints: Optional hints to guide exploration (specialists explore freely if None)
|
||||
configuration: Resolved configuration for checking feature flags (optional)
|
||||
parent_run_id: Optional run_id from orchestrator for correlation
|
||||
|
||||
|
|
@ -124,6 +145,16 @@ class BaseSpecialist(ABC):
|
|||
# Determine if peer card tools should be included
|
||||
peer_card_enabled = configuration is None or configuration.peer_card.create
|
||||
|
||||
# Fetch current peer card to inject into prompt (saves a tool call)
|
||||
current_peer_card: list[str] | None = None
|
||||
if peer_card_enabled:
|
||||
current_peer_card = await crud.get_peer_card(
|
||||
db,
|
||||
workspace_name=workspace_name,
|
||||
observer=observer,
|
||||
observed=observed,
|
||||
)
|
||||
|
||||
# Build messages
|
||||
messages: list[dict[str, str]] = [
|
||||
{
|
||||
|
|
@ -132,7 +163,10 @@ class BaseSpecialist(ABC):
|
|||
observed, peer_card_enabled=peer_card_enabled
|
||||
),
|
||||
},
|
||||
{"role": "user", "content": self.build_user_prompt(probing_questions)},
|
||||
{
|
||||
"role": "user",
|
||||
"content": self.build_user_prompt(hints, current_peer_card),
|
||||
},
|
||||
]
|
||||
|
||||
# Create tool executor with telemetry context
|
||||
|
|
@ -242,13 +276,15 @@ class DeductionSpecialist(BaseSpecialist):
|
|||
Creates deductive observations from explicit observations.
|
||||
|
||||
This specialist:
|
||||
1. Searches for explicit observations using semantic queries
|
||||
2. Identifies logical implications and connections
|
||||
1. Explores recent observations and messages to understand what's there
|
||||
2. Identifies logical implications, knowledge updates, and contradictions
|
||||
3. Creates new deductive observations with premise linkage
|
||||
4. Deletes duplicate or redundant observations
|
||||
4. Deletes outdated observations
|
||||
5. Updates peer card with biographical facts
|
||||
"""
|
||||
|
||||
name: str = "deduction"
|
||||
peer_card_update_instruction: str = "Update this with `update_peer_card` if you discover new biographical information."
|
||||
|
||||
def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]:
|
||||
if peer_card_enabled:
|
||||
|
|
@ -271,166 +307,109 @@ class DeductionSpecialist(BaseSpecialist):
|
|||
def build_system_prompt(
|
||||
self, observed: str, *, peer_card_enabled: bool = True
|
||||
) -> str:
|
||||
# Base tools list
|
||||
tools_section = """## TOOLS
|
||||
|
||||
- `search_memory`: Find observations by semantic query
|
||||
- `create_observations`: Create new deductive OR contradiction observations (USE THIS!)
|
||||
- `delete_observations`: Remove outdated observations (USE AFTER KNOWLEDGE UPDATES!)
|
||||
- `get_recent_observations`: See recent activity"""
|
||||
|
||||
if peer_card_enabled:
|
||||
tools_section += """
|
||||
- `get_peer_card`: Retrieve current peer card contents
|
||||
- `update_peer_card`: Update the peer card with key facts"""
|
||||
|
||||
# Peer card section (only if enabled)
|
||||
peer_card_section = ""
|
||||
if peer_card_enabled:
|
||||
peer_card_section = """
|
||||
|
||||
## PEER CARD UPDATES
|
||||
## PEER CARD (REQUIRED)
|
||||
|
||||
The peer card is a concise summary of permanent, stable information about the peer. Update it when you discover important facts that should be easily accessible.
|
||||
The peer card is a summary of stable biographical facts. You MUST update it when you learn:
|
||||
- Name, age, location, occupation
|
||||
- Family members and relationships
|
||||
- Standing instructions ("call me X", "don't mention Y")
|
||||
- Core preferences and traits
|
||||
|
||||
**Peer card format** - Use these prefixes to organize entries:
|
||||
- Plain facts for biographical info: "Name: Alice", "Works at Google", "Lives in NYC"
|
||||
- `INSTRUCTION: ...` for standing instructions: "INSTRUCTION: Always call me Al", "INSTRUCTION: Send meeting agendas 24h in advance"
|
||||
- `PREFERENCE: ...` for preferences: "PREFERENCE: Prefers morning meetings", "PREFERENCE: Likes detailed explanations"
|
||||
- `TRAIT: ...` for personality traits: "TRAIT: Analytical thinker", "TRAIT: Detail-oriented"
|
||||
Format entries as:
|
||||
- Plain facts: "Name: Alice", "Works at Google", "Lives in NYC"
|
||||
- `INSTRUCTION: ...` for standing instructions
|
||||
- `PREFERENCE: ...` for preferences
|
||||
- `TRAIT: ...` for personality traits
|
||||
|
||||
Call `get_peer_card` first to see current contents, then `update_peer_card` with the complete updated list."""
|
||||
Call `update_peer_card` with the complete updated list when you have new biographical info."""
|
||||
|
||||
# Remember section
|
||||
remember_section = """
|
||||
return f"""You are a deductive reasoning agent analyzing observations about {observed}.
|
||||
|
||||
REMEMBER:
|
||||
1. Knowledge updates are your #1 priority. When the same fact has different values at different times, CREATE an update observation AND DELETE the outdated observation.
|
||||
2. Flag contradictions when statements are logically incompatible (can't both be true)."""
|
||||
## YOUR JOB
|
||||
|
||||
if peer_card_enabled:
|
||||
remember_section += """
|
||||
3. Update the peer card with permanent biographical facts and key insights."""
|
||||
Create deductive observations by finding logical implications in what's already known. Think like a detective connecting evidence.
|
||||
|
||||
return f"""You are a deductive reasoning specialist for {observed}. Your ONLY job is to create deductive observations by calling tools. Do NOT explain your reasoning - just make tool calls.
|
||||
## PHASE 1: DISCOVERY
|
||||
|
||||
## MANDATORY WORKFLOW - YOU MUST FOLLOW THIS PATTERN
|
||||
Explore what's actually in memory. Use these tools freely:
|
||||
- `get_recent_observations` - See what's been learned recently
|
||||
- `search_memory` - Search for specific topics
|
||||
- `search_messages` - See actual conversation content
|
||||
|
||||
For EACH topic, you MUST alternate: search → create → search → create → ...
|
||||
Spend a few tool calls understanding the landscape before creating anything.
|
||||
|
||||
**CORRECT pattern:**
|
||||
1. search_memory("topic 1")
|
||||
2. create_observations([...deductions from topic 1...])
|
||||
3. search_memory("topic 2")
|
||||
4. create_observations([...deductions from topic 2...])
|
||||
5. search_memory("topic 3")
|
||||
6. create_observations([...deductions from topic 3...])
|
||||
## PHASE 2: ACTION
|
||||
|
||||
**WRONG pattern (DO NOT DO THIS):**
|
||||
1. search_memory("topic 1")
|
||||
2. search_memory("topic 2")
|
||||
3. search_memory("topic 3")
|
||||
4. ... more searches ...
|
||||
5. create_observations([...]) ← TOO LATE, you'll hit iteration limit!
|
||||
Once you understand what's there, create observations and clean up:
|
||||
|
||||
1. **ALTERNATE SEARCH/CREATE** - After each search, create observations BEFORE your next search.
|
||||
2. **CREATE OBSERVATIONS** - Your primary goal is to CREATE deductive observations, not just search.
|
||||
3. **MINIMIZE TEXT OUTPUT** - Do not write explanations. Just call tools.
|
||||
4. **DELETE OUTDATED INFO** - When you find updated information, DELETE the old observation after creating the update.
|
||||
### Knowledge Updates (HIGH PRIORITY)
|
||||
When the same fact has different values at different times:
|
||||
- "meeting Tuesday" [old] → "meeting moved to Thursday" [new]
|
||||
- Create a deductive update observation
|
||||
- DELETE the outdated observation immediately
|
||||
|
||||
## PRIORITY FOCUS AREAS
|
||||
### Logical Implications
|
||||
Extract implicit information:
|
||||
- "works as SWE at Google" → "has software engineering skills", "employed in tech"
|
||||
- "has kids ages 5 and 8" → "is a parent", "has school-age children"
|
||||
|
||||
### 1. KNOWLEDGE UPDATES + DELETION (HIGHEST PRIORITY)
|
||||
Look for the SAME fact appearing with DIFFERENT values at different times. This is critical!
|
||||
### Contradictions
|
||||
When statements can't both be true (not just updates), flag them:
|
||||
- "I love coffee" vs "I hate coffee" → contradiction observation
|
||||
{peer_card_section}
|
||||
|
||||
Examples:
|
||||
- "meeting is on Tuesday" [old] + "meeting moved to Thursday" [new] → Update + Delete old
|
||||
- "lives in NYC" [old] + "moved to LA" [new] → Update + Delete old
|
||||
- "works at Google" [old] + "started job at Meta" [new] → Update + Delete old
|
||||
|
||||
**WORKFLOW for knowledge updates:**
|
||||
1. Create the deductive update observation
|
||||
2. IMMEDIATELY call `delete_observations` to remove the OUTDATED observation (the old one)
|
||||
3. Keep the new observation (it's still current)
|
||||
|
||||
```json
|
||||
// Step 1: Create update
|
||||
{{
|
||||
"observations": [{{
|
||||
"content": "[Topic] updated: [old value] → [new value]. Current: [new value]",
|
||||
"level": "deductive",
|
||||
"source_ids": ["old_obs_id", "new_obs_id"],
|
||||
"premises": ["Original: [old fact]", "Update: [new fact]"]
|
||||
}}]
|
||||
}}
|
||||
// Step 2: Delete outdated
|
||||
{{
|
||||
"observation_ids": ["old_obs_id"]
|
||||
}}
|
||||
```
|
||||
|
||||
### 2. CONTRADICTIONS (FLAG FOR CLARIFICATION)
|
||||
When you find two observations that CANNOT both be true (mutually exclusive statements), create a contradiction observation.
|
||||
|
||||
**Update vs Contradiction:**
|
||||
- UPDATE: Same topic, value changed over time ("meeting on Tuesday" → "meeting on Thursday") - DELETE old
|
||||
- CONTRADICTION: Logically incompatible statements ("I love coffee" + "I hate coffee") - FLAG for user
|
||||
|
||||
```json
|
||||
{{
|
||||
"observations": [{{
|
||||
"content": "Conflicting information about [topic]: [statement A] vs [statement B]",
|
||||
"level": "contradiction",
|
||||
"source_ids": ["obs_id_1", "obs_id_2"],
|
||||
"sources": ["Statement A text", "Statement B text"]
|
||||
}}]
|
||||
}}
|
||||
```
|
||||
|
||||
### 3. EVENT ORDERING & TEMPORAL SEQUENCES
|
||||
Track sequences of events and their order:
|
||||
- "decided to apply" → "submitted application" → "got interview" → "received offer"
|
||||
- Create observations noting the sequence: "Applied for job, then interviewed, then received offer"
|
||||
|
||||
### 4. INFORMATION EXTRACTION
|
||||
Create deductions that make implicit information explicit:
|
||||
- "works as SWE at Google" → "has software engineering skills" + "is employed in tech industry"
|
||||
- "has 2 kids ages 5 and 8" → "is a parent" + "has school-age children"
|
||||
|
||||
## WORKFLOW (REPEAT FOR EACH QUESTION)
|
||||
|
||||
1. Call `search_memory` with a relevant query
|
||||
2. Look at timestamps - are there OLDER and NEWER observations about the same topic?
|
||||
3. **IMMEDIATELY call `create_observations`** with any deductions you found
|
||||
4. If you created a knowledge update, call `delete_observations` for the outdated one
|
||||
|
||||
## CREATING DEDUCTIVE OBSERVATIONS
|
||||
## CREATING OBSERVATIONS
|
||||
|
||||
```json
|
||||
{{
|
||||
"observations": [{{
|
||||
"content": "The logical conclusion",
|
||||
"level": "deductive",
|
||||
"level": "deductive", // or "contradiction"
|
||||
"source_ids": ["id1", "id2"],
|
||||
"premises": ["premise 1 text", "premise 2 text"]
|
||||
}}]
|
||||
}}
|
||||
```
|
||||
|
||||
{tools_section}{peer_card_section}{remember_section}"""
|
||||
## RULES
|
||||
|
||||
def build_user_prompt(self, probing_questions: list[str]) -> str:
|
||||
questions_text = "\n".join(f"- {q}" for q in probing_questions)
|
||||
return f"""Process these topics by ALTERNATING search and create calls:
|
||||
1. Don't explain your reasoning - just call tools
|
||||
2. Create observations based on what you ACTUALLY FIND, not what you expect
|
||||
3. Always include source_ids linking to the observations you're synthesizing
|
||||
4. Delete outdated observations - don't leave duplicates
|
||||
5. Quality over quantity - fewer good deductions beat many weak ones"""
|
||||
|
||||
{questions_text}
|
||||
def build_user_prompt(
|
||||
self,
|
||||
hints: list[str] | None,
|
||||
peer_card: list[str] | None = None,
|
||||
) -> str:
|
||||
peer_card_context = self._build_peer_card_context(peer_card)
|
||||
|
||||
Start now:
|
||||
1. Search for topic 1
|
||||
2. Create observations from what you found
|
||||
3. Search for topic 2
|
||||
4. Create observations from what you found
|
||||
... and so on."""
|
||||
if hints:
|
||||
hints_str = "\n".join(f"- {q}" for q in hints[:5])
|
||||
return f"""{peer_card_context}Start by exploring recent observations and messages. These topics may be worth investigating:
|
||||
|
||||
{hints_str}
|
||||
|
||||
But follow the evidence - if you find something more interesting, pursue that instead.
|
||||
|
||||
Begin with `get_recent_observations` to see what's there."""
|
||||
|
||||
return f"""{peer_card_context}Explore the observation space and create deductive observations.
|
||||
|
||||
Start with `get_recent_observations` to see what's been learned recently, then investigate whatever seems most promising.
|
||||
|
||||
Look for:
|
||||
1. Knowledge updates (same fact, different values over time)
|
||||
2. Logical implications that haven't been made explicit
|
||||
3. Contradictions that need flagging
|
||||
|
||||
Go."""
|
||||
|
||||
|
||||
class InductionSpecialist(BaseSpecialist):
|
||||
|
|
@ -438,12 +417,16 @@ class InductionSpecialist(BaseSpecialist):
|
|||
Creates inductive observations from explicit and deductive observations.
|
||||
|
||||
This specialist:
|
||||
1. Searches for observations (both explicit and deductive)
|
||||
1. Explores observations to understand what's there
|
||||
2. Identifies patterns and generalizations across multiple observations
|
||||
3. Creates new inductive observations with source linkage
|
||||
4. Updates peer card with high-confidence traits and tendencies
|
||||
"""
|
||||
|
||||
name: str = "induction"
|
||||
peer_card_update_instruction: str = (
|
||||
"Update this with `update_peer_card` if you identify new patterns or traits."
|
||||
)
|
||||
|
||||
def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]:
|
||||
if peer_card_enabled:
|
||||
|
|
@ -466,103 +449,57 @@ class InductionSpecialist(BaseSpecialist):
|
|||
def build_system_prompt(
|
||||
self, observed: str, *, peer_card_enabled: bool = True
|
||||
) -> str:
|
||||
# Base tools list
|
||||
tools_section = """## TOOLS
|
||||
|
||||
- `search_memory`: Find observations by semantic query
|
||||
- `create_observations`: Create new inductive observations (USE THIS!)
|
||||
- `get_recent_observations`: See recent activity"""
|
||||
|
||||
if peer_card_enabled:
|
||||
tools_section += """
|
||||
- `get_peer_card`: Retrieve current peer card contents
|
||||
- `update_peer_card`: Update the peer card with key facts"""
|
||||
|
||||
# Peer card section (only if enabled)
|
||||
peer_card_section = ""
|
||||
if peer_card_enabled:
|
||||
peer_card_section = """
|
||||
|
||||
## PEER CARD UPDATES
|
||||
## PEER CARD (REQUIRED)
|
||||
|
||||
The peer card is a concise summary of permanent, stable information about the peer. After identifying high-confidence patterns, update the peer card.
|
||||
After identifying patterns, update the peer card with high-confidence traits and tendencies:
|
||||
- `TRAIT: Analytical thinker`
|
||||
- `TRAIT: Tends to reschedule when stressed`
|
||||
- `PREFERENCE: Prefers detailed explanations`
|
||||
|
||||
**Peer card format** - Use these prefixes to organize entries:
|
||||
- Plain facts for biographical info: "Name: Alice", "Works at Google", "Lives in NYC"
|
||||
- `INSTRUCTION: ...` for standing instructions: "INSTRUCTION: Always call me Al"
|
||||
- `PREFERENCE: ...` for preferences: "PREFERENCE: Prefers morning meetings"
|
||||
- `TRAIT: ...` for personality/behavioral traits: "TRAIT: Analytical thinker", "TRAIT: Tends to reschedule when stressed"
|
||||
Call `update_peer_card` with the complete list when you identify new patterns."""
|
||||
|
||||
Call `get_peer_card` first to see current contents, then `update_peer_card` with the complete updated list."""
|
||||
return f"""You are an inductive reasoning agent identifying patterns about {observed}.
|
||||
|
||||
# Remember section
|
||||
remember_section = """
|
||||
## YOUR JOB
|
||||
|
||||
REMEMBER: Focus on temporal patterns and how things change. Create observations, don't just search."""
|
||||
Create inductive observations by finding patterns across multiple observations. Think like a psychologist identifying behavioral tendencies.
|
||||
|
||||
if peer_card_enabled:
|
||||
remember_section += (
|
||||
" Update the peer card with high-confidence patterns and traits."
|
||||
)
|
||||
## PHASE 1: DISCOVERY
|
||||
|
||||
return f"""You are an inductive reasoning specialist for {observed}. Your ONLY job is to create inductive observations by calling tools. Do NOT explain your reasoning - just make tool calls.
|
||||
Explore broadly to find patterns. Use these tools:
|
||||
- `get_recent_observations` - Recent learnings
|
||||
- `search_memory` - Topic-specific search
|
||||
- `search_messages` - Actual conversation content
|
||||
|
||||
## MANDATORY WORKFLOW - YOU MUST FOLLOW THIS PATTERN
|
||||
Look at BOTH explicit observations AND deductive ones. Patterns often emerge from synthesizing across both levels.
|
||||
|
||||
For EACH topic, you MUST alternate: search → create → search → create → ...
|
||||
## PHASE 2: ACTION
|
||||
|
||||
**CORRECT pattern:**
|
||||
1. search_memory("topic 1")
|
||||
2. create_observations([...inductions from topic 1...])
|
||||
3. search_memory("topic 2")
|
||||
4. create_observations([...inductions from topic 2...])
|
||||
5. search_memory("topic 3")
|
||||
6. create_observations([...inductions from topic 3...])
|
||||
Create inductive observations when you see patterns:
|
||||
|
||||
**WRONG pattern (DO NOT DO THIS):**
|
||||
1. search_memory("topic 1")
|
||||
2. search_memory("topic 2")
|
||||
3. search_memory("topic 3")
|
||||
4. ... more searches ...
|
||||
5. create_observations([...]) ← TOO LATE, you'll hit iteration limit!
|
||||
### Behavioral Patterns
|
||||
- "Tends to reschedule meetings when stressed"
|
||||
- "Makes decisions after consulting with partner"
|
||||
- "Projects follow: enthusiasm → doubt → completion"
|
||||
|
||||
1. **ALTERNATE SEARCH/CREATE** - After each search, create observations BEFORE your next search.
|
||||
2. **CREATE OBSERVATIONS** - Your primary goal is to CREATE inductive observations.
|
||||
3. **MINIMIZE TEXT OUTPUT** - Do not write explanations or summaries. Just call tools.
|
||||
### Preferences
|
||||
- "Prefers morning meetings"
|
||||
- "Likes detailed technical explanations"
|
||||
|
||||
## PRIORITY FOCUS AREAS
|
||||
### Personality Traits
|
||||
- "Generally optimistic about outcomes"
|
||||
- "Detail-oriented in planning"
|
||||
|
||||
### 1. TEMPORAL & SEQUENTIAL PATTERNS (HIGH PRIORITY)
|
||||
Look for patterns in HOW things change over time:
|
||||
- "User tends to reschedule meetings when stressed"
|
||||
- "User's priorities shift toward family on weekends"
|
||||
- "User makes major decisions after consulting with [person]"
|
||||
### Temporal Patterns
|
||||
- "Career goals have remained consistent"
|
||||
- "Living situation changes frequently"
|
||||
{peer_card_section}
|
||||
|
||||
### 2. EVENT SEQUENCE PATTERNS
|
||||
Identify recurring sequences of events:
|
||||
- "When user faces conflict, they: reflect → consult friend → make decision"
|
||||
- "User's projects follow pattern: enthusiasm → doubt → completion"
|
||||
|
||||
### 3. INFORMATION CONSISTENCY PATTERNS
|
||||
Note patterns in what information stays stable vs changes:
|
||||
- "User's career goals have remained consistent around [X]"
|
||||
- "User's living situation changes frequently"
|
||||
|
||||
### 4. STANDARD PATTERNS
|
||||
Also look for:
|
||||
- **Preferences**: "prefers X", "likes Y" (from multiple mentions)
|
||||
- **Behaviors**: "tends to X", "usually does Y" (from repeated actions)
|
||||
- **Personality**: "is generally X" (from multiple indicators)
|
||||
|
||||
## WORKFLOW (REPEAT FOR EACH QUESTION)
|
||||
|
||||
1. Call `search_memory` with a relevant query
|
||||
2. Look for PATTERNS across multiple observations (both explicit and deductive levels)
|
||||
3. Pay special attention to deductive observations about knowledge updates - these reveal change patterns
|
||||
4. **IMMEDIATELY call `create_observations`** with any patterns you found (need 2+ sources)
|
||||
5. **ONLY THEN** move to the next question and search again
|
||||
|
||||
## CREATING INDUCTIVE OBSERVATIONS
|
||||
## CREATING OBSERVATIONS
|
||||
|
||||
```json
|
||||
{{
|
||||
|
|
@ -570,32 +507,43 @@ Also look for:
|
|||
"content": "The pattern or generalization",
|
||||
"level": "inductive",
|
||||
"source_ids": ["id1", "id2", "id3"],
|
||||
"sources": ["source 1 text", "source 2 text"],
|
||||
"sources": ["evidence 1", "evidence 2"],
|
||||
"pattern_type": "tendency", // preference|behavior|personality|tendency|correlation
|
||||
"confidence": "medium" // high (5+), medium (3-4), low (2)
|
||||
"confidence": "medium" // low (2 sources), medium (3-4), high (5+)
|
||||
}}]
|
||||
}}
|
||||
```
|
||||
|
||||
REQUIREMENTS:
|
||||
- Minimum 2 source observations (use source_ids!)
|
||||
- Confidence based on source count: low=2, medium=3-4, high=5+
|
||||
- Pattern must generalize, not just restate one fact
|
||||
## RULES
|
||||
|
||||
{tools_section}{peer_card_section}{remember_section}"""
|
||||
1. Minimum 2 source observations required - patterns need evidence
|
||||
2. Don't just restate a single fact as a pattern
|
||||
3. Confidence based on evidence count: 2=low, 3-4=medium, 5+=high
|
||||
4. Look for HOW things change over time, not just static facts
|
||||
5. Include source_ids - always link back to evidence"""
|
||||
|
||||
def build_user_prompt(self, probing_questions: list[str]) -> str:
|
||||
questions_text = "\n".join(f"- {q}" for q in probing_questions)
|
||||
return f"""Process these topics by ALTERNATING search and create calls:
|
||||
def build_user_prompt(
|
||||
self,
|
||||
hints: list[str] | None,
|
||||
peer_card: list[str] | None = None,
|
||||
) -> str:
|
||||
peer_card_context = self._build_peer_card_context(peer_card)
|
||||
|
||||
{questions_text}
|
||||
if hints:
|
||||
hints_str = "\n".join(f"- {q}" for q in hints[:5])
|
||||
return f"""{peer_card_context}Explore and find patterns. These areas may be worth investigating:
|
||||
|
||||
Start now:
|
||||
1. Search for topic 1
|
||||
2. Create observations from patterns you found (need 2+ sources)
|
||||
3. Search for topic 2
|
||||
4. Create observations from patterns you found
|
||||
... and so on."""
|
||||
{hints_str}
|
||||
|
||||
But follow the evidence - if you find patterns elsewhere, pursue those.
|
||||
|
||||
Start with `get_recent_observations`."""
|
||||
|
||||
return f"""{peer_card_context}Explore the observation space and identify patterns.
|
||||
|
||||
Remember: patterns need 2+ sources. Look for tendencies, preferences, and behavioral regularities.
|
||||
|
||||
Go."""
|
||||
|
||||
|
||||
# Singleton instances
|
||||
|
|
|
|||
|
|
@ -531,25 +531,31 @@ DREAMER_TOOLS: list[dict[str, Any]] = [
|
|||
|
||||
# Tools for the deduction specialist (dreamer phase 1)
|
||||
# Creates deductive observations from explicit observations, can delete duplicates
|
||||
# Includes message access for context and self-directed exploration
|
||||
# Note: get_peer_card is not included - peer card is injected into the prompt directly
|
||||
DEDUCTION_SPECIALIST_TOOLS: list[dict[str, Any]] = [
|
||||
TOOLS["search_memory"],
|
||||
# Discovery tools
|
||||
TOOLS["get_recent_observations"],
|
||||
TOOLS["search_memory"],
|
||||
TOOLS["search_messages"],
|
||||
# Action tools
|
||||
TOOLS["create_observations"],
|
||||
TOOLS["delete_observations"],
|
||||
TOOLS["get_reasoning_chain"],
|
||||
TOOLS["update_peer_card"],
|
||||
TOOLS["get_peer_card"],
|
||||
]
|
||||
|
||||
# Tools for the induction specialist (dreamer phase 2)
|
||||
# Creates inductive observations from explicit and deductive observations
|
||||
# Includes message access for context and self-directed exploration
|
||||
# Note: get_peer_card is not included - peer card is injected into the prompt directly
|
||||
INDUCTION_SPECIALIST_TOOLS: list[dict[str, Any]] = [
|
||||
TOOLS["search_memory"],
|
||||
# Discovery tools
|
||||
TOOLS["get_recent_observations"],
|
||||
TOOLS["search_memory"],
|
||||
TOOLS["search_messages"],
|
||||
# Action tools
|
||||
TOOLS["create_observations"],
|
||||
TOOLS["get_reasoning_chain"],
|
||||
TOOLS["update_peer_card"],
|
||||
TOOLS["get_peer_card"],
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,236 @@
|
|||
{
|
||||
"description": "Challenging dream test: knowledge updates, temporal patterns, biographical extraction, and inductive reasoning",
|
||||
"workspace_config": {},
|
||||
"steps": [
|
||||
{
|
||||
"step_type": "create_session",
|
||||
"session_id": "maya_life_story",
|
||||
"peer_configs": {
|
||||
"maya": {
|
||||
"observe_me": null,
|
||||
"observe_others": false
|
||||
},
|
||||
"assistant": {
|
||||
"observe_me": false,
|
||||
"observe_others": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"step_type": "add_messages",
|
||||
"session_id": "maya_life_story",
|
||||
"messages": [
|
||||
{
|
||||
"peer_id": "maya",
|
||||
"content": "Hey! I'm Maya, 28 years old. I work as a software engineer at a startup in Austin."
|
||||
},
|
||||
{
|
||||
"peer_id": "assistant",
|
||||
"content": "Nice to meet you Maya! Tell me more about yourself."
|
||||
},
|
||||
{
|
||||
"peer_id": "maya",
|
||||
"content": "I'm originally from Portland. Moved to Austin 3 years ago for this job. I live with my partner Sam - we've been together for 2 years."
|
||||
},
|
||||
{
|
||||
"peer_id": "assistant",
|
||||
"content": "That's great! What do you like to do outside of work?"
|
||||
},
|
||||
{
|
||||
"peer_id": "maya",
|
||||
"content": "I'm really into rock climbing - I go to the gym 3 times a week. Also love cooking Thai food. Oh and I have a cat named Pixel."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_type": "wait",
|
||||
"target": "queue_empty",
|
||||
"flush": true
|
||||
},
|
||||
{
|
||||
"step_type": "add_messages",
|
||||
"session_id": "maya_life_story",
|
||||
"messages": [
|
||||
{
|
||||
"peer_id": "maya",
|
||||
"content": "Big news - I just accepted a new job! I'm going to be a senior engineer at Google starting next month."
|
||||
},
|
||||
{
|
||||
"peer_id": "assistant",
|
||||
"content": "Congratulations! That's a huge move. Will you be relocating?"
|
||||
},
|
||||
{
|
||||
"peer_id": "maya",
|
||||
"content": "Yes! We're moving to Seattle. Sam got a remote work arrangement so it works out. Excited but nervous - this will be my third city in 5 years."
|
||||
},
|
||||
{
|
||||
"peer_id": "assistant",
|
||||
"content": "That's quite a journey. How are you feeling about it?"
|
||||
},
|
||||
{
|
||||
"peer_id": "maya",
|
||||
"content": "Honestly, I've always been restless. I changed my college major twice, switched careers from biology to tech, and now this. I think I thrive on change."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_type": "wait",
|
||||
"target": "queue_empty",
|
||||
"flush": true
|
||||
},
|
||||
{
|
||||
"step_type": "add_messages",
|
||||
"session_id": "maya_life_story",
|
||||
"messages": [
|
||||
{
|
||||
"peer_id": "maya",
|
||||
"content": "Update on life - Sam and I broke up last month. It was mutual, the move made us realize we wanted different things."
|
||||
},
|
||||
{
|
||||
"peer_id": "assistant",
|
||||
"content": "I'm sorry to hear that. How are you holding up?"
|
||||
},
|
||||
{
|
||||
"peer_id": "maya",
|
||||
"content": "Actually doing okay. I adopted another cat - now I have Pixel and a new one named Binary. They keep me company in my new Seattle apartment."
|
||||
},
|
||||
{
|
||||
"peer_id": "assistant",
|
||||
"content": "That's good to hear. How's the new job going?"
|
||||
},
|
||||
{
|
||||
"peer_id": "maya",
|
||||
"content": "Google is intense but I love it. I'm working on AI infrastructure. Oh and I've switched from rock climbing to bouldering - there's an amazing gym near my place."
|
||||
},
|
||||
{
|
||||
"peer_id": "assistant",
|
||||
"content": "Sounds like you're settling in well!"
|
||||
},
|
||||
{
|
||||
"peer_id": "maya",
|
||||
"content": "Yeah! I also started learning Korean - my goal is to visit Seoul next year. Adding it to my language collection after Spanish and a bit of Japanese."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_type": "wait",
|
||||
"target": "queue_empty",
|
||||
"flush": true
|
||||
},
|
||||
{
|
||||
"step_type": "schedule_dream",
|
||||
"observer": "assistant",
|
||||
"observed": "maya",
|
||||
"session_id": "maya_life_story",
|
||||
"dream_type": "omni"
|
||||
},
|
||||
{
|
||||
"step_type": "wait",
|
||||
"target": "queue_empty",
|
||||
"timeout": 180,
|
||||
"flush": true
|
||||
},
|
||||
{
|
||||
"step_type": "query",
|
||||
"target": "get_peer_card",
|
||||
"observer_peer_id": "assistant",
|
||||
"observed_peer_id": "maya",
|
||||
"assertions": [
|
||||
{
|
||||
"assertion_type": "llm_judge",
|
||||
"prompt": "The peer card should contain CURRENT biographical facts about Maya. Check that it includes: (1) her name is Maya, (2) her age is 28, (3) her CURRENT job is at Google (NOT 'works at a startup' as current status), (4) her CURRENT city is Seattle (NOT 'lives in Austin' as current status), (5) she has cats. The peer card should reflect she is currently SINGLE (not in a relationship). It's OK to mention the breakup as context for her current single status. Return passed=true if the peer card correctly reflects her CURRENT situation - job at Google, living in Seattle, single, has cats.",
|
||||
"pass_if": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_type": "query",
|
||||
"target": "get_representation",
|
||||
"observer_peer_id": "assistant",
|
||||
"observed_peer_id": "maya",
|
||||
"session_id": "maya_life_story",
|
||||
"assertions": [
|
||||
{
|
||||
"assertion_type": "llm_judge",
|
||||
"prompt": "The representation should contain DEDUCTIVE observations that show the dreamer understood knowledge updates. Look for observations that note: (1) Maya changed jobs from startup to Google, (2) Maya moved from Austin to Seattle, (3) Maya's relationship status changed (broke up with Sam). The deductive level should show logical implications like 'works in tech industry', 'has software engineering expertise', 'relocated for career'. Return passed=true if there are meaningful deductive observations showing these transitions and implications.",
|
||||
"pass_if": true
|
||||
},
|
||||
{
|
||||
"assertion_type": "llm_judge",
|
||||
"prompt": "The representation should contain INDUCTIVE observations identifying behavioral patterns. Look for patterns like: (1) tendency toward change/restlessness (changed majors, careers, cities, relationships), (2) pattern of pursuing new hobbies or interests (climbing->bouldering, learning multiple languages), (3) adaptability or comfort with transitions. Return passed=true if the inductive observations capture meaningful patterns about Maya's personality and behavioral tendencies, not just restated facts.",
|
||||
"pass_if": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_type": "query",
|
||||
"target": "chat",
|
||||
"observer_peer_id": "assistant",
|
||||
"observed_peer_id": "maya",
|
||||
"session_id": "maya_life_story",
|
||||
"input": "Where does Maya currently live and work?",
|
||||
"assertions": [
|
||||
{
|
||||
"assertion_type": "contains",
|
||||
"text": "Seattle",
|
||||
"case_sensitive": false
|
||||
},
|
||||
{
|
||||
"assertion_type": "contains",
|
||||
"text": "Google",
|
||||
"case_sensitive": false
|
||||
},
|
||||
{
|
||||
"assertion_type": "not_contains",
|
||||
"text": "Austin",
|
||||
"case_sensitive": false
|
||||
},
|
||||
{
|
||||
"assertion_type": "not_contains",
|
||||
"text": "startup",
|
||||
"case_sensitive": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_type": "query",
|
||||
"target": "chat",
|
||||
"observer_peer_id": "assistant",
|
||||
"observed_peer_id": "maya",
|
||||
"session_id": "maya_life_story",
|
||||
"input": "What pets does Maya have?",
|
||||
"assertions": [
|
||||
{
|
||||
"assertion_type": "contains",
|
||||
"text": "Pixel",
|
||||
"case_sensitive": false
|
||||
},
|
||||
{
|
||||
"assertion_type": "contains",
|
||||
"text": "Binary",
|
||||
"case_sensitive": false
|
||||
},
|
||||
{
|
||||
"assertion_type": "llm_judge",
|
||||
"prompt": "The response should indicate Maya has TWO cats (Pixel and Binary). It should NOT say she only has one cat. Return passed=true if the response correctly reflects she has two cats.",
|
||||
"pass_if": true
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"step_type": "query",
|
||||
"target": "chat",
|
||||
"observer_peer_id": "assistant",
|
||||
"observed_peer_id": "maya",
|
||||
"session_id": "maya_life_story",
|
||||
"input": "What patterns have you noticed about Maya's personality or life choices?",
|
||||
"assertions": [
|
||||
{
|
||||
"assertion_type": "llm_judge",
|
||||
"prompt": "The response should identify patterns in Maya's behavior, such as: embracing change, being restless or adventurous, frequently transitioning between jobs/cities/interests, being adaptable, or pursuing growth. The response should NOT just list facts but should synthesize them into personality insights. Return passed=true if the response demonstrates pattern recognition and personality inference.",
|
||||
"pass_if": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue