From 4b01f8754de15ed3cd739707a9723bd291dbf6d7 Mon Sep 17 00:00:00 2001 From: Benjamin McCormick Date: Mon, 12 Jan 2026 18:25:35 -0500 Subject: [PATCH] feat: make dreamer and dialectic actually respect settings around using peer card --- sdks/python/src/honcho/conclusions.py | 2 +- src/dialectic/chat.py | 55 +++++--- src/dialectic/prompts.py | 19 ++- src/dreamer/dream_scheduler.py | 23 +++- src/dreamer/orchestrator.py | 11 ++ src/dreamer/specialists.py | 182 ++++++++++++++++++-------- src/schemas.py | 4 - src/utils/agent_tools.py | 15 +++ 8 files changed, 225 insertions(+), 86 deletions(-) diff --git a/sdks/python/src/honcho/conclusions.py b/sdks/python/src/honcho/conclusions.py index 7b5ff31d..faede1c9 100644 --- a/sdks/python/src/honcho/conclusions.py +++ b/sdks/python/src/honcho/conclusions.py @@ -197,7 +197,7 @@ class ConclusionScope: ], ) - def representation( + def get_representation( self, search_query: str | None = None, search_top_k: int | None = None, diff --git a/src/dialectic/chat.py b/src/dialectic/chat.py index fe4e2824..e2783db8 100644 --- a/src/dialectic/chat.py +++ b/src/dialectic/chat.py @@ -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( diff --git a/src/dialectic/prompts.py b/src/dialectic/prompts.py index 5430df6b..51ae0ccf 100644 --- a/src/dialectic/prompts.py +++ b/src/dialectic/prompts.py @@ -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):** diff --git a/src/dreamer/dream_scheduler.py b/src/dreamer/dream_scheduler.py index 035483a4..39f7057b 100644 --- a/src/dreamer/dream_scheduler.py +++ b/src/dreamer/dream_scheduler.py @@ -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, diff --git a/src/dreamer/orchestrator.py b/src/dreamer/orchestrator.py index f2a10c5d..56cd7ca3 100644 --- a/src/dreamer/orchestrator.py +++ b/src/dreamer/orchestrator.py @@ -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") diff --git a/src/dreamer/specialists.py b/src/dreamer/specialists.py index de0966cc..946dae89 100644 --- a/src/dreamer/specialists.py +++ b/src/dreamer/specialists.py @@ -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) diff --git a/src/schemas.py b/src/schemas.py index a63a5c34..9b482c7f 100644 --- a/src/schemas.py +++ b/src/schemas.py @@ -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): diff --git a/src/utils/agent_tools.py b/src/utils/agent_tools.py index c07eced9..62faea4f 100644 --- a/src/utils/agent_tools.py +++ b/src/utils/agent_tools.py @@ -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: