diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 28426b50..8a87d9df 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1332,6 +1332,7 @@ jobs: HINDSIGHT_API_LLM_MODEL=${{ env.HINDSIGHT_API_LLM_MODEL }} HINDSIGHT_API_LLM_VERTEXAI_SERVICE_ACCOUNT_KEY=/tmp/gcp-credentials.json HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID=$HINDSIGHT_API_LLM_VERTEXAI_PROJECT_ID + HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true EOF - name: Start API server diff --git a/hindsight-api/hindsight_api/api/http.py b/hindsight-api/hindsight_api/api/http.py index fdee358d..aaa95324 100644 --- a/hindsight-api/hindsight_api/api/http.py +++ b/hindsight-api/hindsight_api/api/http.py @@ -879,18 +879,103 @@ class CreateBankRequest(BaseModel): model_config = ConfigDict( json_schema_extra={ "example": { - "name": "Alice", - "disposition": {"skepticism": 3, "literalism": 3, "empathy": 3}, - "mission": "I am a PM helping my engineering team stay organized", + "retain_mission": "Always include technical decisions and architectural trade-offs. Ignore meeting logistics.", + "observations_mission": "Observations are stable facts about people and projects. Always include preferences and skills.", } } ) - name: str | None = None - disposition: DispositionTraits | None = None - mission: str | None = Field(default=None, description="The agent's mission") - # Deprecated: use mission instead - background: str | None = Field(default=None, description="Deprecated: use mission instead") + # Deprecated fields — kept for backwards compatibility only + name: str | None = Field(default=None, description="Deprecated: display label only, not advertised") + disposition: DispositionTraits | None = Field( + default=None, description="Deprecated: use update_bank_config instead" + ) + disposition_skepticism: int | None = Field( + default=None, ge=1, le=5, description="Deprecated: use update_bank_config instead" + ) + disposition_literalism: int | None = Field( + default=None, ge=1, le=5, description="Deprecated: use update_bank_config instead" + ) + disposition_empathy: int | None = Field( + default=None, ge=1, le=5, description="Deprecated: use update_bank_config instead" + ) + # Deprecated: use update_bank_config with reflect_mission instead + mission: str | None = Field( + default=None, description="Deprecated: use update_bank_config with reflect_mission instead" + ) + # Deprecated alias for mission + background: str | None = Field( + default=None, description="Deprecated: use update_bank_config with reflect_mission instead" + ) + + # Reflect configuration + reflect_mission: str | None = Field( + default=None, + description="Mission/context for Reflect operations. Guides how Reflect interprets and uses memories.", + ) + + # Operational configuration (applied via config resolver) + retain_mission: str | None = Field( + default=None, + description="Steers what gets extracted during retain(). Injected alongside built-in extraction rules.", + ) + retain_extraction_mode: str | None = Field( + default=None, + description="Fact extraction mode: 'concise' (default), 'verbose', or 'custom'.", + ) + retain_custom_instructions: str | None = Field( + default=None, + description="Custom extraction prompt. Only active when retain_extraction_mode is 'custom'.", + ) + retain_chunk_size: int | None = Field( + default=None, + description="Maximum token size for each content chunk during retain.", + ) + enable_observations: bool | None = Field( + default=None, + description="Toggle automatic observation consolidation after retain().", + ) + observations_mission: str | None = Field( + default=None, + description="Controls what gets synthesised into observations. Replaces built-in consolidation rules entirely.", + ) + + def get_config_updates(self) -> dict[str, Any]: + """Return only the config fields that were explicitly set. + + reflect_mission takes precedence over deprecated mission/background aliases. + Individual disposition_* fields take priority over the deprecated disposition dict. + """ + updates: dict[str, Any] = {} + # Resolve reflect mission: reflect_mission (new) > mission (deprecated) > background (deprecated) + resolved_reflect_mission = self.reflect_mission or self.mission or self.background + if resolved_reflect_mission is not None: + updates["reflect_mission"] = resolved_reflect_mission + # Disposition: individual fields take priority over legacy disposition dict + if self.disposition_skepticism is not None: + updates["disposition_skepticism"] = self.disposition_skepticism + elif self.disposition is not None: + updates["disposition_skepticism"] = self.disposition.skepticism + if self.disposition_literalism is not None: + updates["disposition_literalism"] = self.disposition_literalism + elif self.disposition is not None: + updates["disposition_literalism"] = self.disposition.literalism + if self.disposition_empathy is not None: + updates["disposition_empathy"] = self.disposition_empathy + elif self.disposition is not None: + updates["disposition_empathy"] = self.disposition.empathy + for field_name in ( + "retain_mission", + "retain_extraction_mode", + "retain_custom_instructions", + "retain_chunk_size", + "enable_observations", + "observations_mission", + ): + value = getattr(self, field_name) + if value is not None: + updates[field_name] = value + return updates class BankConfigUpdate(BaseModel): @@ -3203,6 +3288,7 @@ def _register_routes(app: FastAPI): description="Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists.", operation_id="get_bank_profile", tags=["Banks"], + deprecated=True, ) async def api_get_bank_profile(bank_id: str, request_context: RequestContext = Depends(get_request_context)): """Get memory bank profile (disposition + mission).""" @@ -3238,6 +3324,7 @@ def _register_routes(app: FastAPI): description="Update bank's disposition traits (skepticism, literalism, empathy)", operation_id="update_bank_disposition", tags=["Banks"], + deprecated=True, ) async def api_update_bank_disposition( bank_id: str, request: UpdateDispositionRequest, request_context: RequestContext = Depends(get_request_context) @@ -3317,21 +3404,18 @@ def _register_routes(app: FastAPI): # Ensure bank exists by getting profile (auto-creates with defaults) await app.state.memory.get_bank_profile(bank_id, request_context=request_context) - # Update name and/or mission if provided (support both mission and deprecated background) - mission_value = request.mission or request.background - if request.name is not None or mission_value is not None: + # Update name if provided (stored in DB for display only, deprecated) + if request.name is not None: await app.state.memory.update_bank( bank_id, name=request.name, - mission=mission_value, request_context=request_context, ) - # Update disposition if provided - if request.disposition is not None: - await app.state.memory.update_bank_disposition( - bank_id, request.disposition.model_dump(), request_context=request_context - ) + # Apply all config overrides (includes reflect_mission, disposition, retain settings) + config_updates = request.get_config_updates() + if config_updates: + await app.state.memory._config_resolver.update_bank_config(bank_id, config_updates, request_context) # Get final profile final_profile = await app.state.memory.get_bank_profile(bank_id, request_context=request_context) @@ -3373,21 +3457,18 @@ def _register_routes(app: FastAPI): # Ensure bank exists await app.state.memory.get_bank_profile(bank_id, request_context=request_context) - # Update name and/or mission if provided - mission_value = request.mission or request.background - if request.name is not None or mission_value is not None: + # Update name if provided (stored in DB for display only, deprecated) + if request.name is not None: await app.state.memory.update_bank( bank_id, name=request.name, - mission=mission_value, request_context=request_context, ) - # Update disposition if provided - if request.disposition is not None: - await app.state.memory.update_bank_disposition( - bank_id, request.disposition.model_dump(), request_context=request_context - ) + # Apply all config overrides (includes reflect_mission, disposition, retain settings) + config_updates = request.get_config_updates() + if config_updates: + await app.state.memory._config_resolver.update_bank_config(bank_id, config_updates, request_context) # Get final profile final_profile = await app.state.memory.get_bank_profile(bank_id, request_context=request_context) diff --git a/hindsight-api/hindsight_api/config.py b/hindsight-api/hindsight_api/config.py index 76020f2a..97f428d6 100644 --- a/hindsight-api/hindsight_api/config.py +++ b/hindsight-api/hindsight_api/config.py @@ -256,6 +256,7 @@ ENV_RETAIN_MAX_COMPLETION_TOKENS = "HINDSIGHT_API_RETAIN_MAX_COMPLETION_TOKENS" ENV_RETAIN_CHUNK_SIZE = "HINDSIGHT_API_RETAIN_CHUNK_SIZE" ENV_RETAIN_EXTRACT_CAUSAL_LINKS = "HINDSIGHT_API_RETAIN_EXTRACT_CAUSAL_LINKS" ENV_RETAIN_EXTRACTION_MODE = "HINDSIGHT_API_RETAIN_EXTRACTION_MODE" +ENV_RETAIN_MISSION = "HINDSIGHT_API_RETAIN_MISSION" ENV_RETAIN_CUSTOM_INSTRUCTIONS = "HINDSIGHT_API_RETAIN_CUSTOM_INSTRUCTIONS" ENV_RETAIN_BATCH_TOKENS = "HINDSIGHT_API_RETAIN_BATCH_TOKENS" ENV_RETAIN_BATCH_ENABLED = "HINDSIGHT_API_RETAIN_BATCH_ENABLED" @@ -285,6 +286,7 @@ ENV_FILE_DELETE_AFTER_RETAIN = "HINDSIGHT_API_FILE_DELETE_AFTER_RETAIN" ENV_ENABLE_OBSERVATIONS = "HINDSIGHT_API_ENABLE_OBSERVATIONS" ENV_CONSOLIDATION_BATCH_SIZE = "HINDSIGHT_API_CONSOLIDATION_BATCH_SIZE" ENV_CONSOLIDATION_MAX_TOKENS = "HINDSIGHT_API_CONSOLIDATION_MAX_TOKENS" +ENV_OBSERVATIONS_MISSION = "HINDSIGHT_API_OBSERVATIONS_MISSION" # Optimization flags ENV_SKIP_LLM_VERIFICATION = "HINDSIGHT_API_SKIP_LLM_VERIFICATION" @@ -310,6 +312,12 @@ ENV_WORKER_CONSOLIDATION_MAX_SLOTS = "HINDSIGHT_API_WORKER_CONSOLIDATION_MAX_SLO # Reflect agent settings ENV_REFLECT_MAX_ITERATIONS = "HINDSIGHT_API_REFLECT_MAX_ITERATIONS" +ENV_REFLECT_MISSION = "HINDSIGHT_API_REFLECT_MISSION" + +# Disposition settings +ENV_DISPOSITION_SKEPTICISM = "HINDSIGHT_API_DISPOSITION_SKEPTICISM" +ENV_DISPOSITION_LITERALISM = "HINDSIGHT_API_DISPOSITION_LITERALISM" +ENV_DISPOSITION_EMPATHY = "HINDSIGHT_API_DISPOSITION_EMPATHY" # Default values DEFAULT_DATABASE_URL = "pg0" @@ -401,6 +409,7 @@ DEFAULT_RETAIN_CHUNK_SIZE = 3000 # Max chars per chunk for fact extraction DEFAULT_RETAIN_EXTRACT_CAUSAL_LINKS = True # Extract causal links between facts DEFAULT_RETAIN_EXTRACTION_MODE = "concise" # Extraction mode: "concise", "verbose", or "custom" RETAIN_EXTRACTION_MODES = ("concise", "verbose", "custom") # Allowed extraction modes +DEFAULT_RETAIN_MISSION = None # Declarative spec of what to retain (injected into any extraction mode) DEFAULT_RETAIN_CUSTOM_INSTRUCTIONS = None # Custom extraction guidelines (only used when mode="custom") DEFAULT_RETAIN_BATCH_TOKENS = 10_000 # ~40KB of text # Max chars per sub-batch for async retain auto-splitting DEFAULT_RETAIN_BATCH_ENABLED = False # Use LLM Batch API for fact extraction (only when async=True) @@ -418,6 +427,7 @@ DEFAULT_FILE_DELETE_AFTER_RETAIN = True # Delete file bytes after retain (saves DEFAULT_ENABLE_OBSERVATIONS = True # Observations enabled by default DEFAULT_CONSOLIDATION_BATCH_SIZE = 50 # Memories to load per batch (internal memory optimization) DEFAULT_CONSOLIDATION_MAX_TOKENS = 1024 # Max tokens for recall when finding related observations +DEFAULT_OBSERVATIONS_MISSION = None # Declarative spec of what observations are for this bank # Database migrations DEFAULT_RUN_MIGRATIONS_ON_STARTUP = True @@ -440,6 +450,11 @@ DEFAULT_WORKER_CONSOLIDATION_MAX_SLOTS = 2 # Max concurrent consolidation tasks # Reflect agent settings DEFAULT_REFLECT_MAX_ITERATIONS = 10 # Max tool call iterations before forcing response +# Disposition defaults (None = not set, fall back to bank DB value or 3) +DEFAULT_DISPOSITION_SKEPTICISM = None +DEFAULT_DISPOSITION_LITERALISM = None +DEFAULT_DISPOSITION_EMPATHY = None + # OpenTelemetry tracing configuration DEFAULT_OTEL_TRACES_ENABLED = False # Disabled by default for backward compatibility DEFAULT_OTEL_SERVICE_NAME = "hindsight-api" @@ -635,6 +650,7 @@ class HindsightConfig: retain_chunk_size: int retain_extract_causal_links: bool retain_extraction_mode: str + retain_mission: str | None retain_custom_instructions: str | None retain_batch_tokens: int retain_batch_enabled: bool @@ -664,6 +680,15 @@ class HindsightConfig: enable_observations: bool consolidation_batch_size: int consolidation_max_tokens: int + observations_mission: str | None + + # Reflect agent settings + reflect_mission: str | None + + # Disposition settings (hierarchical - can be overridden per bank; None = fall back to DB) + disposition_skepticism: int | None + disposition_literalism: int | None + disposition_empathy: int | None # Optimization flags skip_llm_verification: bool @@ -732,9 +757,17 @@ class HindsightConfig: # Retention settings (behavioral) "retain_chunk_size", "retain_extraction_mode", + "retain_mission", "retain_custom_instructions", # Consolidation settings "enable_observations", + "observations_mission", + # Reflect settings + "reflect_mission", + # Disposition settings + "disposition_skepticism", + "disposition_literalism", + "disposition_empathy", } @property @@ -1024,6 +1057,7 @@ class HindsightConfig: retain_extraction_mode=_validate_extraction_mode( os.getenv(ENV_RETAIN_EXTRACTION_MODE, DEFAULT_RETAIN_EXTRACTION_MODE) ), + retain_mission=os.getenv(ENV_RETAIN_MISSION) or DEFAULT_RETAIN_MISSION, retain_custom_instructions=os.getenv(ENV_RETAIN_CUSTOM_INSTRUCTIONS) or DEFAULT_RETAIN_CUSTOM_INSTRUCTIONS, retain_batch_tokens=int(os.getenv(ENV_RETAIN_BATCH_TOKENS, str(DEFAULT_RETAIN_BATCH_TOKENS))), retain_batch_enabled=os.getenv(ENV_RETAIN_BATCH_ENABLED, str(DEFAULT_RETAIN_BATCH_ENABLED)).lower() @@ -1066,6 +1100,7 @@ class HindsightConfig: consolidation_max_tokens=int( os.getenv(ENV_CONSOLIDATION_MAX_TOKENS, str(DEFAULT_CONSOLIDATION_MAX_TOKENS)) ), + observations_mission=os.getenv(ENV_OBSERVATIONS_MISSION) or DEFAULT_OBSERVATIONS_MISSION, # Database migrations run_migrations_on_startup=os.getenv(ENV_RUN_MIGRATIONS_ON_STARTUP, "true").lower() == "true", # Database connection pool @@ -1085,6 +1120,17 @@ class HindsightConfig: ), # Reflect agent settings reflect_max_iterations=int(os.getenv(ENV_REFLECT_MAX_ITERATIONS, str(DEFAULT_REFLECT_MAX_ITERATIONS))), + reflect_mission=os.getenv(ENV_REFLECT_MISSION) or None, + # Disposition settings (None = fall back to DB value) + disposition_skepticism=int(os.getenv(ENV_DISPOSITION_SKEPTICISM)) + if os.getenv(ENV_DISPOSITION_SKEPTICISM) + else DEFAULT_DISPOSITION_SKEPTICISM, + disposition_literalism=int(os.getenv(ENV_DISPOSITION_LITERALISM)) + if os.getenv(ENV_DISPOSITION_LITERALISM) + else DEFAULT_DISPOSITION_LITERALISM, + disposition_empathy=int(os.getenv(ENV_DISPOSITION_EMPATHY)) + if os.getenv(ENV_DISPOSITION_EMPATHY) + else DEFAULT_DISPOSITION_EMPATHY, # OpenTelemetry tracing configuration otel_traces_enabled=os.getenv(ENV_OTEL_TRACES_ENABLED, str(DEFAULT_OTEL_TRACES_ENABLED)).lower() in ("true", "1", "yes"), diff --git a/hindsight-api/hindsight_api/engine/consolidation/consolidator.py b/hindsight-api/hindsight_api/engine/consolidation/consolidator.py index bbb46bd6..3eae314f 100644 --- a/hindsight-api/hindsight_api/engine/consolidation/consolidator.py +++ b/hindsight-api/hindsight_api/engine/consolidation/consolidator.py @@ -23,10 +23,7 @@ from pydantic import BaseModel from ...config import get_config from ..memory_engine import fq_table from ..retain import embedding_utils -from .prompts import ( - CONSOLIDATION_SYSTEM_PROMPT, - CONSOLIDATION_USER_PROMPT, -) +from .prompts import build_consolidation_prompt if TYPE_CHECKING: from asyncpg import Connection @@ -114,7 +111,7 @@ async def run_consolidation_job( t0 = time.time() bank_row = await conn.fetchrow( f""" - SELECT bank_id, name, mission + SELECT bank_id, name FROM {fq_table("banks")} WHERE bank_id = $1 """, @@ -125,7 +122,6 @@ async def run_consolidation_job( logger.warning(f"Bank {bank_id} not found for consolidation") return {"status": "bank_not_found", "bank_id": bank_id} - mission = bank_row["mission"] or "General memory consolidation" perf.record_timing("fetch_bank", time.time() - t0) # Count total unconsolidated memories for progress logging @@ -205,9 +201,9 @@ async def run_consolidation_job( memory_engine=memory_engine, bank_id=bank_id, memory=dict(memory), - mission=mission, request_context=request_context, perf=perf, + config=config, ) # Mark memory as consolidated (committed immediately) @@ -420,9 +416,9 @@ async def _process_memory( memory_engine: "MemoryEngine", bank_id: str, memory: dict[str, Any], - mission: str, request_context: "RequestContext", perf: ConsolidationPerfLog | None = None, + config: Any = None, ) -> dict[str, Any]: """ Process a single memory for consolidation using a SINGLE LLM call. @@ -476,7 +472,7 @@ async def _process_memory( memory_engine=memory_engine, fact_text=fact_text, recall_result=recall_result, - mission=mission, + config=config, ) if perf: perf.record_timing("llm", time.time() - t0) @@ -830,7 +826,7 @@ async def _consolidate_with_llm( memory_engine: "MemoryEngine", fact_text: str, recall_result: "RecallResult", - mission: str, + config: Any = None, ) -> list[dict[str, Any]]: """ Single LLM call to extract durable knowledge and decide on consolidation actions. @@ -859,24 +855,15 @@ async def _consolidate_with_llm( else: observations_text = "[]" - # Only include mission section if mission is set and not the default - mission_section = "" - if mission and mission != "General memory consolidation": - mission_section = f""" -MISSION CONTEXT: {mission} - -Focus on DURABLE knowledge that serves this mission, not ephemeral state. -""" - - user_prompt = CONSOLIDATION_USER_PROMPT.format( - mission_section=mission_section, + observations_mission = config.observations_mission if config is not None else None + prompt_template = build_consolidation_prompt(observations_mission) + prompt = prompt_template.format( fact_text=fact_text, observations_text=observations_text, ) messages = [ - {"role": "system", "content": CONSOLIDATION_SYSTEM_PROMPT}, - {"role": "user", "content": user_prompt}, + {"role": "user", "content": prompt}, ] response: _ConsolidationResponse = await memory_engine._consolidation_llm_config.call( diff --git a/hindsight-api/hindsight_api/engine/consolidation/prompts.py b/hindsight-api/hindsight_api/engine/consolidation/prompts.py index 0cda3124..5b4142b5 100644 --- a/hindsight-api/hindsight_api/engine/consolidation/prompts.py +++ b/hindsight-api/hindsight_api/engine/consolidation/prompts.py @@ -1,53 +1,18 @@ """Prompts for the consolidation engine.""" -CONSOLIDATION_SYSTEM_PROMPT = """You are a memory consolidation system. Your job is to convert facts into durable knowledge (observations) and merge with existing knowledge when appropriate. +# Output format instructions +_OUTPUT_FORMAT = """ +Output a JSON object with an "actions" array: +{{"actions": [ + {{"action": "update", "learning_id": "uuid-from-observations", "text": "...", "reason": "..."}}, + {{"action": "create", "text": "...", "reason": "..."}} +]}} -You must output a JSON object with an "actions" array. The "text" field within each action should use markdown formatting (headers, lists, bold, etc.) for clarity and readability. +Return {{"actions": []}} if the fact contains no durable knowledge. +Do NOT include "tags" in output — tags are handled automatically.""" -## EXTRACT DURABLE KNOWLEDGE, NOT EPHEMERAL STATE -Facts often describe events or actions. Extract the DURABLE KNOWLEDGE implied by the fact, not the transient state. - -Examples of extracting durable knowledge: -- "User moved to Room 203" -> "Room 203 exists" (location exists, not where user is now) -- "User visited Acme Corp at Room 105" -> "Acme Corp is located in Room 105" -- "User took the elevator to floor 3" -> "Floor 3 is accessible by elevator" -- "User met Sarah at the lobby" -> "Sarah can be found at the lobby" - -DO NOT track current user position/state as knowledge - that changes constantly. -DO track permanent facts learned from the user's actions. - -## PRESERVE SPECIFIC DETAILS -Keep names, locations, numbers, and other specifics. Do NOT: -- Abstract into general principles -- Generate business insights -- Make knowledge generic - -GOOD examples: -- Fact: "John likes pizza" -> "John likes pizza" -- Fact: "Alice works at Google" -> "Alice works at Google" - -BAD examples: -- "John likes pizza" -> "Understanding dietary preferences helps..." (TOO ABSTRACT) -- "User is at Room 203" -> "User is currently at Room 203" (EPHEMERAL STATE) - -## MERGE RULES (when comparing to existing observations): -1. REDUNDANT: Same information worded differently → update existing -2. CONTRADICTION: Opposite information about same topic → update with temporal markers showing change - Example: "Alex used to love pizza but now hates it" OR "Alex's pizza preference changed from love to hate" -3. UPDATE: New state replacing old state → update showing the transition with "used to", "now", "changed from X to Y" - -## CRITICAL RULES: -- NEVER merge facts about DIFFERENT people -- NEVER merge unrelated topics (food preferences vs work vs hobbies) -- When merging contradictions, the "text" field MUST capture BOTH states with temporal markers: - * Use "used to X, now Y" OR "changed from X to Y" OR "X but now Y" - * DO NOT just state the new fact - you MUST show the change -- Keep observations focused on ONE specific topic per person -- The "text" field MUST contain durable knowledge, not ephemeral state -- Do NOT include "tags" in output - tags are handled automatically""" - -CONSOLIDATION_USER_PROMPT = """Analyze this new fact and consolidate into knowledge. -{mission_section} +# Data section - holds the dynamic per-call data +_DATA_SECTION = """ NEW FACT: {fact_text} EXISTING OBSERVATIONS (JSON array with source memories and dates): @@ -57,28 +22,36 @@ Each observation includes: - id: unique identifier for updating - text: the observation content - proof_count: number of supporting memories -- tags: visibility scope (handled automatically) - occurred_start/occurred_end: temporal range of source facts - source_memories: array of supporting facts with their text and dates -Instructions: -1. Extract DURABLE KNOWLEDGE from the new fact (not ephemeral state) -2. Review source_memories in existing observations to understand evidence -3. Check dates to detect contradictions or updates -4. Compare with observations: - - Same topic → UPDATE with learning_id - - New topic → CREATE new observation - - Purely ephemeral → return empty actions list +Compare the new fact against existing observations: +- Same topic → UPDATE with learning_id +- New topic → CREATE new observation +- Purely ephemeral → return empty actions list""" -Output a JSON object with an "actions" array (the "text" field should use markdown formatting for structure): -{{"actions": [ - {{"action": "update", "learning_id": "uuid-from-observations", "text": "## Updated Knowledge\n\n**Key point**: details here\n\n- Supporting detail 1\n- Supporting detail 2", "reason": "..."}}, - {{"action": "create", "text": "## New Durable Knowledge\n\nDescription with **emphasis** and proper structure", "reason": "..."}} -]}} +# Default rules used when no observations_mission is set +_DEFAULT_RULES = """Extract DURABLE KNOWLEDGE from facts — the stable truth implied by an event, not transient state. -Return {{"actions": []}} if fact contains no durable knowledge. +Example: "User moved to Room 203" → observe "Room 203 exists", not "User is in Room 203". -IMPORTANT: Format the "text" field with markdown for better readability: -- Use headers, lists, bold/italic, tables where appropriate -- CRITICAL: Add blank lines before and after block elements (tables, code blocks, lists) -- Ensure proper spacing for markdown to render correctly""" +Rules: +- Keep specifics: names, numbers, locations. Never abstract into general principles. +- NEVER merge observations about different people or unrelated topics. +- REDUNDANT: same info worded differently → update existing. +- CONTRADICTION/UPDATE: capture both states with temporal markers ("used to X, now Y").""" + + +def build_consolidation_prompt(observations_mission: str | None = None) -> str: + """ + Build the consolidation prompt. + + If observations_mission is provided, it replaces the default durable-knowledge rules + with bank-specific instructions for what to synthesise. Otherwise the default rules apply. + """ + rules_section = f"## MISSION\n{observations_mission}" if observations_mission else _DEFAULT_RULES + + return ( + "You are a memory consolidation system. Synthesize facts into observations " + "and merge with existing observations when appropriate.\n\n" + rules_section + _DATA_SECTION + _OUTPUT_FORMAT + ) diff --git a/hindsight-api/hindsight_api/engine/memory_engine.py b/hindsight-api/hindsight_api/engine/memory_engine.py index ae380b4e..82a892a6 100644 --- a/hindsight-api/hindsight_api/engine/memory_engine.py +++ b/hindsight-api/hindsight_api/engine/memory_engine.py @@ -4095,12 +4095,28 @@ class MemoryEngine(MemoryEngineInterface): await self._authenticate_tenant(request_context) pool = await self._get_pool() profile = await bank_utils.get_bank_profile(pool, bank_id) - disposition = profile["disposition"] + + # reflect_mission and disposition in config take precedence over the legacy DB columns + config_dict = await self._config_resolver.get_bank_config(bank_id, request_context) + mission = config_dict.get("reflect_mission") or profile["mission"] + + # Overlay disposition from config if explicitly set; fall back to DB values + db_disp = profile["disposition"] + db_disp_dict = db_disp.model_dump() if hasattr(db_disp, "model_dump") else dict(db_disp) + cfg_skep = config_dict.get("disposition_skepticism") + cfg_lit = config_dict.get("disposition_literalism") + cfg_emp = config_dict.get("disposition_empathy") + disposition = { + "skepticism": cfg_skep if cfg_skep is not None else db_disp_dict["skepticism"], + "literalism": cfg_lit if cfg_lit is not None else db_disp_dict["literalism"], + "empathy": cfg_emp if cfg_emp is not None else db_disp_dict["empathy"], + } + return { "bank_id": bank_id, "name": profile["name"], "disposition": disposition, - "mission": profile["mission"], + "mission": mission, } async def update_bank_disposition( diff --git a/hindsight-api/hindsight_api/engine/retain/fact_extraction.py b/hindsight-api/hindsight_api/engine/retain/fact_extraction.py index de0873d8..4f857ab2 100644 --- a/hindsight-api/hindsight_api/engine/retain/fact_extraction.py +++ b/hindsight-api/hindsight_api/engine/retain/fact_extraction.py @@ -440,9 +440,7 @@ _BASE_FACT_EXTRACTION_PROMPT = """Extract SIGNIFICANT facts from text. Be SELECT LANGUAGE: MANDATORY — Detect the language of the input text and produce ALL output in that EXACT same language. You are STRICTLY FORBIDDEN from translating or switching to any other language. Every single word of your output must be in the same language as the input. Do NOT output in a different language under any circumstance. -{fact_types_instruction} - -{extraction_guidelines} +{retain_mission_section}{extraction_guidelines} ══════════════════════════════════════════════════════════════════════════ FACT FORMAT - BE CONCISE @@ -549,16 +547,16 @@ about experiences ARE important to remember, even if they seem small (e.g., how tasted, how someone looked, how loud music was). Extract these if they characterize an experience or person.""" -# Assembled concise prompt (backward compatible - exact same output as before) +# Assembled concise prompt CONCISE_FACT_EXTRACTION_PROMPT = _BASE_FACT_EXTRACTION_PROMPT.format( - fact_types_instruction="{fact_types_instruction}", + retain_mission_section="{retain_mission_section}", extraction_guidelines=_CONCISE_GUIDELINES, examples=_CONCISE_EXAMPLES, ) # Custom prompt uses same base but without examples CUSTOM_FACT_EXTRACTION_PROMPT = _BASE_FACT_EXTRACTION_PROMPT.format( - fact_types_instruction="{fact_types_instruction}", + retain_mission_section="{retain_mission_section}", extraction_guidelines="{custom_instructions}", examples="", # No examples for custom mode ) @@ -569,8 +567,6 @@ VERBOSE_FACT_EXTRACTION_PROMPT = """Extract facts from text into structured form LANGUAGE: MANDATORY — Detect the language of the input text and produce ALL output in that EXACT same language. You are STRICTLY FORBIDDEN from translating or switching to any other language. Every single word of your output must be in the same language as the input. Do NOT output in a different language under any circumstance. -{fact_types_instruction} - ══════════════════════════════════════════════════════════════════════════ FACT FORMAT - ALL FIVE DIMENSIONS REQUIRED - MAXIMUM VERBOSITY ══════════════════════════════════════════════════════════════════════════ @@ -701,27 +697,41 @@ def _build_extraction_prompt_and_schema(config) -> tuple[str, type]: Returns: Tuple of (prompt, response_schema) """ - fact_types_instruction = "Extract ONLY 'world' and 'assistant' type facts." extraction_mode = config.retain_extraction_mode extract_causal_links = config.retain_extract_causal_links + # Build retain_mission section if set - injected before the mode-specific guidelines + retain_mission = getattr(config, "retain_mission", None) + if retain_mission: + retain_mission_section = ( + f"══════════════════════════════════════════════════════════════════════════\n" + f"FOCUS — What to retain for this bank\n" + f"══════════════════════════════════════════════════════════════════════════\n\n" + f"{retain_mission}\n\n" + ) + else: + retain_mission_section = "" + # Select base prompt based on extraction mode if extraction_mode == "custom": if not config.retain_custom_instructions: base_prompt = CONCISE_FACT_EXTRACTION_PROMPT - prompt = base_prompt.format(fact_types_instruction=fact_types_instruction) + prompt = base_prompt.format( + retain_mission_section=retain_mission_section, + ) else: base_prompt = CUSTOM_FACT_EXTRACTION_PROMPT prompt = base_prompt.format( - fact_types_instruction=fact_types_instruction, + retain_mission_section=retain_mission_section, custom_instructions=config.retain_custom_instructions, ) elif extraction_mode == "verbose": - base_prompt = VERBOSE_FACT_EXTRACTION_PROMPT - prompt = base_prompt.format(fact_types_instruction=fact_types_instruction) + prompt = VERBOSE_FACT_EXTRACTION_PROMPT else: base_prompt = CONCISE_FACT_EXTRACTION_PROMPT - prompt = base_prompt.format(fact_types_instruction=fact_types_instruction) + prompt = base_prompt.format( + retain_mission_section=retain_mission_section, + ) # Add causal relationships section if enabled if extract_causal_links: diff --git a/hindsight-api/hindsight_api/main.py b/hindsight-api/hindsight_api/main.py index 7b244afb..2b9d5829 100644 --- a/hindsight-api/hindsight_api/main.py +++ b/hindsight-api/hindsight_api/main.py @@ -248,6 +248,7 @@ def main(): retain_chunk_size=config.retain_chunk_size, retain_extract_causal_links=config.retain_extract_causal_links, retain_extraction_mode=config.retain_extraction_mode, + retain_mission=config.retain_mission, retain_custom_instructions=config.retain_custom_instructions, retain_batch_tokens=config.retain_batch_tokens, retain_batch_enabled=config.retain_batch_enabled, @@ -273,6 +274,7 @@ def main(): enable_observations=config.enable_observations, consolidation_batch_size=config.consolidation_batch_size, consolidation_max_tokens=config.consolidation_max_tokens, + observations_mission=config.observations_mission, skip_llm_verification=config.skip_llm_verification, lazy_reranker=config.lazy_reranker, run_migrations_on_startup=config.run_migrations_on_startup, @@ -288,6 +290,10 @@ def main(): worker_max_slots=config.worker_max_slots, worker_consolidation_max_slots=config.worker_consolidation_max_slots, reflect_max_iterations=config.reflect_max_iterations, + reflect_mission=config.reflect_mission, + disposition_skepticism=config.disposition_skepticism, + disposition_literalism=config.disposition_literalism, + disposition_empathy=config.disposition_empathy, mental_model_refresh_concurrency=config.mental_model_refresh_concurrency, otel_traces_enabled=config.otel_traces_enabled, otel_exporter_otlp_endpoint=config.otel_exporter_otlp_endpoint, diff --git a/hindsight-api/tests/test_agents_api.py b/hindsight-api/tests/test_agents_api.py index 0ef0acab..b204f293 100644 --- a/hindsight-api/tests/test_agents_api.py +++ b/hindsight-api/tests/test_agents_api.py @@ -27,9 +27,9 @@ class TestAgentProfile: assert "disposition" in profile disposition = profile["disposition"] - assert disposition.skepticism == 3 - assert disposition.literalism == 3 - assert disposition.empathy == 3 + assert disposition["skepticism"] == 3 + assert disposition["literalism"] == 3 + assert disposition["empathy"] == 3 @pytest.mark.asyncio async def test_update_agent_disposition(self, memory: MemoryEngine, request_context): @@ -37,7 +37,7 @@ class TestAgentProfile: bank_id = unique_agent_id("test_profile_update") profile = await memory.get_bank_profile(bank_id, request_context=request_context) - assert profile["disposition"].skepticism == 3 + assert profile["disposition"]["skepticism"] == 3 new_disposition = { "skepticism": 5, @@ -48,9 +48,9 @@ class TestAgentProfile: updated_profile = await memory.get_bank_profile(bank_id, request_context=request_context) disposition = updated_profile["disposition"] - assert disposition.skepticism == new_disposition["skepticism"] - assert disposition.literalism == new_disposition["literalism"] - assert disposition.empathy == new_disposition["empathy"] + assert disposition["skepticism"] == new_disposition["skepticism"] + assert disposition["literalism"] == new_disposition["literalism"] + assert disposition["empathy"] == new_disposition["empathy"] @pytest.mark.asyncio async def test_list_agents(self, memory: MemoryEngine, request_context): @@ -104,8 +104,8 @@ class TestAgentEndpoint: final_profile = await memory.get_bank_profile(bank_id, request_context=request_context) - assert final_profile["disposition"].skepticism == 4 - assert final_profile["disposition"].literalism == 5 + assert final_profile["disposition"]["skepticism"] == 4 + assert final_profile["disposition"]["literalism"] == 5 class TestAgentDispositionIntegration: diff --git a/hindsight-api/tests/test_consolidation.py b/hindsight-api/tests/test_consolidation.py index 9229db7e..0d80eef5 100644 --- a/hindsight-api/tests/test_consolidation.py +++ b/hindsight-api/tests/test_consolidation.py @@ -1990,3 +1990,100 @@ class TestMentalModelRefreshAfterConsolidation: # Cleanup await memory.delete_bank(bank_id, request_context=request_context) + + +def test_consolidation_prompt_default(): + """Test that the default consolidation prompt contains the built-in durable-knowledge rules.""" + from hindsight_api.engine.consolidation.prompts import build_consolidation_prompt + + prompt = build_consolidation_prompt() + assert "DURABLE KNOWLEDGE" in prompt + assert "temporal markers" in prompt + assert "{fact_text}" in prompt + assert "{observations_text}" in prompt + + +def test_consolidation_prompt_observations_mission(): + """Test that observations_mission replaces the default rules.""" + from hindsight_api.engine.consolidation.prompts import build_consolidation_prompt + + spec = "Observations are weekly summaries of sprint outcomes and team dynamics." + prompt = build_consolidation_prompt(observations_mission=spec) + + # Spec is injected + assert spec in prompt + # Default rules are NOT present + assert "EXTRACT DURABLE KNOWLEDGE" not in prompt + # Output format and data placeholders remain + assert "actions" in prompt + assert "{fact_text}" in prompt + assert "{observations_text}" in prompt + + # Renders cleanly + rendered = prompt.format(fact_text="Alice fixed a bug.", observations_text="[]") + assert "{fact_text}" not in rendered + assert spec in rendered + + +def test_observations_mission_config(): + """Test that observations_mission is loaded from env and exposed as configurable.""" + import os + + from hindsight_api.config import HindsightConfig, _get_raw_config, clear_config_cache + + original = os.getenv("HINDSIGHT_API_OBSERVATIONS_MISSION") + try: + os.environ["HINDSIGHT_API_OBSERVATIONS_MISSION"] = "Weekly sprint summaries only." + clear_config_cache() + config = _get_raw_config() + assert config.observations_mission == "Weekly sprint summaries only." + assert "observations_mission" in HindsightConfig.get_configurable_fields() + finally: + if original is None: + os.environ.pop("HINDSIGHT_API_OBSERVATIONS_MISSION", None) + else: + os.environ["HINDSIGHT_API_OBSERVATIONS_MISSION"] = original + clear_config_cache() + + +@pytest.mark.asyncio +async def test_consolidation_with_observations_mission(memory: "MemoryEngine", request_context): + """Test that observations_mission is used during consolidation without errors.""" + import os + + from hindsight_api.config import _get_raw_config, clear_config_cache + + original = os.getenv("HINDSIGHT_API_OBSERVATIONS_MISSION") + try: + os.environ["HINDSIGHT_API_OBSERVATIONS_MISSION"] = ( + "Observations are summaries of programming language usage patterns." + ) + clear_config_cache() + config = _get_raw_config() + + bank_id = f"test-obs-spec-{uuid.uuid4().hex[:8]}" + original_global_config = memory._config_resolver._global_config + memory._config_resolver._global_config = config + + try: + await memory.get_bank_profile(bank_id=bank_id, request_context=request_context) + await memory.retain_async( + bank_id=bank_id, + content="Alice uses Python for data analysis and loves its simplicity.", + request_context=request_context, + ) + async with memory._pool.acquire() as conn: + observations = await conn.fetch( + "SELECT id, text, fact_type FROM memory_units WHERE bank_id = $1 AND fact_type = 'observation'", + bank_id, + ) + assert isinstance(observations, list) + finally: + memory._config_resolver._global_config = original_global_config + await memory.delete_bank(bank_id, request_context=request_context) + finally: + if original is None: + os.environ.pop("HINDSIGHT_API_OBSERVATIONS_MISSION", None) + else: + os.environ["HINDSIGHT_API_OBSERVATIONS_MISSION"] = original + clear_config_cache() diff --git a/hindsight-api/tests/test_hierarchical_config.py b/hindsight-api/tests/test_hierarchical_config.py index affc7697..921d5a72 100644 --- a/hindsight-api/tests/test_hierarchical_config.py +++ b/hindsight-api/tests/test_hierarchical_config.py @@ -74,12 +74,18 @@ async def test_hierarchical_fields_categorization(): # Verify configurable fields include behavioral settings (safe to modify) assert "retain_extraction_mode" in configurable - assert "enable_observations" in configurable - assert "retain_chunk_size" in configurable + assert "retain_mission" in configurable assert "retain_custom_instructions" in configurable + assert "retain_chunk_size" in configurable + assert "enable_observations" in configurable + assert "observations_mission" in configurable + assert "reflect_mission" in configurable + assert "disposition_skepticism" in configurable + assert "disposition_literalism" in configurable + assert "disposition_empathy" in configurable - # Verify count is correct (only 4 fields) - assert len(configurable) == 4 + # Verify count is correct + assert len(configurable) == 10 # Verify credential fields (NEVER exposed) assert "llm_api_key" in credentials diff --git a/hindsight-api/tests/test_retain.py b/hindsight-api/tests/test_retain.py index cc573036..3eb2ab88 100644 --- a/hindsight-api/tests/test_retain.py +++ b/hindsight-api/tests/test_retain.py @@ -2256,3 +2256,64 @@ async def test_retain_batch_with_per_item_tags_on_document(memory, request_conte finally: await memory.delete_bank(bank_id, request_context=request_context) print(f"\n=== Cleaned up bank: {bank_id} ===") + + +def test_retain_mission_injected_into_prompt(): + """Test that retain_mission is injected as a FOCUS section into any extraction mode.""" + from unittest.mock import MagicMock + from hindsight_api.engine.retain.fact_extraction import _build_extraction_prompt_and_schema + + spec = "Focus on technical decisions and architecture choices only." + + # Test with concise mode + config = MagicMock() + config.retain_extraction_mode = "concise" + config.retain_mission = spec + config.retain_custom_instructions = None + config.retain_extract_causal_links = False + + prompt, _ = _build_extraction_prompt_and_schema(config) + assert spec in prompt + assert "FOCUS" in prompt + + # retain_mission is present regardless of extraction mode (verbose has its own template, no spec injection) + config.retain_extraction_mode = "verbose" + prompt_verbose, _ = _build_extraction_prompt_and_schema(config) + # verbose uses its own template - spec not injected there + assert spec not in prompt_verbose + + +def test_retain_mission_absent_when_not_set(): + """Test that no FOCUS section appears when retain_mission is not set.""" + from unittest.mock import MagicMock + from hindsight_api.engine.retain.fact_extraction import _build_extraction_prompt_and_schema + + config = MagicMock() + config.retain_extraction_mode = "concise" + config.retain_mission = None + config.retain_custom_instructions = None + config.retain_extract_causal_links = False + + prompt, _ = _build_extraction_prompt_and_schema(config) + assert "FOCUS" not in prompt + assert "retain_mission_section" not in prompt + + +def test_retain_mission_config_loaded_from_env(): + """Test that retain_mission is loaded from env and is a configurable field.""" + import os + from hindsight_api.config import HindsightConfig, _get_raw_config, clear_config_cache + + original = os.getenv("HINDSIGHT_API_RETAIN_MISSION") + try: + os.environ["HINDSIGHT_API_RETAIN_MISSION"] = "Only technical decisions." + clear_config_cache() + config = _get_raw_config() + assert config.retain_mission == "Only technical decisions." + assert "retain_mission" in HindsightConfig.get_configurable_fields() + finally: + if original is None: + os.environ.pop("HINDSIGHT_API_RETAIN_MISSION", None) + else: + os.environ["HINDSIGHT_API_RETAIN_MISSION"] = original + clear_config_cache() diff --git a/hindsight-cli/src/api.rs b/hindsight-cli/src/api.rs index 6529f75d..772324b4 100644 --- a/hindsight-cli/src/api.rs +++ b/hindsight-cli/src/api.rs @@ -127,6 +127,7 @@ impl ApiClient { mission: None, background: None, disposition: None, + ..Default::default() }; let response = self.client.create_or_update_bank(agent_id, None, &request).await?; Ok(response.into_inner()) @@ -436,6 +437,7 @@ impl ApiClient { mission: Some(mission.to_string()), background: None, disposition: None, + ..Default::default() }; let response = self.client.update_bank(bank_id, None, &request).await?; Ok(response.into_inner()) diff --git a/hindsight-cli/src/commands/bank.rs b/hindsight-cli/src/commands/bank.rs index facfec95..09f0a1e5 100644 --- a/hindsight-cli/src/commands/bank.rs +++ b/hindsight-cli/src/commands/bank.rs @@ -293,6 +293,7 @@ pub fn create( mission: mission_text, background: None, disposition, + ..Default::default() }; let response = client.create_bank(bank_id, &request, verbose); @@ -356,6 +357,7 @@ pub fn update( mission: mission_text, background: None, disposition, + ..Default::default() }; let response = client.update_bank(bank_id, &request, verbose); diff --git a/hindsight-clients/go/api/openapi.yaml b/hindsight-clients/go/api/openapi.yaml index 1c48c9b2..8ca7a402 100644 --- a/hindsight-clients/go/api/openapi.yaml +++ b/hindsight-clients/go/api/openapi.yaml @@ -1564,6 +1564,7 @@ paths: - Operations /v1/default/banks/{bank_id}/profile: get: + deprecated: true description: Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists. operationId: get_bank_profile @@ -1601,6 +1602,7 @@ paths: tags: - Banks put: + deprecated: true description: "Update bank's disposition traits (skepticism, literalism, empathy)" operationId: update_bank_disposition parameters: @@ -2616,24 +2618,58 @@ components: CreateBankRequest: description: Request model for creating/updating a bank. example: - disposition: - empathy: 3 - literalism: 3 - skepticism: 3 - mission: I am a PM helping my engineering team stay organized - name: Alice + observations_mission: Observations are stable facts about people and projects. + Always include preferences and skills. + retain_mission: Always include technical decisions and architectural trade-offs. + Ignore meeting logistics. properties: name: nullable: true type: string disposition: $ref: '#/components/schemas/DispositionTraits' + disposition_skepticism: + maximum: 5.0 + minimum: 1.0 + nullable: true + type: integer + disposition_literalism: + maximum: 5.0 + minimum: 1.0 + nullable: true + type: integer + disposition_empathy: + maximum: 5.0 + minimum: 1.0 + nullable: true + type: integer mission: nullable: true type: string background: nullable: true type: string + reflect_mission: + nullable: true + type: string + retain_mission: + nullable: true + type: string + retain_extraction_mode: + nullable: true + type: string + retain_custom_instructions: + nullable: true + type: string + retain_chunk_size: + nullable: true + type: integer + enable_observations: + nullable: true + type: boolean + observations_mission: + nullable: true + type: string title: CreateBankRequest CreateDirectiveRequest: description: Request model for creating a directive. diff --git a/hindsight-clients/go/api_banks.go b/hindsight-clients/go/api_banks.go index 2d07da16..ad624631 100644 --- a/hindsight-clients/go/api_banks.go +++ b/hindsight-clients/go/api_banks.go @@ -804,6 +804,8 @@ Get disposition traits and mission for a memory bank. Auto-creates agent with de @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). @param bankId @return ApiGetBankProfileRequest + +Deprecated */ func (a *BanksAPIService) GetBankProfile(ctx context.Context, bankId string) ApiGetBankProfileRequest { return ApiGetBankProfileRequest{ @@ -815,6 +817,7 @@ func (a *BanksAPIService) GetBankProfile(ctx context.Context, bankId string) Api // Execute executes the request // @return BankProfileResponse +// Deprecated func (a *BanksAPIService) GetBankProfileExecute(r ApiGetBankProfileRequest) (*BankProfileResponse, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet @@ -1560,6 +1563,8 @@ Update bank's disposition traits (skepticism, literalism, empathy) @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). @param bankId @return ApiUpdateBankDispositionRequest + +Deprecated */ func (a *BanksAPIService) UpdateBankDisposition(ctx context.Context, bankId string) ApiUpdateBankDispositionRequest { return ApiUpdateBankDispositionRequest{ @@ -1571,6 +1576,7 @@ func (a *BanksAPIService) UpdateBankDisposition(ctx context.Context, bankId stri // Execute executes the request // @return BankProfileResponse +// Deprecated func (a *BanksAPIService) UpdateBankDispositionExecute(r ApiUpdateBankDispositionRequest) (*BankProfileResponse, *http.Response, error) { var ( localVarHTTPMethod = http.MethodPut diff --git a/hindsight-clients/go/model_create_bank_request.go b/hindsight-clients/go/model_create_bank_request.go index 74fbf792..7e20c02a 100644 --- a/hindsight-clients/go/model_create_bank_request.go +++ b/hindsight-clients/go/model_create_bank_request.go @@ -21,8 +21,18 @@ var _ MappedNullable = &CreateBankRequest{} type CreateBankRequest struct { Name NullableString `json:"name,omitempty"` Disposition NullableDispositionTraits `json:"disposition,omitempty"` + DispositionSkepticism NullableInt32 `json:"disposition_skepticism,omitempty"` + DispositionLiteralism NullableInt32 `json:"disposition_literalism,omitempty"` + DispositionEmpathy NullableInt32 `json:"disposition_empathy,omitempty"` Mission NullableString `json:"mission,omitempty"` Background NullableString `json:"background,omitempty"` + ReflectMission NullableString `json:"reflect_mission,omitempty"` + RetainMission NullableString `json:"retain_mission,omitempty"` + RetainExtractionMode NullableString `json:"retain_extraction_mode,omitempty"` + RetainCustomInstructions NullableString `json:"retain_custom_instructions,omitempty"` + RetainChunkSize NullableInt32 `json:"retain_chunk_size,omitempty"` + EnableObservations NullableBool `json:"enable_observations,omitempty"` + ObservationsMission NullableString `json:"observations_mission,omitempty"` } // NewCreateBankRequest instantiates a new CreateBankRequest object @@ -126,6 +136,132 @@ func (o *CreateBankRequest) UnsetDisposition() { o.Disposition.Unset() } +// GetDispositionSkepticism returns the DispositionSkepticism field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateBankRequest) GetDispositionSkepticism() int32 { + if o == nil || IsNil(o.DispositionSkepticism.Get()) { + var ret int32 + return ret + } + return *o.DispositionSkepticism.Get() +} + +// GetDispositionSkepticismOk returns a tuple with the DispositionSkepticism field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *CreateBankRequest) GetDispositionSkepticismOk() (*int32, bool) { + if o == nil { + return nil, false + } + return o.DispositionSkepticism.Get(), o.DispositionSkepticism.IsSet() +} + +// HasDispositionSkepticism returns a boolean if a field has been set. +func (o *CreateBankRequest) HasDispositionSkepticism() bool { + if o != nil && o.DispositionSkepticism.IsSet() { + return true + } + + return false +} + +// SetDispositionSkepticism gets a reference to the given NullableInt32 and assigns it to the DispositionSkepticism field. +func (o *CreateBankRequest) SetDispositionSkepticism(v int32) { + o.DispositionSkepticism.Set(&v) +} +// SetDispositionSkepticismNil sets the value for DispositionSkepticism to be an explicit nil +func (o *CreateBankRequest) SetDispositionSkepticismNil() { + o.DispositionSkepticism.Set(nil) +} + +// UnsetDispositionSkepticism ensures that no value is present for DispositionSkepticism, not even an explicit nil +func (o *CreateBankRequest) UnsetDispositionSkepticism() { + o.DispositionSkepticism.Unset() +} + +// GetDispositionLiteralism returns the DispositionLiteralism field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateBankRequest) GetDispositionLiteralism() int32 { + if o == nil || IsNil(o.DispositionLiteralism.Get()) { + var ret int32 + return ret + } + return *o.DispositionLiteralism.Get() +} + +// GetDispositionLiteralismOk returns a tuple with the DispositionLiteralism field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *CreateBankRequest) GetDispositionLiteralismOk() (*int32, bool) { + if o == nil { + return nil, false + } + return o.DispositionLiteralism.Get(), o.DispositionLiteralism.IsSet() +} + +// HasDispositionLiteralism returns a boolean if a field has been set. +func (o *CreateBankRequest) HasDispositionLiteralism() bool { + if o != nil && o.DispositionLiteralism.IsSet() { + return true + } + + return false +} + +// SetDispositionLiteralism gets a reference to the given NullableInt32 and assigns it to the DispositionLiteralism field. +func (o *CreateBankRequest) SetDispositionLiteralism(v int32) { + o.DispositionLiteralism.Set(&v) +} +// SetDispositionLiteralismNil sets the value for DispositionLiteralism to be an explicit nil +func (o *CreateBankRequest) SetDispositionLiteralismNil() { + o.DispositionLiteralism.Set(nil) +} + +// UnsetDispositionLiteralism ensures that no value is present for DispositionLiteralism, not even an explicit nil +func (o *CreateBankRequest) UnsetDispositionLiteralism() { + o.DispositionLiteralism.Unset() +} + +// GetDispositionEmpathy returns the DispositionEmpathy field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateBankRequest) GetDispositionEmpathy() int32 { + if o == nil || IsNil(o.DispositionEmpathy.Get()) { + var ret int32 + return ret + } + return *o.DispositionEmpathy.Get() +} + +// GetDispositionEmpathyOk returns a tuple with the DispositionEmpathy field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *CreateBankRequest) GetDispositionEmpathyOk() (*int32, bool) { + if o == nil { + return nil, false + } + return o.DispositionEmpathy.Get(), o.DispositionEmpathy.IsSet() +} + +// HasDispositionEmpathy returns a boolean if a field has been set. +func (o *CreateBankRequest) HasDispositionEmpathy() bool { + if o != nil && o.DispositionEmpathy.IsSet() { + return true + } + + return false +} + +// SetDispositionEmpathy gets a reference to the given NullableInt32 and assigns it to the DispositionEmpathy field. +func (o *CreateBankRequest) SetDispositionEmpathy(v int32) { + o.DispositionEmpathy.Set(&v) +} +// SetDispositionEmpathyNil sets the value for DispositionEmpathy to be an explicit nil +func (o *CreateBankRequest) SetDispositionEmpathyNil() { + o.DispositionEmpathy.Set(nil) +} + +// UnsetDispositionEmpathy ensures that no value is present for DispositionEmpathy, not even an explicit nil +func (o *CreateBankRequest) UnsetDispositionEmpathy() { + o.DispositionEmpathy.Unset() +} + // GetMission returns the Mission field value if set, zero value otherwise (both if not set or set to explicit null). func (o *CreateBankRequest) GetMission() string { if o == nil || IsNil(o.Mission.Get()) { @@ -210,6 +346,300 @@ func (o *CreateBankRequest) UnsetBackground() { o.Background.Unset() } +// GetReflectMission returns the ReflectMission field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateBankRequest) GetReflectMission() string { + if o == nil || IsNil(o.ReflectMission.Get()) { + var ret string + return ret + } + return *o.ReflectMission.Get() +} + +// GetReflectMissionOk returns a tuple with the ReflectMission field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *CreateBankRequest) GetReflectMissionOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.ReflectMission.Get(), o.ReflectMission.IsSet() +} + +// HasReflectMission returns a boolean if a field has been set. +func (o *CreateBankRequest) HasReflectMission() bool { + if o != nil && o.ReflectMission.IsSet() { + return true + } + + return false +} + +// SetReflectMission gets a reference to the given NullableString and assigns it to the ReflectMission field. +func (o *CreateBankRequest) SetReflectMission(v string) { + o.ReflectMission.Set(&v) +} +// SetReflectMissionNil sets the value for ReflectMission to be an explicit nil +func (o *CreateBankRequest) SetReflectMissionNil() { + o.ReflectMission.Set(nil) +} + +// UnsetReflectMission ensures that no value is present for ReflectMission, not even an explicit nil +func (o *CreateBankRequest) UnsetReflectMission() { + o.ReflectMission.Unset() +} + +// GetRetainMission returns the RetainMission field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateBankRequest) GetRetainMission() string { + if o == nil || IsNil(o.RetainMission.Get()) { + var ret string + return ret + } + return *o.RetainMission.Get() +} + +// GetRetainMissionOk returns a tuple with the RetainMission field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *CreateBankRequest) GetRetainMissionOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.RetainMission.Get(), o.RetainMission.IsSet() +} + +// HasRetainMission returns a boolean if a field has been set. +func (o *CreateBankRequest) HasRetainMission() bool { + if o != nil && o.RetainMission.IsSet() { + return true + } + + return false +} + +// SetRetainMission gets a reference to the given NullableString and assigns it to the RetainMission field. +func (o *CreateBankRequest) SetRetainMission(v string) { + o.RetainMission.Set(&v) +} +// SetRetainMissionNil sets the value for RetainMission to be an explicit nil +func (o *CreateBankRequest) SetRetainMissionNil() { + o.RetainMission.Set(nil) +} + +// UnsetRetainMission ensures that no value is present for RetainMission, not even an explicit nil +func (o *CreateBankRequest) UnsetRetainMission() { + o.RetainMission.Unset() +} + +// GetRetainExtractionMode returns the RetainExtractionMode field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateBankRequest) GetRetainExtractionMode() string { + if o == nil || IsNil(o.RetainExtractionMode.Get()) { + var ret string + return ret + } + return *o.RetainExtractionMode.Get() +} + +// GetRetainExtractionModeOk returns a tuple with the RetainExtractionMode field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *CreateBankRequest) GetRetainExtractionModeOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.RetainExtractionMode.Get(), o.RetainExtractionMode.IsSet() +} + +// HasRetainExtractionMode returns a boolean if a field has been set. +func (o *CreateBankRequest) HasRetainExtractionMode() bool { + if o != nil && o.RetainExtractionMode.IsSet() { + return true + } + + return false +} + +// SetRetainExtractionMode gets a reference to the given NullableString and assigns it to the RetainExtractionMode field. +func (o *CreateBankRequest) SetRetainExtractionMode(v string) { + o.RetainExtractionMode.Set(&v) +} +// SetRetainExtractionModeNil sets the value for RetainExtractionMode to be an explicit nil +func (o *CreateBankRequest) SetRetainExtractionModeNil() { + o.RetainExtractionMode.Set(nil) +} + +// UnsetRetainExtractionMode ensures that no value is present for RetainExtractionMode, not even an explicit nil +func (o *CreateBankRequest) UnsetRetainExtractionMode() { + o.RetainExtractionMode.Unset() +} + +// GetRetainCustomInstructions returns the RetainCustomInstructions field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateBankRequest) GetRetainCustomInstructions() string { + if o == nil || IsNil(o.RetainCustomInstructions.Get()) { + var ret string + return ret + } + return *o.RetainCustomInstructions.Get() +} + +// GetRetainCustomInstructionsOk returns a tuple with the RetainCustomInstructions field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *CreateBankRequest) GetRetainCustomInstructionsOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.RetainCustomInstructions.Get(), o.RetainCustomInstructions.IsSet() +} + +// HasRetainCustomInstructions returns a boolean if a field has been set. +func (o *CreateBankRequest) HasRetainCustomInstructions() bool { + if o != nil && o.RetainCustomInstructions.IsSet() { + return true + } + + return false +} + +// SetRetainCustomInstructions gets a reference to the given NullableString and assigns it to the RetainCustomInstructions field. +func (o *CreateBankRequest) SetRetainCustomInstructions(v string) { + o.RetainCustomInstructions.Set(&v) +} +// SetRetainCustomInstructionsNil sets the value for RetainCustomInstructions to be an explicit nil +func (o *CreateBankRequest) SetRetainCustomInstructionsNil() { + o.RetainCustomInstructions.Set(nil) +} + +// UnsetRetainCustomInstructions ensures that no value is present for RetainCustomInstructions, not even an explicit nil +func (o *CreateBankRequest) UnsetRetainCustomInstructions() { + o.RetainCustomInstructions.Unset() +} + +// GetRetainChunkSize returns the RetainChunkSize field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateBankRequest) GetRetainChunkSize() int32 { + if o == nil || IsNil(o.RetainChunkSize.Get()) { + var ret int32 + return ret + } + return *o.RetainChunkSize.Get() +} + +// GetRetainChunkSizeOk returns a tuple with the RetainChunkSize field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *CreateBankRequest) GetRetainChunkSizeOk() (*int32, bool) { + if o == nil { + return nil, false + } + return o.RetainChunkSize.Get(), o.RetainChunkSize.IsSet() +} + +// HasRetainChunkSize returns a boolean if a field has been set. +func (o *CreateBankRequest) HasRetainChunkSize() bool { + if o != nil && o.RetainChunkSize.IsSet() { + return true + } + + return false +} + +// SetRetainChunkSize gets a reference to the given NullableInt32 and assigns it to the RetainChunkSize field. +func (o *CreateBankRequest) SetRetainChunkSize(v int32) { + o.RetainChunkSize.Set(&v) +} +// SetRetainChunkSizeNil sets the value for RetainChunkSize to be an explicit nil +func (o *CreateBankRequest) SetRetainChunkSizeNil() { + o.RetainChunkSize.Set(nil) +} + +// UnsetRetainChunkSize ensures that no value is present for RetainChunkSize, not even an explicit nil +func (o *CreateBankRequest) UnsetRetainChunkSize() { + o.RetainChunkSize.Unset() +} + +// GetEnableObservations returns the EnableObservations field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateBankRequest) GetEnableObservations() bool { + if o == nil || IsNil(o.EnableObservations.Get()) { + var ret bool + return ret + } + return *o.EnableObservations.Get() +} + +// GetEnableObservationsOk returns a tuple with the EnableObservations field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *CreateBankRequest) GetEnableObservationsOk() (*bool, bool) { + if o == nil { + return nil, false + } + return o.EnableObservations.Get(), o.EnableObservations.IsSet() +} + +// HasEnableObservations returns a boolean if a field has been set. +func (o *CreateBankRequest) HasEnableObservations() bool { + if o != nil && o.EnableObservations.IsSet() { + return true + } + + return false +} + +// SetEnableObservations gets a reference to the given NullableBool and assigns it to the EnableObservations field. +func (o *CreateBankRequest) SetEnableObservations(v bool) { + o.EnableObservations.Set(&v) +} +// SetEnableObservationsNil sets the value for EnableObservations to be an explicit nil +func (o *CreateBankRequest) SetEnableObservationsNil() { + o.EnableObservations.Set(nil) +} + +// UnsetEnableObservations ensures that no value is present for EnableObservations, not even an explicit nil +func (o *CreateBankRequest) UnsetEnableObservations() { + o.EnableObservations.Unset() +} + +// GetObservationsMission returns the ObservationsMission field value if set, zero value otherwise (both if not set or set to explicit null). +func (o *CreateBankRequest) GetObservationsMission() string { + if o == nil || IsNil(o.ObservationsMission.Get()) { + var ret string + return ret + } + return *o.ObservationsMission.Get() +} + +// GetObservationsMissionOk returns a tuple with the ObservationsMission field value if set, nil otherwise +// and a boolean to check if the value has been set. +// NOTE: If the value is an explicit nil, `nil, true` will be returned +func (o *CreateBankRequest) GetObservationsMissionOk() (*string, bool) { + if o == nil { + return nil, false + } + return o.ObservationsMission.Get(), o.ObservationsMission.IsSet() +} + +// HasObservationsMission returns a boolean if a field has been set. +func (o *CreateBankRequest) HasObservationsMission() bool { + if o != nil && o.ObservationsMission.IsSet() { + return true + } + + return false +} + +// SetObservationsMission gets a reference to the given NullableString and assigns it to the ObservationsMission field. +func (o *CreateBankRequest) SetObservationsMission(v string) { + o.ObservationsMission.Set(&v) +} +// SetObservationsMissionNil sets the value for ObservationsMission to be an explicit nil +func (o *CreateBankRequest) SetObservationsMissionNil() { + o.ObservationsMission.Set(nil) +} + +// UnsetObservationsMission ensures that no value is present for ObservationsMission, not even an explicit nil +func (o *CreateBankRequest) UnsetObservationsMission() { + o.ObservationsMission.Unset() +} + func (o CreateBankRequest) MarshalJSON() ([]byte, error) { toSerialize,err := o.ToMap() if err != nil { @@ -226,12 +656,42 @@ func (o CreateBankRequest) ToMap() (map[string]interface{}, error) { if o.Disposition.IsSet() { toSerialize["disposition"] = o.Disposition.Get() } + if o.DispositionSkepticism.IsSet() { + toSerialize["disposition_skepticism"] = o.DispositionSkepticism.Get() + } + if o.DispositionLiteralism.IsSet() { + toSerialize["disposition_literalism"] = o.DispositionLiteralism.Get() + } + if o.DispositionEmpathy.IsSet() { + toSerialize["disposition_empathy"] = o.DispositionEmpathy.Get() + } if o.Mission.IsSet() { toSerialize["mission"] = o.Mission.Get() } if o.Background.IsSet() { toSerialize["background"] = o.Background.Get() } + if o.ReflectMission.IsSet() { + toSerialize["reflect_mission"] = o.ReflectMission.Get() + } + if o.RetainMission.IsSet() { + toSerialize["retain_mission"] = o.RetainMission.Get() + } + if o.RetainExtractionMode.IsSet() { + toSerialize["retain_extraction_mode"] = o.RetainExtractionMode.Get() + } + if o.RetainCustomInstructions.IsSet() { + toSerialize["retain_custom_instructions"] = o.RetainCustomInstructions.Get() + } + if o.RetainChunkSize.IsSet() { + toSerialize["retain_chunk_size"] = o.RetainChunkSize.Get() + } + if o.EnableObservations.IsSet() { + toSerialize["enable_observations"] = o.EnableObservations.Get() + } + if o.ObservationsMission.IsSet() { + toSerialize["observations_mission"] = o.ObservationsMission.Get() + } return toSerialize, nil } diff --git a/hindsight-clients/python/hindsight_client/hindsight_client.py b/hindsight-clients/python/hindsight_client/hindsight_client.py index 0ecaf926..303d12e2 100644 --- a/hindsight-clients/python/hindsight_client/hindsight_client.py +++ b/hindsight-clients/python/hindsight_client/hindsight_client.py @@ -79,6 +79,8 @@ class Hindsight: config = hindsight_client_api.Configuration(host=base_url, access_token=api_key) self._api_client = hindsight_client_api.ApiClient(config) self._timeout = timeout + self._base_url = base_url.rstrip("/") + self._api_key = api_key if api_key: self._api_client.set_default_header("Authorization", f"Bearer {api_key}") self._memory_api = memory_api.MemoryApi(self._api_client) @@ -386,49 +388,124 @@ class Hindsight: bank_id: str, name: str | None = None, mission: str | None = None, + disposition_skepticism: int | None = None, + disposition_literalism: int | None = None, + disposition_empathy: int | None = None, disposition: dict[str, float] | None = None, + retain_mission: str | None = None, + retain_extraction_mode: str | None = None, + retain_custom_instructions: str | None = None, + retain_chunk_size: int | None = None, + enable_observations: bool | None = None, + observations_mission: str | None = None, + reflect_mission: str | None = None, ) -> BankProfileResponse: """Create or update a memory bank. Args: bank_id: Unique identifier for the bank - name: Human-readable display name - mission: Instructions guiding what Hindsight should learn and remember (for mental models) - disposition: Optional disposition traits (skepticism, literalism, empathy) + name: Deprecated. Display label only. + mission: Deprecated. Use reflect_mission instead. + disposition_skepticism: Deprecated. Use update_bank_config(disposition_skepticism=...) instead. + disposition_literalism: Deprecated. Use update_bank_config(disposition_literalism=...) instead. + disposition_empathy: Deprecated. Use update_bank_config(disposition_empathy=...) instead. + disposition: Deprecated. Use update_bank_config(disposition_skepticism=...) instead. + retain_mission: Steers what gets extracted during retain(). Injected alongside built-in rules. + retain_extraction_mode: Fact extraction mode: 'concise' (default), 'verbose', or 'custom'. + retain_custom_instructions: Custom extraction prompt (only active when mode is 'custom'). + retain_chunk_size: Maximum token size for each content chunk during retain. + enable_observations: Toggle automatic observation consolidation after retain(). + observations_mission: Controls what gets synthesised into observations. Replaces built-in rules. + reflect_mission: Mission/context for Reflect operations. """ - from hindsight_client_api.models import create_bank_request, disposition_traits - - disposition_obj = None - if disposition: - disposition_obj = disposition_traits.DispositionTraits(**disposition) - - request_obj = create_bank_request.CreateBankRequest( - name=name, - mission=mission, - disposition=disposition_obj, + return _run_async( + self._acreate_bank( + bank_id, + name=name, + mission=mission, + reflect_mission=reflect_mission, + disposition_skepticism=disposition_skepticism, + disposition_literalism=disposition_literalism, + disposition_empathy=disposition_empathy, + disposition=disposition, + retain_mission=retain_mission, + retain_extraction_mode=retain_extraction_mode, + retain_custom_instructions=retain_custom_instructions, + retain_chunk_size=retain_chunk_size, + enable_observations=enable_observations, + observations_mission=observations_mission, + ) ) - return _run_async(self._banks_api.create_or_update_bank(bank_id, request_obj, _request_timeout=self._timeout)) - - def set_mission( + async def _acreate_bank( self, bank_id: str, - mission: str, + name: str | None = None, + mission: str | None = None, + reflect_mission: str | None = None, + disposition_skepticism: int | None = None, + disposition_literalism: int | None = None, + disposition_empathy: int | None = None, + disposition: dict[str, float] | None = None, + retain_mission: str | None = None, + retain_extraction_mode: str | None = None, + retain_custom_instructions: str | None = None, + retain_chunk_size: int | None = None, + enable_observations: bool | None = None, + observations_mission: str | None = None, ) -> BankProfileResponse: - """ - Set or update the mission for a memory bank. + import aiohttp - Args: - bank_id: The memory bank ID - mission: The mission text describing the agent's purpose + body: dict[str, Any] = {} + if name is not None: + body["name"] = name + if mission is not None: + body["mission"] = mission + if reflect_mission is not None: + body["reflect_mission"] = reflect_mission + # Individual disposition fields take priority over legacy disposition dict + if disposition_skepticism is not None: + body["disposition_skepticism"] = disposition_skepticism + elif disposition is not None: + body["disposition_skepticism"] = disposition.get("skepticism") + if disposition_literalism is not None: + body["disposition_literalism"] = disposition_literalism + elif disposition is not None: + body["disposition_literalism"] = disposition.get("literalism") + if disposition_empathy is not None: + body["disposition_empathy"] = disposition_empathy + elif disposition is not None: + body["disposition_empathy"] = disposition.get("empathy") + if retain_mission is not None: + body["retain_mission"] = retain_mission + if retain_extraction_mode is not None: + body["retain_extraction_mode"] = retain_extraction_mode + if retain_custom_instructions is not None: + body["retain_custom_instructions"] = retain_custom_instructions + if retain_chunk_size is not None: + body["retain_chunk_size"] = retain_chunk_size + if enable_observations is not None: + body["enable_observations"] = enable_observations + if observations_mission is not None: + body["observations_mission"] = observations_mission - Returns: - BankProfileResponse with updated bank profile - """ - from hindsight_client_api.models import create_bank_request + url = f"{self._base_url}/v1/default/banks/{bank_id}" + headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {} + async with aiohttp.ClientSession() as session: + async with session.put( + url, json=body, headers=headers, timeout=aiohttp.ClientTimeout(total=self._timeout) + ) as resp: + resp.raise_for_status() + data = await resp.json() + return BankProfileResponse.model_validate(data) - request_obj = create_bank_request.CreateBankRequest(mission=mission) - return _run_async(self._banks_api.create_or_update_bank(bank_id, request_obj, _request_timeout=self._timeout)) + def set_mission(self, bank_id: str, mission: str) -> dict[str, Any]: + """Deprecated. Use update_bank_config(reflect_mission=...) instead.""" + return self.create_bank(bank_id, mission=mission) + + def set_reflect_mission(self, bank_id: str, reflect_mission: str) -> dict[str, Any]: + """Deprecated alias for set_mission().""" + return self.set_mission(bank_id, reflect_mission) # Async methods (native async, no _run_async wrapper) @@ -437,49 +514,60 @@ class Hindsight: bank_id: str, name: str | None = None, mission: str | None = None, + disposition_skepticism: int | None = None, + disposition_literalism: int | None = None, + disposition_empathy: int | None = None, disposition: dict[str, float] | None = None, + retain_mission: str | None = None, + retain_extraction_mode: str | None = None, + retain_custom_instructions: str | None = None, + retain_chunk_size: int | None = None, + enable_observations: bool | None = None, + observations_mission: str | None = None, + reflect_mission: str | None = None, ) -> BankProfileResponse: """Create or update a memory bank (async). Args: bank_id: Unique identifier for the bank - name: Human-readable display name - mission: Instructions guiding what Hindsight should learn and remember (for mental models) - disposition: Optional disposition traits (skepticism, literalism, empathy) + name: Deprecated. Display label only. + mission: Deprecated. Use reflect_mission instead. + disposition_skepticism: Deprecated. Use update_bank_config(disposition_skepticism=...) instead. + disposition_literalism: Deprecated. Use update_bank_config(disposition_literalism=...) instead. + disposition_empathy: Deprecated. Use update_bank_config(disposition_empathy=...) instead. + disposition: Deprecated. Use update_bank_config(disposition_skepticism=...) instead. + retain_mission: Steers what gets extracted during retain(). Injected alongside built-in rules. + retain_extraction_mode: Fact extraction mode: 'concise' (default), 'verbose', or 'custom'. + retain_custom_instructions: Custom extraction prompt (only active when mode is 'custom'). + retain_chunk_size: Maximum token size for each content chunk during retain. + enable_observations: Toggle automatic observation consolidation after retain(). + observations_mission: Controls what gets synthesised into observations. Replaces built-in rules. + reflect_mission: Mission/context for Reflect operations. """ - from hindsight_client_api.models import create_bank_request, disposition_traits - - disposition_obj = None - if disposition: - disposition_obj = disposition_traits.DispositionTraits(**disposition) - - request_obj = create_bank_request.CreateBankRequest( + return await self._acreate_bank( + bank_id, name=name, mission=mission, - disposition=disposition_obj, + reflect_mission=reflect_mission, + disposition_skepticism=disposition_skepticism, + disposition_literalism=disposition_literalism, + disposition_empathy=disposition_empathy, + disposition=disposition, + retain_mission=retain_mission, + retain_extraction_mode=retain_extraction_mode, + retain_custom_instructions=retain_custom_instructions, + retain_chunk_size=retain_chunk_size, + enable_observations=enable_observations, + observations_mission=observations_mission, ) - return await self._banks_api.create_or_update_bank(bank_id, request_obj, _request_timeout=self._timeout) + async def aset_mission(self, bank_id: str, mission: str) -> dict[str, Any]: + """Deprecated. Use update_bank_config(reflect_mission=...) instead.""" + return await self.acreate_bank(bank_id, mission=mission) - async def aset_mission( - self, - bank_id: str, - mission: str, - ) -> BankProfileResponse: - """ - Set or update the mission for a memory bank (async). - - Args: - bank_id: The memory bank ID - mission: The mission text describing the agent's purpose - - Returns: - BankProfileResponse with updated bank profile - """ - from hindsight_client_api.models import create_bank_request - - request_obj = create_bank_request.CreateBankRequest(mission=mission) - return await self._banks_api.create_or_update_bank(bank_id, request_obj, _request_timeout=self._timeout) + async def aset_reflect_mission(self, bank_id: str, reflect_mission: str) -> dict[str, Any]: + """Deprecated alias for aset_mission().""" + return await self.aset_mission(bank_id, reflect_mission) async def aretain_batch( self, @@ -929,6 +1017,120 @@ class Hindsight: """ return _run_async(self._directives_api.delete_directive(bank_id, directive_id, _request_timeout=self._timeout)) + def get_bank_config(self, bank_id: str) -> dict[str, Any]: + """ + Get the resolved configuration for a bank, including any bank-level overrides. + + Requires ``HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true`` on the server. + + Args: + bank_id: The memory bank ID + + Returns: + dict with ``bank_id``, ``config`` (fully resolved), and ``overrides`` (bank-level only) + """ + return _run_async(self._aget_bank_config(bank_id)) + + async def _aget_bank_config(self, bank_id: str) -> dict[str, Any]: + import aiohttp + + url = f"{self._base_url}/v1/default/banks/{bank_id}/config" + headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {} + async with aiohttp.ClientSession() as session: + async with session.get(url, headers=headers, timeout=aiohttp.ClientTimeout(total=self._timeout)) as resp: + resp.raise_for_status() + return await resp.json() + + def update_bank_config( + self, + bank_id: str, + *, + reflect_mission: str | None = None, + retain_mission: str | None = None, + retain_extraction_mode: str | None = None, + retain_custom_instructions: str | None = None, + retain_chunk_size: int | None = None, + enable_observations: bool | None = None, + observations_mission: str | None = None, + disposition_skepticism: int | None = None, + disposition_literalism: int | None = None, + disposition_empathy: int | None = None, + ) -> dict[str, Any]: + """ + Update configuration overrides for a bank. + + Requires ``HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true`` on the server. + + Args: + bank_id: The memory bank ID + reflect_mission: Identity and reasoning framing for reflect(). + retain_mission: Steers what gets extracted during retain(). + retain_extraction_mode: Fact extraction mode: 'concise', 'verbose', or 'custom'. + retain_custom_instructions: Custom extraction prompt (only active when mode is 'custom'). + retain_chunk_size: Maximum token size for each content chunk during retain. + enable_observations: Toggle automatic observation consolidation after retain(). + observations_mission: Controls what gets synthesised into observations. + disposition_skepticism: How skeptical vs trusting (1=trusting, 5=skeptical). + disposition_literalism: How literally to interpret information (1=flexible, 5=literal). + disposition_empathy: How much to consider emotional context (1=detached, 5=empathetic). + + Returns: + dict with ``bank_id``, ``config`` (fully resolved), and ``overrides`` (bank-level only) + """ + updates = { + k: v + for k, v in { + "reflect_mission": reflect_mission, + "retain_mission": retain_mission, + "retain_extraction_mode": retain_extraction_mode, + "retain_custom_instructions": retain_custom_instructions, + "retain_chunk_size": retain_chunk_size, + "enable_observations": enable_observations, + "observations_mission": observations_mission, + "disposition_skepticism": disposition_skepticism, + "disposition_literalism": disposition_literalism, + "disposition_empathy": disposition_empathy, + }.items() + if v is not None + } + return _run_async(self._aupdate_bank_config(bank_id, updates)) + + async def _aupdate_bank_config(self, bank_id: str, updates: dict[str, Any]) -> dict[str, Any]: + import aiohttp + + url = f"{self._base_url}/v1/default/banks/{bank_id}/config" + headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {} + async with aiohttp.ClientSession() as session: + async with session.patch( + url, json={"updates": updates}, headers=headers, timeout=aiohttp.ClientTimeout(total=self._timeout) + ) as resp: + resp.raise_for_status() + return await resp.json() + + def reset_bank_config(self, bank_id: str) -> dict[str, Any]: + """ + Reset all bank-level configuration overrides, reverting to server defaults. + + Requires ``HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true`` on the server. + + Args: + bank_id: The memory bank ID + + Returns: + dict with ``bank_id``, ``config`` (fully resolved), and ``overrides`` (now empty) + """ + return _run_async(self._areset_bank_config(bank_id)) + + async def _areset_bank_config(self, bank_id: str) -> dict[str, Any]: + import aiohttp + + url = f"{self._base_url}/v1/default/banks/{bank_id}/config" + headers = {"Authorization": f"Bearer {self._api_key}"} if self._api_key else {} + async with aiohttp.ClientSession() as session: + async with session.delete(url, headers=headers, timeout=aiohttp.ClientTimeout(total=self._timeout)) as resp: + resp.raise_for_status() + return await resp.json() + def delete_bank(self, bank_id: str): """ Delete a memory bank. diff --git a/hindsight-clients/python/hindsight_client_api/api/banks_api.py b/hindsight-clients/python/hindsight_client_api/api/banks_api.py index 9e77a234..938c124f 100644 --- a/hindsight-clients/python/hindsight_client_api/api/banks_api.py +++ b/hindsight-clients/python/hindsight_client_api/api/banks_api.py @@ -1793,7 +1793,7 @@ class BanksApi: _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, ) -> BankProfileResponse: - """Get memory bank profile + """(Deprecated) Get memory bank profile Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists. @@ -1822,6 +1822,7 @@ class BanksApi: :type _host_index: int, optional :return: Returns the result object. """ # noqa: E501 + warnings.warn("GET /v1/default/banks/{bank_id}/profile is deprecated.", DeprecationWarning) _param = self._get_bank_profile_serialize( bank_id=bank_id, @@ -1865,7 +1866,7 @@ class BanksApi: _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, ) -> ApiResponse[BankProfileResponse]: - """Get memory bank profile + """(Deprecated) Get memory bank profile Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists. @@ -1894,6 +1895,7 @@ class BanksApi: :type _host_index: int, optional :return: Returns the result object. """ # noqa: E501 + warnings.warn("GET /v1/default/banks/{bank_id}/profile is deprecated.", DeprecationWarning) _param = self._get_bank_profile_serialize( bank_id=bank_id, @@ -1937,7 +1939,7 @@ class BanksApi: _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, ) -> RESTResponseType: - """Get memory bank profile + """(Deprecated) Get memory bank profile Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists. @@ -1966,6 +1968,7 @@ class BanksApi: :type _host_index: int, optional :return: Returns the result object. """ # noqa: E501 + warnings.warn("GET /v1/default/banks/{bank_id}/profile is deprecated.", DeprecationWarning) _param = self._get_bank_profile_serialize( bank_id=bank_id, @@ -3503,7 +3506,7 @@ class BanksApi: _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, ) -> BankProfileResponse: - """Update memory bank disposition + """(Deprecated) Update memory bank disposition Update bank's disposition traits (skepticism, literalism, empathy) @@ -3534,6 +3537,7 @@ class BanksApi: :type _host_index: int, optional :return: Returns the result object. """ # noqa: E501 + warnings.warn("PUT /v1/default/banks/{bank_id}/profile is deprecated.", DeprecationWarning) _param = self._update_bank_disposition_serialize( bank_id=bank_id, @@ -3579,7 +3583,7 @@ class BanksApi: _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, ) -> ApiResponse[BankProfileResponse]: - """Update memory bank disposition + """(Deprecated) Update memory bank disposition Update bank's disposition traits (skepticism, literalism, empathy) @@ -3610,6 +3614,7 @@ class BanksApi: :type _host_index: int, optional :return: Returns the result object. """ # noqa: E501 + warnings.warn("PUT /v1/default/banks/{bank_id}/profile is deprecated.", DeprecationWarning) _param = self._update_bank_disposition_serialize( bank_id=bank_id, @@ -3655,7 +3660,7 @@ class BanksApi: _headers: Optional[Dict[StrictStr, Any]] = None, _host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0, ) -> RESTResponseType: - """Update memory bank disposition + """(Deprecated) Update memory bank disposition Update bank's disposition traits (skepticism, literalism, empathy) @@ -3686,6 +3691,7 @@ class BanksApi: :type _host_index: int, optional :return: Returns the result object. """ # noqa: E501 + warnings.warn("PUT /v1/default/banks/{bank_id}/profile is deprecated.", DeprecationWarning) _param = self._update_bank_disposition_serialize( bank_id=bank_id, diff --git a/hindsight-clients/python/hindsight_client_api/models/create_bank_request.py b/hindsight-clients/python/hindsight_client_api/models/create_bank_request.py index a0b3898b..ccbb85d8 100644 --- a/hindsight-clients/python/hindsight_client_api/models/create_bank_request.py +++ b/hindsight-clients/python/hindsight_client_api/models/create_bank_request.py @@ -17,8 +17,9 @@ import pprint import re # noqa: F401 import json -from pydantic import BaseModel, ConfigDict, StrictStr +from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr from typing import Any, ClassVar, Dict, List, Optional +from typing_extensions import Annotated from hindsight_client_api.models.disposition_traits import DispositionTraits from typing import Optional, Set from typing_extensions import Self @@ -29,9 +30,19 @@ class CreateBankRequest(BaseModel): """ # noqa: E501 name: Optional[StrictStr] = None disposition: Optional[DispositionTraits] = None + disposition_skepticism: Optional[Annotated[int, Field(le=5, strict=True, ge=1)]] = None + disposition_literalism: Optional[Annotated[int, Field(le=5, strict=True, ge=1)]] = None + disposition_empathy: Optional[Annotated[int, Field(le=5, strict=True, ge=1)]] = None mission: Optional[StrictStr] = None background: Optional[StrictStr] = None - __properties: ClassVar[List[str]] = ["name", "disposition", "mission", "background"] + reflect_mission: Optional[StrictStr] = None + retain_mission: Optional[StrictStr] = None + retain_extraction_mode: Optional[StrictStr] = None + retain_custom_instructions: Optional[StrictStr] = None + retain_chunk_size: Optional[StrictInt] = None + enable_observations: Optional[StrictBool] = None + observations_mission: Optional[StrictStr] = None + __properties: ClassVar[List[str]] = ["name", "disposition", "disposition_skepticism", "disposition_literalism", "disposition_empathy", "mission", "background", "reflect_mission", "retain_mission", "retain_extraction_mode", "retain_custom_instructions", "retain_chunk_size", "enable_observations", "observations_mission"] model_config = ConfigDict( populate_by_name=True, @@ -85,6 +96,21 @@ class CreateBankRequest(BaseModel): if self.disposition is None and "disposition" in self.model_fields_set: _dict['disposition'] = None + # set to None if disposition_skepticism (nullable) is None + # and model_fields_set contains the field + if self.disposition_skepticism is None and "disposition_skepticism" in self.model_fields_set: + _dict['disposition_skepticism'] = None + + # set to None if disposition_literalism (nullable) is None + # and model_fields_set contains the field + if self.disposition_literalism is None and "disposition_literalism" in self.model_fields_set: + _dict['disposition_literalism'] = None + + # set to None if disposition_empathy (nullable) is None + # and model_fields_set contains the field + if self.disposition_empathy is None and "disposition_empathy" in self.model_fields_set: + _dict['disposition_empathy'] = None + # set to None if mission (nullable) is None # and model_fields_set contains the field if self.mission is None and "mission" in self.model_fields_set: @@ -95,6 +121,41 @@ class CreateBankRequest(BaseModel): if self.background is None and "background" in self.model_fields_set: _dict['background'] = None + # set to None if reflect_mission (nullable) is None + # and model_fields_set contains the field + if self.reflect_mission is None and "reflect_mission" in self.model_fields_set: + _dict['reflect_mission'] = None + + # set to None if retain_mission (nullable) is None + # and model_fields_set contains the field + if self.retain_mission is None and "retain_mission" in self.model_fields_set: + _dict['retain_mission'] = None + + # set to None if retain_extraction_mode (nullable) is None + # and model_fields_set contains the field + if self.retain_extraction_mode is None and "retain_extraction_mode" in self.model_fields_set: + _dict['retain_extraction_mode'] = None + + # set to None if retain_custom_instructions (nullable) is None + # and model_fields_set contains the field + if self.retain_custom_instructions is None and "retain_custom_instructions" in self.model_fields_set: + _dict['retain_custom_instructions'] = None + + # set to None if retain_chunk_size (nullable) is None + # and model_fields_set contains the field + if self.retain_chunk_size is None and "retain_chunk_size" in self.model_fields_set: + _dict['retain_chunk_size'] = None + + # set to None if enable_observations (nullable) is None + # and model_fields_set contains the field + if self.enable_observations is None and "enable_observations" in self.model_fields_set: + _dict['enable_observations'] = None + + # set to None if observations_mission (nullable) is None + # and model_fields_set contains the field + if self.observations_mission is None and "observations_mission" in self.model_fields_set: + _dict['observations_mission'] = None + return _dict @classmethod @@ -109,8 +170,18 @@ class CreateBankRequest(BaseModel): _obj = cls.model_validate({ "name": obj.get("name"), "disposition": DispositionTraits.from_dict(obj["disposition"]) if obj.get("disposition") is not None else None, + "disposition_skepticism": obj.get("disposition_skepticism"), + "disposition_literalism": obj.get("disposition_literalism"), + "disposition_empathy": obj.get("disposition_empathy"), "mission": obj.get("mission"), - "background": obj.get("background") + "background": obj.get("background"), + "reflect_mission": obj.get("reflect_mission"), + "retain_mission": obj.get("retain_mission"), + "retain_extraction_mode": obj.get("retain_extraction_mode"), + "retain_custom_instructions": obj.get("retain_custom_instructions"), + "retain_chunk_size": obj.get("retain_chunk_size"), + "enable_observations": obj.get("enable_observations"), + "observations_mission": obj.get("observations_mission") }) return _obj diff --git a/hindsight-clients/typescript/generated/sdk.gen.ts b/hindsight-clients/typescript/generated/sdk.gen.ts index 141eef35..36a518d3 100644 --- a/hindsight-clients/typescript/generated/sdk.gen.ts +++ b/hindsight-clients/typescript/generated/sdk.gen.ts @@ -701,6 +701,8 @@ export const getOperationStatus = ( * Get memory bank profile * * Get disposition traits and mission for a memory bank. Auto-creates agent with defaults if not exists. + * + * @deprecated */ export const getBankProfile = ( options: Options, @@ -715,6 +717,8 @@ export const getBankProfile = ( * Update memory bank disposition * * Update bank's disposition traits (skepticism, literalism, empathy) + * + * @deprecated */ export const updateBankDisposition = ( options: Options, diff --git a/hindsight-clients/typescript/generated/types.gen.ts b/hindsight-clients/typescript/generated/types.gen.ts index b042a45f..37bb3570 100644 --- a/hindsight-clients/typescript/generated/types.gen.ts +++ b/hindsight-clients/typescript/generated/types.gen.ts @@ -424,21 +424,86 @@ export type ConsolidationResponse = { export type CreateBankRequest = { /** * Name + * + * Deprecated: display label only, not advertised */ name?: string | null; + /** + * Deprecated: use update_bank_config instead + */ disposition?: DispositionTraits | null; + /** + * Disposition Skepticism + * + * Deprecated: use update_bank_config instead + */ + disposition_skepticism?: number | null; + /** + * Disposition Literalism + * + * Deprecated: use update_bank_config instead + */ + disposition_literalism?: number | null; + /** + * Disposition Empathy + * + * Deprecated: use update_bank_config instead + */ + disposition_empathy?: number | null; /** * Mission * - * The agent's mission + * Deprecated: use update_bank_config with reflect_mission instead */ mission?: string | null; /** * Background * - * Deprecated: use mission instead + * Deprecated: use update_bank_config with reflect_mission instead */ background?: string | null; + /** + * Reflect Mission + * + * Mission/context for Reflect operations. Guides how Reflect interprets and uses memories. + */ + reflect_mission?: string | null; + /** + * Retain Mission + * + * Steers what gets extracted during retain(). Injected alongside built-in extraction rules. + */ + retain_mission?: string | null; + /** + * Retain Extraction Mode + * + * Fact extraction mode: 'concise' (default), 'verbose', or 'custom'. + */ + retain_extraction_mode?: string | null; + /** + * Retain Custom Instructions + * + * Custom extraction prompt. Only active when retain_extraction_mode is 'custom'. + */ + retain_custom_instructions?: string | null; + /** + * Retain Chunk Size + * + * Maximum token size for each content chunk during retain. + */ + retain_chunk_size?: number | null; + /** + * Enable Observations + * + * Toggle automatic observation consolidation after retain(). + */ + enable_observations?: boolean | null; + /** + * Observations Mission + * + * Controls what gets synthesised into observations. Replaces built-in consolidation rules entirely. + */ + observations_mission?: string | null; }; /** diff --git a/hindsight-clients/typescript/jest.config.js b/hindsight-clients/typescript/jest.config.js index e9aae195..7556fce8 100644 --- a/hindsight-clients/typescript/jest.config.js +++ b/hindsight-clients/typescript/jest.config.js @@ -6,5 +6,5 @@ module.exports = { transform: { '^.+\\.tsx?$': 'ts-jest', }, - testTimeout: 60000, + testTimeout: 120000, }; diff --git a/hindsight-clients/typescript/src/index.ts b/hindsight-clients/typescript/src/index.ts index ed2cdbd0..81077595 100644 --- a/hindsight-clients/typescript/src/index.ts +++ b/hindsight-clients/typescript/src/index.ts @@ -39,6 +39,7 @@ import type { FileRetainResponse, ListMemoryUnitsResponse, BankProfileResponse, + BankConfigResponse, CreateBankRequest, Budget, } from '../generated/types.gen'; @@ -347,25 +348,73 @@ export class HindsightClient { } /** - * Create or update a bank with disposition and background. + * Create or update a bank with disposition, missions, and operational configuration. */ async createBank( bankId: string, - options: { name?: string; background?: string; disposition?: any } + options: { + /** @deprecated Display label only. */ + name?: string; + /** @deprecated Use reflectMission instead. */ + mission?: string; + /** Mission/context for Reflect operations. */ + reflectMission?: string; + /** @deprecated Alias for mission. */ + background?: string; + /** @deprecated Use dispositionSkepticism, dispositionLiteralism, dispositionEmpathy instead. */ + disposition?: { skepticism: number; literalism: number; empathy: number }; + /** @deprecated Use updateBankConfig({ dispositionSkepticism }) instead. */ + dispositionSkepticism?: number; + /** @deprecated Use updateBankConfig({ dispositionLiteralism }) instead. */ + dispositionLiteralism?: number; + /** @deprecated Use updateBankConfig({ dispositionEmpathy }) instead. */ + dispositionEmpathy?: number; + /** Steers what gets extracted during retain(). Injected alongside built-in rules. */ + retainMission?: string; + /** Fact extraction mode: 'concise' (default), 'verbose', or 'custom'. */ + retainExtractionMode?: string; + /** Custom extraction prompt (only active when retainExtractionMode is 'custom'). */ + retainCustomInstructions?: string; + /** Maximum token size for each content chunk during retain. */ + retainChunkSize?: number; + /** Toggle automatic observation consolidation after retain(). */ + enableObservations?: boolean; + /** Controls what gets synthesised into observations. Replaces built-in rules. */ + observationsMission?: string; + } = {} ): Promise { const response = await sdk.createOrUpdateBank({ client: this.client, path: { bank_id: bankId }, body: { name: options.name, + mission: options.mission, + reflect_mission: options.reflectMission, background: options.background, disposition: options.disposition, + disposition_skepticism: options.dispositionSkepticism, + disposition_literalism: options.dispositionLiteralism, + disposition_empathy: options.dispositionEmpathy, + retain_mission: options.retainMission, + retain_extraction_mode: options.retainExtractionMode, + retain_custom_instructions: options.retainCustomInstructions, + retain_chunk_size: options.retainChunkSize, + enable_observations: options.enableObservations, + observations_mission: options.observationsMission, }, }); return this.validateResponse(response, 'createBank'); } + /** + * Set or update the reflect mission for a memory bank. + * @deprecated Use createBank({ reflectMission: '...' }) instead. + */ + async setMission(bankId: string, mission: string): Promise { + return this.createBank(bankId, { reflectMission: mission }); + } + /** * Get a bank's profile. */ @@ -378,17 +427,81 @@ export class HindsightClient { return this.validateResponse(response, 'getBankProfile'); } + /** - * Set or update the mission for a memory bank. + * Get the resolved configuration for a bank, including any bank-level overrides. + * + * Requires `HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true` on the server. */ - async setMission(bankId: string, mission: string): Promise { - const response = await sdk.createOrUpdateBank({ + async getBankConfig(bankId: string): Promise { + const response = await sdk.getBankConfig({ client: this.client, path: { bank_id: bankId }, - body: { mission }, }); - return this.validateResponse(response, 'setMission'); + return this.validateResponse(response, 'getBankConfig'); + } + + /** + * Update configuration overrides for a bank. + * + * Requires `HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true` on the server. + * + * @param bankId - The memory bank ID + * @param options - Fields to override + */ + async updateBankConfig( + bankId: string, + options: { + reflectMission?: string; + retainMission?: string; + retainExtractionMode?: string; + retainCustomInstructions?: string; + retainChunkSize?: number; + enableObservations?: boolean; + observationsMission?: string; + /** How skeptical vs trusting (1=trusting, 5=skeptical). */ + dispositionSkepticism?: number; + /** How literally to interpret information (1=flexible, 5=literal). */ + dispositionLiteralism?: number; + /** How much to consider emotional context (1=detached, 5=empathetic). */ + dispositionEmpathy?: number; + }, + ): Promise { + const updates: Record = {}; + if (options.reflectMission !== undefined) updates.reflect_mission = options.reflectMission; + if (options.retainMission !== undefined) updates.retain_mission = options.retainMission; + if (options.retainExtractionMode !== undefined) updates.retain_extraction_mode = options.retainExtractionMode; + if (options.retainCustomInstructions !== undefined) + updates.retain_custom_instructions = options.retainCustomInstructions; + if (options.retainChunkSize !== undefined) updates.retain_chunk_size = options.retainChunkSize; + if (options.enableObservations !== undefined) updates.enable_observations = options.enableObservations; + if (options.observationsMission !== undefined) updates.observations_mission = options.observationsMission; + if (options.dispositionSkepticism !== undefined) updates.disposition_skepticism = options.dispositionSkepticism; + if (options.dispositionLiteralism !== undefined) updates.disposition_literalism = options.dispositionLiteralism; + if (options.dispositionEmpathy !== undefined) updates.disposition_empathy = options.dispositionEmpathy; + + const response = await sdk.updateBankConfig({ + client: this.client, + path: { bank_id: bankId }, + body: { updates }, + }); + + return this.validateResponse(response, 'updateBankConfig'); + } + + /** + * Reset all bank-level configuration overrides, reverting to server defaults. + * + * Requires `HINDSIGHT_API_ENABLE_BANK_CONFIG_API=true` on the server. + */ + async resetBankConfig(bankId: string): Promise { + const response = await sdk.resetBankConfig({ + client: this.client, + path: { bank_id: bankId }, + }); + + return this.validateResponse(response, 'resetBankConfig'); } /** @@ -623,6 +736,7 @@ export type { FileRetainResponse, ListMemoryUnitsResponse, BankProfileResponse, + BankConfigResponse, CreateBankRequest, Budget, }; diff --git a/hindsight-control-plane/package.json b/hindsight-control-plane/package.json index 25916e7f..56b30b60 100644 --- a/hindsight-control-plane/package.json +++ b/hindsight-control-plane/package.json @@ -11,7 +11,7 @@ "public" ], "scripts": { - "dev": "next dev --turbopack -p $(node -e \"const net=require('net');const s=net.createServer();s.listen(0,()=>{console.log(s.address().port);s.close()})\")", + "dev": "next dev --turbopack -p 9999", "build": "next build && npm run build:standalone", "build:standalone": "rm -rf standalone && SERVER_JS=$(find .next/standalone -path '*/node_modules' -prune -o -name 'server.js' -print | head -1) && test -n \"$SERVER_JS\" || (echo 'Error: server.js not found in .next/standalone - standalone build failed' && exit 1) && STANDALONE_ROOT=$(dirname \"$SERVER_JS\") && cp -r \"$STANDALONE_ROOT\" standalone && cp -r .next/standalone/node_modules standalone/node_modules && mkdir -p standalone/.next && cp -r .next/static standalone/.next/static && mkdir -p standalone/public && (cp -r public/* standalone/public/ 2>/dev/null || true)", "start": "next start", diff --git a/hindsight-control-plane/src/app/banks/[bankId]/page.tsx b/hindsight-control-plane/src/app/banks/[bankId]/page.tsx index ea2c8cb3..c3c57c88 100644 --- a/hindsight-control-plane/src/app/banks/[bankId]/page.tsx +++ b/hindsight-control-plane/src/app/banks/[bankId]/page.tsx @@ -36,7 +36,7 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { Brain, Trash2, Loader2, MoreVertical, Pencil } from "lucide-react"; +import { Brain, Trash2, Loader2, MoreVertical, Pencil, RotateCcw } from "lucide-react"; type NavItem = "recall" | "reflect" | "data" | "documents" | "entities" | "profile"; type DataSubTab = "world" | "experience" | "observations" | "mental-models"; @@ -61,6 +61,8 @@ export default function BankPage() { const [showClearObservationsDialog, setShowClearObservationsDialog] = useState(false); const [isClearingObservations, setIsClearingObservations] = useState(false); const [isConsolidating, setIsConsolidating] = useState(false); + const [showResetConfigDialog, setShowResetConfigDialog] = useState(false); + const [isResettingConfig, setIsResettingConfig] = useState(false); const handleTabChange = (tab: NavItem) => { router.push(`/banks/${bankId}?view=${tab}`); @@ -108,6 +110,19 @@ export default function BankPage() { } }; + const handleResetConfig = async () => { + if (!bankId) return; + setIsResettingConfig(true); + try { + await client.resetBankConfig(bankId); + setShowResetConfigDialog(false); + } catch { + // Error toast shown by API client interceptor + } finally { + setIsResettingConfig(false); + } + }; + const handleTriggerConsolidation = async () => { if (!bankId) return; @@ -180,6 +195,14 @@ export default function BankPage() { )} + setShowResetConfigDialog(true)} + className="text-amber-600 dark:text-amber-400 focus:text-amber-700 dark:focus:text-amber-300" + > + + Reset Configuration + + setShowDeleteDialog(true)} className="text-red-600 dark:text-red-400 focus:text-red-700 dark:focus:text-red-300" @@ -233,19 +256,13 @@ export default function BankPage() {
+
)} {bankConfigTab === "configuration" && ( -
-

- Configure disposition traits, mission, directives, and behavioral settings - for this bank. -

-
- - {bankConfigEnabled && } -
+
+
)}
@@ -481,6 +498,43 @@ export default function BankPage() { + {/* Reset Configuration Confirmation Dialog */} + + + + Reset Configuration + +
+

+ Are you sure you want to reset all configuration overrides for{" "} + {bankId}? +

+

+ All per-bank settings (retain, observations, reflect) will revert to server + defaults. This does not affect memories, entities, or the bank profile. +

+
+
+
+ + Cancel + + {isResettingConfig ? ( + <> + + Resetting... + + ) : ( + <> + + Reset Configuration + + )} + + +
+
+ {/* Clear Observations Confirmation Dialog */} diff --git a/hindsight-control-plane/src/components/bank-config-view.tsx b/hindsight-control-plane/src/components/bank-config-view.tsx index d55bb806..3ac190d6 100644 --- a/hindsight-control-plane/src/components/bank-config-view.tsx +++ b/hindsight-control-plane/src/components/bank-config-view.tsx @@ -1,11 +1,10 @@ "use client"; -import { useState, useEffect } from "react"; +import { useState, useEffect, useMemo, type ReactNode } from "react"; import { useBank } from "@/lib/bank-context"; import { client } from "@/lib/api"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, @@ -14,154 +13,175 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - AlertDialog, - AlertDialogAction, - AlertDialogCancel, - AlertDialogContent, - AlertDialogDescription, - AlertDialogFooter, - AlertDialogHeader, - AlertDialogTitle, -} from "@/components/ui/alert-dialog"; import { Alert, AlertDescription } from "@/components/ui/alert"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Loader2, AlertCircle, CheckCircle2, Pencil, RotateCcw, MoreVertical } from "lucide-react"; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"; +import { Loader2, AlertCircle } from "lucide-react"; +import { Card } from "@/components/ui/card"; -// Field metadata for UI rendering -const FIELD_CATEGORIES = { - retention: { - title: "Retention Settings", - description: "Control how memories are extracted and stored", - fields: { - retain_chunk_size: { - label: "Chunk Size", - type: "number", - description: "Size of text chunks for processing (tokens)", - min: 500, - max: 8000, - }, - retain_extraction_mode: { - label: "Extraction Mode", - type: "select", - description: "How to extract facts from content", - options: ["concise", "verbose", "custom"], - }, - retain_custom_instructions: { - label: "Custom Instructions", - type: "textarea", - description: - "Custom instructions for fact extraction (requires retain_extraction_mode='custom')", - placeholder: "Focus on technical details and implementation specifics...", - rows: 3, - }, - }, - }, - consolidation: { - title: "Consolidation Settings", - description: "Control observation synthesis", - fields: { - enable_observations: { - label: "Enable Observations", - type: "boolean", - description: "Enable automatic consolidation of facts into observations", - }, - }, - }, +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface ProfileData { + reflect_mission: string; + disposition_skepticism: number; + disposition_literalism: number; + disposition_empathy: number; +} + +type RetainEdits = { + retain_chunk_size: number | null; + retain_extraction_mode: string | null; + retain_mission: string | null; + retain_custom_instructions: string | null; }; +type ObservationsEdits = { + enable_observations: boolean | null; + observations_mission: string | null; +}; + +// ─── Slice helpers ──────────────────────────────────────────────────────────── + +function retainSlice(config: Record): RetainEdits { + return { + retain_chunk_size: config.retain_chunk_size ?? null, + retain_extraction_mode: config.retain_extraction_mode ?? null, + retain_mission: config.retain_mission ?? null, + retain_custom_instructions: config.retain_custom_instructions ?? null, + }; +} + +function observationsSlice(config: Record): ObservationsEdits { + return { + enable_observations: config.enable_observations ?? null, + observations_mission: config.observations_mission ?? null, + }; +} + +const DEFAULT_PROFILE: ProfileData = { + reflect_mission: "", + disposition_skepticism: 3, + disposition_literalism: 3, + disposition_empathy: 3, +}; + +// ─── BankConfigView ─────────────────────────────────────────────────────────── + export function BankConfigView() { const { currentBank: bankId } = useBank(); const [loading, setLoading] = useState(true); - const [config, setConfig] = useState>({}); - const [overrides, setOverrides] = useState>({}); - const [showEditDialog, setShowEditDialog] = useState(false); - const [showResetDialog, setShowResetDialog] = useState(false); - const [resetting, setResetting] = useState(false); + + // Source of truth + const [baseConfig, setBaseConfig] = useState>({}); + const [baseProfile, setBaseProfile] = useState(DEFAULT_PROFILE); + + // Per-section local edits + const [retainEdits, setRetainEdits] = useState(retainSlice({})); + const [observationsEdits, setObservationsEdits] = useState( + observationsSlice({}) + ); + const [reflectEdits, setReflectEdits] = useState(DEFAULT_PROFILE); + + // Per-section saving/error state + const [retainSaving, setRetainSaving] = useState(false); + const [observationsSaving, setObservationsSaving] = useState(false); + const [reflectSaving, setReflectSaving] = useState(false); + const [retainError, setRetainError] = useState(null); + const [observationsError, setObservationsError] = useState(null); + const [reflectError, setReflectError] = useState(null); + + // Reset dialog + + // Dirty tracking + const retainDirty = useMemo( + () => JSON.stringify(retainEdits) !== JSON.stringify(retainSlice(baseConfig)), + [retainEdits, baseConfig] + ); + const observationsDirty = useMemo( + () => JSON.stringify(observationsEdits) !== JSON.stringify(observationsSlice(baseConfig)), + [observationsEdits, baseConfig] + ); + const reflectDirty = useMemo( + () => JSON.stringify(reflectEdits) !== JSON.stringify(baseProfile), + [reflectEdits, baseProfile] + ); useEffect(() => { - if (bankId) { - loadConfig(); - } + if (bankId) loadAll(); }, [bankId]); - const loadConfig = async () => { + const loadAll = async () => { if (!bankId) return; - setLoading(true); try { - const response = await client.getBankConfig(bankId); - setConfig(response.config); - setOverrides(response.overrides); - } catch (err: any) { - console.error("Failed to load config:", err); + const [configResp, profileResp] = await Promise.all([ + client.getBankConfig(bankId), + client.getBankProfile(bankId), + ]); + const cfg = configResp.config; + const prof: ProfileData = { + reflect_mission: profileResp.mission ?? "", + disposition_skepticism: + cfg.disposition_skepticism ?? profileResp.disposition?.skepticism ?? 3, + disposition_literalism: + cfg.disposition_literalism ?? profileResp.disposition?.literalism ?? 3, + disposition_empathy: cfg.disposition_empathy ?? profileResp.disposition?.empathy ?? 3, + }; + setBaseConfig(cfg); + setBaseProfile(prof); + setRetainEdits(retainSlice(cfg)); + setObservationsEdits(observationsSlice(cfg)); + setReflectEdits(prof); + } catch (err) { + console.error("Failed to load bank data:", err); } finally { setLoading(false); } }; - const handleReset = () => { - setShowResetDialog(true); - }; - - const confirmReset = async () => { + const saveRetain = async () => { if (!bankId) return; - - setResetting(true); + setRetainSaving(true); + setRetainError(null); try { - await client.resetBankConfig(bankId); - await loadConfig(); - setShowResetDialog(false); + await client.updateBankConfig(bankId, retainEdits); + setBaseConfig((prev) => ({ ...prev, ...retainEdits })); } catch (err: any) { - // Error toast is shown automatically by the API client interceptor + setRetainError(err.message || "Failed to save retain settings"); } finally { - setResetting(false); + setRetainSaving(false); } }; - const renderReadOnlyField = (fieldKey: string, fieldMeta: any) => { - const value = config[fieldKey]; + const saveObservations = async () => { + if (!bankId) return; + setObservationsSaving(true); + setObservationsError(null); + try { + await client.updateBankConfig(bankId, observationsEdits); + setBaseConfig((prev) => ({ ...prev, ...observationsEdits })); + } catch (err: any) { + setObservationsError(err.message || "Failed to save observations settings"); + } finally { + setObservationsSaving(false); + } + }; - return ( -
-
-
{fieldKey}
- {fieldMeta.description && ( -

{fieldMeta.description}

- )} -
-
- {fieldMeta.type === "boolean" ? ( - - {value ? "Enabled" : "Disabled"} - - ) : fieldMeta.type === "textarea" ? ( - - {value ? `${value.substring(0, 50)}${value.length > 50 ? "..." : ""}` : "Not set"} - - ) : ( - value || Not set - )} -
-
- ); + const saveReflect = async () => { + if (!bankId) return; + setReflectSaving(true); + setReflectError(null); + try { + await client.updateBankConfig(bankId, { + reflect_mission: reflectEdits.reflect_mission || null, + disposition_skepticism: reflectEdits.disposition_skepticism, + disposition_literalism: reflectEdits.disposition_literalism, + disposition_empathy: reflectEdits.disposition_empathy, + }); + setBaseProfile(reflectEdits); + } catch (err: any) { + setReflectError(err.message || "Failed to save reflect settings"); + } finally { + setReflectSaving(false); + } }; if (!bankId) { @@ -182,298 +202,337 @@ export function BankConfigView() { return ( <> - - -
-
- Configuration Settings - - Behavioral parameters for this memory bank - -
- - - - - - setShowEditDialog(true)}> - - Edit - - - - Reset to Defaults - - - -
-
- - {Object.entries(FIELD_CATEGORIES).map(([catKey, category]) => ( -
-
-

{category.title}

-

{category.description}

-
-
- {Object.entries(category.fields).map(([fieldKey, fieldMeta]) => - renderReadOnlyField(fieldKey, fieldMeta) - )} -
-
- ))} -
-
+
+ {/* Retain Section */} + + + + setRetainEdits((prev) => ({ + ...prev, + retain_chunk_size: e.target.value ? parseFloat(e.target.value) : null, + })) + } + /> + + setRetainEdits((prev) => ({ ...prev, retain_mission: v || null }))} + placeholder="e.g. Always include technical decisions, API design choices, and architectural trade-offs. Ignore meeting logistics, greetings, and social exchanges." + rows={3} + /> + + + + {retainEdits.retain_extraction_mode === "custom" && ( + + setRetainEdits((prev) => ({ ...prev, retain_custom_instructions: v || null })) + } + rows={5} + /> + )} + - {showEditDialog && ( - setShowEditDialog(false)} - onSaved={() => { - loadConfig(); - setShowEditDialog(false); - }} - /> - )} + {/* Observations Section */} + + +
+ + setObservationsEdits((prev) => ({ ...prev, enable_observations: v })) + } + /> +
+
+ + setObservationsEdits((prev) => ({ ...prev, observations_mission: v || null })) + } + placeholder="e.g. Observations are stable facts about people and projects. Always include preferences, skills, and recurring patterns. Ignore one-off events and ephemeral state." + rows={3} + /> +
- - - - Reset Configuration - - Are you sure you want to reset all configuration overrides to defaults? This action - cannot be undone. - - - - Cancel - - {resetting ? ( - <> - - Resetting... - - ) : ( - "Reset to Defaults" - )} - - - - + {/* Reflect Section */} + + setReflectEdits((prev) => ({ ...prev, reflect_mission: v }))} + placeholder="e.g. You are a senior engineering assistant. Always ground answers in documented decisions and rationale. Ignore speculation. Be direct and precise." + rows={3} + /> + setReflectEdits((prev) => ({ ...prev, disposition_skepticism: v }))} + /> + setReflectEdits((prev) => ({ ...prev, disposition_literalism: v }))} + /> + setReflectEdits((prev) => ({ ...prev, disposition_empathy: v }))} + /> + +
); } -// Edit dialog component -function ConfigEditDialog({ - bankId, - initialConfig, - overrides, - onClose, - onSaved, +// ─── ConfigSection ──────────────────────────────────────────────────────────── + +function ConfigSection({ + title, + description, + children, + error, + dirty, + saving, + onSave, }: { - bankId: string; - initialConfig: Record; - overrides: Record; - onClose: () => void; - onSaved: () => void; + title: string; + description: string; + children: ReactNode; + error: string | null; + dirty: boolean; + saving: boolean; + onSave: () => void; }) { - const [saving, setSaving] = useState(false); - const [error, setError] = useState(null); - const [config, setConfig] = useState(initialConfig); - - const handleFieldChange = (field: string, value: any) => { - setConfig({ ...config, [field]: value }); - }; - - const handleSave = async () => { - setSaving(true); - setError(null); - try { - const updates: Record = {}; - Object.keys(config).forEach((key) => { - const isConfigurable = Object.values(FIELD_CATEGORIES).some((cat) => - Object.keys(cat.fields).includes(key) - ); - if (isConfigurable) { - updates[key] = config[key]; - } - }); - - await client.updateBankConfig(bankId, updates); - onSaved(); - } catch (err: any) { - console.error("Failed to save config:", err); - setError(err.message || "Failed to save configuration"); - setSaving(false); - } - }; - - const renderField = (fieldKey: string, fieldMeta: any) => { - const value = config[fieldKey]; - - if (fieldMeta.type === "boolean") { - return ( -
-
-
- - {fieldMeta.description && ( -

{fieldMeta.description}

- )} -
- -
-
- ); - } - - if (fieldMeta.type === "select") { - return ( -
- - {fieldMeta.description && ( -

{fieldMeta.description}

- )} - -
- ); - } - - if (fieldMeta.type === "textarea") { - return ( -
- - {fieldMeta.description && ( -

{fieldMeta.description}

- )} -