feat: make dreamer and dialectic actually respect settings around using peer card

This commit is contained in:
Benjamin McCormick 2026-01-12 18:25:35 -05:00
parent 0eb0d6d427
commit 4b01f8754d
8 changed files with 225 additions and 86 deletions

View File

@ -197,7 +197,7 @@ class ConclusionScope:
],
)
def representation(
def get_representation(
self,
search_query: str | None = None,
search_top_k: int | None = None,

View File

@ -12,6 +12,7 @@ from src import crud
from src.config import ReasoningLevel
from src.dependencies import tracked_db
from src.dialectic.core import DialecticAgent
from src.utils.config_helpers import get_configuration
logger = logging.getLogger(__name__)
@ -39,15 +40,26 @@ async def agentic_chat(
The synthesized answer string
"""
async with tracked_db("dialectic.agentic_chat") as db:
# Get peer cards for context
observer_peer_card = await crud.get_peer_card(
db, workspace_name, observer=observer, observed=observer
)
observed_peer_card = None
if observer != observed:
observed_peer_card = await crud.get_peer_card(
db, workspace_name, observer=observer, observed=observed
# Resolve configuration to check if peer cards should be used
session = None
if session_name:
session = await crud.get_session(
db, workspace_name=workspace_name, session_name=session_name
)
workspace = await crud.get_workspace(db, workspace_name=workspace_name)
configuration = get_configuration(None, session, workspace)
# Get peer cards for context (if enabled)
observer_peer_card = None
observed_peer_card = None
if configuration.peer_card.use:
observer_peer_card = await crud.get_peer_card(
db, workspace_name, observer=observer, observed=observer
)
if observer != observed:
observed_peer_card = await crud.get_peer_card(
db, workspace_name, observer=observer, observed=observed
)
# Create and run the dialectic agent
agent = DialecticAgent(
@ -89,15 +101,26 @@ async def agentic_chat_stream(
Chunks of the response text as they are generated
"""
async with tracked_db("dialectic.agentic_chat_stream") as db:
# Get peer cards for context
observer_peer_card = await crud.get_peer_card(
db, workspace_name, observer=observer, observed=observer
)
observed_peer_card = None
if observer != observed:
observed_peer_card = await crud.get_peer_card(
db, workspace_name, observer=observer, observed=observed
# Resolve configuration to check if peer cards should be used
session = None
if session_name:
session = await crud.get_session(
db, workspace_name=workspace_name, session_name=session_name
)
workspace = await crud.get_workspace(db, workspace_name=workspace_name)
configuration = get_configuration(None, session, workspace)
# Get peer cards for context (if enabled)
observer_peer_card = None
observed_peer_card = None
if configuration.peer_card.use:
observer_peer_card = await crud.get_peer_card(
db, workspace_name, observer=observer, observed=observer
)
if observer != observed:
observed_peer_card = await crud.get_peer_card(
db, workspace_name, observer=observer, observed=observed
)
# Create and run the dialectic agent
agent = DialecticAgent(

View File

@ -21,6 +21,10 @@ def agent_system_prompt(
Returns:
Formatted system prompt string for the agent
"""
# Determine if we have any peer card data
peer_cards_enabled = (
observer_peer_card is not None or observed_peer_card is not None
)
# Build peer card sections
if observer != observed:
# Directional query: observer asking about observed
@ -64,6 +68,15 @@ Known biographical information about {observed}:
You are answering queries about '{observed}'.
{peer_card_section}
"""
# Build peer card explanation section (only if peer cards are being used)
peer_card_explanation = ""
if peer_cards_enabled:
peer_card_explanation = """
Peer cards are **constructed summaries** - they are synthesized from the same observations stored in memory. This means:
- Information in a peer card originates from observations you can also find via `search_memory`
- The peer card is a convenience summary, not a separate source of truth
"""
return f"""
@ -72,11 +85,7 @@ You are a helpful and concise context synthesis agent that answers questions abo
Always give users the answer *they expect* based on the message history -- the goal is to help recall and *reason through* insights that the memory system has already gathered. You have many tools for gathering context. Search wisely.
{perspective_section}
Peer cards are **constructed summaries** - they are synthesized from the same observations stored in memory. This means:
- Information in a peer card originates from observations you can also find via `search_memory`
- The peer card is a convenience summary, not a separate source of truth
{peer_card_explanation}
## AVAILABLE TOOLS
**Observation Tools (read):**

View File

@ -200,7 +200,9 @@ class DreamScheduler:
) -> None:
"""Execute the dream by enqueueing it and updating collection metadata."""
# Import here to avoid circular dependency
from src import crud
from src.deriver.enqueue import enqueue_dream
from src.utils.config_helpers import get_configuration
# Find the most recent session for this observer/observed pair
async with tracked_db("dream_session_lookup") as db:
@ -216,11 +218,24 @@ class DreamScheduler:
)
session_name = await db.scalar(stmt)
if not session_name:
logger.warning(
f"No documents found for {workspace_name}/{observer}/{observed}, skipping dream"
if not session_name:
logger.warning(
f"No documents found for {workspace_name}/{observer}/{observed}, skipping dream"
)
return
session = await crud.get_session(
db, workspace_name=workspace_name, session_name=session_name
)
return
workspace = await crud.get_workspace(db, workspace_name=workspace_name)
configuration = get_configuration(None, session, workspace)
if not configuration.dream.enabled:
logger.info(
f"Dreams disabled for {workspace_name}/{session_name}, skipping dream"
)
return
await enqueue_dream(
workspace_name,

View File

@ -17,10 +17,12 @@ from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from src import crud
from src.config import settings
from src.dreamer.specialists import SPECIALISTS
from src.dreamer.surprisal import SurprisalScore # type: ignore
from src.exceptions import SpecialistExecutionError, SurprisalError
from src.utils.config_helpers import get_configuration
from src.utils.logging import (
accumulate_metric,
log_performance_metrics,
@ -80,6 +82,13 @@ async def run_dream(
f"[{run_id}] Starting dream cycle for {workspace_name}/{observer}/{observed}"
)
session = await crud.get_session(
db, workspace_name=workspace_name, session_name=session_name
)
workspace = await crud.get_workspace(db, workspace_name=workspace_name)
configuration = get_configuration(None, session, workspace)
# Phase 0: Surprisal-based sampling (if enabled)
probing_questions = PROBING_QUESTIONS # Default
@ -149,6 +158,7 @@ async def run_dream(
observed=observed,
session_name=session_name,
probing_questions=probing_questions,
configuration=configuration,
)
logger.info(f"[{run_id}] Deduction completed: {deduction_result[:200]}...")
accumulate_metric(task_name, "deduction_result", deduction_result, "blob")
@ -167,6 +177,7 @@ async def run_dream(
observed=observed,
session_name=session_name,
probing_questions=probing_questions,
configuration=configuration,
)
logger.info(f"[{run_id}] Induction completed: {induction_result[:200]}...")
accumulate_metric(task_name, "induction_result", induction_result, "blob")

View File

@ -21,6 +21,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from src import prometheus
from src.config import settings
from src.schemas import ResolvedConfiguration
from src.utils.agent_tools import (
DEDUCTION_SPECIALIST_TOOLS,
INDUCTION_SPECIALIST_TOOLS,
@ -32,13 +33,17 @@ from src.utils.logging import accumulate_metric, log_performance_metrics
logger = logging.getLogger(__name__)
# Tool names to exclude when peer card creation is disabled
PEER_CARD_TOOL_NAMES = {"update_peer_card", "get_peer_card"}
class BaseSpecialist(ABC):
"""Base class for agentic specialists."""
name: str = "base"
@abstractmethod
def get_tools(self) -> list[dict[str, Any]]:
def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]:
"""Get the tools available to this specialist."""
...
@ -56,7 +61,9 @@ class BaseSpecialist(ABC):
return 15
@abstractmethod
def build_system_prompt(self, observed: str) -> str:
def build_system_prompt(
self, observed: str, *, peer_card_enabled: bool = True
) -> str:
"""Build the system prompt for this specialist."""
...
@ -73,6 +80,7 @@ class BaseSpecialist(ABC):
observed: str,
session_name: str,
probing_questions: list[str],
configuration: ResolvedConfiguration | None = None,
) -> str:
"""
Run the specialist agent.
@ -84,6 +92,7 @@ class BaseSpecialist(ABC):
observed: The peer being observed
session_name: Session identifier
probing_questions: Entry point questions to guide exploration
configuration: Resolved configuration for checking feature flags (optional)
Returns:
Summary of work done
@ -92,9 +101,17 @@ class BaseSpecialist(ABC):
task_name = f"dreamer_{self.name}_{run_id}"
start_time = time.perf_counter()
# Determine if peer card tools should be included
peer_card_enabled = configuration is None or configuration.peer_card.create
# Build messages
messages: list[dict[str, str]] = [
{"role": "system", "content": self.build_system_prompt(observed)},
{
"role": "system",
"content": self.build_system_prompt(
observed, peer_card_enabled=peer_card_enabled
),
},
{"role": "user", "content": self.build_user_prompt(probing_questions)},
]
@ -109,6 +126,7 @@ class BaseSpecialist(ABC):
session_name=session_name,
include_observation_ids=True,
history_token_limit=settings.DREAM.HISTORY_TOKEN_LIMIT,
configuration=configuration,
)
# Get model with potential override
@ -120,7 +138,7 @@ class BaseSpecialist(ABC):
llm_settings=llm_settings,
prompt="", # Ignored since we pass messages
max_tokens=self.get_max_tokens(),
tools=self.get_tools(),
tools=self.get_tools(peer_card_enabled=peer_card_enabled),
tool_choice=None,
tool_executor=tool_executor,
max_tool_iterations=self.get_max_iterations(),
@ -172,8 +190,14 @@ class DeductionSpecialist(BaseSpecialist):
name: str = "deduction"
def get_tools(self) -> list[dict[str, Any]]:
return DEDUCTION_SPECIALIST_TOOLS
def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]:
if peer_card_enabled:
return DEDUCTION_SPECIALIST_TOOLS
return [
t
for t in DEDUCTION_SPECIALIST_TOOLS
if t["name"] not in PEER_CARD_TOOL_NAMES
]
def get_model(self) -> str:
return settings.DREAM.DEDUCTION_MODEL
@ -184,7 +208,50 @@ class DeductionSpecialist(BaseSpecialist):
def get_max_iterations(self) -> int:
return 12
def build_system_prompt(self, observed: str) -> str:
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
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.
**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"
Call `get_peer_card` first to see current contents, then `update_peer_card` with the complete updated list."""
# Remember section
remember_section = """
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)."""
if peer_card_enabled:
remember_section += """
3. Update the peer card with permanent biographical facts and key insights."""
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.
## MANDATORY WORKFLOW - YOU MUST FOLLOW THIS PATTERN
@ -290,31 +357,7 @@ Create deductions that make implicit information explicit:
}}
```
## 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
- `get_peer_card`: Retrieve current peer card contents
- `update_peer_card`: Update the peer card with key facts
## PEER CARD UPDATES
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.
**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"
Call `get_peer_card` first to see current contents, then `update_peer_card` with the complete updated list.
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).
3. Update the peer card with permanent biographical facts and key insights."""
{tools_section}{peer_card_section}{remember_section}"""
def build_user_prompt(self, probing_questions: list[str]) -> str:
questions_text = "\n".join(f"- {q}" for q in probing_questions)
@ -342,8 +385,14 @@ class InductionSpecialist(BaseSpecialist):
name: str = "induction"
def get_tools(self) -> list[dict[str, Any]]:
return INDUCTION_SPECIALIST_TOOLS
def get_tools(self, *, peer_card_enabled: bool = True) -> list[dict[str, Any]]:
if peer_card_enabled:
return INDUCTION_SPECIALIST_TOOLS
return [
t
for t in INDUCTION_SPECIALIST_TOOLS
if t["name"] not in PEER_CARD_TOOL_NAMES
]
def get_model(self) -> str:
return settings.DREAM.INDUCTION_MODEL
@ -354,7 +403,48 @@ class InductionSpecialist(BaseSpecialist):
def get_max_iterations(self) -> int:
return 10
def build_system_prompt(self, observed: str) -> str:
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
The peer card is a concise summary of permanent, stable information about the peer. After identifying high-confidence patterns, update the peer card.
**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 `get_peer_card` first to see current contents, then `update_peer_card` with the complete updated list."""
# Remember section
remember_section = """
REMEMBER: Focus on temporal patterns and how things change. Create observations, don't just search."""
if peer_card_enabled:
remember_section += (
" Update the peer card with high-confidence patterns and traits."
)
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.
## MANDATORY WORKFLOW - YOU MUST FOLLOW THIS PATTERN
@ -432,27 +522,7 @@ REQUIREMENTS:
- Confidence based on source count: low=2, medium=3-4, high=5+
- Pattern must generalize, not just restate one fact
## TOOLS
- `search_memory`: Find observations by semantic query
- `create_observations`: Create new inductive observations (USE THIS!)
- `get_recent_observations`: See recent activity
- `get_peer_card`: Retrieve current peer card contents
- `update_peer_card`: Update the peer card with key facts
## PEER CARD UPDATES
The peer card is a concise summary of permanent, stable information about the peer. After identifying high-confidence patterns, update the peer card.
**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 `get_peer_card` first to see current contents, then `update_peer_card` with the complete updated list.
REMEMBER: Focus on temporal patterns and how things change. Create observations, don't just search. Update the peer card with high-confidence patterns and traits."""
{tools_section}{peer_card_section}{remember_section}"""
def build_user_prompt(self, probing_questions: list[str]) -> str:
questions_text = "\n".join(f"- {q}" for q in probing_questions)

View File

@ -134,10 +134,6 @@ class MessageConfiguration(BaseModel):
default=None,
description="Configuration for reasoning functionality.",
)
peer_card: PeerCardConfiguration | None = Field(
default=None,
description="Configuration for peer card functionality. If reasoning is disabled, peer cards will also be disabled and these settings will be ignored.",
)
class ResolvedReasoningConfiguration(BaseModel):

View File

@ -12,6 +12,7 @@ from src import crud, models, schemas
from src.config import settings
from src.embedding_client import embedding_client
from src.models import Document
from src.schemas import ResolvedConfiguration
from src.utils import summarizer
from src.utils.formatting import format_new_turn_with_timestamp, utc_now_iso
from src.utils.representation import Representation
@ -884,6 +885,8 @@ class ToolContext:
# This lock is obtained from the module-level registry to ensure all concurrent
# tool executors for the same data share the same lock.
db_lock: asyncio.Lock
# Optional resolved configuration for checking feature flags
configuration: ResolvedConfiguration | None = None
async def _handle_create_observations(
@ -998,6 +1001,15 @@ async def _handle_create_observations(
async def _handle_update_peer_card(ctx: ToolContext, tool_input: dict[str, Any]) -> str:
"""Handle update_peer_card tool."""
# Check if peer card creation is disabled via configuration
if ctx.configuration is not None and not ctx.configuration.peer_card.create:
logger.info(
f"Peer card creation disabled for {ctx.workspace_name}, skipping update"
)
return (
"Peer card creation is disabled for this workspace/session configuration."
)
async with ctx.db_lock:
await crud.set_peer_card(
ctx.db,
@ -1544,6 +1556,7 @@ async def create_tool_executor(
current_messages: list[models.Message] | None = None,
include_observation_ids: bool = False,
history_token_limit: int = 8192,
configuration: ResolvedConfiguration | None = None,
) -> Callable[[str, dict[str, Any]], Any]:
"""
Create a unified tool executor function for all agent operations.
@ -1560,6 +1573,7 @@ async def create_tool_executor(
current_messages: List of current messages being processed (optional, for deriver)
include_observation_ids: If True, include observation IDs in output (for dreamer agent)
history_token_limit: Maximum tokens for get_recent_history (default: 8192)
configuration: Resolved configuration for checking feature flags (optional)
Returns:
An async callable that executes tools with the captured context
@ -1578,6 +1592,7 @@ async def create_tool_executor(
include_observation_ids=include_observation_ids,
history_token_limit=history_token_limit,
db_lock=shared_lock,
configuration=configuration,
)
async def execute_tool(tool_name: str, tool_input: dict[str, Any]) -> str: