chore: internal renames (#204)

This commit renames the terminology across the entire codebase:
- "mental models" (fact_type='mental_model' in memory_units) → "observations"
- "reflections" table (stored reflect responses) → "mental_models"

Changes include:
- Database migration to rename tables, indexes, and constraints
- API endpoints: /reflections → /mental-models, /mental-models → /observations
- Config: ENABLE_MENTAL_MODELS → ENABLE_OBSERVATIONS
- Response models and Pydantic classes
- Reflect agent tools and prompts
- Control plane UI and routes
- Documentation and examples
- Regenerated OpenAPI spec and client SDKs (Python, TypeScript)
- Rust CLI: reflection commands → mental-model commands
- LiteLLM: updated fact_types documentation
This commit is contained in:
Nicolò Boschi 2026-01-27 09:53:28 +01:00 committed by GitHub
parent f3c5a9c1c2
commit 5b52a84fff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 2799 additions and 2923 deletions

View file

@ -100,7 +100,7 @@ cd hindsight-control-plane && npm run dev
Main operations:
- **Retain**: Store memories, extracts facts/entities/relationships
- **Recall**: Retrieve memories via 4 parallel strategies (semantic, BM25, graph, temporal) + reranking
- **Reflect**: Disposition-aware reasoning using memories and mental models
- **Reflect**: Disposition-aware reasoning using memories and mental models.
### Database
PostgreSQL with pgvector. Schema managed via Alembic migrations in `hindsight-api/hindsight_api/alembic/`. Migrations run automatically on API startup.

View file

@ -0,0 +1,134 @@
"""Rename mental_model fact_type to observation and reflections table to mental_models
Revision ID: t5o6p7q8r9s0
Revises: s4n5o6p7q8r9
Create Date: 2026-01-26
This migration implements the terminology rename:
1. mental_model (fact_type in memory_units) -> observation
2. reflections table -> mental_models table
The new terminology:
- Observations: Consolidated knowledge synthesized from facts (was mental_model)
- Mental Models: Stored reflect responses (was reflections)
"""
from collections.abc import Sequence
from alembic import context, op
revision: str = "t5o6p7q8r9s0"
down_revision: str | Sequence[str] | None = "s4n5o6p7q8r9"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _get_schema_prefix() -> str:
"""Get schema prefix for table names (required for multi-tenant support)."""
schema = context.config.get_main_option("target_schema")
return f'"{schema}".' if schema else ""
def upgrade() -> None:
"""Rename mental_model -> observation and reflections -> mental_models."""
schema = _get_schema_prefix()
# 1. Update fact_type values: mental_model -> observation
op.execute(f"""
UPDATE {schema}memory_units
SET fact_type = 'observation'
WHERE fact_type = 'mental_model'
""")
# 2. Update the CHECK constraint - remove mental_model, keep observation
op.execute(f"ALTER TABLE {schema}memory_units DROP CONSTRAINT IF EXISTS memory_units_fact_type_check")
op.execute(f"""
ALTER TABLE {schema}memory_units
ADD CONSTRAINT memory_units_fact_type_check
CHECK (fact_type IN ('world', 'experience', 'opinion', 'observation'))
""")
# 3. Rename the index for observations (was for mental_models)
op.execute(f"DROP INDEX IF EXISTS {schema}idx_memory_units_mental_models")
op.execute(f"""
CREATE INDEX IF NOT EXISTS idx_memory_units_observations
ON {schema}memory_units(bank_id, fact_type)
WHERE fact_type = 'observation'
""")
# 4. Update the unconsolidated index to not filter by fact_type since observations
# are now the consolidated type
op.execute(f"DROP INDEX IF EXISTS {schema}idx_memory_units_unconsolidated")
op.execute(f"""
CREATE INDEX IF NOT EXISTS idx_memory_units_unconsolidated
ON {schema}memory_units (bank_id, created_at)
WHERE consolidated_at IS NULL AND fact_type IN ('experience', 'world')
""")
# 5. Rename reflections table to mental_models
op.execute(f"ALTER TABLE IF EXISTS {schema}reflections RENAME TO mental_models")
# 6. Rename indexes for mental_models (was reflections)
op.execute(f"ALTER INDEX IF EXISTS {schema}idx_reflections_bank_id RENAME TO idx_mental_models_bank_id")
op.execute(f"ALTER INDEX IF EXISTS {schema}idx_reflections_embedding RENAME TO idx_mental_models_embedding")
op.execute(f"ALTER INDEX IF EXISTS {schema}idx_reflections_tags RENAME TO idx_mental_models_tags")
op.execute(f"ALTER INDEX IF EXISTS {schema}idx_reflections_text_search RENAME TO idx_mental_models_text_search")
# 7. Rename foreign key constraint
op.execute(f"""
ALTER TABLE {schema}mental_models
DROP CONSTRAINT IF EXISTS fk_reflections_bank_id
""")
op.execute(f"""
ALTER TABLE {schema}mental_models
ADD CONSTRAINT fk_mental_models_bank_id
FOREIGN KEY (bank_id) REFERENCES {schema}banks(bank_id) ON DELETE CASCADE
""")
def downgrade() -> None:
"""Reverse: observation -> mental_model and mental_models -> reflections."""
schema = _get_schema_prefix()
# 1. Rename mental_models table back to reflections
op.execute(f"ALTER TABLE IF EXISTS {schema}mental_models RENAME TO reflections")
# 2. Rename indexes back
op.execute(f"ALTER INDEX IF EXISTS {schema}idx_mental_models_bank_id RENAME TO idx_reflections_bank_id")
op.execute(f"ALTER INDEX IF EXISTS {schema}idx_mental_models_embedding RENAME TO idx_reflections_embedding")
op.execute(f"ALTER INDEX IF EXISTS {schema}idx_mental_models_tags RENAME TO idx_reflections_tags")
op.execute(f"ALTER INDEX IF EXISTS {schema}idx_mental_models_text_search RENAME TO idx_reflections_text_search")
# 3. Rename foreign key back
op.execute(f"""
ALTER TABLE {schema}reflections
DROP CONSTRAINT IF EXISTS fk_mental_models_bank_id
""")
op.execute(f"""
ALTER TABLE {schema}reflections
ADD CONSTRAINT fk_reflections_bank_id
FOREIGN KEY (bank_id) REFERENCES {schema}banks(bank_id) ON DELETE CASCADE
""")
# 4. Update fact_type values: observation -> mental_model
op.execute(f"""
UPDATE {schema}memory_units
SET fact_type = 'mental_model'
WHERE fact_type = 'observation'
""")
# 5. Update the CHECK constraint back
op.execute(f"ALTER TABLE {schema}memory_units DROP CONSTRAINT IF EXISTS memory_units_fact_type_check")
op.execute(f"""
ALTER TABLE {schema}memory_units
ADD CONSTRAINT memory_units_fact_type_check
CHECK (fact_type IN ('world', 'experience', 'opinion', 'observation', 'mental_model'))
""")
# 6. Rename index back
op.execute(f"DROP INDEX IF EXISTS {schema}idx_memory_units_observations")
op.execute(f"""
CREATE INDEX IF NOT EXISTS idx_memory_units_mental_models
ON {schema}memory_units(bank_id, fact_type)
WHERE fact_type = 'mental_model'
""")

View file

@ -92,7 +92,7 @@ class RecallRequest(BaseModel):
query: str
types: list[str] | None = Field(
default=None,
description="List of fact types to recall: 'world', 'experience', 'mental_model'. Defaults to world and experience if not specified. "
description="List of fact types to recall: 'world', 'experience', 'observation'. Defaults to world and experience if not specified. "
"Note: 'opinion' is accepted but ignored (opinions are excluded from recall).",
)
budget: Budget = Budget.MID
@ -588,6 +588,14 @@ class ReflectResponse(BaseModel):
"trace": {
"tool_calls": [{"tool": "recall", "input": {"query": "AI"}, "duration_ms": 150}],
"llm_calls": [{"scope": "agent_1", "duration_ms": 1200}],
"observations": [
{
"id": "obs-1",
"name": "AI Technology",
"type": "concept",
"subtype": "structural",
}
],
},
}
}
@ -991,7 +999,7 @@ class BankStatsResponse(BaseModel):
"failed_operations": 0,
"last_consolidated_at": "2024-01-15T10:30:00Z",
"pending_consolidation": 0,
"total_mental_models": 45,
"total_observations": 45,
}
}
)
@ -1008,8 +1016,8 @@ class BankStatsResponse(BaseModel):
failed_operations: int
# Consolidation stats
last_consolidated_at: str | None = Field(default=None, description="When consolidation last ran (ISO format)")
pending_consolidation: int = Field(default=0, description="Number of memories not yet processed into mental models")
total_mental_models: int = Field(default=0, description="Total number of mental models")
pending_consolidation: int = Field(default=0, description="Number of memories not yet processed into observations")
total_observations: int = Field(default=0, description="Total number of observations")
# Mental Model models
@ -1070,12 +1078,12 @@ class UpdateDirectiveRequest(BaseModel):
# =========================================================================
# Reflections Models
# Mental Models (stored reflect responses)
# =========================================================================
class ReflectionResponse(BaseModel):
"""Response model for a reflection."""
class MentalModelResponse(BaseModel):
"""Response model for a mental model (stored reflect response)."""
id: str
bank_id: str
@ -1087,18 +1095,18 @@ class ReflectionResponse(BaseModel):
created_at: str | None = None
reflect_response: dict | None = Field(
default=None,
description="Full reflect API response payload including based_on facts and mental_models",
description="Full reflect API response payload including based_on facts and observations",
)
class ReflectionListResponse(BaseModel):
"""Response model for listing reflections."""
class MentalModelListResponse(BaseModel):
"""Response model for listing mental models."""
items: list[ReflectionResponse]
items: list[MentalModelResponse]
class CreateReflectionRequest(BaseModel):
"""Request model for creating a reflection."""
class CreateMentalModelRequest(BaseModel):
"""Request model for creating a mental model."""
model_config = ConfigDict(
json_schema_extra={
@ -1111,20 +1119,20 @@ class CreateReflectionRequest(BaseModel):
}
)
name: str = Field(description="Human-readable name for the reflection")
name: str = Field(description="Human-readable name for the mental model")
source_query: str = Field(description="The query to run to generate content")
tags: list[str] = Field(default_factory=list, description="Tags for scoped visibility")
max_tokens: int = Field(default=2048, ge=256, le=8192, description="Maximum tokens for generated content")
class CreateReflectionResponse(BaseModel):
"""Response model for reflection creation."""
class CreateMentalModelResponse(BaseModel):
"""Response model for mental model creation."""
operation_id: str = Field(description="Operation ID to track progress")
class UpdateReflectionRequest(BaseModel):
"""Request model for updating a reflection."""
class UpdateMentalModelRequest(BaseModel):
"""Request model for updating a mental model."""
model_config = ConfigDict(
json_schema_extra={
@ -1134,7 +1142,7 @@ class UpdateReflectionRequest(BaseModel):
}
)
name: str | None = Field(default=None, description="New name for the reflection")
name: str | None = Field(default=None, description="New name for the mental model")
class OperationResponse(BaseModel):
@ -1263,7 +1271,7 @@ class AsyncOperationSubmitResponse(BaseModel):
class FeaturesInfo(BaseModel):
"""Feature flags indicating which capabilities are enabled."""
mental_models: bool = Field(description="Whether mental models (auto-consolidation) are enabled")
observations: bool = Field(description="Whether observations (auto-consolidation) are enabled")
mcp: bool = Field(description="Whether MCP (Model Context Protocol) server is enabled")
worker: bool = Field(description="Whether the background worker is enabled")
@ -1276,7 +1284,7 @@ class VersionResponse(BaseModel):
"example": {
"api_version": "1.0.0",
"features": {
"mental_models": False,
"observations": False,
"mcp": True,
"worker": True,
},
@ -1523,7 +1531,7 @@ def _register_routes(app: FastAPI):
return VersionResponse(
api_version="1.0.0",
features=FeaturesInfo(
mental_models=config.enable_mental_models,
observations=config.enable_observations,
mcp=config.mcp_enabled,
worker=config.worker_enabled,
),
@ -1837,7 +1845,7 @@ def _register_routes(app: FastAPI):
tags_match=request.tags_match,
)
# Build based_on (memories + mental_models) if facts are requested
# Build based_on (memories + observations) if facts are requested
based_on_result: ReflectBasedOn | None = None
if request.include.facts is not None:
memories = []
@ -1855,7 +1863,7 @@ def _register_routes(app: FastAPI):
)
based_on_result = ReflectBasedOn(memories=memories)
# Build trace (tool_calls + llm_calls + mental_models) if tool_calls is requested
# Build trace (tool_calls + llm_calls + observations) if tool_calls is requested
trace_result: ReflectTrace | None = None
if request.include.tool_calls is not None:
include_output = request.include.tool_calls.output
@ -2020,16 +2028,16 @@ def _register_routes(app: FastAPI):
last_consolidated_at = consolidation_stats["last_consolidated_at"] if consolidation_stats else None
pending_consolidation = consolidation_stats["pending"] if consolidation_stats else 0
# Count total mental models
mental_model_count_result = await conn.fetchrow(
# Count total observations (consolidated knowledge)
observation_count_result = await conn.fetchrow(
f"""
SELECT COUNT(*) as count
FROM {fq_table("memory_units")}
WHERE bank_id = $1 AND fact_type = 'mental_model'
WHERE bank_id = $1 AND fact_type = 'observation'
""",
bank_id,
)
total_mental_models = mental_model_count_result["count"] if mental_model_count_result else 0
total_observations = observation_count_result["count"] if observation_count_result else 0
# Format results
nodes_by_type = {row["fact_type"]: row["count"] for row in node_stats}
@ -2062,7 +2070,7 @@ def _register_routes(app: FastAPI):
failed_operations=failed_operations,
last_consolidated_at=(last_consolidated_at.isoformat() if last_consolidated_at else None),
pending_consolidation=pending_consolidation,
total_mental_models=total_mental_models,
total_observations=total_observations,
)
except (AuthenticationError, HTTPException):
@ -2169,18 +2177,18 @@ def _register_routes(app: FastAPI):
# =========================================================================
# =========================================================================
# REFLECTIONS ENDPOINTS
# MENTAL MODELS ENDPOINTS (stored reflect responses)
# =========================================================================
@app.get(
"/v1/default/banks/{bank_id}/reflections",
response_model=ReflectionListResponse,
summary="List reflections",
"/v1/default/banks/{bank_id}/mental-models",
response_model=MentalModelListResponse,
summary="List mental models",
description="List user-curated living documents that stay current.",
operation_id="list_reflections",
tags=["Reflections"],
operation_id="list_mental_models",
tags=["Mental Models"],
)
async def api_list_reflections(
async def api_list_mental_models(
bank_id: str,
tags_filter: list[str] | None = Query(None, alias="tags", description="Filter by tags"),
tags_match: Literal["any", "all", "exact"] = Query("any", description="How to match tags"),
@ -2188,9 +2196,9 @@ def _register_routes(app: FastAPI):
offset: int = Query(0, ge=0),
request_context: RequestContext = Depends(get_request_context),
):
"""List reflections for a bank."""
"""List mental models for a bank."""
try:
reflections = await app.state.memory.list_reflections(
mental_models = await app.state.memory.list_mental_models(
bank_id=bank_id,
tags=tags_filter,
tags_match=tags_match,
@ -2198,66 +2206,66 @@ def _register_routes(app: FastAPI):
offset=offset,
request_context=request_context,
)
return ReflectionListResponse(items=[ReflectionResponse(**r) for r in reflections])
return MentalModelListResponse(items=[MentalModelResponse(**m) for m in mental_models])
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in GET /v1/default/banks/{bank_id}/reflections: {error_detail}")
logger.error(f"Error in GET /v1/default/banks/{bank_id}/mental-models: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.get(
"/v1/default/banks/{bank_id}/reflections/{reflection_id}",
response_model=ReflectionResponse,
summary="Get reflection",
description="Get a specific reflection by ID.",
operation_id="get_reflection",
tags=["Reflections"],
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}",
response_model=MentalModelResponse,
summary="Get mental model",
description="Get a specific mental model by ID.",
operation_id="get_mental_model",
tags=["Mental Models"],
)
async def api_get_reflection(
async def api_get_mental_model(
bank_id: str,
reflection_id: str,
mental_model_id: str,
request_context: RequestContext = Depends(get_request_context),
):
"""Get a reflection by ID."""
"""Get a mental model by ID."""
try:
reflection = await app.state.memory.get_reflection(
mental_model = await app.state.memory.get_mental_model(
bank_id=bank_id,
reflection_id=reflection_id,
mental_model_id=mental_model_id,
request_context=request_context,
)
if reflection is None:
raise HTTPException(status_code=404, detail=f"Reflection '{reflection_id}' not found")
return ReflectionResponse(**reflection)
if mental_model is None:
raise HTTPException(status_code=404, detail=f"Mental model '{mental_model_id}' not found")
return MentalModelResponse(**mental_model)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in GET /v1/default/banks/{bank_id}/reflections/{reflection_id}: {error_detail}")
logger.error(f"Error in GET /v1/default/banks/{bank_id}/mental-models/{mental_model_id}: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.post(
"/v1/default/banks/{bank_id}/reflections",
response_model=CreateReflectionResponse,
summary="Create reflection",
description="Create a reflection by running reflect with the source query in the background. "
"/v1/default/banks/{bank_id}/mental-models",
response_model=CreateMentalModelResponse,
summary="Create mental model",
description="Create a mental model by running reflect with the source query in the background. "
"Returns an operation ID to track progress. The content is auto-generated by the reflect endpoint. "
"Use the operations endpoint to check completion status.",
operation_id="create_reflection",
tags=["Reflections"],
operation_id="create_mental_model",
tags=["Mental Models"],
)
async def api_create_reflection(
async def api_create_mental_model(
bank_id: str,
body: CreateReflectionRequest,
body: CreateMentalModelRequest,
request_context: RequestContext = Depends(get_request_context),
):
"""Create a reflection (async - returns operation_id)."""
"""Create a mental model (async - returns operation_id)."""
try:
result = await app.state.memory.submit_async_create_reflection(
result = await app.state.memory.submit_async_create_mental_model(
bank_id=bank_id,
name=body.name,
source_query=body.source_query,
@ -2265,7 +2273,7 @@ def _register_routes(app: FastAPI):
max_tokens=body.max_tokens,
request_context=request_context,
)
return CreateReflectionResponse(operation_id=result["operation_id"])
return CreateMentalModelResponse(operation_id=result["operation_id"])
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except (AuthenticationError, HTTPException):
@ -2274,27 +2282,27 @@ def _register_routes(app: FastAPI):
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in POST /v1/default/banks/{bank_id}/reflections: {error_detail}")
logger.error(f"Error in POST /v1/default/banks/{bank_id}/mental-models: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.post(
"/v1/default/banks/{bank_id}/reflections/{reflection_id}/refresh",
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/refresh",
response_model=AsyncOperationSubmitResponse,
summary="Refresh reflection",
summary="Refresh mental model",
description="Submit an async task to re-run the source query through reflect and update the content.",
operation_id="refresh_reflection",
tags=["Reflections"],
operation_id="refresh_mental_model",
tags=["Mental Models"],
)
async def api_refresh_reflection(
async def api_refresh_mental_model(
bank_id: str,
reflection_id: str,
mental_model_id: str,
request_context: RequestContext = Depends(get_request_context),
):
"""Refresh a reflection by re-running its source query (async)."""
"""Refresh a mental model by re-running its source query (async)."""
try:
result = await app.state.memory.submit_async_refresh_reflection(
result = await app.state.memory.submit_async_refresh_mental_model(
bank_id=bank_id,
reflection_id=reflection_id,
mental_model_id=mental_model_id,
request_context=request_context,
)
return AsyncOperationSubmitResponse(operation_id=result["operation_id"], status="queued")
@ -2307,65 +2315,65 @@ def _register_routes(app: FastAPI):
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(
f"Error in POST /v1/default/banks/{bank_id}/reflections/{reflection_id}/refresh: {error_detail}"
f"Error in POST /v1/default/banks/{bank_id}/mental-models/{mental_model_id}/refresh: {error_detail}"
)
raise HTTPException(status_code=500, detail=str(e))
@app.patch(
"/v1/default/banks/{bank_id}/reflections/{reflection_id}",
response_model=ReflectionResponse,
summary="Update reflection",
description="Update a reflection's name.",
operation_id="update_reflection",
tags=["Reflections"],
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}",
response_model=MentalModelResponse,
summary="Update mental model",
description="Update a mental model's name.",
operation_id="update_mental_model",
tags=["Mental Models"],
)
async def api_update_reflection(
async def api_update_mental_model(
bank_id: str,
reflection_id: str,
body: UpdateReflectionRequest,
mental_model_id: str,
body: UpdateMentalModelRequest,
request_context: RequestContext = Depends(get_request_context),
):
"""Update a reflection."""
"""Update a mental model."""
try:
reflection = await app.state.memory.update_reflection(
mental_model = await app.state.memory.update_mental_model(
bank_id=bank_id,
reflection_id=reflection_id,
mental_model_id=mental_model_id,
name=body.name,
request_context=request_context,
)
if reflection is None:
raise HTTPException(status_code=404, detail=f"Reflection '{reflection_id}' not found")
return ReflectionResponse(**reflection)
if mental_model is None:
raise HTTPException(status_code=404, detail=f"Mental model '{mental_model_id}' not found")
return MentalModelResponse(**mental_model)
except (AuthenticationError, HTTPException):
raise
except Exception as e:
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in PATCH /v1/default/banks/{bank_id}/reflections/{reflection_id}: {error_detail}")
logger.error(f"Error in PATCH /v1/default/banks/{bank_id}/mental-models/{mental_model_id}: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.delete(
"/v1/default/banks/{bank_id}/reflections/{reflection_id}",
summary="Delete reflection",
description="Delete a reflection.",
operation_id="delete_reflection",
tags=["Reflections"],
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}",
summary="Delete mental model",
description="Delete a mental model.",
operation_id="delete_mental_model",
tags=["Mental Models"],
)
async def api_delete_reflection(
async def api_delete_mental_model(
bank_id: str,
reflection_id: str,
mental_model_id: str,
request_context: RequestContext = Depends(get_request_context),
):
"""Delete a reflection."""
"""Delete a mental model."""
try:
deleted = await app.state.memory.delete_reflection(
deleted = await app.state.memory.delete_mental_model(
bank_id=bank_id,
reflection_id=reflection_id,
mental_model_id=mental_model_id,
request_context=request_context,
)
if not deleted:
raise HTTPException(status_code=404, detail=f"Reflection '{reflection_id}' not found")
raise HTTPException(status_code=404, detail=f"Mental model '{mental_model_id}' not found")
return {"status": "deleted"}
except (AuthenticationError, HTTPException):
raise
@ -2373,7 +2381,7 @@ def _register_routes(app: FastAPI):
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/reflections/{reflection_id}: {error_detail}")
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/mental-models/{mental_model_id}: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
# =========================================================================
@ -3097,20 +3105,20 @@ def _register_routes(app: FastAPI):
raise HTTPException(status_code=500, detail=str(e))
@app.delete(
"/v1/default/banks/{bank_id}/mental-models",
"/v1/default/banks/{bank_id}/observations",
response_model=DeleteResponse,
summary="Clear all mental models",
description="Delete all mental models for a memory bank. This is useful for resetting the consolidated knowledge.",
operation_id="clear_mental_models",
summary="Clear all observations",
description="Delete all observations for a memory bank. This is useful for resetting the consolidated knowledge.",
operation_id="clear_observations",
tags=["Banks"],
)
async def api_clear_mental_models(bank_id: str, request_context: RequestContext = Depends(get_request_context)):
"""Clear all mental models for a bank."""
async def api_clear_observations(bank_id: str, request_context: RequestContext = Depends(get_request_context)):
"""Clear all observations for a bank."""
try:
result = await app.state.memory.clear_mental_models(bank_id, request_context=request_context)
result = await app.state.memory.clear_observations(bank_id, request_context=request_context)
return DeleteResponse(
success=True,
message=f"Cleared {result.get('deleted_count', 0)} mental models",
message=f"Cleared {result.get('deleted_count', 0)} observations",
deleted_count=result.get("deleted_count", 0),
)
except (AuthenticationError, HTTPException):
@ -3119,14 +3127,14 @@ def _register_routes(app: FastAPI):
import traceback
error_detail = f"{str(e)}\n\nTraceback:\n{traceback.format_exc()}"
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/mental-models: {error_detail}")
logger.error(f"Error in DELETE /v1/default/banks/{bank_id}/observations: {error_detail}")
raise HTTPException(status_code=500, detail=str(e))
@app.post(
"/v1/default/banks/{bank_id}/consolidate",
response_model=ConsolidationResponse,
summary="Trigger consolidation",
description="Run memory consolidation to create/update mental models from recent memories.",
description="Run memory consolidation to create/update observations from recent memories.",
operation_id="trigger_consolidation",
tags=["Banks"],
)

View file

@ -87,7 +87,7 @@ ENV_MCP_LOCAL_BANK_ID = "HINDSIGHT_API_MCP_LOCAL_BANK_ID"
ENV_MCP_INSTRUCTIONS = "HINDSIGHT_API_MCP_INSTRUCTIONS"
ENV_MENTAL_MODEL_REFRESH_CONCURRENCY = "HINDSIGHT_API_MENTAL_MODEL_REFRESH_CONCURRENCY"
# Observation thresholds
# Observation settings (consolidated knowledge from facts)
ENV_OBSERVATION_MIN_FACTS = "HINDSIGHT_API_OBSERVATION_MIN_FACTS"
ENV_OBSERVATION_TOP_ENTITIES = "HINDSIGHT_API_OBSERVATION_TOP_ENTITIES"
@ -98,8 +98,8 @@ ENV_RETAIN_EXTRACT_CAUSAL_LINKS = "HINDSIGHT_API_RETAIN_EXTRACT_CAUSAL_LINKS"
ENV_RETAIN_EXTRACTION_MODE = "HINDSIGHT_API_RETAIN_EXTRACTION_MODE"
ENV_RETAIN_OBSERVATIONS_ASYNC = "HINDSIGHT_API_RETAIN_OBSERVATIONS_ASYNC"
# Mental models settings
ENV_ENABLE_MENTAL_MODELS = "HINDSIGHT_API_ENABLE_MENTAL_MODELS"
# Observations settings (consolidated knowledge from facts)
ENV_ENABLE_OBSERVATIONS = "HINDSIGHT_API_ENABLE_OBSERVATIONS"
ENV_CONSOLIDATION_SIMILARITY_THRESHOLD = "HINDSIGHT_API_CONSOLIDATION_SIMILARITY_THRESHOLD"
ENV_CONSOLIDATION_BATCH_SIZE = "HINDSIGHT_API_CONSOLIDATION_BATCH_SIZE"
@ -181,8 +181,8 @@ DEFAULT_RETAIN_EXTRACTION_MODE = "concise" # Extraction mode: "concise" or "ver
RETAIN_EXTRACTION_MODES = ("concise", "verbose") # Allowed extraction modes
DEFAULT_RETAIN_OBSERVATIONS_ASYNC = False # Run observation generation async (after retain completes)
# Mental models defaults
DEFAULT_ENABLE_MENTAL_MODELS = False # Mental models disabled by default (experimental)
# Observations defaults (consolidated knowledge from facts)
DEFAULT_ENABLE_OBSERVATIONS = False # Observations disabled by default (experimental)
DEFAULT_CONSOLIDATION_SIMILARITY_THRESHOLD = 0.75 # Minimum similarity to consider a learning related
DEFAULT_CONSOLIDATION_BATCH_SIZE = 50 # Memories to load per batch (internal memory optimization)
@ -344,8 +344,8 @@ class HindsightConfig:
retain_extraction_mode: str
retain_observations_async: bool
# Mental models settings
enable_mental_models: bool
# Observations settings (consolidated knowledge from facts)
enable_observations: bool
consolidation_similarity_threshold: float
consolidation_batch_size: int
@ -455,9 +455,8 @@ class HindsightConfig:
ENV_RETAIN_OBSERVATIONS_ASYNC, str(DEFAULT_RETAIN_OBSERVATIONS_ASYNC)
).lower()
== "true",
# Mental models settings
enable_mental_models=os.getenv(ENV_ENABLE_MENTAL_MODELS, str(DEFAULT_ENABLE_MENTAL_MODELS)).lower()
== "true",
# Observations settings (consolidated knowledge from facts)
enable_observations=os.getenv(ENV_ENABLE_OBSERVATIONS, str(DEFAULT_ENABLE_OBSERVATIONS)).lower() == "true",
consolidation_similarity_threshold=float(
os.getenv(ENV_CONSOLIDATION_SIMILARITY_THRESHOLD, str(DEFAULT_CONSOLIDATION_SIMILARITY_THRESHOLD))
),

View file

@ -1,13 +1,13 @@
"""Consolidation engine for automatic mental model creation from memories.
"""Consolidation engine for automatic observation creation from memories.
The consolidation engine runs as a background job after retain operations complete.
It processes new memories and either:
- Creates new mental models from novel facts
- Updates existing mental models when new evidence supports/contradicts/refines them
- Creates new observations from novel facts
- Updates existing observations when new evidence supports/contradicts/refines them
Mental models are stored in memory_units with fact_type='mental_model' and include:
Observations are stored in memory_units with fact_type='observation' and include:
- proof_count: Number of supporting memories
- source_memory_ids: Array of memory UUIDs that contribute to this mental model
- source_memory_ids: Array of memory UUIDs that contribute to this observation
- history: JSONB tracking changes over time
"""
@ -89,7 +89,7 @@ async def run_consolidation_job(
max_memories_per_batch = config.consolidation_batch_size
# Check if consolidation is enabled
if not config.enable_mental_models:
if not config.enable_observations:
logger.debug(f"Consolidation disabled for bank {bank_id}")
return {"status": "disabled", "bank_id": bank_id}
@ -136,9 +136,9 @@ async def run_consolidation_job(
# Process each memory with individual commits for crash recovery
stats = {
"memories_processed": 0,
"mental_models_created": 0,
"mental_models_updated": 0,
"mental_models_merged": 0,
"observations_created": 0,
"observations_updated": 0,
"observations_merged": 0,
"actions_executed": 0,
"skipped": 0,
}
@ -201,18 +201,18 @@ async def run_consolidation_job(
action = result.get("action")
if action == "created":
stats["mental_models_created"] += 1
stats["observations_created"] += 1
stats["actions_executed"] += 1
elif action == "updated":
stats["mental_models_updated"] += 1
stats["observations_updated"] += 1
stats["actions_executed"] += 1
elif action == "merged":
stats["mental_models_merged"] += 1
stats["observations_merged"] += 1
stats["actions_executed"] += 1
elif action == "multiple":
stats["mental_models_created"] += result.get("created", 0)
stats["mental_models_updated"] += result.get("updated", 0)
stats["mental_models_merged"] += result.get("merged", 0)
stats["observations_created"] += result.get("created", 0)
stats["observations_updated"] += result.get("updated", 0)
stats["observations_merged"] += result.get("merged", 0)
stats["actions_executed"] += result.get("total_actions", 0)
elif action == "skipped":
stats["skipped"] += 1
@ -234,9 +234,9 @@ async def run_consolidation_job(
perf.log(
f"[3] Results: {stats['memories_processed']} memories -> "
f"{stats['actions_executed']} actions "
f"({stats['mental_models_created']} created, "
f"{stats['mental_models_updated']} updated, "
f"{stats['mental_models_merged']} merged, "
f"({stats['observations_created']} created, "
f"{stats['observations_updated']} updated, "
f"{stats['observations_merged']} merged, "
f"{stats['skipped']} skipped)"
)
@ -272,13 +272,13 @@ async def _process_memory(
Process a single memory for consolidation using a SINGLE LLM call.
This function:
1. Finds related mental models (can be empty)
1. Finds related observations (can be empty)
2. Uses ONE LLM call to extract durable knowledge AND decide on actions
3. Executes array of actions (can be multiple creates/updates)
The LLM handles all cases:
- No related models: returns create action(s) with extracted durable knowledge
- Related models exist: returns update/create actions based on tag routing
- No related observations: returns create action(s) with extracted durable knowledge
- Related observations exist: returns update/create actions based on tag routing
- Purely ephemeral fact: returns empty array (skip)
Returns:
@ -288,9 +288,9 @@ async def _process_memory(
memory_id = memory["id"]
fact_tags = memory.get("tags") or []
# Find related mental models using the full recall system (NO tag filtering)
# Find related observations using the full recall system (NO tag filtering)
t0 = time.time()
related_mental_models = await _find_related_mental_models(
related_observations = await _find_related_observations(
conn=conn,
memory_engine=memory_engine,
bank_id=bank_id,
@ -300,13 +300,13 @@ async def _process_memory(
if perf:
perf.record_timing("recall", time.time() - t0)
# Single LLM call handles ALL cases (with or without existing models)
# Single LLM call handles ALL cases (with or without existing observations)
t0 = time.time()
actions = await _consolidate_with_llm(
memory_engine=memory_engine,
fact_text=fact_text,
fact_tags=fact_tags,
mental_models=related_mental_models, # Can be empty list
observations=related_observations, # Can be empty list
mission=mission,
)
if perf:
@ -327,7 +327,7 @@ async def _process_memory(
bank_id=bank_id,
memory_id=memory_id,
action=action,
mental_models=related_mental_models,
observations=related_observations,
source_mentioned_at=memory.get("mentioned_at"),
perf=perf,
)
@ -373,14 +373,14 @@ async def _execute_update_action(
bank_id: str,
memory_id: uuid.UUID,
action: dict[str, Any],
mental_models: list[dict[str, Any]],
observations: list[dict[str, Any]],
source_mentioned_at: datetime | None = None,
perf: ConsolidationPerfLog | None = None,
) -> dict[str, Any]:
"""
Execute an update action on an existing mental model.
Execute an update action on an existing observation.
Updates the mental model text, adds to history, increments proof_count,
Updates the observation text, adds to history, increments proof_count,
and updates mentioned_at if the new source memory has a more recent date.
"""
learning_id = action.get("learning_id")
@ -390,8 +390,8 @@ async def _execute_update_action(
if not learning_id or not new_text:
return {"action": "skipped", "reason": "missing_learning_id_or_text"}
# Find the mental model
model = next((m for m in mental_models if str(m["id"]) == learning_id), None)
# Find the observation
model = next((m for m in observations if str(m["id"]) == learning_id), None)
if not model:
return {"action": "skipped", "reason": "learning_not_found"}
@ -441,14 +441,14 @@ async def _execute_update_action(
source_mentioned_at,
)
# Create links from memory to mental model
# Create links from memory to observation
await _create_memory_links(conn, memory_id, uuid.UUID(learning_id))
if perf:
perf.record_timing("db_write", time.time() - t0)
logger.debug(f"Updated mental model {learning_id} with memory {memory_id}")
logger.debug(f"Updated observation {learning_id} with memory {memory_id}")
return {"action": "updated", "mental_model_id": learning_id}
return {"action": "updated", "observation_id": learning_id}
async def _execute_create_action(
@ -463,9 +463,9 @@ async def _execute_create_action(
perf: ConsolidationPerfLog | None = None,
) -> dict[str, Any]:
"""
Execute a create action for a new mental model.
Execute a create action for a new observation.
Creates a new mental model with the specified text and tags.
Creates a new observation with the specified text and tags.
The text comes directly from the classify LLM - no second LLM call needed.
"""
text = action.get("text")
@ -475,12 +475,12 @@ async def _execute_create_action(
return {"action": "skipped", "reason": "missing_text"}
# Use text directly from classify - skip the redundant LLM call
result = await _create_mental_model_directly(
result = await _create_observation_directly(
conn=conn,
memory_engine=memory_engine,
bank_id=bank_id,
source_memory_id=memory_id,
mental_model_text=text, # Text already processed by classify LLM
observation_text=text, # Text already processed by classify LLM
tags=tags,
event_date=event_date,
occurred_start=occurred_start,
@ -488,7 +488,7 @@ async def _execute_create_action(
perf=perf,
)
logger.debug(f"Created mental model {result.get('mental_model_id')} from memory {memory_id} (tags: {tags})")
logger.debug(f"Created observation {result.get('observation_id')} from memory {memory_id} (tags: {tags})")
return result
@ -496,17 +496,17 @@ async def _execute_create_action(
async def _create_memory_links(
conn: "Connection",
memory_id: uuid.UUID,
mental_model_id: uuid.UUID,
observation_id: uuid.UUID,
) -> None:
"""
Create links between a source memory and its mental model.
Create links between a source memory and its observation.
This:
1. Creates bidirectional semantic links between memory and mental model
2. Copies existing memory_links from the source memory to the mental model
3. Copies entity links from the source memory to the mental model
1. Creates bidirectional semantic links between memory and observation
2. Copies existing memory_links from the source memory to the observation
3. Copies entity links from the source memory to the observation
This enables graph traversal to find related memories via their mental models.
This enables graph traversal to find related memories via their observations.
Note: Uses EXISTS checks to handle the case where source memory was deleted
by a concurrent operation between fetching and link creation.
@ -515,7 +515,7 @@ async def _create_memory_links(
ml_table = fq_table("memory_links")
ue_table = fq_table("unit_entities")
# 1. Bidirectional link between memory and mental model
# 1. Bidirectional link between memory and observation
# Only insert if both units exist (handles concurrent deletion)
await conn.execute(
f"""
@ -526,7 +526,7 @@ async def _create_memory_links(
ON CONFLICT DO NOTHING
""",
memory_id,
mental_model_id,
observation_id,
)
await conn.execute(
f"""
@ -536,12 +536,12 @@ async def _create_memory_links(
AND EXISTS (SELECT 1 FROM {mu_table} WHERE id = $2)
ON CONFLICT DO NOTHING
""",
mental_model_id,
observation_id,
memory_id,
)
# 2. Copy outgoing memory_links from source memory to mental model
# If source memory links to X, mental model should also link to X
# 2. Copy outgoing memory_links from source memory to observation
# If source memory links to X, observation should also link to X
await conn.execute(
f"""
INSERT INTO {ml_table} (from_unit_id, to_unit_id, link_type, entity_id, weight)
@ -552,12 +552,12 @@ async def _create_memory_links(
AND EXISTS (SELECT 1 FROM {mu_table} WHERE id = ml.to_unit_id)
ON CONFLICT DO NOTHING
""",
mental_model_id,
observation_id,
memory_id,
)
# 3. Copy incoming memory_links from source memory to mental model
# If X links to source memory, X should also link to mental model
# 3. Copy incoming memory_links from source memory to observation
# If X links to source memory, X should also link to observation
await conn.execute(
f"""
INSERT INTO {ml_table} (from_unit_id, to_unit_id, link_type, entity_id, weight)
@ -568,11 +568,11 @@ async def _create_memory_links(
AND EXISTS (SELECT 1 FROM {mu_table} WHERE id = ml.from_unit_id)
ON CONFLICT DO NOTHING
""",
mental_model_id,
observation_id,
memory_id,
)
# 4. Copy entity links from source memory to mental model
# 4. Copy entity links from source memory to observation
await conn.execute(
f"""
INSERT INTO {ue_table} (unit_id, entity_id)
@ -582,12 +582,12 @@ async def _create_memory_links(
AND EXISTS (SELECT 1 FROM {mu_table} WHERE id = $1)
ON CONFLICT DO NOTHING
""",
mental_model_id,
observation_id,
memory_id,
)
async def _find_related_mental_models(
async def _find_related_observations(
conn: "Connection",
memory_engine: "MemoryEngine",
bank_id: str,
@ -595,10 +595,10 @@ async def _find_related_mental_models(
request_context: "RequestContext",
) -> list[dict[str, Any]]:
"""
Find mental models related to the given query using the full recall system.
Find observations related to the given query using the full recall system.
IMPORTANT: We do NOT filter by tags here. Consolidation needs to see ALL
potentially related mental models regardless of scope, so the LLM can
potentially related observations regardless of scope, so the LLM can
decide on tag routing (same scope update vs cross-scope create).
This leverages:
@ -608,37 +608,37 @@ async def _find_related_mental_models(
- Graph traversal (connected via entity links)
Returns:
List of related mental models with their tags for LLM tag routing
List of related observations with their tags for LLM tag routing
"""
# Use recall to find related mental models
# NO tags parameter - we want ALL mental models regardless of scope
# Use low max_tokens since we only need mental models, not memories
# Use recall to find related observations
# NO tags parameter - we want ALL observations regardless of scope
# Use low max_tokens since we only need observations, not memories
recall_result = await memory_engine.recall_async(
bank_id=bank_id,
query=query,
max_tokens=5000, # Token budget for mental models
fact_type=["mental_model"], # Only retrieve mental models
max_tokens=5000, # Token budget for observations
fact_type=["observation"], # Only retrieve observations
request_context=request_context,
_quiet=True, # Suppress logging
# NO tags parameter - intentionally get ALL mental models
# NO tags parameter - intentionally get ALL observations
)
# If no mental models returned, return empty list
# When fact_type=["mental_model"], results come back in `results` field
# If no observations returned, return empty list
# When fact_type=["observation"], results come back in `results` field
if not recall_result.results:
return []
# Trust recall's relevance filtering - fetch full data for each mental model
# Trust recall's relevance filtering - fetch full data for each observation
results = []
for mm in recall_result.results:
# Fetch full mental model data from DB to get history, source_memory_ids, tags
for obs in recall_result.results:
# Fetch full observation data from DB to get history, source_memory_ids, tags
row = await conn.fetchrow(
f"""
SELECT id, text, proof_count, history, tags, source_memory_ids, created_at, updated_at
FROM {fq_table("memory_units")}
WHERE id = $1 AND bank_id = $2 AND fact_type = 'mental_model'
WHERE id = $1 AND bank_id = $2 AND fact_type = 'observation'
""",
uuid.UUID(mm.id),
uuid.UUID(obs.id),
bank_id,
)
@ -668,15 +668,15 @@ async def _consolidate_with_llm(
memory_engine: "MemoryEngine",
fact_text: str,
fact_tags: list[str],
mental_models: list[dict[str, Any]],
observations: list[dict[str, Any]],
mission: str,
) -> list[dict[str, Any]]:
"""
Single LLM call to extract durable knowledge and decide on consolidation actions.
This handles ALL cases:
- No related mental models: extracts durable knowledge, returns create action
- Related models exist: compares and returns update/create actions
- No related observations: extracts durable knowledge, returns create action
- Related observations exist: compares and returns update/create actions
- Purely ephemeral fact: returns empty array
Returns:
@ -685,14 +685,14 @@ async def _consolidate_with_llm(
- {"action": "create", "tags": [...], "text": "...", "reason": "..."}
- [] if fact is purely ephemeral (no durable knowledge)
"""
# Format mental models WITH their tags (or "None" if empty)
if mental_models:
mental_models_text = "\n".join(
f'- ID: {mm["id"]}, Tags: {json.dumps(mm["tags"])}, Text: "{mm["text"]}" (proof_count: {mm["proof_count"]})'
for mm in mental_models
# Format observations WITH their tags (or "None" if empty)
if observations:
observations_text = "\n".join(
f'- ID: {obs["id"]}, Tags: {json.dumps(obs["tags"])}, Text: "{obs["text"]}" (proof_count: {obs["proof_count"]})'
for obs in observations
)
else:
mental_models_text = "None (this is a new topic - create if fact contains durable knowledge)"
observations_text = "None (this is a new topic - create if fact contains durable knowledge)"
# Only include mission section if mission is set and not the default
mission_section = ""
@ -707,7 +707,7 @@ Focus on DURABLE knowledge that serves this mission, not ephemeral state.
mission_section=mission_section,
fact_text=fact_text,
fact_tags=json.dumps(fact_tags),
mental_models_text=mental_models_text,
observations_text=observations_text,
)
messages = [
@ -746,12 +746,12 @@ Focus on DURABLE knowledge that serves this mission, not ephemeral state.
return []
async def _create_mental_model_directly(
async def _create_observation_directly(
conn: "Connection",
memory_engine: "MemoryEngine",
bank_id: str,
source_memory_id: uuid.UUID,
mental_model_text: str,
observation_text: str,
tags: list[str] | None = None,
event_date: datetime | None = None,
occurred_start: datetime | None = None,
@ -759,52 +759,52 @@ async def _create_mental_model_directly(
perf: ConsolidationPerfLog | None = None,
) -> dict[str, Any]:
"""
Create a mental model directly with pre-processed text (no LLM call).
Create an observation directly with pre-processed text (no LLM call).
Used when the classify LLM has already provided the learning text.
This avoids the redundant second LLM call.
"""
# Generate embedding for the mental model (convert to string for pgvector)
# Generate embedding for the observation (convert to string for pgvector)
t0 = time.time()
embeddings = await embedding_utils.generate_embeddings_batch(memory_engine.embeddings, [mental_model_text])
embeddings = await embedding_utils.generate_embeddings_batch(memory_engine.embeddings, [observation_text])
embedding_str = str(embeddings[0]) if embeddings else None
if perf:
perf.record_timing("embedding", time.time() - t0)
# Create the mental model as a memory_unit
# Create the observation as a memory_unit
now = datetime.now(timezone.utc)
mm_event_date = event_date or now
mm_occurred_start = occurred_start or now
mm_mentioned_at = mentioned_at or now
mm_tags = tags or []
obs_event_date = event_date or now
obs_occurred_start = occurred_start or now
obs_mentioned_at = mentioned_at or now
obs_tags = tags or []
t0 = time.time()
mental_model_id = uuid.uuid4()
observation_id = uuid.uuid4()
row = await conn.fetchrow(
f"""
INSERT INTO {fq_table("memory_units")} (
id, bank_id, text, fact_type, embedding, proof_count, source_memory_ids, history,
tags, event_date, occurred_start, mentioned_at
)
VALUES ($1, $2, $3, 'mental_model', $4::vector, 1, $5, '[]'::jsonb, $6, $7, $8, $9)
VALUES ($1, $2, $3, 'observation', $4::vector, 1, $5, '[]'::jsonb, $6, $7, $8, $9)
RETURNING id
""",
mental_model_id,
observation_id,
bank_id,
mental_model_text,
observation_text,
embedding_str,
[source_memory_id],
mm_tags,
mm_event_date,
mm_occurred_start,
mm_mentioned_at,
obs_tags,
obs_event_date,
obs_occurred_start,
obs_mentioned_at,
)
# Create links between memory and mental model (includes entity links, memory_links)
await _create_memory_links(conn, source_memory_id, mental_model_id)
# Create links between memory and observation (includes entity links, memory_links)
await _create_memory_links(conn, source_memory_id, observation_id)
if perf:
perf.record_timing("db_write", time.time() - t0)
logger.debug(f"Created mental model {mental_model_id} from memory {source_memory_id} (tags: {mm_tags})")
logger.debug(f"Created observation {observation_id} from memory {source_memory_id} (tags: {obs_tags})")
return {"action": "created", "mental_model_id": str(row["id"]), "tags": mm_tags}
return {"action": "created", "observation_id": str(row["id"]), "tags": obs_tags}

View file

@ -1,6 +1,6 @@
"""Prompts for the consolidation engine."""
CONSOLIDATION_SYSTEM_PROMPT = """You are a memory consolidation system. Your job is to convert facts into durable knowledge (mental models) and merge with existing knowledge when appropriate.
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.
You must output ONLY valid JSON with no markdown formatting, no code blocks, and no additional text.
@ -30,28 +30,28 @@ 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 mental models):
## MERGE RULES (when comparing to existing observations):
1. REDUNDANT: Same information worded differently update existing
2. CONTRADICTION: Opposite information about same topic update with history (e.g., "used to X, now Y")
3. UPDATE: New state replacing old state update with history
## TAG ROUTING RULES:
Tags define visibility scopes. The fact and each mental model have tags (can be empty = global).
Tags define visibility scopes. The fact and each observation have tags (can be empty = global).
| Fact Tags | Model Tags | Action |
|-----------|------------|--------|
| [alice] | [alice] | UPDATE the model (same scope) |
| [alice] | [] | UPDATE the model (global absorbs all scopes) |
| [alice] | [bob] | CREATE new untagged model (cross-scope insight) |
| [] | [alice] | UPDATE the model (untagged facts can update any scope) |
| [] | [] | UPDATE the model (global to global) |
| Fact Tags | Obs Tags | Action |
|-----------|----------|--------|
| [alice] | [alice] | UPDATE the observation (same scope) |
| [alice] | [] | UPDATE the observation (global absorbs all scopes) |
| [alice] | [bob] | CREATE new untagged observation (cross-scope insight) |
| [] | [alice] | UPDATE the observation (untagged facts can update any scope) |
| [] | [] | UPDATE the observation (global to global) |
When NO existing model matches the fact's topic: CREATE new model with fact's tags.
When NO existing observation matches the fact's topic: CREATE new observation with fact's tags.
## MULTIPLE ACTIONS:
One fact can trigger MULTIPLE actions. For example:
- Update a scoped model [alice] about pizza preferences
- AND update a global model [] about pizza in general
- Update a scoped observation [alice] about pizza preferences
- AND update a global observation [] about pizza in general
Output an ARRAY of actions (can be empty, one, or many).
@ -59,7 +59,7 @@ Output an ARRAY of actions (can be empty, one, or many).
- NEVER merge facts about DIFFERENT people
- NEVER merge unrelated topics (food preferences vs work vs hobbies)
- When merging contradictions, capture the CHANGE (before after)
- Keep mental models focused on ONE specific topic per person
- Keep observations focused on ONE specific topic per person
- Cross-scope insights (alice's fact about bob's topic) become UNTAGGED (global)
- The "text" field MUST contain durable knowledge, not ephemeral state"""
@ -68,14 +68,14 @@ CONSOLIDATION_USER_PROMPT = """Analyze this new fact and consolidate into knowle
NEW FACT: {fact_text}
FACT TAGS: {fact_tags}
EXISTING MENTAL MODELS:
{mental_models_text}
EXISTING OBSERVATIONS:
{observations_text}
Instructions:
1. First, extract the DURABLE KNOWLEDGE from the fact (not ephemeral state like "user is at X")
2. Then compare with existing mental models:
- If a model covers the same topic: UPDATE it with the new knowledge
- If no model covers the topic: CREATE a new one
2. Then compare with existing observations:
- If an observation covers the same topic: UPDATE it with the new knowledge
- If no observation covers the topic: CREATE a new one
- If fact is about different scope: apply tag routing rules
Output JSON array of actions (ALWAYS an array, even for single action):
@ -87,5 +87,5 @@ Output JSON array of actions (ALWAYS an array, even for single action):
If NO consolidation is needed (fact is purely ephemeral with no durable knowledge):
[]
If no models exist and fact contains durable knowledge:
If no observations exist and fact contains durable knowledge:
[{{"action": "create", "tags": {fact_tags}, "text": "durable knowledge text", "reason": "new topic"}}]"""

View file

@ -141,15 +141,15 @@ from .entity_resolver import EntityResolver
from .llm_wrapper import LLMConfig
from .query_analyzer import QueryAnalyzer
from .reflect import run_reflect_agent
from .reflect.models import MentalModelInput
from .reflect.tools import tool_expand, tool_recall, tool_search_mental_models
from .reflect.models import ObservationInput
from .reflect.tools import tool_expand, tool_recall, tool_search_mental_models, tool_search_observations
from .response_models import (
VALID_RECALL_FACT_TYPES,
EntityObservation,
EntityState,
LLMCallTrace,
MemoryFact,
MentalModelRef,
ObservationRef,
ReflectResult,
TokenUsage,
ToolCallTrace,
@ -561,29 +561,29 @@ class MemoryEngine(MemoryEngineInterface):
logger.info(f"[CONSOLIDATION] bank={bank_id} completed: {result.get('memories_processed', 0)} processed")
async def _handle_create_reflection(self, task_dict: dict[str, Any]):
async def _handle_create_mental_model(self, task_dict: dict[str, Any]):
"""
Handler for create_reflection tasks.
Handler for create_mental_model tasks.
Runs reflect with the source query and updates the reflection with the generated content.
The reflection should already exist in the database (created during submit_async_create_reflection).
Runs reflect with the source query and updates the mental model with the generated content.
The mental model should already exist in the database (created during submit_async_create_mental_model).
Args:
task_dict: Dict with 'bank_id', 'reflection_id', 'source_query', 'max_tokens', 'operation_id'
task_dict: Dict with 'bank_id', 'mental_model_id', 'source_query', 'max_tokens', 'operation_id'
Raises:
ValueError: If required fields are missing
Exception: Any exception from reflect/update (propagates to execute_task for retry)
"""
bank_id = task_dict.get("bank_id")
reflection_id = task_dict.get("reflection_id")
mental_model_id = task_dict.get("mental_model_id")
source_query = task_dict.get("source_query")
max_tokens = task_dict.get("max_tokens", 2048)
if not bank_id or not reflection_id or not source_query:
raise ValueError("bank_id, reflection_id, and source_query are required for create_reflection task")
if not bank_id or not mental_model_id or not source_query:
raise ValueError("bank_id, mental_model_id, and source_query are required for create_mental_model task")
logger.info(f"[CREATE_REFLECTION_TASK] Starting for bank_id={bank_id}, reflection_id={reflection_id}")
logger.info(f"[CREATE_MENTAL_MODEL_TASK] Starting for bank_id={bank_id}, mental_model_id={mental_model_id}")
from hindsight_api.models import RequestContext
@ -615,55 +615,55 @@ class MemoryEngine(MemoryEngineInterface):
},
}
# Update the reflection with the generated content and reflect_response
await self.update_reflection(
# Update the mental model with the generated content and reflect_response
await self.update_mental_model(
bank_id=bank_id,
reflection_id=reflection_id,
mental_model_id=mental_model_id,
content=generated_content,
reflect_response=reflect_response,
request_context=internal_context,
)
logger.info(f"[CREATE_REFLECTION_TASK] Completed for bank_id={bank_id}, reflection_id={reflection_id}")
logger.info(f"[CREATE_MENTAL_MODEL_TASK] Completed for bank_id={bank_id}, mental_model_id={mental_model_id}")
async def _handle_refresh_reflection(self, task_dict: dict[str, Any]):
async def _handle_refresh_mental_model(self, task_dict: dict[str, Any]):
"""
Handler for refresh_reflection tasks.
Handler for refresh_mental_model tasks.
Re-runs the source query through reflect and updates the reflection content.
Re-runs the source query through reflect and updates the mental model content.
Args:
task_dict: Dict with 'bank_id', 'reflection_id', 'operation_id'
task_dict: Dict with 'bank_id', 'mental_model_id', 'operation_id'
Raises:
ValueError: If required fields are missing
Exception: Any exception from reflect/update (propagates to execute_task for retry)
"""
bank_id = task_dict.get("bank_id")
reflection_id = task_dict.get("reflection_id")
mental_model_id = task_dict.get("mental_model_id")
if not bank_id or not reflection_id:
raise ValueError("bank_id and reflection_id are required for refresh_reflection task")
if not bank_id or not mental_model_id:
raise ValueError("bank_id and mental_model_id are required for refresh_mental_model task")
logger.info(f"[REFRESH_REFLECTION_TASK] Starting for bank_id={bank_id}, reflection_id={reflection_id}")
logger.info(f"[REFRESH_MENTAL_MODEL_TASK] Starting for bank_id={bank_id}, mental_model_id={mental_model_id}")
from hindsight_api.models import RequestContext
internal_context = RequestContext()
# Get the current reflection to get source_query
reflection = await self.get_reflection(bank_id, reflection_id, request_context=internal_context)
if not reflection:
raise ValueError(f"Reflection {reflection_id} not found in bank {bank_id}")
# Get the current mental model to get source_query
mental_model = await self.get_mental_model(bank_id, mental_model_id, request_context=internal_context)
if not mental_model:
raise ValueError(f"Mental model {mental_model_id} not found in bank {bank_id}")
source_query = reflection["source_query"]
source_query = mental_model["source_query"]
# Run reflect to generate new content, excluding the reflection being refreshed
# Run reflect to generate new content, excluding the mental model being refreshed
reflect_result = await self.reflect_async(
bank_id=bank_id,
query=source_query,
request_context=internal_context,
exclude_reflection_ids=[reflection_id],
exclude_mental_model_ids=[mental_model_id],
)
generated_content = reflect_result.text or "No content generated"
@ -684,16 +684,16 @@ class MemoryEngine(MemoryEngineInterface):
},
}
# Update the reflection with the generated content and reflect_response
await self.update_reflection(
# Update the mental model with the generated content and reflect_response
await self.update_mental_model(
bank_id=bank_id,
reflection_id=reflection_id,
mental_model_id=mental_model_id,
content=generated_content,
reflect_response=reflect_response,
request_context=internal_context,
)
logger.info(f"[REFRESH_REFLECTION_TASK] Completed for bank_id={bank_id}, reflection_id={reflection_id}")
logger.info(f"[REFRESH_MENTAL_MODEL_TASK] Completed for bank_id={bank_id}, mental_model_id={mental_model_id}")
async def execute_task(self, task_dict: dict[str, Any]):
"""
@ -733,10 +733,10 @@ class MemoryEngine(MemoryEngineInterface):
await self._handle_batch_retain(task_dict)
elif task_type == "consolidation":
await self._handle_consolidation(task_dict)
elif task_type == "create_reflection":
await self._handle_create_reflection(task_dict)
elif task_type == "refresh_reflection":
await self._handle_refresh_reflection(task_dict)
elif task_type == "create_mental_model":
await self._handle_create_mental_model(task_dict)
elif task_type == "refresh_mental_model":
await self._handle_refresh_mental_model(task_dict)
else:
logger.error(f"Unknown task type: {task_type}")
# Don't retry unknown task types
@ -1436,7 +1436,7 @@ class MemoryEngine(MemoryEngineInterface):
from ..config import get_config
config = get_config()
if config.enable_mental_models:
if config.enable_observations:
try:
await self.submit_async_consolidation(bank_id=bank_id, request_context=request_context)
except Exception as e:
@ -2690,35 +2690,35 @@ class MemoryEngine(MemoryEngineInterface):
except Exception as e:
raise Exception(f"Failed to delete agent data: {str(e)}")
async def clear_mental_models(
async def clear_observations(
self,
bank_id: str,
*,
request_context: "RequestContext",
) -> dict[str, int]:
"""
Clear all mental models for a bank.
Clear all observations for a bank (consolidated knowledge).
Args:
bank_id: Bank ID to clear mental models for
bank_id: Bank ID to clear observations for
request_context: Request context for authentication.
Returns:
Dictionary with count of deleted mental models
Dictionary with count of deleted observations
"""
await self._authenticate_tenant(request_context)
pool = await self._get_pool()
async with acquire_with_retry(pool) as conn:
async with conn.transaction():
# Count mental models before deletion
# Count observations before deletion
count = await conn.fetchval(
f"SELECT COUNT(*) FROM {fq_table('memory_units')} WHERE bank_id = $1 AND fact_type = 'mental_model'",
f"SELECT COUNT(*) FROM {fq_table('memory_units')} WHERE bank_id = $1 AND fact_type = 'observation'",
bank_id,
)
# Delete all mental models
# Delete all observations
await conn.execute(
f"DELETE FROM {fq_table('memory_units')} WHERE bank_id = $1 AND fact_type = 'mental_model'",
f"DELETE FROM {fq_table('memory_units')} WHERE bank_id = $1 AND fact_type = 'observation'",
bank_id,
)
@ -3153,8 +3153,8 @@ class MemoryEngine(MemoryEngineInterface):
"tags": row["tags"] if row["tags"] else [],
}
# For mental models, include source_memory_ids and fetch source_memories
if row["fact_type"] == "mental_model" and row["source_memory_ids"]:
# For observations, include source_memory_ids and fetch source_memories
if row["fact_type"] == "observation" and row["source_memory_ids"]:
source_ids = row["source_memory_ids"]
result["source_memory_ids"] = [str(sid) for sid in source_ids]
@ -3485,7 +3485,7 @@ class MemoryEngine(MemoryEngineInterface):
request_context: "RequestContext",
tags: list[str] | None = None,
tags_match: TagsMatch = "any",
exclude_reflection_ids: list[str] | None = None,
exclude_mental_model_ids: list[str] | None = None,
) -> ReflectResult:
"""
Reflect and formulate an answer using an agentic loop with tools.
@ -3509,8 +3509,8 @@ class MemoryEngine(MemoryEngineInterface):
response_schema: Optional JSON Schema for structured output (not yet supported)
tags: Optional tags to filter memories
tags_match: How to match tags - "any" (OR), "all" (AND)
exclude_reflection_ids: Optional list of reflection IDs to exclude from search
(used when refreshing a reflection to avoid circular reference)
exclude_mental_model_ids: Optional list of mental model IDs to exclude from search
(used when refreshing a mental model to avoid circular reference)
Returns:
ReflectResult containing:
@ -3569,15 +3569,14 @@ class MemoryEngine(MemoryEngineInterface):
pending_consolidation = bank_stats.pending_consolidation if hasattr(bank_stats, "pending_consolidation") else 0
# Create tool callbacks that acquire connections only when needed
from .reflect.tools import tool_search_reflections
from .retain import embedding_utils
async def search_reflections_fn(q: str, max_results: int = 5) -> dict[str, Any]:
async def search_mental_models_fn(q: str, max_results: int = 5) -> dict[str, Any]:
# Generate embedding for the query
embeddings = await embedding_utils.generate_embeddings_batch(self.embeddings, [q])
query_embedding = embeddings[0]
async with pool.acquire() as conn:
return await tool_search_reflections(
return await tool_search_mental_models(
conn,
bank_id,
q,
@ -3585,11 +3584,11 @@ class MemoryEngine(MemoryEngineInterface):
max_results=max_results,
tags=tags,
tags_match=tags_match,
exclude_ids=exclude_reflection_ids,
exclude_ids=exclude_mental_model_ids,
)
async def search_mental_models_fn(q: str, max_tokens: int = 5000) -> dict[str, Any]:
return await tool_search_mental_models(
async def search_observations_fn(q: str, max_tokens: int = 5000) -> dict[str, Any]:
return await tool_search_observations(
self,
bank_id,
q,
@ -3638,8 +3637,8 @@ class MemoryEngine(MemoryEngineInterface):
bank_id=bank_id,
query=query,
bank_profile=profile,
search_reflections_fn=search_reflections_fn,
search_mental_models_fn=search_mental_models_fn,
search_observations_fn=search_observations_fn,
recall_fn=recall_fn,
expand_fn=expand_fn,
context=context,
@ -3673,7 +3672,7 @@ class MemoryEngine(MemoryEngineInterface):
# Extract memories from recall tool outputs - only include memories the agent actually used
# agent_result.used_memory_ids contains validated IDs from the done action
used_memory_ids_set = set(agent_result.used_memory_ids) if agent_result.used_memory_ids else set()
based_on: dict[str, list[MemoryFact]] = {"world": [], "experience": [], "opinion": []}
based_on: dict[str, list[MemoryFact]] = {"world": [], "experience": [], "opinion": [], "observation": []}
seen_memory_ids: set[str] = set()
for tc in agent_result.tool_trace:
if tc.tool == "recall" and "memories" in tc.output:
@ -3714,13 +3713,14 @@ class MemoryEngine(MemoryEngineInterface):
continue # Skip models not actually used by the agent
seen_model_ids.add(model_id)
# Add to based_on as MemoryFact with type "mental-models"
# Mental models have a "text" field containing the consolidated knowledge
model_name = model.get("name", "")
model_summary = model.get("summary") or model.get("description", "")
based_on["mental-models"].append(
MemoryFact(
id=model_id,
text=model.get("text", ""),
text=f"{model_name}: {model_summary}",
fact_type="mental-models",
context=None,
context=f"{model.get('type', 'concept')} ({model.get('subtype', 'structural')})",
occurred_start=None,
occurred_end=None,
)
@ -3735,38 +3735,39 @@ class MemoryEngine(MemoryEngineInterface):
continue # Skip models not actually used by the agent
seen_model_ids.add(model_id)
# Add to based_on as MemoryFact with type "mental-models"
# Mental models have a "text" field containing the consolidated knowledge
model_name = model.get("name", "")
model_summary = model.get("summary") or model.get("description", "")
based_on["mental-models"].append(
MemoryFact(
id=model_id,
text=model.get("text", ""),
text=f"{model_name}: {model_summary}",
fact_type="mental-models",
context=None,
context=f"{model.get('type', 'concept')} ({model.get('subtype', 'structural')})",
occurred_start=None,
occurred_end=None,
)
)
elif tc.tool == "search_reflections":
# Search reflections - include all returned reflections (filtered by used_reflection_ids_set if specified)
used_reflection_ids_set = (
set(agent_result.used_reflection_ids) if agent_result.used_reflection_ids else set()
elif tc.tool == "search_mental_models":
# Search mental models - include all returned mental models (filtered by used_mental_model_ids_set if specified)
used_mental_model_ids_set = (
set(agent_result.used_mental_model_ids) if agent_result.used_mental_model_ids else set()
)
for reflection in tc.output.get("reflections", []):
reflection_id = reflection.get("id")
if reflection_id and reflection_id not in seen_model_ids:
# Only include reflections that the agent declared as used (or all if none specified)
if used_reflection_ids_set and reflection_id not in used_reflection_ids_set:
continue # Skip reflections not actually used by the agent
seen_model_ids.add(reflection_id)
# Add to based_on as MemoryFact with type "mental-models" (reflections are synthesized knowledge)
reflection_name = reflection.get("name", "")
reflection_content = reflection.get("content", "")
for mental_model in tc.output.get("mental_models", []):
mental_model_id = mental_model.get("id")
if mental_model_id and mental_model_id not in seen_model_ids:
# Only include mental models that the agent declared as used (or all if none specified)
if used_mental_model_ids_set and mental_model_id not in used_mental_model_ids_set:
continue # Skip mental models not actually used by the agent
seen_model_ids.add(mental_model_id)
# Add to based_on as MemoryFact with type "mental-models" (mental models are synthesized knowledge)
mental_model_name = mental_model.get("name", "")
mental_model_content = mental_model.get("content", "")
based_on["mental-models"].append(
MemoryFact(
id=reflection_id,
text=f"{reflection_name}: {reflection_content}",
id=mental_model_id,
text=f"{mental_model_name}: {mental_model_content}",
fact_type="mental-models",
context="reflection (user-curated)",
context="mental model (user-curated)",
occurred_start=None,
occurred_end=None,
)
@ -4309,18 +4310,18 @@ class MemoryEngine(MemoryEngineInterface):
fact_ids: list[str],
) -> int:
"""
Remove fact IDs from mental model source_memory_ids when memories are deleted.
Remove fact IDs from observation source_memory_ids when memories are deleted.
Mental models are now stored in memory_units with fact_type='mental_model'
Observations are stored in memory_units with fact_type='observation'
and have a source_memory_ids column (UUID[]) tracking their source memories.
Args:
conn: Database connection
bank_id: Bank identifier
fact_ids: List of fact IDs to remove from mental models
fact_ids: List of fact IDs to remove from observations
Returns:
Number of mental models updated
Number of observations updated
"""
if not fact_ids:
return 0
@ -4330,7 +4331,7 @@ class MemoryEngine(MemoryEngineInterface):
fact_uuids = [uuid_module.UUID(fid) for fid in fact_ids]
# Update mental models (memory_units with fact_type='mental_model')
# Update observations (memory_units with fact_type='observation')
# by removing the deleted fact IDs from source_memory_ids
# Use array subtraction: source_memory_ids - deleted_ids
result = await conn.execute(
@ -4343,7 +4344,7 @@ class MemoryEngine(MemoryEngineInterface):
),
updated_at = NOW()
WHERE bank_id = $1
AND fact_type = 'mental_model'
AND fact_type = 'observation'
AND source_memory_ids && $2::uuid[]
""",
bank_id,
@ -4354,7 +4355,7 @@ class MemoryEngine(MemoryEngineInterface):
updated_count = int(result.split()[-1]) if result and "UPDATE" in result else 0
if updated_count > 0:
logger.info(
f"[MENTAL_MODELS] Invalidated {len(fact_ids)} fact IDs from {updated_count} mental models in bank {bank_id}"
f"[OBSERVATIONS] Invalidated {len(fact_ids)} fact IDs from {updated_count} observations in bank {bank_id}"
)
return updated_count
@ -4680,9 +4681,9 @@ class MemoryEngine(MemoryEngineInterface):
offset: int = 0,
request_context: "RequestContext",
) -> list[dict[str, Any]]:
"""List auto-consolidated mental models for a bank.
"""List auto-consolidated observations for a bank.
Mental models are stored in memory_units with fact_type='mental_model'.
Observations are stored in memory_units with fact_type='observation'.
They are automatically created and updated by the consolidation engine.
Args:
@ -4694,7 +4695,7 @@ class MemoryEngine(MemoryEngineInterface):
request_context: Request context for authentication
Returns:
List of mental model dicts
List of observation dicts
"""
await self._authenticate_tenant(request_context)
pool = await self._get_pool()
@ -4716,33 +4717,33 @@ class MemoryEngine(MemoryEngineInterface):
f"""
SELECT id, bank_id, text, proof_count, history, tags, source_memory_ids, created_at, updated_at
FROM {fq_table("memory_units")}
WHERE bank_id = $1 AND fact_type = 'mental_model' {tag_filter}
WHERE bank_id = $1 AND fact_type = 'observation' {tag_filter}
ORDER BY updated_at DESC NULLS LAST
LIMIT $2 OFFSET $3
""",
*params,
)
return [self._row_to_mental_model_consolidated(row) for row in rows]
return [self._row_to_observation_consolidated(row) for row in rows]
async def get_mental_model_consolidated(
async def get_observation_consolidated(
self,
bank_id: str,
model_id: str,
observation_id: str,
*,
include_source_memories: bool = True,
request_context: "RequestContext",
) -> dict[str, Any] | None:
"""Get a single mental model by ID.
"""Get a single observation by ID.
Args:
bank_id: Bank identifier
model_id: Mental model ID
observation_id: Observation ID
include_source_memories: Whether to include full source memory details
request_context: Request context for authentication
Returns:
Mental model dict or None if not found
Observation dict or None if not found
"""
await self._authenticate_tenant(request_context)
pool = await self._get_pool()
@ -4752,16 +4753,16 @@ class MemoryEngine(MemoryEngineInterface):
f"""
SELECT id, bank_id, text, proof_count, history, tags, source_memory_ids, created_at, updated_at
FROM {fq_table("memory_units")}
WHERE bank_id = $1 AND id = $2 AND fact_type = 'mental_model'
WHERE bank_id = $1 AND id = $2 AND fact_type = 'observation'
""",
bank_id,
model_id,
observation_id,
)
if not row:
return None
result = self._row_to_mental_model_consolidated(row)
result = self._row_to_observation_consolidated(row)
# Fetch source memories if requested and source_memory_ids exist
if include_source_memories and result.get("source_memory_ids"):
@ -4789,8 +4790,8 @@ class MemoryEngine(MemoryEngineInterface):
return result
def _row_to_mental_model_consolidated(self, row: Any) -> dict[str, Any]:
"""Convert a database row to a mental model dict."""
def _row_to_observation_consolidated(self, row: Any) -> dict[str, Any]:
"""Convert a database row to an observation dict."""
import json
history = row["history"]
@ -4817,10 +4818,10 @@ class MemoryEngine(MemoryEngineInterface):
}
# =========================================================================
# REFLECTIONS CRUD
# MENTAL MODELS CRUD
# =========================================================================
async def list_reflections(
async def list_mental_models(
self,
bank_id: str,
*,
@ -4830,7 +4831,7 @@ class MemoryEngine(MemoryEngineInterface):
offset: int = 0,
request_context: "RequestContext",
) -> list[dict[str, Any]]:
"""List pinned reflections for a bank.
"""List pinned mental models for a bank.
Args:
bank_id: Bank identifier
@ -4841,7 +4842,7 @@ class MemoryEngine(MemoryEngineInterface):
request_context: Request context for authentication
Returns:
List of pinned reflection dicts
List of pinned mental model dicts
"""
await self._authenticate_tenant(request_context)
pool = await self._get_pool()
@ -4863,7 +4864,7 @@ class MemoryEngine(MemoryEngineInterface):
f"""
SELECT id, bank_id, name, source_query, content, tags,
last_refreshed_at, created_at, reflect_response
FROM {fq_table("reflections")}
FROM {fq_table("mental_models")}
WHERE bank_id = $1 {tag_filter}
ORDER BY last_refreshed_at DESC
LIMIT $2 OFFSET $3
@ -4871,24 +4872,24 @@ class MemoryEngine(MemoryEngineInterface):
*params,
)
return [self._row_to_reflection(row) for row in rows]
return [self._row_to_mental_model(row) for row in rows]
async def get_reflection(
async def get_mental_model(
self,
bank_id: str,
reflection_id: str,
mental_model_id: str,
*,
request_context: "RequestContext",
) -> dict[str, Any] | None:
"""Get a single pinned reflection by ID.
"""Get a single pinned mental model by ID.
Args:
bank_id: Bank identifier
reflection_id: Pinned reflection UUID
mental_model_id: Pinned mental model UUID
request_context: Request context for authentication
Returns:
Pinned reflection dict or None if not found
Pinned mental model dict or None if not found
"""
await self._authenticate_tenant(request_context)
pool = await self._get_pool()
@ -4898,16 +4899,16 @@ class MemoryEngine(MemoryEngineInterface):
f"""
SELECT id, bank_id, name, source_query, content, tags,
last_refreshed_at, created_at, reflect_response
FROM {fq_table("reflections")}
FROM {fq_table("mental_models")}
WHERE bank_id = $1 AND id = $2
""",
bank_id,
reflection_id,
mental_model_id,
)
return self._row_to_reflection(row) if row else None
return self._row_to_mental_model(row) if row else None
async def create_reflection(
async def create_mental_model(
self,
bank_id: str,
name: str,
@ -4917,18 +4918,18 @@ class MemoryEngine(MemoryEngineInterface):
tags: list[str] | None = None,
request_context: "RequestContext",
) -> dict[str, Any]:
"""Create a new pinned reflection.
"""Create a new pinned mental model.
Args:
bank_id: Bank identifier
name: Human-readable name for the reflection
source_query: The query that generated this reflection
name: Human-readable name for the mental model
source_query: The query that generated this mental model
content: The synthesized content
tags: Optional tags for scoped visibility
request_context: Request context for authentication
Returns:
The created pinned reflection dict
The created pinned mental model dict
"""
await self._authenticate_tenant(request_context)
pool = await self._get_pool()
@ -4942,7 +4943,7 @@ class MemoryEngine(MemoryEngineInterface):
async with acquire_with_retry(pool) as conn:
row = await conn.fetchrow(
f"""
INSERT INTO {fq_table("reflections")}
INSERT INTO {fq_table("mental_models")}
(bank_id, name, source_query, content, embedding, tags)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, bank_id, name, source_query, content, tags,
@ -4956,45 +4957,45 @@ class MemoryEngine(MemoryEngineInterface):
tags or [],
)
logger.info(f"[REFLECTIONS] Created pinned reflection '{name}' for bank {bank_id}")
return self._row_to_reflection(row)
logger.info(f"[MENTAL_MODELS] Created pinned mental model '{name}' for bank {bank_id}")
return self._row_to_mental_model(row)
async def refresh_reflection(
async def refresh_mental_model(
self,
bank_id: str,
reflection_id: str,
mental_model_id: str,
*,
request_context: "RequestContext",
) -> dict[str, Any] | None:
"""Refresh a pinned reflection by re-running its source query.
"""Refresh a pinned mental model by re-running its source query.
This method:
1. Gets the pinned reflection
1. Gets the pinned mental model
2. Runs the source_query through reflect
3. Updates the content with the new synthesis
4. Updates last_refreshed_at
Args:
bank_id: Bank identifier
reflection_id: Pinned reflection UUID
mental_model_id: Pinned mental model UUID
request_context: Request context for authentication
Returns:
Updated pinned reflection dict or None if not found
Updated pinned mental model dict or None if not found
"""
await self._authenticate_tenant(request_context)
# Get the current reflection
reflection = await self.get_reflection(bank_id, reflection_id, request_context=request_context)
if not reflection:
# Get the current mental model
mental_model = await self.get_mental_model(bank_id, mental_model_id, request_context=request_context)
if not mental_model:
return None
# Run reflect with the source query, excluding the reflection being refreshed
# Run reflect with the source query, excluding the mental model being refreshed
reflect_result = await self.reflect_async(
bank_id=bank_id,
query=reflection["source_query"],
query=mental_model["source_query"],
request_context=request_context,
exclude_reflection_ids=[reflection_id],
exclude_mental_model_ids=[mental_model_id],
)
# Build reflect_response payload to store
@ -5011,39 +5012,40 @@ class MemoryEngine(MemoryEngineInterface):
]
for fact_type, facts in reflect_result.based_on.items()
},
"mental_models": [], # Mental models are included in based_on["mental-models"]
}
# Update the reflection with new content and reflect_response
return await self.update_reflection(
# Update the mental model with new content and reflect_response
return await self.update_mental_model(
bank_id,
reflection_id,
mental_model_id,
content=reflect_result.text,
reflect_response=reflect_response_payload,
request_context=request_context,
)
async def update_reflection(
async def update_mental_model(
self,
bank_id: str,
reflection_id: str,
mental_model_id: str,
*,
name: str | None = None,
content: str | None = None,
reflect_response: dict[str, Any] | None = None,
request_context: "RequestContext",
) -> dict[str, Any] | None:
"""Update a pinned reflection.
"""Update a pinned mental model.
Args:
bank_id: Bank identifier
reflection_id: Pinned reflection UUID
mental_model_id: Pinned mental model UUID
name: New name (if changing)
content: New content (if changing)
reflect_response: Full reflect API response payload (if changing)
request_context: Request context for authentication
Returns:
Updated pinned reflection dict or None if not found
Updated pinned mental model dict or None if not found
"""
await self._authenticate_tenant(request_context)
pool = await self._get_pool()
@ -5051,7 +5053,7 @@ class MemoryEngine(MemoryEngineInterface):
async with acquire_with_retry(pool) as conn:
# Build dynamic update
updates = []
params: list[Any] = [bank_id, reflection_id]
params: list[Any] = [bank_id, mental_model_id]
param_idx = 3
if name is not None:
@ -5081,7 +5083,7 @@ class MemoryEngine(MemoryEngineInterface):
return None
query = f"""
UPDATE {fq_table("reflections")}
UPDATE {fq_table("mental_models")}
SET {", ".join(updates)}
WHERE bank_id = $1 AND id = $2
RETURNING id, bank_id, name, source_query, content, tags,
@ -5090,20 +5092,20 @@ class MemoryEngine(MemoryEngineInterface):
row = await conn.fetchrow(query, *params)
return self._row_to_reflection(row) if row else None
return self._row_to_mental_model(row) if row else None
async def delete_reflection(
async def delete_mental_model(
self,
bank_id: str,
reflection_id: str,
mental_model_id: str,
*,
request_context: "RequestContext",
) -> bool:
"""Delete a pinned reflection.
"""Delete a pinned mental model.
Args:
bank_id: Bank identifier
reflection_id: Pinned reflection UUID
mental_model_id: Pinned mental model UUID
request_context: Request context for authentication
Returns:
@ -5114,15 +5116,15 @@ class MemoryEngine(MemoryEngineInterface):
async with acquire_with_retry(pool) as conn:
result = await conn.execute(
f"DELETE FROM {fq_table('reflections')} WHERE bank_id = $1 AND id = $2",
f"DELETE FROM {fq_table('mental_models')} WHERE bank_id = $1 AND id = $2",
bank_id,
reflection_id,
mental_model_id,
)
return result == "DELETE 1"
def _row_to_reflection(self, row) -> dict[str, Any]:
"""Convert a database row to a reflection dict."""
def _row_to_mental_model(self, row) -> dict[str, Any]:
"""Convert a database row to a mental model dict."""
reflect_response = row.get("reflect_response")
# Parse JSON string to dict if needed (asyncpg may return JSONB as string)
if isinstance(reflect_response, str):
@ -5747,7 +5749,7 @@ class MemoryEngine(MemoryEngineInterface):
dedupe_by_bank=True,
)
async def submit_async_create_reflection(
async def submit_async_create_mental_model(
self,
bank_id: str,
name: str,
@ -5757,16 +5759,16 @@ class MemoryEngine(MemoryEngineInterface):
max_tokens: int = 2048,
request_context: "RequestContext",
) -> dict[str, Any]:
"""Submit an async reflection creation operation.
"""Submit an async mental model creation operation.
This:
1. Creates the reflection in the database immediately (with placeholder content)
1. Creates the mental model in the database immediately (with placeholder content)
2. Schedules a background task to run reflect and update the content
3. Returns operation_id for tracking
Args:
bank_id: Bank identifier
name: Human-readable name for the reflection
name: Human-readable name for the mental model
source_query: The query to run to generate content
tags: Optional tags for scoped visibility
max_tokens: Maximum tokens for the reflect response
@ -5777,8 +5779,8 @@ class MemoryEngine(MemoryEngineInterface):
"""
await self._authenticate_tenant(request_context)
# 1. Create the reflection in the database with placeholder content
reflection = await self.create_reflection(
# 1. Create the mental model in the database with placeholder content
mental_model = await self.create_mental_model(
bank_id=bank_id,
name=name,
source_query=source_query,
@ -5786,36 +5788,36 @@ class MemoryEngine(MemoryEngineInterface):
tags=tags,
request_context=request_context,
)
reflection_id = reflection["id"]
mental_model_id = mental_model["id"]
# 2. Submit async operation
return await self._submit_async_operation(
bank_id=bank_id,
operation_type="create_reflection",
task_type="create_reflection",
operation_type="create_mental_model",
task_type="create_mental_model",
task_payload={
"reflection_id": reflection_id,
"mental_model_id": mental_model_id,
"source_query": source_query,
"max_tokens": max_tokens,
},
result_metadata={"reflection_id": reflection_id, "name": name, "source_query": source_query},
result_metadata={"mental_model_id": mental_model_id, "name": name, "source_query": source_query},
dedupe_by_bank=False,
)
async def submit_async_refresh_reflection(
async def submit_async_refresh_mental_model(
self,
bank_id: str,
reflection_id: str,
mental_model_id: str,
*,
request_context: "RequestContext",
) -> dict[str, Any]:
"""Submit an async reflection refresh operation.
"""Submit an async mental model refresh operation.
This schedules a background task to re-run the source query and update the content.
Args:
bank_id: Bank identifier
reflection_id: Reflection UUID to refresh
mental_model_id: Mental model UUID to refresh
request_context: Request context for authentication
Returns:
@ -5823,18 +5825,18 @@ class MemoryEngine(MemoryEngineInterface):
"""
await self._authenticate_tenant(request_context)
# Verify reflection exists
reflection = await self.get_reflection(bank_id, reflection_id, request_context=request_context)
if not reflection:
raise ValueError(f"Reflection {reflection_id} not found in bank {bank_id}")
# Verify mental model exists
mental_model = await self.get_mental_model(bank_id, mental_model_id, request_context=request_context)
if not mental_model:
raise ValueError(f"Mental model {mental_model_id} not found in bank {bank_id}")
return await self._submit_async_operation(
bank_id=bank_id,
operation_type="refresh_reflection",
task_type="refresh_reflection",
operation_type="refresh_mental_model",
task_type="refresh_mental_model",
task_payload={
"reflection_id": reflection_id,
"mental_model_id": mental_model_id,
},
result_metadata={"reflection_id": reflection_id, "name": reflection["name"]},
result_metadata={"mental_model_id": mental_model_id, "name": mental_model["name"]},
dedupe_by_bank=False,
)

View file

@ -4,17 +4,17 @@ Reflect agent module for agentic reflection with tools.
The reflect agent uses an iterative loop with tools to:
1. Lookup mental models (existing knowledge)
2. Recall facts (semantic + temporal search)
3. Learn new insights (create/update mental models)
3. Learn new insights (create/update observations)
4. Expand memories (get chunk/document context)
"""
from .agent import ReflectAgentResult, run_reflect_agent
from .models import MentalModelInput, ReflectAction, ReflectActionBatch
from .models import ObservationInput, ReflectAction, ReflectActionBatch
__all__ = [
"run_reflect_agent",
"ReflectAgentResult",
"ReflectAction",
"ReflectActionBatch",
"MentalModelInput",
"ObservationInput",
]

View file

@ -2,8 +2,8 @@
Reflect agent - agentic loop for reflection with native tool calling.
Uses hierarchical retrieval:
1. search_reflections - User-curated summaries (highest quality)
2. search_mental_models - Consolidated knowledge with freshness
1. search_mental_models - User-curated summaries (highest quality)
2. search_observations - Consolidated knowledge with freshness
3. recall - Raw facts as ground truth
"""
@ -202,8 +202,8 @@ async def run_reflect_agent(
bank_id: str,
query: str,
bank_profile: dict[str, Any],
search_reflections_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
search_mental_models_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
search_observations_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
recall_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
expand_fn: Callable[[list[str], str], Awaitable[dict[str, Any]]],
context: str | None = None,
@ -216,8 +216,8 @@ async def run_reflect_agent(
Execute the reflect agent loop using native tool calling.
The agent uses hierarchical retrieval:
1. search_reflections - User-curated summaries (try first)
2. search_mental_models - Consolidated knowledge with freshness
1. search_mental_models - User-curated summaries (try first)
2. search_observations - Consolidated knowledge with freshness
3. recall - Raw facts as ground truth
Args:
@ -225,8 +225,8 @@ async def run_reflect_agent(
bank_id: Bank identifier
query: Question to answer
bank_profile: Bank profile with name and mission
search_reflections_fn: Tool callback for searching reflections (query, max_results) -> result
search_mental_models_fn: Tool callback for searching mental models (query, max_results) -> result
search_observations_fn: Tool callback for searching observations (query, max_results) -> result
recall_fn: Tool callback for recall (query, max_tokens) -> result
expand_fn: Tool callback for expand (memory_ids, depth) -> result
context: Optional additional context
@ -270,8 +270,8 @@ async def run_reflect_agent(
# Track available IDs for validation (prevents hallucinated citations)
available_memory_ids: set[str] = set()
available_reflection_ids: set[str] = set()
available_mental_model_ids: set[str] = set()
available_observation_ids: set[str] = set()
def _get_llm_trace() -> list[LLMCall]:
return [
@ -394,7 +394,7 @@ async def run_reflect_agent(
llm_trace.append({"scope": f"agent_{iteration + 1}_err", "duration_ms": err_duration})
# Guardrail: If no evidence gathered yet, retry
has_gathered_evidence = (
bool(available_memory_ids) or bool(available_reflection_ids) or bool(available_mental_model_ids)
bool(available_memory_ids) or bool(available_mental_model_ids) or bool(available_observation_ids)
)
if not has_gathered_evidence and iteration < max_iterations - 1:
continue
@ -519,7 +519,7 @@ async def run_reflect_agent(
if done_call:
# Guardrail: Require evidence before done
has_gathered_evidence = (
bool(available_memory_ids) or bool(available_reflection_ids) or bool(available_mental_model_ids)
bool(available_memory_ids) or bool(available_mental_model_ids) or bool(available_observation_ids)
)
if not has_gathered_evidence and iteration < max_iterations - 1:
# Add assistant message and fake tool result asking for evidence
@ -536,7 +536,7 @@ async def run_reflect_agent(
"name": done_call.name, # Required by Gemini
"content": json.dumps(
{
"error": "You must search for information first. Use search_reflections(), search_mental_models(), or recall() before providing your final answer."
"error": "You must search for information first. Use search_mental_models(), search_observations(), or recall() before providing your final answer."
}
),
}
@ -547,8 +547,8 @@ async def run_reflect_agent(
return await _process_done_tool(
done_call,
available_memory_ids,
available_reflection_ids,
available_mental_model_ids,
available_observation_ids,
iteration + 1,
total_tools_called,
tool_trace,
@ -576,8 +576,8 @@ async def run_reflect_agent(
tool_tasks = [
_execute_tool_with_timing(
tc,
search_reflections_fn,
search_mental_models_fn,
search_observations_fn,
recall_fn,
expand_fn,
)
@ -606,15 +606,6 @@ async def run_reflect_agent(
)
# Track available IDs from tool results (only for successful responses)
if (
normalized_tool_name == "search_reflections"
and isinstance(output, dict)
and "reflections" in output
):
for reflection in output["reflections"]:
if "id" in reflection:
available_reflection_ids.add(reflection["id"])
if (
normalized_tool_name == "search_mental_models"
and isinstance(output, dict)
@ -624,6 +615,15 @@ async def run_reflect_agent(
if "id" in mm:
available_mental_model_ids.add(mm["id"])
if (
normalized_tool_name == "search_observations"
and isinstance(output, dict)
and "observations" in output
):
for obs in output["observations"]:
if "id" in obs:
available_observation_ids.add(obs["id"])
if normalized_tool_name == "recall" and isinstance(output, dict) and "memories" in output:
for memory in output["memories"]:
if "id" in memory:
@ -695,8 +695,8 @@ def _tool_call_to_dict(tc: "LLMToolCall") -> dict[str, Any]:
async def _process_done_tool(
done_call: "LLMToolCall",
available_memory_ids: set[str],
available_reflection_ids: set[str],
available_mental_model_ids: set[str],
available_observation_ids: set[str],
iterations: int,
total_tools_called: int,
tool_trace: list[ToolCall],
@ -717,8 +717,8 @@ async def _process_done_tool(
# Validate IDs (only include IDs that were actually retrieved)
used_memory_ids = [mid for mid in args.get("memory_ids", []) if mid in available_memory_ids]
used_reflection_ids = [rid for rid in args.get("reflection_ids", []) if rid in available_reflection_ids]
used_mental_model_ids = [mid for mid in args.get("mental_model_ids", []) if mid in available_mental_model_ids]
used_observation_ids = [oid for oid in args.get("observation_ids", []) if oid in available_observation_ids]
# Generate structured output if schema provided
structured_output = None
@ -744,16 +744,16 @@ async def _process_done_tool(
llm_trace=llm_trace,
usage=final_usage,
used_memory_ids=used_memory_ids,
used_reflection_ids=used_reflection_ids,
used_mental_model_ids=used_mental_model_ids,
used_observation_ids=used_observation_ids,
directives_applied=directives_applied,
)
async def _execute_tool_with_timing(
tc: "LLMToolCall",
search_reflections_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
search_mental_models_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
search_observations_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
recall_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
expand_fn: Callable[[list[str], str], Awaitable[dict[str, Any]]],
) -> tuple[dict[str, Any], int]:
@ -762,8 +762,8 @@ async def _execute_tool_with_timing(
result = await _execute_tool(
tc.name,
tc.arguments,
search_reflections_fn,
search_mental_models_fn,
search_observations_fn,
recall_fn,
expand_fn,
)
@ -774,8 +774,8 @@ async def _execute_tool_with_timing(
async def _execute_tool(
tool_name: str,
args: dict[str, Any],
search_reflections_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
search_mental_models_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
search_observations_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
recall_fn: Callable[[str, int], Awaitable[dict[str, Any]]],
expand_fn: Callable[[list[str], str], Awaitable[dict[str, Any]]],
) -> dict[str, Any]:
@ -783,19 +783,19 @@ async def _execute_tool(
# Normalize tool name for various LLM output formats
tool_name = _normalize_tool_name(tool_name)
if tool_name == "search_reflections":
query = args.get("query")
if not query:
return {"error": "search_reflections requires a query parameter"}
max_results = args.get("max_results") or 5
return await search_reflections_fn(query, max_results)
elif tool_name == "search_mental_models":
if tool_name == "search_mental_models":
query = args.get("query")
if not query:
return {"error": "search_mental_models requires a query parameter"}
max_results = args.get("max_results") or 5
return await search_mental_models_fn(query, max_results)
elif tool_name == "search_observations":
query = args.get("query")
if not query:
return {"error": "search_observations requires a query parameter"}
max_tokens = max(args.get("max_tokens") or 5000, 1000) # Default 5000, min 1000
return await search_mental_models_fn(query, max_tokens)
return await search_observations_fn(query, max_tokens)
elif tool_name == "recall":
query = args.get("query")
@ -817,12 +817,12 @@ async def _execute_tool(
def _summarize_input(tool_name: str, args: dict[str, Any]) -> str:
"""Create a summary of tool input for logging, showing all params."""
if tool_name == "search_reflections":
if tool_name == "search_mental_models":
query = args.get("query", "")
query_preview = f"'{query[:30]}...'" if len(query) > 30 else f"'{query}'"
max_results = args.get("max_results") or 5
return f"(query={query_preview}, max_results={max_results})"
elif tool_name == "search_mental_models":
elif tool_name == "search_observations":
query = args.get("query", "")
query_preview = f"'{query[:30]}...'" if len(query) > 30 else f"'{query}'"
max_tokens = max(args.get("max_tokens") or 5000, 1000)
@ -841,9 +841,9 @@ def _summarize_input(tool_name: str, args: dict[str, Any]) -> str:
answer = args.get("answer", "")
answer_preview = f"'{answer[:30]}...'" if len(answer) > 30 else f"'{answer}'"
memory_ids = args.get("memory_ids", [])
reflection_ids = args.get("reflection_ids", [])
mental_model_ids = args.get("mental_model_ids", [])
observation_ids = args.get("observation_ids", [])
return (
f"(answer={answer_preview}, mem={len(memory_ids)}, ref={len(reflection_ids)}, mm={len(mental_model_ids)})"
f"(answer={answer_preview}, mem={len(memory_ids)}, mm={len(mental_model_ids)}, obs={len(observation_ids)})"
)
return str(args)

View file

@ -7,22 +7,22 @@ from typing import Any, Literal
from pydantic import BaseModel, Field
class MentalModelObservation(BaseModel):
"""An observation within a mental model with its supporting memories."""
class ObservationSection(BaseModel):
"""A section within an observation with its supporting memories."""
title: str = Field(description="Observation header (can be empty for intro)")
text: str = Field(description="Observation content - no headers, use lists/tables/bold")
memory_ids: list[str] = Field(default_factory=list, description="Memory IDs supporting this observation")
title: str = Field(description="Section header (can be empty for intro)")
text: str = Field(description="Section content - no headers, use lists/tables/bold")
memory_ids: list[str] = Field(default_factory=list, description="Memory IDs supporting this section")
class MentalModelInput(BaseModel):
"""Input for the learn tool to create a mental model placeholder.
class ObservationInput(BaseModel):
"""Input for the learn tool to create an observation placeholder.
The agent only specifies name and description - the actual content/observations
The agent only specifies name and description - the actual content/sections
are generated during refresh, similar to pinned models.
"""
name: str = Field(description="Human-readable name for the mental model")
name: str = Field(description="Human-readable name for the observation")
description: str = Field(description="What to track - used as prompt for content generation during refresh")
entity_id: str | None = Field(default=None, description="Optional link to existing entity ID")
@ -39,19 +39,19 @@ class AnswerSection(BaseModel):
class ReflectAction(BaseModel):
"""Single action the reflect agent can take."""
tool: Literal["list_mental_models", "get_mental_model", "recall", "learn", "expand", "done"] = Field(
description="Tool to invoke: list_mental_models, get_mental_model, recall, learn, expand, or done"
tool: Literal["list_observations", "get_observation", "recall", "learn", "expand", "done"] = Field(
description="Tool to invoke: list_observations, get_observation, recall, learn, expand, or done"
)
# Tool-specific parameters
model_id: str | None = Field(default=None, description="Mental model ID for get_mental_model")
observation_id: str | None = Field(default=None, description="Observation ID for get_observation")
query: str | None = Field(default=None, description="Search query for recall")
max_tokens: int | None = Field(default=None, description="Max tokens for recall results (default 2048)")
mental_model: MentalModelInput | None = Field(default=None, description="Mental model to create/update for learn")
observation: ObservationInput | None = Field(default=None, description="Observation to create/update for learn")
memory_ids: list[str] | None = Field(default=None, description="Memory unit IDs for expand (batched)")
depth: Literal["chunk", "document"] | None = Field(default=None, description="Expansion depth for expand")
sections: list[AnswerSection] | None = Field(default=None, description="DEPRECATED: Use answer field instead")
observations: list[MentalModelObservation] | None = Field(
default=None, description="Observations for done action (when output_mode=observations)"
observation_sections: list[ObservationSection] | None = Field(
default=None, description="Observation sections for done action (when output_mode=observations)"
)
# Plain text answer fields (for output_mode=answer)
answer: str | None = Field(default=None, description="Plain text answer for done action (no markdown)")
@ -120,12 +120,12 @@ class ReflectAgentResult(BaseModel):
default_factory=TokenUsageSummary, description="Total token usage across all LLM calls"
)
used_memory_ids: list[str] = Field(default_factory=list, description="Validated memory IDs actually used in answer")
used_reflection_ids: list[str] = Field(
default_factory=list, description="Validated reflection IDs actually used in answer"
)
used_mental_model_ids: list[str] = Field(
default_factory=list, description="Validated mental model IDs actually used in answer"
)
used_observation_ids: list[str] = Field(
default_factory=list, description="Validated observation IDs actually used in answer"
)
directives_applied: list[DirectiveInfo] = Field(
default_factory=list, description="Directive mental models that affected this reflection"
)

View file

@ -2,8 +2,8 @@
System prompts for the reflect agent.
The reflect agent uses hierarchical retrieval:
1. search_reflections - User-curated summaries (highest quality)
2. search_mental_models - Consolidated knowledge with freshness awareness
1. search_mental_models - User-curated summaries (highest quality)
2. search_observations - Consolidated knowledge with freshness awareness
3. recall - Raw facts as ground truth fallback
"""
@ -125,21 +125,21 @@ def build_system_prompt_for_tools(
bank_profile: dict[str, Any],
context: str | None = None,
directives: list[dict[str, Any]] | None = None,
has_reflections: bool = False,
has_mental_models: bool = False,
) -> str:
"""
Build the system prompt for tool-calling reflect agent.
The agent uses hierarchical retrieval:
1. search_reflections - User-curated summaries (try first, if available)
2. search_mental_models - Consolidated knowledge with freshness
1. search_mental_models - User-curated summaries (try first, if available)
2. search_observations - Consolidated knowledge with freshness
3. recall - Raw facts as ground truth
Args:
bank_profile: Bank profile with name and mission
context: Optional additional context
directives: Optional list of directive mental models to inject as hard rules
has_reflections: Whether the bank has any reflections (skip if not)
has_mental_models: Whether the bank has any mental models (skip if not)
"""
name = bank_profile.get("name", "Assistant")
mission = bank_profile.get("mission", "")
@ -176,25 +176,25 @@ def build_system_prompt_for_tools(
)
# Build retrieval levels based on what's available
if has_reflections:
if has_mental_models:
parts.extend(
[
"You have access to THREE levels of knowledge. Use them in this order:",
"",
"### 1. REFLECTIONS (search_reflections) - Try First",
"### 1. MENTAL MODELS (search_mental_models) - Try First",
"- User-curated summaries about specific topics",
"- HIGHEST quality - manually created and maintained",
"- If a relevant reflection exists and is FRESH, it may fully answer the question",
"- If a relevant mental model exists and is FRESH, it may fully answer the question",
"- Check `is_stale` field - if stale, also verify with lower levels",
"",
"### 2. MENTAL MODELS (search_mental_models) - Second Priority",
"### 2. OBSERVATIONS (search_observations) - Second Priority",
"- Auto-consolidated knowledge from memories",
"- Check `is_stale` field - if stale, ALSO use recall() to verify",
"- Good for understanding patterns and summaries",
"",
"### 3. RAW FACTS (recall) - Ground Truth",
"- Individual memories (world facts and experiences)",
"- Use when: no reflections/models exist, they're stale, or you need specific details",
"- Use when: no mental models/observations exist, they're stale, or you need specific details",
"- This is the source of truth that other levels are built from",
"",
]
@ -204,15 +204,15 @@ def build_system_prompt_for_tools(
[
"You have access to TWO levels of knowledge. Use them in this order:",
"",
"### 1. MENTAL MODELS (search_mental_models) - Try First",
"### 1. OBSERVATIONS (search_observations) - Try First",
"- Auto-consolidated knowledge from memories",
"- Check `is_stale` field - if stale, ALSO use recall() to verify",
"- Good for understanding patterns and summaries",
"",
"### 2. RAW FACTS (recall) - Ground Truth",
"- Individual memories (world facts and experiences)",
"- Use when: no mental models exist, they're stale, or you need specific details",
"- This is the source of truth that mental models are built from",
"- Use when: no observations exist, they're stale, or you need specific details",
"- This is the source of truth that observations are built from",
"",
]
)
@ -234,12 +234,12 @@ def build_system_prompt_for_tools(
]
)
if has_reflections:
if has_mental_models:
parts.extend(
[
"1. First, try search_reflections() - check if a curated summary exists",
"2. If no reflection or it's stale, try search_mental_models() for consolidated knowledge",
"3. If mental models are stale OR you need specific details, use recall() for raw facts",
"1. First, try search_mental_models() - check if a curated summary exists",
"2. If no mental model or it's stale, try search_observations() for consolidated knowledge",
"3. If observations are stale OR you need specific details, use recall() for raw facts",
"4. Use expand() if you need more context on specific memories",
"5. When ready, call done() with your answer and supporting IDs",
]
@ -247,8 +247,8 @@ def build_system_prompt_for_tools(
else:
parts.extend(
[
"1. First, try search_mental_models() - check for consolidated knowledge",
"2. If mental models are stale OR you need specific details, use recall() for raw facts",
"1. First, try search_observations() - check for consolidated knowledge",
"2. If observations are stale OR you need specific details, use recall() for raw facts",
"3. Use expand() if you need more context on specific memories",
"4. When ready, call done() with your answer and supporting IDs",
]
@ -261,7 +261,7 @@ def build_system_prompt_for_tools(
"Call done() with a plain text 'answer' field.",
"- Do NOT use markdown formatting",
"- NEVER include memory IDs, UUIDs, or 'Memory references' in the answer text",
"- Put IDs ONLY in the memory_ids/reflection_ids/mental_model_ids arrays, not in the answer",
"- Put IDs ONLY in the memory_ids/mental_model_ids/observation_ids arrays, not in the answer",
]
)
@ -356,8 +356,8 @@ def build_agent_prompt(
parts.append(
"\n## Instructions\n"
"Start by searching for relevant information using the hierarchical retrieval strategy:\n"
"1. Try search_reflections() first for curated summaries\n"
"2. Try search_mental_models() for consolidated knowledge\n"
"1. Try search_mental_models() first for curated summaries\n"
"2. Try search_observations() for consolidated knowledge\n"
"3. Use recall() for specific details or to verify stale data"
)

View file

@ -2,8 +2,8 @@
Tool implementations for the reflect agent.
Implements hierarchical retrieval:
1. search_reflections - User-curated summaries (highest quality)
2. search_mental_models - Consolidated knowledge with freshness
1. search_mental_models - User-curated stored reflect responses (highest quality)
2. search_observations - Consolidated knowledge with freshness
3. recall - Raw facts as ground truth
"""
@ -20,11 +20,11 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
# Mental model is considered stale if not updated in this many days
# Observation is considered stale if not updated in this many days
STALE_THRESHOLD_DAYS = 7
async def tool_search_reflections(
async def tool_search_mental_models(
conn: "Connection",
bank_id: str,
query: str,
@ -35,9 +35,9 @@ async def tool_search_reflections(
exclude_ids: list[str] | None = None,
) -> dict[str, Any]:
"""
Search user-curated reflections by semantic similarity.
Search user-curated mental models by semantic similarity.
Reflections are high-quality, manually created summaries about specific topics.
Mental models are high-quality, manually created summaries about specific topics.
They should be searched FIRST as they represent the most reliable synthesized knowledge.
Args:
@ -45,13 +45,13 @@ async def tool_search_reflections(
bank_id: Bank identifier
query: Search query (for logging/tracing)
query_embedding: Pre-computed embedding for semantic search
max_results: Maximum number of reflections to return
tags: Optional tags to filter reflections
max_results: Maximum number of mental models to return
tags: Optional tags to filter mental models
tags_match: How to match tags - "any" (OR), "all" (AND)
exclude_ids: Optional list of reflection IDs to exclude (e.g., when refreshing a reflection)
exclude_ids: Optional list of mental model IDs to exclude (e.g., when refreshing a mental model)
Returns:
Dict with matching reflections including content and freshness info
Dict with matching mental models including content and freshness info
"""
from ..memory_engine import fq_table
@ -73,14 +73,14 @@ async def tool_search_reflections(
params.append(exclude_ids)
next_param += 1
# Search reflections by embedding similarity
# Search mental models by embedding similarity
rows = await conn.fetch(
f"""
SELECT
id, name, content, reflect_response,
tags, created_at, last_refreshed_at,
1 - (embedding <=> $2::vector) as relevance
FROM {fq_table("reflections")}
FROM {fq_table("mental_models")}
WHERE bank_id = $1 AND embedding IS NOT NULL {filters}
ORDER BY embedding <=> $2::vector
LIMIT $3
@ -89,7 +89,7 @@ async def tool_search_reflections(
)
now = datetime.now(timezone.utc)
reflections = []
mental_models = []
for row in rows:
last_refreshed_at = row["last_refreshed_at"]
@ -102,7 +102,7 @@ async def tool_search_reflections(
age = now - last_refreshed_at
is_stale = age > timedelta(days=STALE_THRESHOLD_DAYS)
reflections.append(
mental_models.append(
{
"id": str(row["id"]),
"name": row["name"],
@ -117,12 +117,12 @@ async def tool_search_reflections(
return {
"query": query,
"count": len(reflections),
"reflections": reflections,
"count": len(mental_models),
"mental_models": mental_models,
}
async def tool_search_mental_models(
async def tool_search_observations(
memory_engine: "MemoryEngine",
bank_id: str,
query: str,
@ -134,9 +134,9 @@ async def tool_search_mental_models(
pending_consolidation: int = 0,
) -> dict[str, Any]:
"""
Search consolidated mental models using recall with include_mental_models.
Search consolidated observations using recall with include_observations.
Mental models are auto-generated from memories. Returns freshness info
Observations are auto-generated from memories. Returns freshness info
so the agent knows if it should also verify with recall().
Args:
@ -145,22 +145,22 @@ async def tool_search_mental_models(
query: Search query
request_context: Request context for authentication
max_tokens: Maximum tokens for results (default 5000)
tags: Optional tags to filter models
tags: Optional tags to filter observations
tags_match: How to match tags - "any" (OR), "all" (AND)
last_consolidated_at: When consolidation last ran (for staleness check)
pending_consolidation: Number of memories waiting to be consolidated
Returns:
Dict with matching mental models including freshness info
Dict with matching observations including freshness info
"""
from ..memory_engine import fq_table
# Use recall to search mental models (they come back in results field when fact_type=["mental_model"])
# Use recall to search observations (they come back in results field when fact_type=["observation"])
result = await memory_engine.recall_async(
bank_id=bank_id,
query=query,
fact_type=["mental_model"], # Only retrieve mental models
max_tokens=max_tokens, # Token budget controls how many mental models are returned
fact_type=["observation"], # Only retrieve observations
max_tokens=max_tokens, # Token budget controls how many observations are returned
enable_trace=False,
request_context=request_context,
tags=tags,
@ -169,29 +169,29 @@ async def tool_search_mental_models(
_quiet=True,
)
mental_models = []
observations = []
# When fact_type=["mental_model"], results come back in `results` field as MemoryFact objects
# When fact_type=["observation"], results come back in `results` field as MemoryFact objects
# We need to fetch additional fields (proof_count, source_memory_ids) from the database
if result.results:
mm_ids = [m.id for m in result.results]
obs_ids = [m.id for m in result.results]
# Fetch proof_count and source_memory_ids for these mental models
# Fetch proof_count and source_memory_ids for these observations
pool = await memory_engine._get_pool()
async with pool.acquire() as conn:
mm_rows = await conn.fetch(
obs_rows = await conn.fetch(
f"""
SELECT id, proof_count, source_memory_ids
FROM {fq_table("memory_units")}
WHERE id = ANY($1::uuid[])
""",
mm_ids,
obs_ids,
)
mm_data = {str(row["id"]): row for row in mm_rows}
obs_data = {str(row["id"]): row for row in obs_rows}
for m in result.results:
# Get additional data from DB lookup
extra = mm_data.get(m.id, {})
extra = obs_data.get(m.id, {})
proof_count = extra.get("proof_count", 1) if extra else 1
source_ids = extra.get("source_memory_ids", []) if extra else []
# Convert UUIDs to strings
@ -204,7 +204,7 @@ async def tool_search_mental_models(
is_stale = True
staleness_reason = f"{pending_consolidation} memories pending consolidation"
mental_models.append(
observations.append(
{
"id": str(m.id),
"text": m.text,
@ -226,8 +226,8 @@ async def tool_search_mental_models(
return {
"query": query,
"count": len(mental_models),
"mental_models": mental_models,
"count": len(observations),
"observations": observations,
"freshness": freshness,
}
@ -247,7 +247,7 @@ async def tool_recall(
Search memories using TEMPR retrieval.
This is the ground truth - raw facts and experiences.
Use when reflections/mental models don't exist, are stale, or need verification.
Use when mental models/observations don't exist, are stale, or need verification.
Args:
memory_engine: Memory engine instance
@ -266,7 +266,7 @@ async def tool_recall(
result = await memory_engine.recall_async(
bank_id=bank_id,
query=query,
fact_type=["experience", "world"], # Exclude opinions and mental_models
fact_type=["experience", "world"], # Exclude opinions and observations
max_tokens=max_tokens,
enable_trace=False,
request_context=request_context,

View file

@ -3,47 +3,21 @@ Tool schema definitions for the reflect agent.
These are OpenAI-format tool definitions used with native tool calling.
The reflect agent uses a hierarchical retrieval strategy:
1. search_reflections - User-curated summaries (highest quality, if applicable)
2. search_mental_models - Consolidated knowledge with freshness awareness
1. search_mental_models - User-curated stored reflect responses (highest quality, if applicable)
2. search_observations - Consolidated knowledge with freshness awareness
3. recall - Raw facts (world/experience) as ground truth fallback
"""
# Tool definitions in OpenAI format
TOOL_SEARCH_REFLECTIONS = {
"type": "function",
"function": {
"name": "search_reflections",
"description": (
"Search user-curated reflections (summaries). These are high-quality, manually created "
"summaries about specific topics. Use FIRST when the question might be covered by an "
"existing reflection. Returns reflections with their content and last refresh time."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query to find relevant reflections",
},
"max_results": {
"type": "integer",
"description": "Maximum number of reflections to return (default 5)",
},
},
"required": ["query"],
},
},
}
TOOL_SEARCH_MENTAL_MODELS = {
"type": "function",
"function": {
"name": "search_mental_models",
"description": (
"Search consolidated mental models (auto-generated knowledge). These are automatically "
"synthesized from memories. Returns models with freshness info (updated_at, is_stale). "
"If a model is STALE, you should ALSO use recall() to verify with current facts."
"Search user-curated mental models (stored reflect responses). These are high-quality, manually created "
"summaries about specific topics. Use FIRST when the question might be covered by an "
"existing mental model. Returns mental models with their content and last refresh time."
),
"parameters": {
"type": "object",
@ -52,6 +26,32 @@ TOOL_SEARCH_MENTAL_MODELS = {
"type": "string",
"description": "Search query to find relevant mental models",
},
"max_results": {
"type": "integer",
"description": "Maximum number of mental models to return (default 5)",
},
},
"required": ["query"],
},
},
}
TOOL_SEARCH_OBSERVATIONS = {
"type": "function",
"function": {
"name": "search_observations",
"description": (
"Search consolidated observations (auto-generated knowledge). These are automatically "
"synthesized from memories. Returns observations with freshness info (updated_at, is_stale). "
"If an observation is STALE, you should ALSO use recall() to verify with current facts."
),
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Search query to find relevant observations",
},
"max_tokens": {
"type": "integer",
"description": "Maximum tokens for results (default 5000). Use higher values for broader searches.",
@ -130,16 +130,16 @@ TOOL_DONE_ANSWER = {
"items": {"type": "string"},
"description": "Array of memory IDs that support your answer (put IDs here, NOT in answer text)",
},
"reflection_ids": {
"type": "array",
"items": {"type": "string"},
"description": "Array of reflection IDs that support your answer",
},
"mental_model_ids": {
"type": "array",
"items": {"type": "string"},
"description": "Array of mental model IDs that support your answer",
},
"observation_ids": {
"type": "array",
"items": {"type": "string"},
"description": "Array of observation IDs that support your answer",
},
},
"required": ["answer"],
},
@ -181,16 +181,16 @@ def _build_done_tool_with_directives(directive_rules: list[str]) -> dict:
"items": {"type": "string"},
"description": "Array of memory IDs that support your answer (put IDs here, NOT in answer text)",
},
"reflection_ids": {
"type": "array",
"items": {"type": "string"},
"description": "Array of reflection IDs that support your answer",
},
"mental_model_ids": {
"type": "array",
"items": {"type": "string"},
"description": "Array of mental model IDs that support your answer",
},
"observation_ids": {
"type": "array",
"items": {"type": "string"},
"description": "Array of observation IDs that support your answer",
},
"directive_compliance": {
"type": "string",
"description": f"REQUIRED: Confirm your answer complies with ALL directives. List each directive and how your answer follows it:\n{rules_list}\n\nFormat: 'Directive 1: [how answer complies]. Directive 2: [how answer complies]...'",
@ -207,8 +207,8 @@ def get_reflect_tools(directive_rules: list[str] | None = None) -> list[dict]:
Get the list of tools for the reflect agent.
The tools support a hierarchical retrieval strategy:
1. search_reflections - User-curated summaries (try first)
2. search_mental_models - Consolidated knowledge with freshness
1. search_mental_models - User-curated stored reflect responses (try first)
2. search_observations - Consolidated knowledge with freshness
3. recall - Raw facts as ground truth
Args:
@ -219,8 +219,8 @@ def get_reflect_tools(directive_rules: list[str] | None = None) -> list[dict]:
List of tool definitions in OpenAI format
"""
tools = [
TOOL_SEARCH_REFLECTIONS,
TOOL_SEARCH_MENTAL_MODELS,
TOOL_SEARCH_OBSERVATIONS,
TOOL_RECALL,
TOOL_EXPAND,
]

View file

@ -10,8 +10,8 @@ from typing import Any
from pydantic import BaseModel, ConfigDict, Field
# Valid fact types for recall operations (excludes 'observation' which is internal, and 'opinion' which is deprecated)
VALID_RECALL_FACT_TYPES = frozenset(["world", "experience", "mental_model"])
# Valid fact types for recall operations (excludes 'opinion' which is deprecated)
VALID_RECALL_FACT_TYPES = frozenset(["world", "experience", "observation"])
class LLMToolCall(BaseModel):
@ -49,13 +49,13 @@ class LLMCallTrace(BaseModel):
duration_ms: int = Field(description="Execution time in milliseconds")
class MentalModelRef(BaseModel):
"""Reference to a mental model accessed during reflect."""
class ObservationRef(BaseModel):
"""Reference to an observation accessed during reflect."""
id: str = Field(description="Mental model ID")
name: str = Field(description="Mental model name")
type: str = Field(description="Mental model type: entity, concept, event")
subtype: str = Field(description="Mental model subtype: structural, emergent, learned")
id: str = Field(description="Observation ID")
name: str = Field(description="Observation name")
type: str = Field(description="Observation type: entity, concept, event")
subtype: str = Field(description="Observation subtype: structural, emergent, learned")
description: str = Field(description="Brief description")
summary: str | None = Field(default=None, description="Full summary (when looked up in detail)")
@ -168,23 +168,23 @@ class ChunkInfo(BaseModel):
truncated: bool = Field(default=False, description="Whether the chunk was truncated due to token limits")
class MentalModelResult(BaseModel):
"""A mental model result from recall."""
class ObservationResult(BaseModel):
"""An observation result from recall (consolidated knowledge synthesized from facts)."""
id: str = Field(description="Unique mental model ID")
text: str = Field(description="The mental model text")
proof_count: int = Field(description="Number of facts supporting this mental model")
id: str = Field(description="Unique observation ID")
text: str = Field(description="The observation text")
proof_count: int = Field(description="Number of facts supporting this observation")
relevance: float = Field(default=0.0, description="Relevance score to the query")
tags: list[str] | None = Field(default=None, description="Tags for visibility scoping")
source_memory_ids: list[str] = Field(
default_factory=list, description="IDs of facts that contribute to this mental model"
default_factory=list, description="IDs of facts that contribute to this observation"
)
class ReflectionResult(BaseModel):
"""A reflection result from recall."""
class MentalModelResult(BaseModel):
"""A mental model result from recall (stored reflect response)."""
id: str = Field(description="Unique reflection ID")
id: str = Field(description="Unique mental model ID")
name: str = Field(description="Human-readable name")
content: str = Field(description="The synthesized content")
relevance: float = Field(default=0.0, description="Relevance score to the query")

View file

@ -216,7 +216,7 @@ def main():
retain_extract_causal_links=config.retain_extract_causal_links,
retain_extraction_mode=config.retain_extraction_mode,
retain_observations_async=config.retain_observations_async,
enable_mental_models=config.enable_mental_models,
enable_observations=config.enable_observations,
consolidation_similarity_threshold=config.consolidation_similarity_threshold,
consolidation_batch_size=config.consolidation_batch_size,
skip_llm_verification=config.skip_llm_verification,

File diff suppressed because it is too large Load diff

View file

@ -241,8 +241,8 @@ class TestReflectToolSchemas:
tools = get_reflect_tools()
tool_names = [t["function"]["name"] for t in tools]
assert "search_reflections" in tool_names
assert "search_mental_models" in tool_names
assert "search_observations" in tool_names
assert "recall" in tool_names
assert "expand" in tool_names
assert "done" in tool_names
@ -273,8 +273,8 @@ class TestReflectToolSchemas:
assert "answer" in params
assert "memory_ids" in params
assert "observation_ids" in params
assert "mental_model_ids" in params
assert "reflection_ids" in params
class TestLLMToolCallResult:

View file

@ -68,15 +68,15 @@ class TestToolNameNormalization:
"""Standard tool names should pass through unchanged."""
assert _normalize_tool_name("done") == "done"
assert _normalize_tool_name("recall") == "recall"
assert _normalize_tool_name("search_reflections") == "search_reflections"
assert _normalize_tool_name("search_mental_models") == "search_mental_models"
assert _normalize_tool_name("search_observations") == "search_observations"
assert _normalize_tool_name("expand") == "expand"
def test_normalize_functions_prefix(self):
"""Tool names with 'functions.' prefix should be normalized."""
assert _normalize_tool_name("functions.done") == "done"
assert _normalize_tool_name("functions.recall") == "recall"
assert _normalize_tool_name("functions.search_reflections") == "search_reflections"
assert _normalize_tool_name("functions.search_mental_models") == "search_mental_models"
def test_normalize_call_equals_prefix(self):
"""Tool names with 'call=' prefix should be normalized."""
@ -87,7 +87,7 @@ class TestToolNameNormalization:
"""Tool names with 'call=functions.' prefix should be normalized."""
assert _normalize_tool_name("call=functions.done") == "done"
assert _normalize_tool_name("call=functions.recall") == "recall"
assert _normalize_tool_name("call=functions.search_mental_models") == "search_mental_models"
assert _normalize_tool_name("call=functions.search_observations") == "search_observations"
def test_is_done_tool(self):
"""Test _is_done_tool helper."""
@ -123,8 +123,8 @@ class TestReflectAgentMocked:
def mock_functions(self):
"""Create mock search/recall functions."""
return {
"search_reflections_fn": AsyncMock(return_value={"reflections": []}),
"search_mental_models_fn": AsyncMock(return_value={"mental_models": []}),
"search_observations_fn": AsyncMock(return_value={"observations": []}),
"recall_fn": AsyncMock(return_value={"memories": [{"id": "mem-1", "content": "test memory"}]}),
"expand_fn": AsyncMock(return_value={"memories": []}),
}

View file

@ -1,4 +1,4 @@
"""Tests for reflections, mental models, and learnings functionality."""
"""Tests for mental models (formerly reflections), observations, and learnings functionality."""
import uuid
@ -21,22 +21,22 @@ async def api_client(memory):
@pytest.fixture
def test_bank_id():
"""Provide a unique bank ID for this test run."""
return f"test_reflections_{uuid.uuid4().hex[:8]}"
return f"test_mental_models_{uuid.uuid4().hex[:8]}"
class TestReflectionsCRUD:
"""Test reflections CRUD operations via memory engine."""
class TestMentalModelsCRUD:
"""Test mental models CRUD operations via memory engine."""
@pytest.mark.asyncio
async def test_create_and_get_reflection(self, memory: MemoryEngine, request_context):
"""Test creating and retrieving a reflection."""
bank_id = f"test-reflection-{uuid.uuid4().hex[:8]}"
async def test_create_and_get_mental_model(self, memory: MemoryEngine, request_context):
"""Test creating and retrieving a mental model."""
bank_id = f"test-mental-model-{uuid.uuid4().hex[:8]}"
# Create the bank first
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
# Create a reflection
reflection = await memory.create_reflection(
# Create a mental model
mental_model = await memory.create_mental_model(
bank_id=bank_id,
name="Team Preferences",
source_query="What are the team's communication preferences?",
@ -45,45 +45,45 @@ class TestReflectionsCRUD:
request_context=request_context,
)
assert reflection["name"] == "Team Preferences"
assert reflection["source_query"] == "What are the team's communication preferences?"
assert reflection["content"] == "The team prefers async communication via Slack"
assert reflection["tags"] == ["team"]
assert "id" in reflection
assert mental_model["name"] == "Team Preferences"
assert mental_model["source_query"] == "What are the team's communication preferences?"
assert mental_model["content"] == "The team prefers async communication via Slack"
assert mental_model["tags"] == ["team"]
assert "id" in mental_model
# Get the reflection
fetched = await memory.get_reflection(
# Get the mental model
fetched = await memory.get_mental_model(
bank_id=bank_id,
reflection_id=reflection["id"],
mental_model_id=mental_model["id"],
request_context=request_context,
)
assert fetched["id"] == reflection["id"]
assert fetched["id"] == mental_model["id"]
assert fetched["name"] == "Team Preferences"
# Cleanup
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_list_reflections(self, memory: MemoryEngine, request_context):
"""Test listing reflections with filters."""
bank_id = f"test-reflection-list-{uuid.uuid4().hex[:8]}"
async def test_list_mental_models(self, memory: MemoryEngine, request_context):
"""Test listing mental models with filters."""
bank_id = f"test-mental-model-list-{uuid.uuid4().hex[:8]}"
# Create the bank first
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
# Create multiple reflections
await memory.create_reflection(
# Create multiple mental models
await memory.create_mental_model(
bank_id=bank_id,
name="Reflection 1",
name="Mental Model 1",
source_query="Query 1",
content="Content 1",
tags=["tag1"],
request_context=request_context,
)
await memory.create_reflection(
await memory.create_mental_model(
bank_id=bank_id,
name="Reflection 2",
name="Mental Model 2",
source_query="Query 2",
content="Content 2",
tags=["tag2"],
@ -91,33 +91,33 @@ class TestReflectionsCRUD:
)
# List all
all_reflections = await memory.list_reflections(
all_mental_models = await memory.list_mental_models(
bank_id=bank_id,
request_context=request_context,
)
assert len(all_reflections) == 2
assert len(all_mental_models) == 2
# List with tag filter
tag1_reflections = await memory.list_reflections(
tag1_mental_models = await memory.list_mental_models(
bank_id=bank_id,
tags=["tag1"],
request_context=request_context,
)
assert len(tag1_reflections) == 1
assert len(tag1_mental_models) == 1
# Cleanup
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_update_reflection(self, memory: MemoryEngine, request_context):
"""Test updating a reflection."""
bank_id = f"test-reflection-update-{uuid.uuid4().hex[:8]}"
async def test_update_mental_model(self, memory: MemoryEngine, request_context):
"""Test updating a mental model."""
bank_id = f"test-mental-model-update-{uuid.uuid4().hex[:8]}"
# Create the bank first
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
# Create a reflection
reflection = await memory.create_reflection(
# Create a mental model
mental_model = await memory.create_mental_model(
bank_id=bank_id,
name="Original Name",
source_query="Original Query",
@ -125,10 +125,10 @@ class TestReflectionsCRUD:
request_context=request_context,
)
# Update the reflection
updated = await memory.update_reflection(
# Update the mental model
updated = await memory.update_mental_model(
bank_id=bank_id,
reflection_id=reflection["id"],
mental_model_id=mental_model["id"],
name="Updated Name",
content="Updated Content",
request_context=request_context,
@ -141,15 +141,15 @@ class TestReflectionsCRUD:
await memory.delete_bank(bank_id, request_context=request_context)
@pytest.mark.asyncio
async def test_delete_reflection(self, memory: MemoryEngine, request_context):
"""Test deleting a reflection."""
bank_id = f"test-reflection-delete-{uuid.uuid4().hex[:8]}"
async def test_delete_mental_model(self, memory: MemoryEngine, request_context):
"""Test deleting a mental model."""
bank_id = f"test-mental-model-delete-{uuid.uuid4().hex[:8]}"
# Create the bank first
await memory.get_bank_profile(bank_id=bank_id, request_context=request_context)
# Create a reflection
reflection = await memory.create_reflection(
# Create a mental model
mental_model = await memory.create_mental_model(
bank_id=bank_id,
name="To Delete",
source_query="Query",
@ -157,17 +157,17 @@ class TestReflectionsCRUD:
request_context=request_context,
)
# Delete the reflection
await memory.delete_reflection(
# Delete the mental model
await memory.delete_mental_model(
bank_id=bank_id,
reflection_id=reflection["id"],
mental_model_id=mental_model["id"],
request_context=request_context,
)
# Verify deletion - should return None
fetched = await memory.get_reflection(
fetched = await memory.get_mental_model(
bank_id=bank_id,
reflection_id=reflection["id"],
mental_model_id=mental_model["id"],
request_context=request_context,
)
assert fetched is None
@ -176,45 +176,45 @@ class TestReflectionsCRUD:
await memory.delete_bank(bank_id, request_context=request_context)
class TestMentalModelsAPI:
"""Test mental models API endpoints.
class TestObservationsAPI:
"""Test observations API endpoints.
NOTE: Mental models are now stored in memory_units with fact_type='mental_model'
and accessed via recall with fact_type=["mental_model"]. The old /mental-models
NOTE: Observations are now stored in memory_units with fact_type='observation'
and accessed via recall with fact_type=["observation"]. The old /observations
endpoint was removed. These tests are skipped.
"""
@pytest.mark.skip(reason="Mental models endpoint removed - use recall with fact_type=['mental_model']")
@pytest.mark.skip(reason="Observations endpoint removed - use recall with fact_type=['observation']")
@pytest.mark.asyncio
async def test_list_mental_models_empty(self, api_client, test_bank_id):
"""Test listing mental models when none exist."""
async def test_list_observations_empty(self, api_client, test_bank_id):
"""Test listing observations when none exist."""
pass
@pytest.mark.skip(reason="Mental models endpoint removed - use recall with fact_type=['mental_model']")
@pytest.mark.skip(reason="Observations endpoint removed - use recall with fact_type=['observation']")
@pytest.mark.asyncio
async def test_get_mental_model_not_found(self, api_client, test_bank_id):
"""Test getting a non-existent mental model."""
async def test_get_observation_not_found(self, api_client, test_bank_id):
"""Test getting a non-existent observation."""
pass
class TestReflectionsAPI:
"""Test reflections API endpoints."""
class TestMentalModelsAPI:
"""Test mental models API endpoints."""
@pytest.mark.asyncio
async def test_reflections_api_crud(self, api_client, test_bank_id):
async def test_mental_models_api_crud(self, api_client, test_bank_id):
"""Test full CRUD cycle through API."""
import asyncio
# Create bank first via profile endpoint
await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
# Create a reflection (async operation)
# Create a mental model (async operation)
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/reflections",
f"/v1/default/banks/{test_bank_id}/mental-models",
json={
"name": "API Test Reflection",
"name": "API Test Mental Model",
"source_query": "What is the API test about?",
"content": "This is an API test reflection",
"content": "This is an API test mental model",
"tags": ["api-test"],
},
)
@ -232,44 +232,72 @@ class TestReflectionsAPI:
break
await asyncio.sleep(1)
# List reflections to get the created reflection
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/reflections")
# List mental models to get the created mental model
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/mental-models")
assert response.status_code == 200
reflections = response.json()["items"]
assert len(reflections) >= 1
mental_models = response.json()["items"]
assert len(mental_models) >= 1
# Find our reflection
reflection = next((r for r in reflections if r["name"] == "API Test Reflection"), None)
assert reflection is not None, f"Reflection not found. Items: {reflections}"
reflection_id = reflection["id"]
# Find our mental model
mental_model = next((m for m in mental_models if m["name"] == "API Test Mental Model"), None)
assert mental_model is not None, f"Mental model not found. Items: {mental_models}"
mental_model_id = mental_model["id"]
# Get the reflection
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/reflections/{reflection_id}")
# Get the mental model
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/mental-models/{mental_model_id}")
assert response.status_code == 200
assert response.json()["name"] == "API Test Reflection"
assert response.json()["name"] == "API Test Mental Model"
# Update the reflection
# Update the mental model
response = await api_client.patch(
f"/v1/default/banks/{test_bank_id}/reflections/{reflection_id}",
json={"name": "Updated API Test Reflection"},
f"/v1/default/banks/{test_bank_id}/mental-models/{mental_model_id}",
json={"name": "Updated API Test Mental Model"},
)
assert response.status_code == 200
assert response.json()["name"] == "Updated API Test Reflection"
assert response.json()["name"] == "Updated API Test Mental Model"
# Delete the reflection
response = await api_client.delete(f"/v1/default/banks/{test_bank_id}/reflections/{reflection_id}")
# Delete the mental model
response = await api_client.delete(f"/v1/default/banks/{test_bank_id}/mental-models/{mental_model_id}")
assert response.status_code == 200
# Verify deletion
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/reflections/{reflection_id}")
response = await api_client.get(f"/v1/default/banks/{test_bank_id}/mental-models/{mental_model_id}")
assert response.status_code == 404
# Cleanup
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
class TestRecallWithMentalModelsAndReflections:
"""Test recall integration with mental models and reflections."""
class TestRecallWithObservationsAndMentalModels:
"""Test recall integration with observations and mental models."""
@pytest.mark.asyncio
async def test_recall_includes_observations(self, api_client, test_bank_id):
"""Test that recall can include observations in the response."""
# Create bank first via profile endpoint
await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
# Note: Observations are auto-created via consolidation, not manually
# This test just verifies the include parameter works
# Recall with observations included
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/memories/recall",
json={
"query": "What is machine learning?",
"include": {
"observations": {"max_results": 5},
},
},
)
assert response.status_code == 200
result = response.json()
# Should have observations field in response (may be empty)
assert "observations" in result or result.get("observations") is None
# Cleanup
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
@pytest.mark.asyncio
async def test_recall_includes_mental_models(self, api_client, test_bank_id):
@ -277,37 +305,9 @@ class TestRecallWithMentalModelsAndReflections:
# Create bank first via profile endpoint
await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
# Note: Mental models are auto-created via consolidation, not manually
# This test just verifies the include parameter works
# Recall with mental models included
# Create a mental model first
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/memories/recall",
json={
"query": "What is machine learning?",
"include": {
"mental_models": {"max_results": 5},
},
},
)
assert response.status_code == 200
result = response.json()
# Should have mental_models field in response (may be empty)
assert "mental_models" in result or result.get("mental_models") is None
# Cleanup
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
@pytest.mark.asyncio
async def test_recall_includes_reflections(self, api_client, test_bank_id):
"""Test that recall can include reflections in the response."""
# Create bank first via profile endpoint
await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
# Create a reflection first
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/reflections",
f"/v1/default/banks/{test_bank_id}/mental-models",
json={
"name": "AI Overview",
"source_query": "What is AI?",
@ -317,32 +317,32 @@ class TestRecallWithMentalModelsAndReflections:
)
assert response.status_code == 200
# Recall with reflections included
# Recall with mental models included
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/memories/recall",
json={
"query": "What is artificial intelligence?",
"include": {
"reflections": {"max_results": 5},
"mental_models": {"max_results": 5},
},
},
)
assert response.status_code == 200
result = response.json()
# Should have reflections in response (may be empty if embedding not generated yet)
assert "reflections" in result or result.get("reflections") is None
# Should have mental_models in response (may be empty if embedding not generated yet)
assert "mental_models" in result or result.get("mental_models") is None
# Cleanup
await api_client.delete(f"/v1/default/banks/{test_bank_id}")
@pytest.mark.asyncio
async def test_recall_without_mental_models_by_default(self, api_client, test_bank_id):
"""Test that recall does not include mental models by default."""
async def test_recall_without_observations_by_default(self, api_client, test_bank_id):
"""Test that recall does not include observations by default."""
# Create bank first via profile endpoint
await api_client.get(f"/v1/default/banks/{test_bank_id}/profile")
# Recall without specifying mental models
# Recall without specifying observations
response = await api_client.post(
f"/v1/default/banks/{test_bank_id}/memories/recall",
json={
@ -352,8 +352,8 @@ class TestRecallWithMentalModelsAndReflections:
assert response.status_code == 200
result = response.json()
# Mental models should not be in response
assert result.get("mental_models") is None
# Observations should not be in response
assert result.get("observations") is None
# Cleanup
await api_client.delete(f"/v1/default/banks/{test_bank_id}")

View file

@ -22,7 +22,7 @@ TABLES = [
"chunks",
"async_operations",
"directives",
"reflections",
"mental_models",
]
# Files to scan for SQL queries

View file

@ -437,57 +437,57 @@ impl ApiClient {
})
}
// --- Reflection Methods ---
// --- Mental Model Methods ---
pub fn list_reflections(&self, bank_id: &str, _verbose: bool) -> Result<types::ReflectionListResponse> {
pub fn list_mental_models(&self, bank_id: &str, _verbose: bool) -> Result<types::MentalModelListResponse> {
self.runtime.block_on(async {
let response = self.client.list_reflections(bank_id, None, None, None, None, None).await?;
let response = self.client.list_mental_models(bank_id, None, None, None, None, None).await?;
Ok(response.into_inner())
})
}
pub fn get_reflection(&self, bank_id: &str, reflection_id: &str, _verbose: bool) -> Result<types::ReflectionResponse> {
pub fn get_mental_model(&self, bank_id: &str, mental_model_id: &str, _verbose: bool) -> Result<types::MentalModelResponse> {
self.runtime.block_on(async {
let response = self.client.get_reflection(bank_id, reflection_id, None).await?;
let response = self.client.get_mental_model(bank_id, mental_model_id, None).await?;
Ok(response.into_inner())
})
}
pub fn create_reflection(
pub fn create_mental_model(
&self,
bank_id: &str,
request: &types::CreateReflectionRequest,
request: &types::CreateMentalModelRequest,
_verbose: bool,
) -> Result<types::CreateReflectionResponse> {
) -> Result<types::CreateMentalModelResponse> {
self.runtime.block_on(async {
let response = self.client.create_reflection(bank_id, None, request).await?;
let response = self.client.create_mental_model(bank_id, None, request).await?;
Ok(response.into_inner())
})
}
pub fn update_reflection(
pub fn update_mental_model(
&self,
bank_id: &str,
reflection_id: &str,
request: &types::UpdateReflectionRequest,
mental_model_id: &str,
request: &types::UpdateMentalModelRequest,
_verbose: bool,
) -> Result<types::ReflectionResponse> {
) -> Result<types::MentalModelResponse> {
self.runtime.block_on(async {
let response = self.client.update_reflection(bank_id, reflection_id, None, request).await?;
let response = self.client.update_mental_model(bank_id, mental_model_id, None, request).await?;
Ok(response.into_inner())
})
}
pub fn delete_reflection(&self, bank_id: &str, reflection_id: &str, _verbose: bool) -> Result<serde_json::Value> {
pub fn delete_mental_model(&self, bank_id: &str, mental_model_id: &str, _verbose: bool) -> Result<serde_json::Value> {
self.runtime.block_on(async {
let response = self.client.delete_reflection(bank_id, reflection_id, None).await?;
let response = self.client.delete_mental_model(bank_id, mental_model_id, None).await?;
Ok(response.into_inner())
})
}
pub fn refresh_reflection(&self, bank_id: &str, reflection_id: &str, _verbose: bool) -> Result<types::AsyncOperationSubmitResponse> {
pub fn refresh_mental_model(&self, bank_id: &str, mental_model_id: &str, _verbose: bool) -> Result<types::AsyncOperationSubmitResponse> {
self.runtime.block_on(async {
let response = self.client.refresh_reflection(bank_id, reflection_id, None).await?;
let response = self.client.refresh_mental_model(bank_id, mental_model_id, None).await?;
Ok(response.into_inner())
})
}

View file

@ -1,4 +1,4 @@
//! Reflection commands for managing user-curated summaries.
//! Mental model commands for managing user-curated summaries.
use anyhow::Result;
@ -8,7 +8,7 @@ use crate::ui;
use hindsight_client::types;
/// List reflections for a bank
/// List mental models for a bank
pub fn list(
client: &ApiClient,
bank_id: &str,
@ -16,12 +16,12 @@ pub fn list(
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching reflections..."))
Some(ui::create_spinner("Fetching mental models..."))
} else {
None
};
let response = client.list_reflections(bank_id, verbose);
let response = client.list_mental_models(bank_id, verbose);
if let Some(mut sp) = spinner {
sp.finish();
@ -30,21 +30,21 @@ pub fn list(
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
ui::print_section_header(&format!("Reflections: {}", bank_id));
ui::print_section_header(&format!("Mental Models: {}", bank_id));
if result.items.is_empty() {
println!(" {}", ui::dim("No reflections found."));
println!(" {}", ui::dim("No mental models found."));
} else {
for reflection in &result.items {
for mental_model in &result.items {
println!(
" {} {}",
ui::gradient_start(&reflection.id),
reflection.name
ui::gradient_start(&mental_model.id),
mental_model.name
);
// Show content preview
let preview: String = reflection.content.chars().take(80).collect();
let ellipsis = if reflection.content.len() > 80 { "..." } else { "" };
let preview: String = mental_model.content.chars().take(80).collect();
let ellipsis = if mental_model.content.len() > 80 { "..." } else { "" };
println!(" {}{}", ui::dim(&preview), ellipsis);
println!();
@ -59,32 +59,32 @@ pub fn list(
}
}
/// Get a specific reflection
/// Get a specific mental model
pub fn get(
client: &ApiClient,
bank_id: &str,
reflection_id: &str,
mental_model_id: &str,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Fetching reflection..."))
Some(ui::create_spinner("Fetching mental model..."))
} else {
None
};
let response = client.get_reflection(bank_id, reflection_id, verbose);
let response = client.get_mental_model(bank_id, mental_model_id, verbose);
if let Some(mut sp) = spinner {
sp.finish();
}
match response {
Ok(reflection) => {
Ok(mental_model) => {
if output_format == OutputFormat::Pretty {
print_reflection_detail(&reflection);
print_mental_model_detail(&mental_model);
} else {
output::print_output(&reflection, output_format)?;
output::print_output(&mental_model, output_format)?;
}
Ok(())
}
@ -92,7 +92,7 @@ pub fn get(
}
}
/// Create a new reflection
/// Create a new mental model
pub fn create(
client: &ApiClient,
bank_id: &str,
@ -102,19 +102,19 @@ pub fn create(
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Creating reflection..."))
Some(ui::create_spinner("Creating mental model..."))
} else {
None
};
let request = types::CreateReflectionRequest {
let request = types::CreateMentalModelRequest {
name: name.to_string(),
source_query: source_query.to_string(),
max_tokens: 2048,
tags: vec![],
};
let response = client.create_reflection(bank_id, &request, verbose);
let response = client.create_mental_model(bank_id, &request, verbose);
if let Some(mut sp) = spinner {
sp.finish();
@ -123,7 +123,7 @@ pub fn create(
match response {
Ok(result) => {
if output_format == OutputFormat::Pretty {
ui::print_success(&format!("Reflection created, operation_id: {}", result.operation_id));
ui::print_success(&format!("Mental model created, operation_id: {}", result.operation_id));
} else {
output::print_output(&result, output_format)?;
}
@ -133,11 +133,11 @@ pub fn create(
}
}
/// Update a reflection
/// Update a mental model
pub fn update(
client: &ApiClient,
bank_id: &str,
reflection_id: &str,
mental_model_id: &str,
name: Option<String>,
verbose: bool,
output_format: OutputFormat,
@ -147,27 +147,27 @@ pub fn update(
}
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Updating reflection..."))
Some(ui::create_spinner("Updating mental model..."))
} else {
None
};
let request = types::UpdateReflectionRequest { name };
let request = types::UpdateMentalModelRequest { name };
let response = client.update_reflection(bank_id, reflection_id, &request, verbose);
let response = client.update_mental_model(bank_id, mental_model_id, &request, verbose);
if let Some(mut sp) = spinner {
sp.finish();
}
match response {
Ok(reflection) => {
Ok(mental_model) => {
if output_format == OutputFormat::Pretty {
ui::print_success(&format!("Reflection '{}' updated successfully", reflection_id));
ui::print_success(&format!("Mental model '{}' updated successfully", mental_model_id));
println!();
print_reflection_detail(&reflection);
print_mental_model_detail(&mental_model);
} else {
output::print_output(&reflection, output_format)?;
output::print_output(&mental_model, output_format)?;
}
Ok(())
}
@ -175,11 +175,11 @@ pub fn update(
}
}
/// Delete a reflection
/// Delete a mental model
pub fn delete(
client: &ApiClient,
bank_id: &str,
reflection_id: &str,
mental_model_id: &str,
yes: bool,
verbose: bool,
output_format: OutputFormat,
@ -187,8 +187,8 @@ pub fn delete(
// Confirmation prompt unless -y flag is used
if !yes && output_format == OutputFormat::Pretty {
let message = format!(
"Are you sure you want to delete reflection '{}'? This cannot be undone.",
reflection_id
"Are you sure you want to delete mental model '{}'? This cannot be undone.",
mental_model_id
);
let confirmed = ui::prompt_confirmation(&message)?;
@ -200,12 +200,12 @@ pub fn delete(
}
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Deleting reflection..."))
Some(ui::create_spinner("Deleting mental model..."))
} else {
None
};
let response = client.delete_reflection(bank_id, reflection_id, verbose);
let response = client.delete_mental_model(bank_id, mental_model_id, verbose);
if let Some(mut sp) = spinner {
sp.finish();
@ -214,7 +214,7 @@ pub fn delete(
match response {
Ok(_) => {
if output_format == OutputFormat::Pretty {
ui::print_success(&format!("Reflection '{}' deleted successfully", reflection_id));
ui::print_success(&format!("Mental model '{}' deleted successfully", mental_model_id));
} else {
println!("{{\"success\": true}}");
}
@ -224,21 +224,21 @@ pub fn delete(
}
}
/// Refresh a reflection
/// Refresh a mental model
pub fn refresh(
client: &ApiClient,
bank_id: &str,
reflection_id: &str,
mental_model_id: &str,
verbose: bool,
output_format: OutputFormat,
) -> Result<()> {
let spinner = if output_format == OutputFormat::Pretty {
Some(ui::create_spinner("Submitting reflection refresh..."))
Some(ui::create_spinner("Submitting mental model refresh..."))
} else {
None
};
let response = client.refresh_reflection(bank_id, reflection_id, verbose);
let response = client.refresh_mental_model(bank_id, mental_model_id, verbose);
if let Some(mut sp) = spinner {
sp.finish();
@ -248,7 +248,7 @@ pub fn refresh(
Ok(operation) => {
if output_format == OutputFormat::Pretty {
ui::print_success(&format!(
"Reflection refresh submitted. Operation ID: {}",
"Mental model refresh submitted. Operation ID: {}",
operation.operation_id
));
println!(" {} {}", ui::dim("Status:"), operation.status);
@ -263,16 +263,16 @@ pub fn refresh(
}
}
// Helper function to print reflection details
fn print_reflection_detail(reflection: &types::ReflectionResponse) {
ui::print_section_header(&reflection.name);
// Helper function to print mental model details
fn print_mental_model_detail(mental_model: &types::MentalModelResponse) {
ui::print_section_header(&mental_model.name);
println!(" {} {}", ui::dim("ID:"), ui::gradient_start(&reflection.id));
println!(" {} {}", ui::dim("Source Query:"), &reflection.source_query);
println!(" {} {}", ui::dim("ID:"), ui::gradient_start(&mental_model.id));
println!(" {} {}", ui::dim("Source Query:"), &mental_model.source_query);
println!();
println!("{}", ui::gradient_text("─── Content ───"));
println!();
println!("{}", &reflection.content);
println!("{}", &mental_model.content);
println!();
}

View file

@ -7,5 +7,5 @@ pub mod explore;
pub mod health;
pub mod memory;
pub mod operation;
pub mod reflection;
pub mod mental_model;
pub mod tag;

View file

@ -95,9 +95,9 @@ enum Commands {
#[command(subcommand)]
Operation(OperationCommands),
/// Manage reflections (user-curated summaries)
/// Manage mental models (user-curated summaries)
#[command(subcommand)]
Reflection(ReflectionCommands),
MentalModel(MentalModelCommands),
/// Manage directives (behavioral rules)
#[command(subcommand)]
@ -539,67 +539,67 @@ enum ChunkCommands {
}
#[derive(Subcommand)]
enum ReflectionCommands {
/// List reflections for a bank
enum MentalModelCommands {
/// List mental models for a bank
List {
/// Bank ID
bank_id: String,
},
/// Get a specific reflection
/// Get a specific mental model
Get {
/// Bank ID
bank_id: String,
/// Reflection ID
reflection_id: String,
/// Mental model ID
mental_model_id: String,
},
/// Create a new reflection
/// Create a new mental model
Create {
/// Bank ID
bank_id: String,
/// Reflection name
/// Mental model name
name: String,
/// Source query to generate the reflection from
/// Source query to generate the mental model from
source_query: String,
},
/// Update a reflection
/// Update a mental model
Update {
/// Bank ID
bank_id: String,
/// Reflection ID
reflection_id: String,
/// Mental model ID
mental_model_id: String,
/// New name
#[arg(long)]
name: Option<String>,
},
/// Delete a reflection
/// Delete a mental model
Delete {
/// Bank ID
bank_id: String,
/// Reflection ID
reflection_id: String,
/// Mental model ID
mental_model_id: String,
/// Skip confirmation prompt
#[arg(short = 'y', long)]
yes: bool,
},
/// Refresh a reflection (re-run the source query)
/// Refresh a mental model (re-run the source query)
Refresh {
/// Bank ID
bank_id: String,
/// Reflection ID
reflection_id: String,
/// Mental model ID
mental_model_id: String,
},
}
@ -817,25 +817,25 @@ fn run() -> Result<()> {
}
},
// Reflection commands
Commands::Reflection(ref_cmd) => match ref_cmd {
ReflectionCommands::List { bank_id } => {
commands::reflection::list(&client, &bank_id, verbose, output_format)
// Mental model commands
Commands::MentalModel(mm_cmd) => match mm_cmd {
MentalModelCommands::List { bank_id } => {
commands::mental_model::list(&client, &bank_id, verbose, output_format)
}
ReflectionCommands::Get { bank_id, reflection_id } => {
commands::reflection::get(&client, &bank_id, &reflection_id, verbose, output_format)
MentalModelCommands::Get { bank_id, mental_model_id } => {
commands::mental_model::get(&client, &bank_id, &mental_model_id, verbose, output_format)
}
ReflectionCommands::Create { bank_id, name, source_query } => {
commands::reflection::create(&client, &bank_id, &name, &source_query, verbose, output_format)
MentalModelCommands::Create { bank_id, name, source_query } => {
commands::mental_model::create(&client, &bank_id, &name, &source_query, verbose, output_format)
}
ReflectionCommands::Update { bank_id, reflection_id, name } => {
commands::reflection::update(&client, &bank_id, &reflection_id, name, verbose, output_format)
MentalModelCommands::Update { bank_id, mental_model_id, name } => {
commands::mental_model::update(&client, &bank_id, &mental_model_id, name, verbose, output_format)
}
ReflectionCommands::Delete { bank_id, reflection_id, yes } => {
commands::reflection::delete(&client, &bank_id, &reflection_id, yes, verbose, output_format)
MentalModelCommands::Delete { bank_id, mental_model_id, yes } => {
commands::mental_model::delete(&client, &bank_id, &mental_model_id, yes, verbose, output_format)
}
ReflectionCommands::Refresh { bank_id, reflection_id } => {
commands::reflection::refresh(&client, &bank_id, &reflection_id, verbose, output_format)
MentalModelCommands::Refresh { bank_id, mental_model_id } => {
commands::mental_model::refresh(&client, &bank_id, &mental_model_id, verbose, output_format)
}
},

View file

@ -5,9 +5,9 @@ hindsight_client_api/api/directives_api.py
hindsight_client_api/api/documents_api.py
hindsight_client_api/api/entities_api.py
hindsight_client_api/api/memory_api.py
hindsight_client_api/api/mental_models_api.py
hindsight_client_api/api/monitoring_api.py
hindsight_client_api/api/operations_api.py
hindsight_client_api/api/reflections_api.py
hindsight_client_api/api_client.py
hindsight_client_api/api_response.py
hindsight_client_api/configuration.py
@ -28,8 +28,8 @@ hindsight_client_api/models/chunk_response.py
hindsight_client_api/models/consolidation_response.py
hindsight_client_api/models/create_bank_request.py
hindsight_client_api/models/create_directive_request.py
hindsight_client_api/models/create_reflection_request.py
hindsight_client_api/models/create_reflection_response.py
hindsight_client_api/models/create_mental_model_request.py
hindsight_client_api/models/create_mental_model_response.py
hindsight_client_api/models/delete_document_response.py
hindsight_client_api/models/delete_response.py
hindsight_client_api/models/directive_list_response.py
@ -51,6 +51,8 @@ hindsight_client_api/models/list_documents_response.py
hindsight_client_api/models/list_memory_units_response.py
hindsight_client_api/models/list_tags_response.py
hindsight_client_api/models/memory_item.py
hindsight_client_api/models/mental_model_list_response.py
hindsight_client_api/models/mental_model_response.py
hindsight_client_api/models/operation_response.py
hindsight_client_api/models/operation_status_response.py
hindsight_client_api/models/operations_list_response.py
@ -61,13 +63,10 @@ hindsight_client_api/models/reflect_based_on.py
hindsight_client_api/models/reflect_fact.py
hindsight_client_api/models/reflect_include_options.py
hindsight_client_api/models/reflect_llm_call.py
hindsight_client_api/models/reflect_mental_model.py
hindsight_client_api/models/reflect_request.py
hindsight_client_api/models/reflect_response.py
hindsight_client_api/models/reflect_tool_call.py
hindsight_client_api/models/reflect_trace.py
hindsight_client_api/models/reflection_list_response.py
hindsight_client_api/models/reflection_response.py
hindsight_client_api/models/retain_request.py
hindsight_client_api/models/retain_response.py
hindsight_client_api/models/tag_item.py
@ -75,7 +74,7 @@ hindsight_client_api/models/token_usage.py
hindsight_client_api/models/tool_calls_include_options.py
hindsight_client_api/models/update_directive_request.py
hindsight_client_api/models/update_disposition_request.py
hindsight_client_api/models/update_reflection_request.py
hindsight_client_api/models/update_mental_model_request.py
hindsight_client_api/models/validation_error.py
hindsight_client_api/models/validation_error_loc_inner.py
hindsight_client_api/models/version_response.py

View file

@ -22,9 +22,9 @@ from hindsight_client_api.api.directives_api import DirectivesApi
from hindsight_client_api.api.documents_api import DocumentsApi
from hindsight_client_api.api.entities_api import EntitiesApi
from hindsight_client_api.api.memory_api import MemoryApi
from hindsight_client_api.api.mental_models_api import MentalModelsApi
from hindsight_client_api.api.monitoring_api import MonitoringApi
from hindsight_client_api.api.operations_api import OperationsApi
from hindsight_client_api.api.reflections_api import ReflectionsApi
# import ApiClient
from hindsight_client_api.api_response import ApiResponse
@ -53,8 +53,8 @@ from hindsight_client_api.models.chunk_response import ChunkResponse
from hindsight_client_api.models.consolidation_response import ConsolidationResponse
from hindsight_client_api.models.create_bank_request import CreateBankRequest
from hindsight_client_api.models.create_directive_request import CreateDirectiveRequest
from hindsight_client_api.models.create_reflection_request import CreateReflectionRequest
from hindsight_client_api.models.create_reflection_response import CreateReflectionResponse
from hindsight_client_api.models.create_mental_model_request import CreateMentalModelRequest
from hindsight_client_api.models.create_mental_model_response import CreateMentalModelResponse
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse
from hindsight_client_api.models.delete_response import DeleteResponse
from hindsight_client_api.models.directive_list_response import DirectiveListResponse
@ -76,6 +76,8 @@ from hindsight_client_api.models.list_documents_response import ListDocumentsRes
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse
from hindsight_client_api.models.list_tags_response import ListTagsResponse
from hindsight_client_api.models.memory_item import MemoryItem
from hindsight_client_api.models.mental_model_list_response import MentalModelListResponse
from hindsight_client_api.models.mental_model_response import MentalModelResponse
from hindsight_client_api.models.operation_response import OperationResponse
from hindsight_client_api.models.operation_status_response import OperationStatusResponse
from hindsight_client_api.models.operations_list_response import OperationsListResponse
@ -86,13 +88,10 @@ from hindsight_client_api.models.reflect_based_on import ReflectBasedOn
from hindsight_client_api.models.reflect_fact import ReflectFact
from hindsight_client_api.models.reflect_include_options import ReflectIncludeOptions
from hindsight_client_api.models.reflect_llm_call import ReflectLLMCall
from hindsight_client_api.models.reflect_mental_model import ReflectMentalModel
from hindsight_client_api.models.reflect_request import ReflectRequest
from hindsight_client_api.models.reflect_response import ReflectResponse
from hindsight_client_api.models.reflect_tool_call import ReflectToolCall
from hindsight_client_api.models.reflect_trace import ReflectTrace
from hindsight_client_api.models.reflection_list_response import ReflectionListResponse
from hindsight_client_api.models.reflection_response import ReflectionResponse
from hindsight_client_api.models.retain_request import RetainRequest
from hindsight_client_api.models.retain_response import RetainResponse
from hindsight_client_api.models.tag_item import TagItem
@ -100,7 +99,7 @@ from hindsight_client_api.models.token_usage import TokenUsage
from hindsight_client_api.models.tool_calls_include_options import ToolCallsIncludeOptions
from hindsight_client_api.models.update_directive_request import UpdateDirectiveRequest
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
from hindsight_client_api.models.update_reflection_request import UpdateReflectionRequest
from hindsight_client_api.models.update_mental_model_request import UpdateMentalModelRequest
from hindsight_client_api.models.validation_error import ValidationError
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner
from hindsight_client_api.models.version_response import VersionResponse

View file

@ -6,7 +6,7 @@ from hindsight_client_api.api.directives_api import DirectivesApi
from hindsight_client_api.api.documents_api import DocumentsApi
from hindsight_client_api.api.entities_api import EntitiesApi
from hindsight_client_api.api.memory_api import MemoryApi
from hindsight_client_api.api.mental_models_api import MentalModelsApi
from hindsight_client_api.api.monitoring_api import MonitoringApi
from hindsight_client_api.api.operations_api import OperationsApi
from hindsight_client_api.api.reflections_api import ReflectionsApi

View file

@ -356,7 +356,7 @@ class BanksApi:
@validate_call
async def clear_mental_models(
async def clear_observations(
self,
bank_id: StrictStr,
authorization: Optional[StrictStr] = None,
@ -373,9 +373,9 @@ class BanksApi:
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> DeleteResponse:
"""Clear all mental models
"""Clear all observations
Delete all mental models for a memory bank. This is useful for resetting the consolidated knowledge.
Delete all observations for a memory bank. This is useful for resetting the consolidated knowledge.
:param bank_id: (required)
:type bank_id: str
@ -403,7 +403,7 @@ class BanksApi:
:return: Returns the result object.
""" # noqa: E501
_param = self._clear_mental_models_serialize(
_param = self._clear_observations_serialize(
bank_id=bank_id,
authorization=authorization,
_request_auth=_request_auth,
@ -428,7 +428,7 @@ class BanksApi:
@validate_call
async def clear_mental_models_with_http_info(
async def clear_observations_with_http_info(
self,
bank_id: StrictStr,
authorization: Optional[StrictStr] = None,
@ -445,9 +445,9 @@ class BanksApi:
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> ApiResponse[DeleteResponse]:
"""Clear all mental models
"""Clear all observations
Delete all mental models for a memory bank. This is useful for resetting the consolidated knowledge.
Delete all observations for a memory bank. This is useful for resetting the consolidated knowledge.
:param bank_id: (required)
:type bank_id: str
@ -475,7 +475,7 @@ class BanksApi:
:return: Returns the result object.
""" # noqa: E501
_param = self._clear_mental_models_serialize(
_param = self._clear_observations_serialize(
bank_id=bank_id,
authorization=authorization,
_request_auth=_request_auth,
@ -500,7 +500,7 @@ class BanksApi:
@validate_call
async def clear_mental_models_without_preload_content(
async def clear_observations_without_preload_content(
self,
bank_id: StrictStr,
authorization: Optional[StrictStr] = None,
@ -517,9 +517,9 @@ class BanksApi:
_headers: Optional[Dict[StrictStr, Any]] = None,
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
) -> RESTResponseType:
"""Clear all mental models
"""Clear all observations
Delete all mental models for a memory bank. This is useful for resetting the consolidated knowledge.
Delete all observations for a memory bank. This is useful for resetting the consolidated knowledge.
:param bank_id: (required)
:type bank_id: str
@ -547,7 +547,7 @@ class BanksApi:
:return: Returns the result object.
""" # noqa: E501
_param = self._clear_mental_models_serialize(
_param = self._clear_observations_serialize(
bank_id=bank_id,
authorization=authorization,
_request_auth=_request_auth,
@ -567,7 +567,7 @@ class BanksApi:
return response_data.response
def _clear_mental_models_serialize(
def _clear_observations_serialize(
self,
bank_id,
authorization,
@ -617,7 +617,7 @@ class BanksApi:
return self.api_client.param_serialize(
method='DELETE',
resource_path='/v1/default/banks/{bank_id}/mental-models',
resource_path='/v1/default/banks/{bank_id}/observations',
path_params=_path_params,
query_params=_query_params,
header_params=_header_params,
@ -2056,7 +2056,7 @@ class BanksApi:
) -> ConsolidationResponse:
"""Trigger consolidation
Run memory consolidation to create/update mental models from recent memories.
Run memory consolidation to create/update observations from recent memories.
:param bank_id: (required)
:type bank_id: str
@ -2128,7 +2128,7 @@ class BanksApi:
) -> ApiResponse[ConsolidationResponse]:
"""Trigger consolidation
Run memory consolidation to create/update mental models from recent memories.
Run memory consolidation to create/update observations from recent memories.
:param bank_id: (required)
:type bank_id: str
@ -2200,7 +2200,7 @@ class BanksApi:
) -> RESTResponseType:
"""Trigger consolidation
Run memory consolidation to create/update mental models from recent memories.
Run memory consolidation to create/update observations from recent memories.
:param bank_id: (required)
:type bank_id: str

View file

@ -29,8 +29,8 @@ from hindsight_client_api.models.chunk_response import ChunkResponse
from hindsight_client_api.models.consolidation_response import ConsolidationResponse
from hindsight_client_api.models.create_bank_request import CreateBankRequest
from hindsight_client_api.models.create_directive_request import CreateDirectiveRequest
from hindsight_client_api.models.create_reflection_request import CreateReflectionRequest
from hindsight_client_api.models.create_reflection_response import CreateReflectionResponse
from hindsight_client_api.models.create_mental_model_request import CreateMentalModelRequest
from hindsight_client_api.models.create_mental_model_response import CreateMentalModelResponse
from hindsight_client_api.models.delete_document_response import DeleteDocumentResponse
from hindsight_client_api.models.delete_response import DeleteResponse
from hindsight_client_api.models.directive_list_response import DirectiveListResponse
@ -52,6 +52,8 @@ from hindsight_client_api.models.list_documents_response import ListDocumentsRes
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse
from hindsight_client_api.models.list_tags_response import ListTagsResponse
from hindsight_client_api.models.memory_item import MemoryItem
from hindsight_client_api.models.mental_model_list_response import MentalModelListResponse
from hindsight_client_api.models.mental_model_response import MentalModelResponse
from hindsight_client_api.models.operation_response import OperationResponse
from hindsight_client_api.models.operation_status_response import OperationStatusResponse
from hindsight_client_api.models.operations_list_response import OperationsListResponse
@ -62,13 +64,10 @@ from hindsight_client_api.models.reflect_based_on import ReflectBasedOn
from hindsight_client_api.models.reflect_fact import ReflectFact
from hindsight_client_api.models.reflect_include_options import ReflectIncludeOptions
from hindsight_client_api.models.reflect_llm_call import ReflectLLMCall
from hindsight_client_api.models.reflect_mental_model import ReflectMentalModel
from hindsight_client_api.models.reflect_request import ReflectRequest
from hindsight_client_api.models.reflect_response import ReflectResponse
from hindsight_client_api.models.reflect_tool_call import ReflectToolCall
from hindsight_client_api.models.reflect_trace import ReflectTrace
from hindsight_client_api.models.reflection_list_response import ReflectionListResponse
from hindsight_client_api.models.reflection_response import ReflectionResponse
from hindsight_client_api.models.retain_request import RetainRequest
from hindsight_client_api.models.retain_response import RetainResponse
from hindsight_client_api.models.tag_item import TagItem
@ -76,7 +75,7 @@ from hindsight_client_api.models.token_usage import TokenUsage
from hindsight_client_api.models.tool_calls_include_options import ToolCallsIncludeOptions
from hindsight_client_api.models.update_directive_request import UpdateDirectiveRequest
from hindsight_client_api.models.update_disposition_request import UpdateDispositionRequest
from hindsight_client_api.models.update_reflection_request import UpdateReflectionRequest
from hindsight_client_api.models.update_mental_model_request import UpdateMentalModelRequest
from hindsight_client_api.models.validation_error import ValidationError
from hindsight_client_api.models.validation_error_loc_inner import ValidationErrorLocInner
from hindsight_client_api.models.version_response import VersionResponse

View file

@ -37,9 +37,9 @@ class BankStatsResponse(BaseModel):
pending_operations: StrictInt
failed_operations: StrictInt
last_consolidated_at: Optional[StrictStr] = None
pending_consolidation: Optional[StrictInt] = Field(default=0, description="Number of memories not yet processed into mental models")
total_mental_models: Optional[StrictInt] = Field(default=0, description="Total number of mental models")
__properties: ClassVar[List[str]] = ["bank_id", "total_nodes", "total_links", "total_documents", "nodes_by_fact_type", "links_by_link_type", "links_by_fact_type", "links_breakdown", "pending_operations", "failed_operations", "last_consolidated_at", "pending_consolidation", "total_mental_models"]
pending_consolidation: Optional[StrictInt] = Field(default=0, description="Number of memories not yet processed into observations")
total_observations: Optional[StrictInt] = Field(default=0, description="Total number of observations")
__properties: ClassVar[List[str]] = ["bank_id", "total_nodes", "total_links", "total_documents", "nodes_by_fact_type", "links_by_link_type", "links_by_fact_type", "links_breakdown", "pending_operations", "failed_operations", "last_consolidated_at", "pending_consolidation", "total_observations"]
model_config = ConfigDict(
populate_by_name=True,
@ -109,7 +109,7 @@ class BankStatsResponse(BaseModel):
"failed_operations": obj.get("failed_operations"),
"last_consolidated_at": obj.get("last_consolidated_at"),
"pending_consolidation": obj.get("pending_consolidation") if obj.get("pending_consolidation") is not None else 0,
"total_mental_models": obj.get("total_mental_models") if obj.get("total_mental_models") is not None else 0
"total_observations": obj.get("total_observations") if obj.get("total_observations") is not None else 0
})
return _obj

View file

@ -23,11 +23,11 @@ from typing_extensions import Annotated
from typing import Optional, Set
from typing_extensions import Self
class CreateReflectionRequest(BaseModel):
class CreateMentalModelRequest(BaseModel):
"""
Request model for creating a reflection.
Request model for creating a mental model.
""" # noqa: E501
name: StrictStr = Field(description="Human-readable name for the reflection")
name: StrictStr = Field(description="Human-readable name for the mental model")
source_query: StrictStr = Field(description="The query to run to generate content")
tags: Optional[List[StrictStr]] = Field(default=None, description="Tags for scoped visibility")
max_tokens: Optional[Annotated[int, Field(le=8192, strict=True, ge=256)]] = Field(default=2048, description="Maximum tokens for generated content")
@ -51,7 +51,7 @@ class CreateReflectionRequest(BaseModel):
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of CreateReflectionRequest from a JSON string"""
"""Create an instance of CreateMentalModelRequest from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
@ -76,7 +76,7 @@ class CreateReflectionRequest(BaseModel):
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of CreateReflectionRequest from a dict"""
"""Create an instance of CreateMentalModelRequest from a dict"""
if obj is None:
return None

View file

@ -22,9 +22,9 @@ from typing import Any, ClassVar, Dict, List
from typing import Optional, Set
from typing_extensions import Self
class CreateReflectionResponse(BaseModel):
class CreateMentalModelResponse(BaseModel):
"""
Response model for reflection creation.
Response model for mental model creation.
""" # noqa: E501
operation_id: StrictStr = Field(description="Operation ID to track progress")
__properties: ClassVar[List[str]] = ["operation_id"]
@ -47,7 +47,7 @@ class CreateReflectionResponse(BaseModel):
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of CreateReflectionResponse from a JSON string"""
"""Create an instance of CreateMentalModelResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
@ -72,7 +72,7 @@ class CreateReflectionResponse(BaseModel):
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of CreateReflectionResponse from a dict"""
"""Create an instance of CreateMentalModelResponse from a dict"""
if obj is None:
return None

View file

@ -26,10 +26,10 @@ class FeaturesInfo(BaseModel):
"""
Feature flags indicating which capabilities are enabled.
""" # noqa: E501
mental_models: StrictBool = Field(description="Whether mental models (auto-consolidation) are enabled")
observations: StrictBool = Field(description="Whether observations (auto-consolidation) are enabled")
mcp: StrictBool = Field(description="Whether MCP (Model Context Protocol) server is enabled")
worker: StrictBool = Field(description="Whether the background worker is enabled")
__properties: ClassVar[List[str]] = ["mental_models", "mcp", "worker"]
__properties: ClassVar[List[str]] = ["observations", "mcp", "worker"]
model_config = ConfigDict(
populate_by_name=True,
@ -82,7 +82,7 @@ class FeaturesInfo(BaseModel):
return cls.model_validate(obj)
_obj = cls.model_validate({
"mental_models": obj.get("mental_models"),
"observations": obj.get("observations"),
"mcp": obj.get("mcp"),
"worker": obj.get("worker")
})

View file

@ -19,15 +19,15 @@ import json
from pydantic import BaseModel, ConfigDict
from typing import Any, ClassVar, Dict, List
from hindsight_client_api.models.reflection_response import ReflectionResponse
from hindsight_client_api.models.mental_model_response import MentalModelResponse
from typing import Optional, Set
from typing_extensions import Self
class ReflectionListResponse(BaseModel):
class MentalModelListResponse(BaseModel):
"""
Response model for listing reflections.
Response model for listing mental models.
""" # noqa: E501
items: List[ReflectionResponse]
items: List[MentalModelResponse]
__properties: ClassVar[List[str]] = ["items"]
model_config = ConfigDict(
@ -48,7 +48,7 @@ class ReflectionListResponse(BaseModel):
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of ReflectionListResponse from a JSON string"""
"""Create an instance of MentalModelListResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
@ -80,7 +80,7 @@ class ReflectionListResponse(BaseModel):
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of ReflectionListResponse from a dict"""
"""Create an instance of MentalModelListResponse from a dict"""
if obj is None:
return None
@ -88,7 +88,7 @@ class ReflectionListResponse(BaseModel):
return cls.model_validate(obj)
_obj = cls.model_validate({
"items": [ReflectionResponse.from_dict(_item) for _item in obj["items"]] if obj.get("items") is not None else None
"items": [MentalModelResponse.from_dict(_item) for _item in obj["items"]] if obj.get("items") is not None else None
})
return _obj

View file

@ -22,9 +22,9 @@ from typing import Any, ClassVar, Dict, List, Optional
from typing import Optional, Set
from typing_extensions import Self
class ReflectionResponse(BaseModel):
class MentalModelResponse(BaseModel):
"""
Response model for a reflection.
Response model for a mental model (stored reflect response).
""" # noqa: E501
id: StrictStr
bank_id: StrictStr
@ -55,7 +55,7 @@ class ReflectionResponse(BaseModel):
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of ReflectionResponse from a JSON string"""
"""Create an instance of MentalModelResponse from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
@ -95,7 +95,7 @@ class ReflectionResponse(BaseModel):
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of ReflectionResponse from a dict"""
"""Create an instance of MentalModelResponse from a dict"""
if obj is None:
return None

View file

@ -1,100 +0,0 @@
# coding: utf-8
"""
Hindsight HTTP API
HTTP API for Hindsight
The version of the OpenAPI document: 0.1.0
Generated by OpenAPI Generator (https://openapi-generator.tech)
Do not edit the class manually.
""" # noqa: E501
from __future__ import annotations
import pprint
import re # noqa: F401
import json
from pydantic import BaseModel, ConfigDict, Field, StrictStr
from typing import Any, ClassVar, Dict, List, Optional
from typing import Optional, Set
from typing_extensions import Self
class ReflectMentalModel(BaseModel):
"""
A mental model accessed during reflect.
""" # noqa: E501
id: StrictStr = Field(description="Mental model ID")
name: StrictStr = Field(description="Mental model name")
type: StrictStr = Field(description="Mental model type: entity, concept, event")
subtype: StrictStr = Field(description="Mental model subtype: structural, emergent, learned, directive")
observations: Optional[List[StrictStr]] = None
__properties: ClassVar[List[str]] = ["id", "name", "type", "subtype", "observations"]
model_config = ConfigDict(
populate_by_name=True,
validate_assignment=True,
protected_namespaces=(),
)
def to_str(self) -> str:
"""Returns the string representation of the model using alias"""
return pprint.pformat(self.model_dump(by_alias=True))
def to_json(self) -> str:
"""Returns the JSON representation of the model using alias"""
# TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead
return json.dumps(self.to_dict())
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of ReflectMentalModel from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
"""Return the dictionary representation of the model using alias.
This has the following differences from calling pydantic's
`self.model_dump(by_alias=True)`:
* `None` is only added to the output dict for nullable fields that
were set at model initialization. Other fields with value `None`
are ignored.
"""
excluded_fields: Set[str] = set([
])
_dict = self.model_dump(
by_alias=True,
exclude=excluded_fields,
exclude_none=True,
)
# set to None if observations (nullable) is None
# and model_fields_set contains the field
if self.observations is None and "observations" in self.model_fields_set:
_dict['observations'] = None
return _dict
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of ReflectMentalModel from a dict"""
if obj is None:
return None
if not isinstance(obj, dict):
return cls.model_validate(obj)
_obj = cls.model_validate({
"id": obj.get("id"),
"name": obj.get("name"),
"type": obj.get("type"),
"subtype": obj.get("subtype"),
"observations": obj.get("observations")
})
return _obj

View file

@ -20,7 +20,6 @@ import json
from pydantic import BaseModel, ConfigDict, Field
from typing import Any, ClassVar, Dict, List, Optional
from hindsight_client_api.models.reflect_llm_call import ReflectLLMCall
from hindsight_client_api.models.reflect_mental_model import ReflectMentalModel
from hindsight_client_api.models.reflect_tool_call import ReflectToolCall
from typing import Optional, Set
from typing_extensions import Self
@ -31,8 +30,7 @@ class ReflectTrace(BaseModel):
""" # noqa: E501
tool_calls: Optional[List[ReflectToolCall]] = Field(default=None, description="Tool calls made during reflection")
llm_calls: Optional[List[ReflectLLMCall]] = Field(default=None, description="LLM calls made during reflection")
mental_models: Optional[List[ReflectMentalModel]] = Field(default=None, description="Mental models used during reflection (includes directives with subtype='directive')")
__properties: ClassVar[List[str]] = ["tool_calls", "llm_calls", "mental_models"]
__properties: ClassVar[List[str]] = ["tool_calls", "llm_calls"]
model_config = ConfigDict(
populate_by_name=True,
@ -87,13 +85,6 @@ class ReflectTrace(BaseModel):
if _item_llm_calls:
_items.append(_item_llm_calls.to_dict())
_dict['llm_calls'] = _items
# override the default output from pydantic by calling `to_dict()` of each item in mental_models (list)
_items = []
if self.mental_models:
for _item_mental_models in self.mental_models:
if _item_mental_models:
_items.append(_item_mental_models.to_dict())
_dict['mental_models'] = _items
return _dict
@classmethod
@ -107,8 +98,7 @@ class ReflectTrace(BaseModel):
_obj = cls.model_validate({
"tool_calls": [ReflectToolCall.from_dict(_item) for _item in obj["tool_calls"]] if obj.get("tool_calls") is not None else None,
"llm_calls": [ReflectLLMCall.from_dict(_item) for _item in obj["llm_calls"]] if obj.get("llm_calls") is not None else None,
"mental_models": [ReflectMentalModel.from_dict(_item) for _item in obj["mental_models"]] if obj.get("mental_models") is not None else None
"llm_calls": [ReflectLLMCall.from_dict(_item) for _item in obj["llm_calls"]] if obj.get("llm_calls") is not None else None
})
return _obj

View file

@ -22,9 +22,9 @@ from typing import Any, ClassVar, Dict, List, Optional
from typing import Optional, Set
from typing_extensions import Self
class UpdateReflectionRequest(BaseModel):
class UpdateMentalModelRequest(BaseModel):
"""
Request model for updating a reflection.
Request model for updating a mental model.
""" # noqa: E501
name: Optional[StrictStr] = None
__properties: ClassVar[List[str]] = ["name"]
@ -47,7 +47,7 @@ class UpdateReflectionRequest(BaseModel):
@classmethod
def from_json(cls, json_str: str) -> Optional[Self]:
"""Create an instance of UpdateReflectionRequest from a JSON string"""
"""Create an instance of UpdateMentalModelRequest from a JSON string"""
return cls.from_dict(json.loads(json_str))
def to_dict(self) -> Dict[str, Any]:
@ -77,7 +77,7 @@ class UpdateReflectionRequest(BaseModel):
@classmethod
def from_dict(cls, obj: Optional[Dict[str, Any]]) -> Optional[Self]:
"""Create an instance of UpdateReflectionRequest from a dict"""
"""Create an instance of UpdateMentalModelRequest from a dict"""
if obj is None:
return None

View file

@ -12,18 +12,18 @@ import type {
ClearBankMemoriesData,
ClearBankMemoriesErrors,
ClearBankMemoriesResponses,
ClearMentalModelsData,
ClearMentalModelsErrors,
ClearMentalModelsResponses,
ClearObservationsData,
ClearObservationsErrors,
ClearObservationsResponses,
CreateDirectiveData,
CreateDirectiveErrors,
CreateDirectiveResponses,
CreateMentalModelData,
CreateMentalModelErrors,
CreateMentalModelResponses,
CreateOrUpdateBankData,
CreateOrUpdateBankErrors,
CreateOrUpdateBankResponses,
CreateReflectionData,
CreateReflectionErrors,
CreateReflectionResponses,
DeleteBankData,
DeleteBankErrors,
DeleteBankResponses,
@ -33,9 +33,9 @@ import type {
DeleteDocumentData,
DeleteDocumentErrors,
DeleteDocumentResponses,
DeleteReflectionData,
DeleteReflectionErrors,
DeleteReflectionResponses,
DeleteMentalModelData,
DeleteMentalModelErrors,
DeleteMentalModelResponses,
GetAgentStatsData,
GetAgentStatsErrors,
GetAgentStatsResponses,
@ -60,12 +60,12 @@ import type {
GetMemoryData,
GetMemoryErrors,
GetMemoryResponses,
GetMentalModelData,
GetMentalModelErrors,
GetMentalModelResponses,
GetOperationStatusData,
GetOperationStatusErrors,
GetOperationStatusResponses,
GetReflectionData,
GetReflectionErrors,
GetReflectionResponses,
GetVersionData,
GetVersionResponses,
HealthEndpointHealthGetData,
@ -85,12 +85,12 @@ import type {
ListMemoriesData,
ListMemoriesErrors,
ListMemoriesResponses,
ListMentalModelsData,
ListMentalModelsErrors,
ListMentalModelsResponses,
ListOperationsData,
ListOperationsErrors,
ListOperationsResponses,
ListReflectionsData,
ListReflectionsErrors,
ListReflectionsResponses,
ListTagsData,
ListTagsErrors,
ListTagsResponses,
@ -102,9 +102,9 @@ import type {
ReflectData,
ReflectErrors,
ReflectResponses,
RefreshReflectionData,
RefreshReflectionErrors,
RefreshReflectionResponses,
RefreshMentalModelData,
RefreshMentalModelErrors,
RefreshMentalModelResponses,
RegenerateEntityObservationsData,
RegenerateEntityObservationsErrors,
RegenerateEntityObservationsResponses,
@ -123,9 +123,9 @@ import type {
UpdateDirectiveData,
UpdateDirectiveErrors,
UpdateDirectiveResponses,
UpdateReflectionData,
UpdateReflectionErrors,
UpdateReflectionResponses,
UpdateMentalModelData,
UpdateMentalModelErrors,
UpdateMentalModelResponses,
} from "./types.gen";
export type Options<
@ -363,33 +363,33 @@ export const regenerateEntityObservations = <
});
/**
* List reflections
* List mental models
*
* List user-curated living documents that stay current.
*/
export const listReflections = <ThrowOnError extends boolean = false>(
options: Options<ListReflectionsData, ThrowOnError>,
export const listMentalModels = <ThrowOnError extends boolean = false>(
options: Options<ListMentalModelsData, ThrowOnError>,
) =>
(options.client ?? client).get<
ListReflectionsResponses,
ListReflectionsErrors,
ListMentalModelsResponses,
ListMentalModelsErrors,
ThrowOnError
>({ url: "/v1/default/banks/{bank_id}/reflections", ...options });
>({ url: "/v1/default/banks/{bank_id}/mental-models", ...options });
/**
* Create reflection
* Create mental model
*
* Create a reflection by running reflect with the source query in the background. Returns an operation ID to track progress. The content is auto-generated by the reflect endpoint. Use the operations endpoint to check completion status.
* Create a mental model by running reflect with the source query in the background. Returns an operation ID to track progress. The content is auto-generated by the reflect endpoint. Use the operations endpoint to check completion status.
*/
export const createReflection = <ThrowOnError extends boolean = false>(
options: Options<CreateReflectionData, ThrowOnError>,
export const createMentalModel = <ThrowOnError extends boolean = false>(
options: Options<CreateMentalModelData, ThrowOnError>,
) =>
(options.client ?? client).post<
CreateReflectionResponses,
CreateReflectionErrors,
CreateMentalModelResponses,
CreateMentalModelErrors,
ThrowOnError
>({
url: "/v1/default/banks/{bank_id}/reflections",
url: "/v1/default/banks/{bank_id}/mental-models",
...options,
headers: {
"Content-Type": "application/json",
@ -398,53 +398,53 @@ export const createReflection = <ThrowOnError extends boolean = false>(
});
/**
* Delete reflection
* Delete mental model
*
* Delete a reflection.
* Delete a mental model.
*/
export const deleteReflection = <ThrowOnError extends boolean = false>(
options: Options<DeleteReflectionData, ThrowOnError>,
export const deleteMentalModel = <ThrowOnError extends boolean = false>(
options: Options<DeleteMentalModelData, ThrowOnError>,
) =>
(options.client ?? client).delete<
DeleteReflectionResponses,
DeleteReflectionErrors,
DeleteMentalModelResponses,
DeleteMentalModelErrors,
ThrowOnError
>({
url: "/v1/default/banks/{bank_id}/reflections/{reflection_id}",
url: "/v1/default/banks/{bank_id}/mental-models/{mental_model_id}",
...options,
});
/**
* Get reflection
* Get mental model
*
* Get a specific reflection by ID.
* Get a specific mental model by ID.
*/
export const getReflection = <ThrowOnError extends boolean = false>(
options: Options<GetReflectionData, ThrowOnError>,
export const getMentalModel = <ThrowOnError extends boolean = false>(
options: Options<GetMentalModelData, ThrowOnError>,
) =>
(options.client ?? client).get<
GetReflectionResponses,
GetReflectionErrors,
GetMentalModelResponses,
GetMentalModelErrors,
ThrowOnError
>({
url: "/v1/default/banks/{bank_id}/reflections/{reflection_id}",
url: "/v1/default/banks/{bank_id}/mental-models/{mental_model_id}",
...options,
});
/**
* Update reflection
* Update mental model
*
* Update a reflection's name.
* Update a mental model's name.
*/
export const updateReflection = <ThrowOnError extends boolean = false>(
options: Options<UpdateReflectionData, ThrowOnError>,
export const updateMentalModel = <ThrowOnError extends boolean = false>(
options: Options<UpdateMentalModelData, ThrowOnError>,
) =>
(options.client ?? client).patch<
UpdateReflectionResponses,
UpdateReflectionErrors,
UpdateMentalModelResponses,
UpdateMentalModelErrors,
ThrowOnError
>({
url: "/v1/default/banks/{bank_id}/reflections/{reflection_id}",
url: "/v1/default/banks/{bank_id}/mental-models/{mental_model_id}",
...options,
headers: {
"Content-Type": "application/json",
@ -453,19 +453,19 @@ export const updateReflection = <ThrowOnError extends boolean = false>(
});
/**
* Refresh reflection
* Refresh mental model
*
* Submit an async task to re-run the source query through reflect and update the content.
*/
export const refreshReflection = <ThrowOnError extends boolean = false>(
options: Options<RefreshReflectionData, ThrowOnError>,
export const refreshMentalModel = <ThrowOnError extends boolean = false>(
options: Options<RefreshMentalModelData, ThrowOnError>,
) =>
(options.client ?? client).post<
RefreshReflectionResponses,
RefreshReflectionErrors,
RefreshMentalModelResponses,
RefreshMentalModelErrors,
ThrowOnError
>({
url: "/v1/default/banks/{bank_id}/reflections/{reflection_id}/refresh",
url: "/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/refresh",
...options,
});
@ -799,23 +799,23 @@ export const createOrUpdateBank = <ThrowOnError extends boolean = false>(
});
/**
* Clear all mental models
* Clear all observations
*
* Delete all mental models for a memory bank. This is useful for resetting the consolidated knowledge.
* Delete all observations for a memory bank. This is useful for resetting the consolidated knowledge.
*/
export const clearMentalModels = <ThrowOnError extends boolean = false>(
options: Options<ClearMentalModelsData, ThrowOnError>,
export const clearObservations = <ThrowOnError extends boolean = false>(
options: Options<ClearObservationsData, ThrowOnError>,
) =>
(options.client ?? client).delete<
ClearMentalModelsResponses,
ClearMentalModelsErrors,
ClearObservationsResponses,
ClearObservationsErrors,
ThrowOnError
>({ url: "/v1/default/banks/{bank_id}/mental-models", ...options });
>({ url: "/v1/default/banks/{bank_id}/observations", ...options });
/**
* Trigger consolidation
*
* Run memory consolidation to create/update mental models from recent memories.
* Run memory consolidation to create/update observations from recent memories.
*/
export const triggerConsolidation = <ThrowOnError extends boolean = false>(
options: Options<TriggerConsolidationData, ThrowOnError>,

View file

@ -194,15 +194,15 @@ export type BankStatsResponse = {
/**
* Pending Consolidation
*
* Number of memories not yet processed into mental models
* Number of memories not yet processed into observations
*/
pending_consolidation?: number;
/**
* Total Mental Models
* Total Observations
*
* Total number of mental models
* Total number of observations
*/
total_mental_models?: number;
total_observations?: number;
};
/**
@ -388,15 +388,15 @@ export type CreateDirectiveRequest = {
};
/**
* CreateReflectionRequest
* CreateMentalModelRequest
*
* Request model for creating a reflection.
* Request model for creating a mental model.
*/
export type CreateReflectionRequest = {
export type CreateMentalModelRequest = {
/**
* Name
*
* Human-readable name for the reflection
* Human-readable name for the mental model
*/
name: string;
/**
@ -420,11 +420,11 @@ export type CreateReflectionRequest = {
};
/**
* CreateReflectionResponse
* CreateMentalModelResponse
*
* Response model for reflection creation.
* Response model for mental model creation.
*/
export type CreateReflectionResponse = {
export type CreateMentalModelResponse = {
/**
* Operation Id
*
@ -783,11 +783,11 @@ export type FactsIncludeOptions = {
*/
export type FeaturesInfo = {
/**
* Mental Models
* Observations
*
* Whether mental models (auto-consolidation) are enabled
* Whether observations (auto-consolidation) are enabled
*/
mental_models: boolean;
observations: boolean;
/**
* Mcp
*
@ -982,6 +982,66 @@ export type MemoryItem = {
tags?: Array<string> | null;
};
/**
* MentalModelListResponse
*
* Response model for listing mental models.
*/
export type MentalModelListResponse = {
/**
* Items
*/
items: Array<MentalModelResponse>;
};
/**
* MentalModelResponse
*
* Response model for a mental model (stored reflect response).
*/
export type MentalModelResponse = {
/**
* Id
*/
id: string;
/**
* Bank Id
*/
bank_id: string;
/**
* Name
*/
name: string;
/**
* Source Query
*/
source_query: string;
/**
* Content
*/
content: string;
/**
* Tags
*/
tags?: Array<string>;
/**
* Last Refreshed At
*/
last_refreshed_at?: string | null;
/**
* Created At
*/
created_at?: string | null;
/**
* Reflect Response
*
* Full reflect API response payload including based_on facts and observations
*/
reflect_response?: {
[key: string]: unknown;
} | null;
};
/**
* OperationResponse
*
@ -1095,7 +1155,7 @@ export type RecallRequest = {
/**
* Types
*
* List of fact types to recall: 'world', 'experience', 'mental_model'. Defaults to world and experience if not specified. Note: 'opinion' is accepted but ignored (opinions are excluded from recall).
* List of fact types to recall: 'world', 'experience', 'observation'. Defaults to world and experience if not specified. Note: 'opinion' is accepted but ignored (opinions are excluded from recall).
*/
types?: Array<string> | null;
budget?: Budget;
@ -1305,44 +1365,6 @@ export type ReflectLlmCall = {
duration_ms: number;
};
/**
* ReflectMentalModel
*
* A mental model accessed during reflect.
*/
export type ReflectMentalModel = {
/**
* Id
*
* Mental model ID
*/
id: string;
/**
* Name
*
* Mental model name
*/
name: string;
/**
* Type
*
* Mental model type: entity, concept, event
*/
type: string;
/**
* Subtype
*
* Mental model subtype: structural, emergent, learned, directive
*/
subtype: string;
/**
* Observations
*
* Observations for directive mental models (subtype='directive')
*/
observations?: Array<string> | null;
};
/**
* ReflectRequest
*
@ -1486,72 +1508,6 @@ export type ReflectTrace = {
* LLM calls made during reflection
*/
llm_calls?: Array<ReflectLlmCall>;
/**
* Mental Models
*
* Mental models used during reflection (includes directives with subtype='directive')
*/
mental_models?: Array<ReflectMentalModel>;
};
/**
* ReflectionListResponse
*
* Response model for listing reflections.
*/
export type ReflectionListResponse = {
/**
* Items
*/
items: Array<ReflectionResponse>;
};
/**
* ReflectionResponse
*
* Response model for a reflection.
*/
export type ReflectionResponse = {
/**
* Id
*/
id: string;
/**
* Bank Id
*/
bank_id: string;
/**
* Name
*/
name: string;
/**
* Source Query
*/
source_query: string;
/**
* Content
*/
content: string;
/**
* Tags
*/
tags?: Array<string>;
/**
* Last Refreshed At
*/
last_refreshed_at?: string | null;
/**
* Created At
*/
created_at?: string | null;
/**
* Reflect Response
*
* Full reflect API response payload including based_on facts and mental_models
*/
reflect_response?: {
[key: string]: unknown;
} | null;
};
/**
@ -1725,15 +1681,15 @@ export type UpdateDispositionRequest = {
};
/**
* UpdateReflectionRequest
* UpdateMentalModelRequest
*
* Request model for updating a reflection.
* Request model for updating a mental model.
*/
export type UpdateReflectionRequest = {
export type UpdateMentalModelRequest = {
/**
* Name
*
* New name for the reflection
* New name for the mental model
*/
name?: string | null;
};
@ -2229,7 +2185,7 @@ export type RegenerateEntityObservationsResponses = {
export type RegenerateEntityObservationsResponse =
RegenerateEntityObservationsResponses[keyof RegenerateEntityObservationsResponses];
export type ListReflectionsData = {
export type ListMentalModelsData = {
body?: never;
headers?: {
/**
@ -2265,31 +2221,31 @@ export type ListReflectionsData = {
*/
offset?: number;
};
url: "/v1/default/banks/{bank_id}/reflections";
url: "/v1/default/banks/{bank_id}/mental-models";
};
export type ListReflectionsErrors = {
export type ListMentalModelsErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type ListReflectionsError =
ListReflectionsErrors[keyof ListReflectionsErrors];
export type ListMentalModelsError =
ListMentalModelsErrors[keyof ListMentalModelsErrors];
export type ListReflectionsResponses = {
export type ListMentalModelsResponses = {
/**
* Successful Response
*/
200: ReflectionListResponse;
200: MentalModelListResponse;
};
export type ListReflectionsResponse =
ListReflectionsResponses[keyof ListReflectionsResponses];
export type ListMentalModelsResponse =
ListMentalModelsResponses[keyof ListMentalModelsResponses];
export type CreateReflectionData = {
body: CreateReflectionRequest;
export type CreateMentalModelData = {
body: CreateMentalModelRequest;
headers?: {
/**
* Authorization
@ -2303,30 +2259,30 @@ export type CreateReflectionData = {
bank_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/reflections";
url: "/v1/default/banks/{bank_id}/mental-models";
};
export type CreateReflectionErrors = {
export type CreateMentalModelErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type CreateReflectionError =
CreateReflectionErrors[keyof CreateReflectionErrors];
export type CreateMentalModelError =
CreateMentalModelErrors[keyof CreateMentalModelErrors];
export type CreateReflectionResponses = {
export type CreateMentalModelResponses = {
/**
* Successful Response
*/
200: CreateReflectionResponse;
200: CreateMentalModelResponse;
};
export type CreateReflectionResponse2 =
CreateReflectionResponses[keyof CreateReflectionResponses];
export type CreateMentalModelResponse2 =
CreateMentalModelResponses[keyof CreateMentalModelResponses];
export type DeleteReflectionData = {
export type DeleteMentalModelData = {
body?: never;
headers?: {
/**
@ -2340,32 +2296,32 @@ export type DeleteReflectionData = {
*/
bank_id: string;
/**
* Reflection Id
* Mental Model Id
*/
reflection_id: string;
mental_model_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/reflections/{reflection_id}";
url: "/v1/default/banks/{bank_id}/mental-models/{mental_model_id}";
};
export type DeleteReflectionErrors = {
export type DeleteMentalModelErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type DeleteReflectionError =
DeleteReflectionErrors[keyof DeleteReflectionErrors];
export type DeleteMentalModelError =
DeleteMentalModelErrors[keyof DeleteMentalModelErrors];
export type DeleteReflectionResponses = {
export type DeleteMentalModelResponses = {
/**
* Successful Response
*/
200: unknown;
};
export type GetReflectionData = {
export type GetMentalModelData = {
body?: never;
headers?: {
/**
@ -2379,35 +2335,36 @@ export type GetReflectionData = {
*/
bank_id: string;
/**
* Reflection Id
* Mental Model Id
*/
reflection_id: string;
mental_model_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/reflections/{reflection_id}";
url: "/v1/default/banks/{bank_id}/mental-models/{mental_model_id}";
};
export type GetReflectionErrors = {
export type GetMentalModelErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type GetReflectionError = GetReflectionErrors[keyof GetReflectionErrors];
export type GetMentalModelError =
GetMentalModelErrors[keyof GetMentalModelErrors];
export type GetReflectionResponses = {
export type GetMentalModelResponses = {
/**
* Successful Response
*/
200: ReflectionResponse;
200: MentalModelResponse;
};
export type GetReflectionResponse =
GetReflectionResponses[keyof GetReflectionResponses];
export type GetMentalModelResponse =
GetMentalModelResponses[keyof GetMentalModelResponses];
export type UpdateReflectionData = {
body: UpdateReflectionRequest;
export type UpdateMentalModelData = {
body: UpdateMentalModelRequest;
headers?: {
/**
* Authorization
@ -2420,35 +2377,35 @@ export type UpdateReflectionData = {
*/
bank_id: string;
/**
* Reflection Id
* Mental Model Id
*/
reflection_id: string;
mental_model_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/reflections/{reflection_id}";
url: "/v1/default/banks/{bank_id}/mental-models/{mental_model_id}";
};
export type UpdateReflectionErrors = {
export type UpdateMentalModelErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type UpdateReflectionError =
UpdateReflectionErrors[keyof UpdateReflectionErrors];
export type UpdateMentalModelError =
UpdateMentalModelErrors[keyof UpdateMentalModelErrors];
export type UpdateReflectionResponses = {
export type UpdateMentalModelResponses = {
/**
* Successful Response
*/
200: ReflectionResponse;
200: MentalModelResponse;
};
export type UpdateReflectionResponse =
UpdateReflectionResponses[keyof UpdateReflectionResponses];
export type UpdateMentalModelResponse =
UpdateMentalModelResponses[keyof UpdateMentalModelResponses];
export type RefreshReflectionData = {
export type RefreshMentalModelData = {
body?: never;
headers?: {
/**
@ -2462,33 +2419,33 @@ export type RefreshReflectionData = {
*/
bank_id: string;
/**
* Reflection Id
* Mental Model Id
*/
reflection_id: string;
mental_model_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/reflections/{reflection_id}/refresh";
url: "/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/refresh";
};
export type RefreshReflectionErrors = {
export type RefreshMentalModelErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type RefreshReflectionError =
RefreshReflectionErrors[keyof RefreshReflectionErrors];
export type RefreshMentalModelError =
RefreshMentalModelErrors[keyof RefreshMentalModelErrors];
export type RefreshReflectionResponses = {
export type RefreshMentalModelResponses = {
/**
* Successful Response
*/
200: AsyncOperationSubmitResponse;
};
export type RefreshReflectionResponse =
RefreshReflectionResponses[keyof RefreshReflectionResponses];
export type RefreshMentalModelResponse =
RefreshMentalModelResponses[keyof RefreshMentalModelResponses];
export type ListDirectivesData = {
body?: never;
@ -3304,7 +3261,7 @@ export type CreateOrUpdateBankResponses = {
export type CreateOrUpdateBankResponse =
CreateOrUpdateBankResponses[keyof CreateOrUpdateBankResponses];
export type ClearMentalModelsData = {
export type ClearObservationsData = {
body?: never;
headers?: {
/**
@ -3319,28 +3276,28 @@ export type ClearMentalModelsData = {
bank_id: string;
};
query?: never;
url: "/v1/default/banks/{bank_id}/mental-models";
url: "/v1/default/banks/{bank_id}/observations";
};
export type ClearMentalModelsErrors = {
export type ClearObservationsErrors = {
/**
* Validation Error
*/
422: HttpValidationError;
};
export type ClearMentalModelsError =
ClearMentalModelsErrors[keyof ClearMentalModelsErrors];
export type ClearObservationsError =
ClearObservationsErrors[keyof ClearObservationsErrors];
export type ClearMentalModelsResponses = {
export type ClearObservationsResponses = {
/**
* Successful Response
*/
200: DeleteResponse;
};
export type ClearMentalModelsResponse =
ClearMentalModelsResponses[keyof ClearMentalModelsResponses];
export type ClearObservationsResponse =
ClearObservationsResponses[keyof ClearObservationsResponses];
export type TriggerConsolidationData = {
body?: never;

View file

@ -4,28 +4,28 @@ const DATAPLANE_URL = process.env.HINDSIGHT_CP_DATAPLANE_API_URL || "http://loca
export async function POST(
request: Request,
{ params }: { params: Promise<{ bankId: string; reflectionId: string }> }
{ params }: { params: Promise<{ bankId: string; mentalModelId: string }> }
) {
try {
const { bankId, reflectionId } = await params;
const { bankId, mentalModelId } = await params;
if (!bankId || !reflectionId) {
if (!bankId || !mentalModelId) {
return NextResponse.json(
{ error: "bank_id and reflection_id are required" },
{ error: "bank_id and mental_model_id are required" },
{ status: 400 }
);
}
const response = await fetch(
`${DATAPLANE_URL}/v1/default/banks/${bankId}/reflections/${reflectionId}/refresh`,
`${DATAPLANE_URL}/v1/default/banks/${bankId}/mental-models/${mentalModelId}/refresh`,
{ method: "POST" }
);
if (!response.ok) {
const errorText = await response.text();
console.error("API error refreshing reflection:", errorText);
console.error("API error refreshing mental model:", errorText);
return NextResponse.json(
{ error: errorText || "Failed to refresh reflection" },
{ error: errorText || "Failed to refresh mental model" },
{ status: response.status }
);
}
@ -33,7 +33,7 @@ export async function POST(
const data = await response.json();
return NextResponse.json(data, { status: 200 });
} catch (error) {
console.error("Error refreshing reflection:", error);
return NextResponse.json({ error: "Failed to refresh reflection" }, { status: 500 });
console.error("Error refreshing mental model:", error);
return NextResponse.json({ error: "Failed to refresh mental model" }, { status: 500 });
}
}

View file

@ -0,0 +1,116 @@
import { NextResponse } from "next/server";
const DATAPLANE_URL = process.env.HINDSIGHT_CP_DATAPLANE_API_URL || "http://localhost:8888";
export async function GET(
request: Request,
{ params }: { params: Promise<{ bankId: string; mentalModelId: string }> }
) {
try {
const { bankId, mentalModelId } = await params;
if (!bankId || !mentalModelId) {
return NextResponse.json(
{ error: "bank_id and mental_model_id are required" },
{ status: 400 }
);
}
const response = await fetch(
`${DATAPLANE_URL}/v1/default/banks/${bankId}/mental-models/${mentalModelId}`,
{ method: "GET" }
);
if (!response.ok) {
const errorText = await response.text();
console.error("API error getting mental model:", errorText);
return NextResponse.json(
{ error: "Failed to get mental model" },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data, { status: 200 });
} catch (error) {
console.error("Error getting mental model:", error);
return NextResponse.json({ error: "Failed to get mental model" }, { status: 500 });
}
}
export async function PATCH(
request: Request,
{ params }: { params: Promise<{ bankId: string; mentalModelId: string }> }
) {
try {
const { bankId, mentalModelId } = await params;
if (!bankId || !mentalModelId) {
return NextResponse.json(
{ error: "bank_id and mental_model_id are required" },
{ status: 400 }
);
}
const body = await request.json();
const response = await fetch(
`${DATAPLANE_URL}/v1/default/banks/${bankId}/mental-models/${mentalModelId}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}
);
if (!response.ok) {
const errorText = await response.text();
console.error("API error updating mental model:", errorText);
return NextResponse.json(
{ error: errorText || "Failed to update mental model" },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data, { status: 200 });
} catch (error) {
console.error("Error updating mental model:", error);
return NextResponse.json({ error: "Failed to update mental model" }, { status: 500 });
}
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ bankId: string; mentalModelId: string }> }
) {
try {
const { bankId, mentalModelId } = await params;
if (!bankId || !mentalModelId) {
return NextResponse.json(
{ error: "bank_id and mental_model_id are required" },
{ status: 400 }
);
}
const response = await fetch(
`${DATAPLANE_URL}/v1/default/banks/${bankId}/mental-models/${mentalModelId}`,
{ method: "DELETE" }
);
if (!response.ok) {
const errorText = await response.text();
console.error("API error deleting mental model:", errorText);
return NextResponse.json(
{ error: errorText || "Failed to delete mental model" },
{ status: response.status }
);
}
return NextResponse.json({ success: true }, { status: 200 });
} catch (error) {
console.error("Error deleting mental model:", error);
return NextResponse.json({ error: "Failed to delete mental model" }, { status: 500 });
}
}

View file

@ -1,54 +1,47 @@
import { NextResponse } from "next/server";
import { sdk, lowLevelClient } from "@/lib/hindsight-client";
const DATAPLANE_URL = process.env.HINDSIGHT_CP_DATAPLANE_API_URL || "http://localhost:8888";
export async function GET(request: Request, { params }: { params: Promise<{ bankId: string }> }) {
try {
const { bankId } = await params;
const { searchParams } = new URL(request.url);
const tags = searchParams.getAll("tags");
const tagsMatch = searchParams.get("tags_match");
if (!bankId) {
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
}
// Note: tags filtering is not supported by the list_memories API endpoint
const response = await sdk.listMemories({
client: lowLevelClient,
path: { bank_id: bankId },
query: {
type: "mental_model",
limit: 1000,
},
});
if (response.error) {
console.error("API error listing mental models:", response.error);
return NextResponse.json({ error: "Failed to list mental models" }, { status: 500 });
const queryParams = new URLSearchParams();
if (tags.length > 0) {
tags.forEach((t) => queryParams.append("tags", t));
}
if (tagsMatch) {
queryParams.append("tags_match", tagsMatch);
}
// Transform list memories response to mental models format
const items = (response.data?.items || []).map((item) => ({
id: item.id,
bank_id: bankId,
text: item.text,
proof_count: 1,
history: [],
tags: item.tags || [],
source_memory_ids: [],
source_memories: [],
created_at: item.date,
updated_at: item.date,
}));
const url = `${DATAPLANE_URL}/v1/default/banks/${bankId}/mental-models${queryParams.toString() ? `?${queryParams}` : ""}`;
const response = await fetch(url, { method: "GET" });
return NextResponse.json({ items }, { status: 200 });
if (!response.ok) {
const errorText = await response.text();
console.error("API error listing mental models:", errorText);
return NextResponse.json(
{ error: "Failed to list mental models" },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data, { status: 200 });
} catch (error) {
console.error("Error listing mental models:", error);
return NextResponse.json({ error: "Failed to list mental models" }, { status: 500 });
}
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ bankId: string }> }
) {
export async function POST(request: Request, { params }: { params: Promise<{ bankId: string }> }) {
try {
const { bankId } = await params;
@ -56,19 +49,28 @@ export async function DELETE(
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
}
const response = await sdk.clearMentalModels({
client: lowLevelClient,
path: { bank_id: bankId },
const body = await request.json();
const response = await fetch(`${DATAPLANE_URL}/v1/default/banks/${bankId}/mental-models`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (response.error) {
console.error("API error clearing mental models:", response.error);
return NextResponse.json({ error: "Failed to clear mental models" }, { status: 500 });
if (!response.ok) {
const errorText = await response.text();
console.error("API error creating mental model:", errorText);
return NextResponse.json(
{ error: errorText || "Failed to create mental model" },
{ status: response.status }
);
}
return NextResponse.json(response.data, { status: 200 });
const data = await response.json();
// Returns operation_id - content is generated in background
return NextResponse.json(data, { status: 202 });
} catch (error) {
console.error("Error clearing mental models:", error);
return NextResponse.json({ error: "Failed to clear mental models" }, { status: 500 });
console.error("Error creating mental model:", error);
return NextResponse.json({ error: "Failed to create mental model" }, { status: 500 });
}
}

View file

@ -0,0 +1,74 @@
import { NextResponse } from "next/server";
import { sdk, lowLevelClient } from "@/lib/hindsight-client";
export async function GET(request: Request, { params }: { params: Promise<{ bankId: string }> }) {
try {
const { bankId } = await params;
if (!bankId) {
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
}
// Note: tags filtering is not supported by the list_memories API endpoint
const response = await sdk.listMemories({
client: lowLevelClient,
path: { bank_id: bankId },
query: {
type: "observation",
limit: 1000,
},
});
if (response.error) {
console.error("API error listing observations:", response.error);
return NextResponse.json({ error: "Failed to list observations" }, { status: 500 });
}
// Transform list memories response to observations format
const items = (response.data?.items || []).map((item) => ({
id: item.id,
bank_id: bankId,
text: item.text,
proof_count: 1,
history: [],
tags: item.tags || [],
source_memory_ids: [],
source_memories: [],
created_at: item.date,
updated_at: item.date,
}));
return NextResponse.json({ items }, { status: 200 });
} catch (error) {
console.error("Error listing observations:", error);
return NextResponse.json({ error: "Failed to list observations" }, { status: 500 });
}
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ bankId: string }> }
) {
try {
const { bankId } = await params;
if (!bankId) {
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
}
const response = await sdk.clearObservations({
client: lowLevelClient,
path: { bank_id: bankId },
});
if (response.error) {
console.error("API error clearing observations:", response.error);
return NextResponse.json({ error: "Failed to clear observations" }, { status: 500 });
}
return NextResponse.json(response.data, { status: 200 });
} catch (error) {
console.error("Error clearing observations:", error);
return NextResponse.json({ error: "Failed to clear observations" }, { status: 500 });
}
}

View file

@ -1,113 +0,0 @@
import { NextResponse } from "next/server";
const DATAPLANE_URL = process.env.HINDSIGHT_CP_DATAPLANE_API_URL || "http://localhost:8888";
export async function GET(
request: Request,
{ params }: { params: Promise<{ bankId: string; reflectionId: string }> }
) {
try {
const { bankId, reflectionId } = await params;
if (!bankId || !reflectionId) {
return NextResponse.json(
{ error: "bank_id and reflection_id are required" },
{ status: 400 }
);
}
const response = await fetch(
`${DATAPLANE_URL}/v1/default/banks/${bankId}/reflections/${reflectionId}`,
{ method: "GET" }
);
if (!response.ok) {
const errorText = await response.text();
console.error("API error getting reflection:", errorText);
return NextResponse.json({ error: "Failed to get reflection" }, { status: response.status });
}
const data = await response.json();
return NextResponse.json(data, { status: 200 });
} catch (error) {
console.error("Error getting reflection:", error);
return NextResponse.json({ error: "Failed to get reflection" }, { status: 500 });
}
}
export async function PATCH(
request: Request,
{ params }: { params: Promise<{ bankId: string; reflectionId: string }> }
) {
try {
const { bankId, reflectionId } = await params;
if (!bankId || !reflectionId) {
return NextResponse.json(
{ error: "bank_id and reflection_id are required" },
{ status: 400 }
);
}
const body = await request.json();
const response = await fetch(
`${DATAPLANE_URL}/v1/default/banks/${bankId}/reflections/${reflectionId}`,
{
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
}
);
if (!response.ok) {
const errorText = await response.text();
console.error("API error updating reflection:", errorText);
return NextResponse.json(
{ error: errorText || "Failed to update reflection" },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data, { status: 200 });
} catch (error) {
console.error("Error updating reflection:", error);
return NextResponse.json({ error: "Failed to update reflection" }, { status: 500 });
}
}
export async function DELETE(
request: Request,
{ params }: { params: Promise<{ bankId: string; reflectionId: string }> }
) {
try {
const { bankId, reflectionId } = await params;
if (!bankId || !reflectionId) {
return NextResponse.json(
{ error: "bank_id and reflection_id are required" },
{ status: 400 }
);
}
const response = await fetch(
`${DATAPLANE_URL}/v1/default/banks/${bankId}/reflections/${reflectionId}`,
{ method: "DELETE" }
);
if (!response.ok) {
const errorText = await response.text();
console.error("API error deleting reflection:", errorText);
return NextResponse.json(
{ error: errorText || "Failed to delete reflection" },
{ status: response.status }
);
}
return NextResponse.json({ success: true }, { status: 200 });
} catch (error) {
console.error("Error deleting reflection:", error);
return NextResponse.json({ error: "Failed to delete reflection" }, { status: 500 });
}
}

View file

@ -1,76 +0,0 @@
import { NextResponse } from "next/server";
const DATAPLANE_URL = process.env.HINDSIGHT_CP_DATAPLANE_API_URL || "http://localhost:8888";
export async function GET(request: Request, { params }: { params: Promise<{ bankId: string }> }) {
try {
const { bankId } = await params;
const { searchParams } = new URL(request.url);
const tags = searchParams.getAll("tags");
const tagsMatch = searchParams.get("tags_match");
if (!bankId) {
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
}
const queryParams = new URLSearchParams();
if (tags.length > 0) {
tags.forEach((t) => queryParams.append("tags", t));
}
if (tagsMatch) {
queryParams.append("tags_match", tagsMatch);
}
const url = `${DATAPLANE_URL}/v1/default/banks/${bankId}/reflections${queryParams.toString() ? `?${queryParams}` : ""}`;
const response = await fetch(url, { method: "GET" });
if (!response.ok) {
const errorText = await response.text();
console.error("API error listing reflections:", errorText);
return NextResponse.json(
{ error: "Failed to list reflections" },
{ status: response.status }
);
}
const data = await response.json();
return NextResponse.json(data, { status: 200 });
} catch (error) {
console.error("Error listing reflections:", error);
return NextResponse.json({ error: "Failed to list reflections" }, { status: 500 });
}
}
export async function POST(request: Request, { params }: { params: Promise<{ bankId: string }> }) {
try {
const { bankId } = await params;
if (!bankId) {
return NextResponse.json({ error: "bank_id is required" }, { status: 400 });
}
const body = await request.json();
const response = await fetch(`${DATAPLANE_URL}/v1/default/banks/${bankId}/reflections`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!response.ok) {
const errorText = await response.text();
console.error("API error creating reflection:", errorText);
return NextResponse.json(
{ error: errorText || "Failed to create reflection" },
{ status: response.status }
);
}
const data = await response.json();
// Returns operation_id - content is generated in background
return NextResponse.json(data, { status: 202 });
} catch (error) {
console.error("Error creating reflection:", error);
return NextResponse.json({ error: "Failed to create reflection" }, { status: 500 });
}
}

View file

@ -9,11 +9,11 @@ import { EntitiesView } from "@/components/entities-view";
import { ThinkView } from "@/components/think-view";
import { SearchDebugView } from "@/components/search-debug-view";
import { BankProfileView } from "@/components/bank-profile-view";
import { ReflectionsView } from "@/components/reflections-view";
import { MentalModelsView } from "@/components/mental-models-view";
import { useFeatures } from "@/lib/features-context";
type NavItem = "recall" | "reflect" | "data" | "documents" | "entities" | "profile";
type DataSubTab = "world" | "experience" | "models" | "reflections";
type DataSubTab = "world" | "experience" | "observations" | "mental-models";
export default function BankPage() {
const params = useParams();
@ -24,7 +24,7 @@ export default function BankPage() {
const bankId = params.bankId as string;
const view = (searchParams.get("view") || "profile") as NavItem;
const subTab = (searchParams.get("subTab") || "world") as DataSubTab;
const mentalModelsEnabled = features?.mental_models ?? false;
const observationsEnabled = features?.observations ?? false;
const handleTabChange = (tab: NavItem) => {
router.push(`/banks/${bankId}?view=${tab}`);
@ -115,33 +115,33 @@ export default function BankPage() {
)}
</button>
<button
onClick={() => handleDataSubTabChange("models")}
onClick={() => handleDataSubTabChange("observations")}
className={`px-6 py-3 font-semibold text-sm transition-all relative ${
subTab === "models"
subTab === "observations"
? "text-primary"
: "text-muted-foreground hover:text-foreground"
}`}
>
Observations
{!observationsEnabled && (
<span className="ml-2 text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground">
Off
</span>
)}
{subTab === "observations" && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
)}
</button>
<button
onClick={() => handleDataSubTabChange("mental-models")}
className={`px-6 py-3 font-semibold text-sm transition-all relative ${
subTab === "mental-models"
? "text-primary"
: "text-muted-foreground hover:text-foreground"
}`}
>
Mental Models
{!mentalModelsEnabled && (
<span className="ml-2 text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground">
Off
</span>
)}
{subTab === "models" && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
)}
</button>
<button
onClick={() => handleDataSubTabChange("reflections")}
className={`px-6 py-3 font-semibold text-sm transition-all relative ${
subTab === "reflections"
? "text-primary"
: "text-muted-foreground hover:text-foreground"
}`}
>
Reflections
{subTab === "reflections" && (
{subTab === "mental-models" && (
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-primary" />
)}
</button>
@ -151,9 +151,9 @@ export default function BankPage() {
<div>
{subTab === "world" && <DataView key="world" factType="world" />}
{subTab === "experience" && <DataView key="experience" factType="experience" />}
{subTab === "models" &&
(mentalModelsEnabled ? (
<DataView key="models" factType="mental_model" />
{subTab === "observations" &&
(observationsEnabled ? (
<DataView key="observations" factType="observation" />
) : (
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="text-muted-foreground mb-2">
@ -174,18 +174,18 @@ export default function BankPage() {
</svg>
</div>
<h3 className="text-lg font-semibold text-foreground mb-1">
Mental Models Not Enabled
Observations Not Enabled
</h3>
<p className="text-sm text-muted-foreground max-w-md">
Mental models consolidation is disabled on this server. Set{" "}
Observations consolidation is disabled on this server. Set{" "}
<code className="px-1 py-0.5 bg-muted rounded text-xs">
HINDSIGHT_API_ENABLE_MENTAL_MODELS=true
HINDSIGHT_API_ENABLE_OBSERVATIONS=true
</code>{" "}
to enable.
</p>
</div>
))}
{subTab === "reflections" && <ReflectionsView key="reflections" />}
{subTab === "mental-models" && <MentalModelsView key="mental-models" />}
</div>
</div>
)}

View file

@ -214,7 +214,7 @@ export function BankProfileView() {
const router = useRouter();
const { currentBank, setCurrentBank, loadBanks } = useBank();
const { features } = useFeatures();
const mentalModelsEnabled = features?.mental_models ?? false;
const observationsEnabled = features?.observations ?? false;
const [profile, setProfile] = useState<BankProfile | null>(null);
const [stats, setStats] = useState<BankStats | null>(null);
const [operations, setOperations] = useState<Operation[]>([]);
@ -243,9 +243,9 @@ export function BankProfileView() {
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
const [isDeleting, setIsDeleting] = useState(false);
// Clear mental models state
const [showClearMentalModelsDialog, setShowClearMentalModelsDialog] = useState(false);
const [isClearingMentalModels, setIsClearingMentalModels] = useState(false);
// Clear observations state
const [showClearObservationsDialog, setShowClearObservationsDialog] = useState(false);
const [isClearingObservations, setIsClearingObservations] = useState(false);
// Consolidation state
const [isConsolidating, setIsConsolidating] = useState(false);
@ -372,20 +372,20 @@ export function BankProfileView() {
}
};
const handleClearMentalModels = async () => {
const handleClearObservations = async () => {
if (!currentBank) return;
setIsClearingMentalModels(true);
setIsClearingObservations(true);
try {
const result = await client.clearMentalModels(currentBank);
setShowClearMentalModelsDialog(false);
const result = await client.clearObservations(currentBank);
setShowClearObservationsDialog(false);
await loadData();
alert(result.message || "Mental models cleared successfully");
alert(result.message || "Observations cleared successfully");
} catch (error) {
console.error("Error clearing mental models:", error);
alert("Error clearing mental models: " + (error as Error).message);
console.error("Error clearing observations:", error);
alert("Error clearing observations: " + (error as Error).message);
} finally {
setIsClearingMentalModels(false);
setIsClearingObservations(false);
}
};
@ -537,8 +537,8 @@ export function BankProfileView() {
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleTriggerConsolidation}
disabled={isConsolidating || !mentalModelsEnabled}
title={!mentalModelsEnabled ? "Mental models feature is not enabled" : undefined}
disabled={isConsolidating || !observationsEnabled}
title={!observationsEnabled ? "Observations feature is not enabled" : undefined}
>
{isConsolidating ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
@ -546,19 +546,19 @@ export function BankProfileView() {
<Brain className="w-4 h-4 mr-2" />
)}
{isConsolidating ? "Consolidating..." : "Run Consolidation"}
{!mentalModelsEnabled && (
{!observationsEnabled && (
<span className="ml-auto text-xs text-muted-foreground">Off</span>
)}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => setShowClearMentalModelsDialog(true)}
disabled={!mentalModelsEnabled}
onClick={() => setShowClearObservationsDialog(true)}
disabled={!observationsEnabled}
className="text-amber-600 dark:text-amber-400 focus:text-amber-700 dark:focus:text-amber-300"
title={!mentalModelsEnabled ? "Mental models feature is not enabled" : undefined}
title={!observationsEnabled ? "Observations feature is not enabled" : undefined}
>
<Trash2 className="w-4 h-4 mr-2" />
Clear Mental Models
{!mentalModelsEnabled && (
Clear Observations
{!observationsEnabled && (
<span className="ml-auto text-xs text-muted-foreground">Off</span>
)}
</DropdownMenuItem>
@ -664,26 +664,26 @@ export function BankProfileView() {
</div>
<div
className={`rounded-xl p-4 text-center ${
mentalModelsEnabled
observationsEnabled
? "bg-amber-500/10 border border-amber-500/20"
: "bg-muted/50 border border-muted"
}`}
title={!mentalModelsEnabled ? "Mental models feature is not enabled" : undefined}
title={!observationsEnabled ? "Observations feature is not enabled" : undefined}
>
<p
className={`text-xs font-semibold uppercase tracking-wide ${
mentalModelsEnabled ? "text-amber-600 dark:text-amber-400" : "text-muted-foreground"
observationsEnabled ? "text-amber-600 dark:text-amber-400" : "text-muted-foreground"
}`}
>
Mental Models
{!mentalModelsEnabled && <span className="ml-1 normal-case">(Off)</span>}
Observations
{!observationsEnabled && <span className="ml-1 normal-case">(Off)</span>}
</p>
<p
className={`text-2xl font-bold mt-1 ${
mentalModelsEnabled ? "text-amber-600 dark:text-amber-400" : "text-muted-foreground"
observationsEnabled ? "text-amber-600 dark:text-amber-400" : "text-muted-foreground"
}`}
>
{mentalModelsEnabled ? stats.total_mental_models || 0 : "—"}
{observationsEnabled ? stats.total_mental_models || 0 : "—"}
</p>
</div>
<div className="bg-rose-500/10 border border-rose-500/20 rounded-xl p-4 text-center">
@ -1024,35 +1024,35 @@ export function BankProfileView() {
</AlertDialogContent>
</AlertDialog>
{/* Clear Mental Models Confirmation Dialog */}
<AlertDialog open={showClearMentalModelsDialog} onOpenChange={setShowClearMentalModelsDialog}>
{/* Clear Observations Confirmation Dialog */}
<AlertDialog open={showClearObservationsDialog} onOpenChange={setShowClearObservationsDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Clear Mental Models</AlertDialogTitle>
<AlertDialogTitle>Clear Observations</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-2 text-sm text-muted-foreground">
<p>
Are you sure you want to clear all mental models for{" "}
Are you sure you want to clear all observations for{" "}
<span className="font-semibold text-foreground">{currentBank}</span>?
</p>
<p className="text-amber-600 dark:text-amber-400 font-medium">
This will delete all consolidated knowledge. Mental models will be regenerated the
This will delete all consolidated knowledge. Observations will be regenerated the
next time consolidation runs.
</p>
{stats && stats.total_mental_models > 0 && (
<p>This will delete {stats.total_mental_models} mental models.</p>
<p>This will delete {stats.total_mental_models} observations.</p>
)}
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel disabled={isClearingMentalModels}>Cancel</AlertDialogCancel>
<AlertDialogCancel disabled={isClearingObservations}>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={handleClearMentalModels}
disabled={isClearingMentalModels}
onClick={handleClearObservations}
disabled={isClearingObservations}
className="bg-amber-500 text-white hover:bg-amber-600"
>
{isClearingMentalModels ? (
{isClearingObservations ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Clearing...
@ -1060,7 +1060,7 @@ export function BankProfileView() {
) : (
<>
<Trash2 className="w-4 h-4 mr-2" />
Clear Mental Models
Clear Observations
</>
)}
</AlertDialogAction>

View file

@ -36,7 +36,7 @@ import { Switch } from "@/components/ui/switch";
import { MemoryDetailPanel } from "./memory-detail-panel";
import { Graph2D, convertHindsightGraphData, GraphNode } from "./graph-2d";
type FactType = "world" | "experience" | "mental_model";
type FactType = "world" | "experience" | "observation";
type ViewMode = "graph" | "table" | "timeline";
interface DataViewProps {
@ -117,8 +117,8 @@ export function DataView({ factType }: DataViewProps) {
});
setData(graphData);
// Fetch consolidation status for mental models
if (factType === "mental_model") {
// Fetch consolidation status for observations
if (factType === "observation") {
const stats: any = await client.getBankStats(currentBank);
setConsolidationStatus({
pending_consolidation: stats.pending_consolidation || 0,
@ -307,8 +307,8 @@ export function DataView({ factType }: DataViewProps) {
)}
</div>
{/* Consolidation status for mental models */}
{factType === "mental_model" && consolidationStatus && (
{/* Consolidation status for observations */}
{factType === "observation" && consolidationStatus && (
<div
className={`flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${
consolidationStatus.pending_consolidation === 0
@ -617,11 +617,11 @@ export function DataView({ factType }: DataViewProps) {
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead
className={factType === "mental_model" ? "w-[55%]" : "w-[45%]"}
className={factType === "observation" ? "w-[55%]" : "w-[45%]"}
>
{factType === "mental_model" ? "Mental Model" : "Memory"}
{factType === "observation" ? "Observation" : "Memory"}
</TableHead>
{factType === "mental_model" ? (
{factType === "observation" ? (
<>
<TableHead className="w-[10%]">Sources</TableHead>
<TableHead className="w-[15%]">Created</TableHead>
@ -676,7 +676,7 @@ export function DataView({ factType }: DataViewProps) {
</div>
)}
</TableCell>
{factType === "mental_model" ? (
{factType === "observation" ? (
<>
<TableCell className="text-xs py-2 text-foreground text-center">
{row.proof_count || 1}

View file

@ -58,8 +58,8 @@ export function MemoryDetailPanel({
// Use full memory data if available, otherwise fall back to the partial data passed in
const displayMemory = fullMemory || memory;
const isMentalModel =
displayMemory?.fact_type === "mental_model" || displayMemory?.type === "mental_model";
const isObservation =
displayMemory?.fact_type === "observation" || displayMemory?.type === "observation";
const copyToClipboard = async (text: string) => {
try {
@ -127,8 +127,8 @@ export function MemoryDetailPanel({
</div>
</div>
{/* Context (not shown for mental models) */}
{displayMemory.context && !isMentalModel && (
{/* Context (not shown for observations) */}
{displayMemory.context && !isObservation && (
<div className="p-4 bg-muted/50 rounded-lg">
<div className="text-xs font-bold text-muted-foreground uppercase mb-2">
Context
@ -209,7 +209,7 @@ export function MemoryDetailPanel({
</div>
)}
{/* Source Memories (for mental models) */}
{/* Source Memories (for observations) */}
{displayMemory.source_memories && displayMemory.source_memories.length > 0 && (
<div className="border-t border-border pt-5">
<div className="text-xs font-bold text-muted-foreground uppercase mb-3">

View file

@ -33,7 +33,6 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Plus,
Sparkles,
@ -60,7 +59,7 @@ interface ReflectResponse {
based_on: Record<string, ReflectResponseBasedOnFact[]>;
}
interface Reflection {
interface MentalModel {
id: string;
bank_id: string;
name: string;
@ -72,30 +71,31 @@ interface Reflection {
reflect_response?: ReflectResponse;
}
export function ReflectionsView() {
export function MentalModelsView() {
const { currentBank } = useBank();
const [reflections, setReflections] = useState<Reflection[]>([]);
const [mentalModels, setMentalModels] = useState<MentalModel[]>([]);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [currentPage, setCurrentPage] = useState(1);
const itemsPerPage = 100;
const [showCreateReflection, setShowCreateReflection] = useState(false);
const [selectedReflection, setSelectedReflection] = useState<Reflection | null>(null);
const [showCreateMentalModel, setShowCreateMentalModel] = useState(false);
const [selectedMentalModel, setSelectedMentalModel] = useState<MentalModel | null>(null);
const [deleteTarget, setDeleteTarget] = useState<{
id: string;
name: string;
} | null>(null);
const [deleting, setDeleting] = useState(false);
// Filter reflections based on search query
const filteredReflections = reflections.filter((r) => {
// Filter mental models based on search query
const filteredMentalModels = mentalModels.filter((m) => {
if (!searchQuery) return true;
const query = searchQuery.toLowerCase();
return (
r.name.toLowerCase().includes(query) ||
r.source_query.toLowerCase().includes(query) ||
r.content.toLowerCase().includes(query)
m.id.toLowerCase().includes(query) ||
m.name.toLowerCase().includes(query) ||
m.source_query.toLowerCase().includes(query) ||
m.content.toLowerCase().includes(query)
);
});
@ -104,10 +104,10 @@ export function ReflectionsView() {
setLoading(true);
try {
const reflectionsData = await client.listReflections(currentBank);
setReflections(reflectionsData.items || []);
const mentalModelsData = await client.listMentalModels(currentBank);
setMentalModels(mentalModelsData.items || []);
} catch (error) {
console.error("Error loading reflections:", error);
console.error("Error loading mental models:", error);
} finally {
setLoading(false);
}
@ -118,12 +118,12 @@ export function ReflectionsView() {
setDeleting(true);
try {
await client.deleteReflection(currentBank, deleteTarget.id);
setReflections((prev) => prev.filter((r) => r.id !== deleteTarget.id));
if (selectedReflection?.id === deleteTarget.id) setSelectedReflection(null);
await client.deleteMentalModel(currentBank, deleteTarget.id);
setMentalModels((prev) => prev.filter((m) => m.id !== deleteTarget.id));
if (selectedMentalModel?.id === deleteTarget.id) setSelectedMentalModel(null);
setDeleteTarget(null);
} catch (error) {
console.error("Error deleting reflection:", error);
console.error("Error deleting mental model:", error);
alert("Error deleting: " + (error as Error).message);
} finally {
setDeleting(false);
@ -139,7 +139,7 @@ export function ReflectionsView() {
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
setSelectedReflection(null);
setSelectedMentalModel(null);
}
};
window.addEventListener("keydown", handleKeyDown);
@ -155,17 +155,17 @@ export function ReflectionsView() {
return (
<Card>
<CardContent className="p-10 text-center">
<p className="text-muted-foreground">Select a memory bank to view reflections.</p>
<p className="text-muted-foreground">Select a memory bank to view mental models.</p>
</CardContent>
</Card>
);
}
// Pagination calculations
const totalPages = Math.ceil(filteredReflections.length / itemsPerPage);
const totalPages = Math.ceil(filteredMentalModels.length / itemsPerPage);
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedReflections = filteredReflections.slice(startIndex, endIndex);
const paginatedMentalModels = filteredMentalModels.slice(startIndex, endIndex);
return (
<div>
@ -182,7 +182,7 @@ export function ReflectionsView() {
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Filter reflections by name, query, or content..."
placeholder="Filter mental models by name, query, or content..."
className="max-w-md"
/>
</div>
@ -190,30 +190,31 @@ export function ReflectionsView() {
<div className="flex items-center justify-between mb-6">
<div className="text-sm text-muted-foreground">
{searchQuery
? `${filteredReflections.length} of ${reflections.length} reflections`
: `${reflections.length} reflection${reflections.length !== 1 ? "s" : ""}`}
? `${filteredMentalModels.length} of ${mentalModels.length} mental models`
: `${mentalModels.length} mental model${mentalModels.length !== 1 ? "s" : ""}`}
</div>
<Button onClick={() => setShowCreateReflection(true)} variant="outline" size="sm">
<Button onClick={() => setShowCreateMentalModel(true)} variant="outline" size="sm">
<Plus className="w-4 h-4 mr-2" />
Add Reflection
Add Mental Model
</Button>
</div>
{filteredReflections.length > 0 ? (
{filteredMentalModels.length > 0 ? (
<>
<div className="border rounded-lg overflow-hidden">
<Table className="table-fixed">
<TableHeader>
<TableRow className="bg-muted/50">
<TableHead className="w-[25%]">Name</TableHead>
<TableHead className="w-[45%]">Source Query</TableHead>
<TableHead className="w-[20%]">Last Refreshed</TableHead>
<TableHead className="w-[20%]">ID</TableHead>
<TableHead className="w-[20%]">Name</TableHead>
<TableHead className="w-[35%]">Source Query</TableHead>
<TableHead className="w-[15%]">Last Refreshed</TableHead>
<TableHead className="w-[10%]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedReflections.map((r) => {
const refreshedDate = new Date(r.last_refreshed_at);
{paginatedMentalModels.map((m) => {
const refreshedDate = new Date(m.last_refreshed_at);
const dateDisplay = refreshedDate.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
@ -227,18 +228,23 @@ export function ReflectionsView() {
return (
<TableRow
key={r.id}
key={m.id}
className={`cursor-pointer hover:bg-muted/50 ${
selectedReflection?.id === r.id ? "bg-primary/10" : ""
selectedMentalModel?.id === m.id ? "bg-primary/10" : ""
}`}
onClick={() => setSelectedReflection(r)}
onClick={() => setSelectedMentalModel(m)}
>
<TableCell className="py-2">
<div className="font-medium text-foreground">{r.name}</div>
<code className="text-xs font-mono text-muted-foreground truncate block">
{m.id}
</code>
</TableCell>
<TableCell className="py-2">
<div className="font-medium text-foreground">{m.name}</div>
</TableCell>
<TableCell className="py-2">
<div className="text-sm text-muted-foreground truncate">
{r.source_query}
{m.source_query}
</div>
</TableCell>
<TableCell className="py-2 text-sm text-foreground">
@ -252,7 +258,7 @@ export function ReflectionsView() {
className="h-8 w-8 p-0 text-muted-foreground hover:text-destructive"
onClick={(e) => {
e.stopPropagation();
setDeleteTarget({ id: r.id, name: r.name });
setDeleteTarget({ id: m.id, name: m.name });
}}
>
<Trash2 className="h-4 w-4" />
@ -269,8 +275,8 @@ export function ReflectionsView() {
{totalPages > 1 && (
<div className="flex items-center justify-between mt-3 pt-3 border-t">
<div className="text-xs text-muted-foreground">
{startIndex + 1}-{Math.min(endIndex, filteredReflections.length)} of{" "}
{filteredReflections.length}
{startIndex + 1}-{Math.min(endIndex, filteredMentalModels.length)} of{" "}
{filteredMentalModels.length}
</div>
<div className="flex items-center gap-1">
<Button
@ -321,20 +327,20 @@ export function ReflectionsView() {
<Sparkles className="w-6 h-6 mx-auto mb-2 text-muted-foreground" />
<p className="text-sm text-muted-foreground">
{searchQuery
? "No reflections match your filter"
: "No reflections yet. Create a reflection to generate and save a summary from your memories."}
? "No mental models match your filter"
: "No mental models yet. Create a mental model to generate and save a summary from your memories."}
</p>
</div>
)}
</>
)}
<CreateReflectionDialog
open={showCreateReflection}
onClose={() => setShowCreateReflection(false)}
<CreateMentalModelDialog
open={showCreateMentalModel}
onClose={() => setShowCreateMentalModel(false)}
onCreated={() => {
setShowCreateReflection(false);
// Reload the list immediately to show the new reflection
setShowCreateMentalModel(false);
// Reload the list immediately to show the new mental model
loadData();
}}
/>
@ -342,7 +348,7 @@ export function ReflectionsView() {
<AlertDialog open={!!deleteTarget} onOpenChange={(open) => !open && setDeleteTarget(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Delete Reflection</AlertDialogTitle>
<AlertDialogTitle>Delete Mental Model</AlertDialogTitle>
<AlertDialogDescription>
Are you sure you want to delete{" "}
<span className="font-semibold">&quot;{deleteTarget?.name}&quot;</span>?
@ -365,16 +371,16 @@ export function ReflectionsView() {
</AlertDialogContent>
</AlertDialog>
{selectedReflection && (
<ReflectionDetailPanel
reflection={selectedReflection}
onClose={() => setSelectedReflection(null)}
{selectedMentalModel && (
<MentalModelDetailPanel
mentalModel={selectedMentalModel}
onClose={() => setSelectedMentalModel(null)}
onDelete={() =>
setDeleteTarget({ id: selectedReflection.id, name: selectedReflection.name })
setDeleteTarget({ id: selectedMentalModel.id, name: selectedMentalModel.name })
}
onRefreshed={(updated) => {
setReflections((prev) => prev.map((r) => (r.id === updated.id ? updated : r)));
setSelectedReflection(updated);
setMentalModels((prev) => prev.map((m) => (m.id === updated.id ? updated : m)));
setSelectedMentalModel(updated);
}}
/>
)}
@ -382,7 +388,7 @@ export function ReflectionsView() {
);
}
function CreateReflectionDialog({
function CreateMentalModelDialog({
open,
onClose,
onCreated,
@ -407,8 +413,8 @@ function CreateReflectionDialog({
const maxTokens = parseInt(form.maxTokens) || 2048;
// Submit reflection creation - content will be generated in background
await client.createReflection(currentBank, {
// Submit mental model creation - content will be generated in background
await client.createMentalModel(currentBank, {
name: form.name.trim(),
source_query: form.sourceQuery.trim(),
tags: tags.length > 0 ? tags : undefined,
@ -418,8 +424,8 @@ function CreateReflectionDialog({
setForm({ name: "", sourceQuery: "", maxTokens: "2048", tags: "" });
onCreated();
} catch (error) {
console.error("Error creating reflection:", error);
alert("Error creating reflection: " + (error as Error).message);
console.error("Error creating mental model:", error);
alert("Error creating mental model: " + (error as Error).message);
} finally {
setCreating(false);
}
@ -437,9 +443,9 @@ function CreateReflectionDialog({
>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Create Reflection</DialogTitle>
<DialogTitle>Create Mental Model</DialogTitle>
<DialogDescription>
Create a reflection by running a query. The content will be auto-generated and can be
Create a mental model by running a query. The content will be auto-generated and can be
refreshed later.
</DialogDescription>
</DialogHeader>
@ -513,39 +519,39 @@ function CreateReflectionDialog({
);
}
function ReflectionDetailPanel({
reflection,
function MentalModelDetailPanel({
mentalModel,
onClose,
onDelete,
onRefreshed,
}: {
reflection: Reflection;
mentalModel: MentalModel;
onClose: () => void;
onDelete: () => void;
onRefreshed: (r: Reflection) => void;
onRefreshed: (m: MentalModel) => void;
}) {
const { currentBank } = useBank();
const [refreshing, setRefreshing] = useState(false);
const [viewMemoryId, setViewMemoryId] = useState<string | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [editName, setEditName] = useState(reflection.name);
const [editName, setEditName] = useState(mentalModel.name);
const [saving, setSaving] = useState(false);
// Reset edit form when reflection changes
// Reset edit form when mental model changes
useEffect(() => {
setEditName(reflection.name);
setEditName(mentalModel.name);
setIsEditing(false);
}, [reflection.id, reflection.name]);
}, [mentalModel.id, mentalModel.name]);
const handleRefresh = async () => {
if (!currentBank) return;
setRefreshing(true);
const originalRefreshedAt = reflection.last_refreshed_at;
const originalRefreshedAt = mentalModel.last_refreshed_at;
try {
// Submit the refresh task
await client.refreshReflection(currentBank, reflection.id);
await client.refreshMentalModel(currentBank, mentalModel.id);
// Poll until last_refreshed_at changes
const pollInterval = 1000; // 1 second
@ -555,7 +561,7 @@ function ReflectionDetailPanel({
const poll = async (): Promise<void> => {
attempts++;
try {
const updated = await client.getReflection(currentBank, reflection.id);
const updated = await client.getMentalModel(currentBank, mentalModel.id);
if (updated.last_refreshed_at !== originalRefreshedAt) {
// Refresh complete
onRefreshed(updated);
@ -571,7 +577,7 @@ function ReflectionDetailPanel({
// Continue polling
setTimeout(poll, pollInterval);
} catch (error) {
console.error("Error polling reflection:", error);
console.error("Error polling mental model:", error);
setRefreshing(false);
}
};
@ -579,7 +585,7 @@ function ReflectionDetailPanel({
// Start polling after a short delay
setTimeout(poll, pollInterval);
} catch (error) {
console.error("Error refreshing reflection:", error);
console.error("Error refreshing mental model:", error);
alert("Error refreshing: " + (error as Error).message);
setRefreshing(false);
}
@ -590,13 +596,13 @@ function ReflectionDetailPanel({
setSaving(true);
try {
const updated = await client.updateReflection(currentBank, reflection.id, {
const updated = await client.updateMentalModel(currentBank, mentalModel.id, {
name: editName.trim(),
});
onRefreshed(updated);
setIsEditing(false);
} catch (error) {
console.error("Error updating reflection:", error);
console.error("Error updating mental model:", error);
alert("Error updating: " + (error as Error).message);
} finally {
setSaving(false);
@ -616,12 +622,15 @@ function ReflectionDetailPanel({
})}`;
};
// Extract facts by type from based_on
const basedOn = reflection.reflect_response?.based_on || {};
const worldFacts = basedOn["world"] || [];
const experienceFacts = basedOn["experience"] || [];
const mentalModels = basedOn["mental-models"] || [];
const totalFacts = worldFacts.length + experienceFacts.length + mentalModels.length;
// Extract all memories from based_on (excluding observations which are shown separately)
const basedOnFacts = mentalModel.reflect_response?.based_on
? Object.entries(mentalModel.reflect_response.based_on)
.filter(([factType]) => factType !== "observation")
.flatMap(([factType, facts]) => facts.map((fact) => ({ ...fact, factType })))
: [];
// Observations are now in based_on with type=observation
const observations = mentalModel.reflect_response?.based_on?.observation || [];
return (
<div className="fixed right-0 top-0 h-screen w-1/2 bg-card border-l shadow-2xl z-50 overflow-y-auto animate-in slide-in-from-right duration-300 ease-out">
@ -645,7 +654,7 @@ function ReflectionDetailPanel({
size="sm"
variant="outline"
onClick={() => {
setEditName(reflection.name);
setEditName(mentalModel.name);
setIsEditing(false);
}}
>
@ -656,7 +665,7 @@ function ReflectionDetailPanel({
) : (
<>
<div className="flex items-center gap-2">
<h3 className="text-xl font-bold text-foreground">{reflection.name}</h3>
<h3 className="text-xl font-bold text-foreground">{mentalModel.name}</h3>
<Button
variant="ghost"
size="sm"
@ -666,7 +675,7 @@ function ReflectionDetailPanel({
<Pencil className="h-3.5 w-3.5" />
</Button>
</div>
<p className="text-sm text-muted-foreground mt-1">{reflection.source_query}</p>
<p className="text-sm text-muted-foreground mt-1">{mentalModel.source_query}</p>
</>
)}
</div>
@ -697,123 +706,84 @@ function ReflectionDetailPanel({
Content
</div>
<div className="prose prose-base dark:prose-invert max-w-none">
<ReactMarkdown>{reflection.content}</ReactMarkdown>
<ReactMarkdown>{mentalModel.content}</ReactMarkdown>
</div>
</div>
{/* Based On Section with Tabs */}
{totalFacts > 0 ? (
{/* Based On Facts Section */}
{basedOnFacts.length > 0 && (
<div className="border-t border-border pt-5">
<div className="text-xs font-bold text-muted-foreground uppercase mb-3">
Based On ({totalFacts} {totalFacts === 1 ? "item" : "items"})
Based On ({basedOnFacts.length} {basedOnFacts.length === 1 ? "fact" : "facts"})
</div>
<Tabs
defaultValue={
worldFacts.length > 0
? "world"
: experienceFacts.length > 0
? "experience"
: "mental-models"
}
>
<TabsList className="mb-4">
<TabsTrigger value="world" disabled={worldFacts.length === 0}>
World
<span className="ml-1.5 px-1.5 py-0.5 rounded-full text-xs bg-blue-500/10 text-blue-600 dark:text-blue-400">
{worldFacts.length}
</span>
</TabsTrigger>
<TabsTrigger value="experience" disabled={experienceFacts.length === 0}>
Experience
<span className="ml-1.5 px-1.5 py-0.5 rounded-full text-xs bg-green-500/10 text-green-600 dark:text-green-400">
{experienceFacts.length}
</span>
</TabsTrigger>
<TabsTrigger value="mental-models" disabled={mentalModels.length === 0}>
Mental Models
<span className="ml-1.5 px-1.5 py-0.5 rounded-full text-xs bg-amber-500/10 text-amber-600 dark:text-amber-400">
{mentalModels.length}
</span>
</TabsTrigger>
</TabsList>
<TabsContent value="world">
<div className="space-y-3">
{worldFacts.map((fact, i) => (
<div
key={fact.id || i}
className="p-4 bg-muted/50 rounded-lg border border-border/50"
<div className="space-y-3">
{basedOnFacts.map((fact, i) => (
<div
key={fact.id || i}
className="p-4 bg-muted/50 rounded-lg border border-border/50"
>
<div className="flex items-start justify-between gap-2 mb-2">
<span
className={`px-2 py-0.5 rounded text-xs font-medium ${
fact.factType === "world"
? "bg-blue-500/10 text-blue-600 dark:text-blue-400"
: fact.factType === "experience"
? "bg-green-500/10 text-green-600 dark:text-green-400"
: "bg-purple-500/10 text-purple-600 dark:text-purple-400"
}`}
>
<div className="flex items-start justify-between gap-2">
<p className="text-sm text-foreground leading-relaxed flex-1">
{fact.text}
</p>
<Button
variant="outline"
size="sm"
className="h-6 text-xs shrink-0"
onClick={() => setViewMemoryId(fact.id)}
>
View
</Button>
</div>
</div>
))}
</div>
</TabsContent>
<TabsContent value="experience">
<div className="space-y-3">
{experienceFacts.map((fact, i) => (
<div
key={fact.id || i}
className="p-4 bg-muted/50 rounded-lg border border-border/50"
{fact.factType}
</span>
<Button
variant="outline"
size="sm"
className="h-6 text-xs"
onClick={() => setViewMemoryId(fact.id)}
>
<div className="flex items-start justify-between gap-2">
<p className="text-sm text-foreground leading-relaxed flex-1">
{fact.text}
</p>
<Button
variant="outline"
size="sm"
className="h-6 text-xs shrink-0"
onClick={() => setViewMemoryId(fact.id)}
>
View
</Button>
</div>
</div>
))}
View
</Button>
</div>
<p className="text-sm text-foreground leading-relaxed">{fact.text}</p>
</div>
</TabsContent>
<TabsContent value="mental-models">
<div className="space-y-3">
{mentalModels.map((model, i) => (
<div
key={model.id || i}
className="p-4 bg-muted/50 rounded-lg border border-border/50"
>
<div className="flex items-start justify-between gap-2">
<p className="text-sm text-foreground leading-relaxed flex-1">
{model.text}
</p>
<Button
variant="outline"
size="sm"
className="h-6 text-xs shrink-0"
onClick={() => setViewMemoryId(model.id)}
>
View
</Button>
</div>
</div>
))}
</div>
</TabsContent>
</Tabs>
))}
</div>
</div>
) : !reflection.reflect_response ? (
)}
{/* Observations Used Section */}
{observations.length > 0 && (
<div className="border-t border-border pt-5">
<div className="text-xs font-bold text-muted-foreground uppercase mb-3">
Observations Used ({observations.length})
</div>
<div className="space-y-3">
{observations.map((obs, i) => (
<div
key={obs.id || i}
className="p-4 bg-muted/50 rounded-lg border border-border/50"
>
<div className="flex items-start justify-between gap-2 mb-2">
<span className="px-2 py-0.5 rounded text-xs font-medium bg-amber-500/10 text-amber-600 dark:text-amber-400">
observation
</span>
<Button
variant="outline"
size="sm"
className="h-6 text-xs"
onClick={() => setViewMemoryId(obs.id)}
>
View
</Button>
</div>
<p className="text-sm text-foreground leading-relaxed">{obs.text}</p>
</div>
))}
</div>
</div>
)}
{/* No based_on data yet */}
{!mentalModel.reflect_response && (
<div className="border-t border-border pt-5">
<div className="text-xs font-bold text-muted-foreground uppercase mb-3">Based On</div>
<p className="text-sm text-muted-foreground">
@ -821,15 +791,15 @@ function ReflectionDetailPanel({
tracking.
</p>
</div>
) : null}
)}
{reflection.tags && reflection.tags.length > 0 && (
{mentalModel.tags && mentalModel.tags.length > 0 && (
<div>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-3">
Tags
</div>
<div className="flex flex-wrap gap-2">
{reflection.tags.map((tag) => (
{mentalModel.tags.map((tag) => (
<span
key={tag}
className="px-2 py-1 rounded bg-muted text-muted-foreground text-sm"
@ -842,8 +812,8 @@ function ReflectionDetailPanel({
)}
<div className="flex gap-6 text-sm text-muted-foreground">
<span>Created: {formatDateTime(reflection.created_at)}</span>
<span>Refreshed: {formatDateTime(reflection.last_refreshed_at)}</span>
<span>Created: {formatDateTime(mentalModel.created_at)}</span>
<span>Refreshed: {formatDateTime(mentalModel.last_refreshed_at)}</span>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
@ -851,7 +821,7 @@ function ReflectionDetailPanel({
ID
</div>
<code className="text-sm font-mono break-all text-muted-foreground">
{reflection.id}
{mentalModel.id}
</code>
</div>

View file

@ -32,7 +32,7 @@ import JsonView from "react18-json-view";
import "react18-json-view/src/style.css";
import { MemoryDetailPanel } from "./memory-detail-panel";
type FactType = "world" | "experience" | "mental_model";
type FactType = "world" | "experience" | "observation";
type Budget = "low" | "mid" | "high";
type TagsMatch = "any" | "all" | "any_strict" | "all_strict";
type ViewMode = "results" | "trace" | "json";
@ -55,7 +55,7 @@ export function SearchDebugView() {
const [results, setResults] = useState<any[] | null>(null);
const [entities, setEntities] = useState<any[] | null>(null);
const [chunks, setChunks] = useState<any[] | null>(null);
const [mentalModels, setMentalModels] = useState<any[] | null>(null);
const [observations, setObservations] = useState<any[] | null>(null);
const [trace, setTrace] = useState<any | null>(null);
const [loading, setLoading] = useState(false);
const [viewMode, setViewMode] = useState<ViewMode>("results");
@ -109,7 +109,7 @@ export function SearchDebugView() {
// Must select at least one type
if (factTypes.length === 0) {
alert("Please select at least one type (World, Experience, or Mental Models)");
alert("Please select at least one type (World, Experience, or Observations)");
return;
}
@ -142,7 +142,7 @@ export function SearchDebugView() {
setResults(data.results || []);
setEntities(data.entities || null);
setChunks(data.chunks || null);
setMentalModels(data.mental_models || null);
setObservations(data.observations || null);
setTrace(data.trace || null);
setViewMode("results");
} catch (error) {
@ -208,10 +208,10 @@ export function SearchDebugView() {
))}
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={factTypes.includes("mental_model")}
onCheckedChange={() => toggleFactType("mental_model")}
checked={factTypes.includes("observation")}
onCheckedChange={() => toggleFactType("observation")}
/>
<span className="text-sm">Mental Models</span>
<span className="text-sm">Observations</span>
</label>
</div>
</div>
@ -360,29 +360,29 @@ export function SearchDebugView() {
{/* Results View */}
{viewMode === "results" && (
<div className="space-y-4">
{/* Mental Models Section */}
{mentalModels && mentalModels.length > 0 && (
{/* Observations Section */}
{observations && observations.length > 0 && (
<Card className="border-orange-500/30 bg-orange-500/5">
<CardHeader className="py-3">
<CardTitle className="text-base flex items-center gap-2">
<Database className="h-4 w-4 text-orange-500" />
<span>Mental Models</span>
<span className="text-xs text-muted-foreground">({mentalModels.length})</span>
<span>Observations</span>
<span className="text-xs text-muted-foreground">({observations.length})</span>
</CardTitle>
</CardHeader>
<CardContent className="pt-0 space-y-2">
{mentalModels.map((mm: any, idx: number) => (
{observations.map((obs: any, idx: number) => (
<div
key={mm.id || idx}
key={obs.id || idx}
className="p-3 bg-background rounded-lg border border-orange-500/20"
>
<p className="text-sm text-foreground">{mm.text}</p>
<p className="text-sm text-foreground">{obs.text}</p>
<div className="flex items-center gap-3 mt-2 text-xs text-muted-foreground">
<span className="px-2 py-0.5 rounded bg-orange-500/10 text-orange-600">
Mental Model
Observation
</span>
<span>Proof count: {mm.proof_count || 1}</span>
<span>Relevance: {(mm.relevance || 0).toFixed(3)}</span>
<span>Proof count: {obs.proof_count || 1}</span>
<span>Relevance: {(obs.relevance || 0).toFixed(3)}</span>
</div>
</div>
))}
@ -392,7 +392,7 @@ export function SearchDebugView() {
{/* Memories Section */}
<div className="space-y-3">
{results.length === 0 && (!mentalModels || mentalModels.length === 0) ? (
{results.length === 0 && (!observations || observations.length === 0) ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Search className="h-12 w-12 text-muted-foreground mb-4" />
@ -1010,7 +1010,7 @@ export function SearchDebugView() {
results,
...(entities && { entities }),
...(chunks && { chunks }),
...(mentalModels && { mental_models: mentalModels }),
...(observations && { observations }),
trace,
}}
collapsed={2}

View file

@ -52,9 +52,9 @@ export function ThinkView() {
const [selectedDirective, setSelectedDirective] = useState<any | null>(null);
const [fullDirective, setFullDirective] = useState<any | null>(null);
const [loadingDirective, setLoadingDirective] = useState(false);
const [selectedMentalModel, setSelectedMentalModel] = useState<any | null>(null);
const [fullMentalModel, setFullMentalModel] = useState<any | null>(null);
const [loadingMentalModel, setLoadingMentalModel] = useState(false);
const [selectedObservation, setSelectedObservation] = useState<any | null>(null);
const [fullObservation, setFullObservation] = useState<any | null>(null);
const [loadingObservation, setLoadingObservation] = useState(false);
const FEEDBACK_DIRECTIVE_NAME = "General Feedback";
@ -77,22 +77,22 @@ export function ThinkView() {
}
};
// Load full mental model data when one is selected
const handleSelectMentalModel = async (model: any) => {
setSelectedMentalModel(model);
setFullMentalModel(null);
if (!currentBank || !model?.id) return;
// Load full observation data when one is selected
const handleSelectObservation = async (observation: any) => {
setSelectedObservation(observation);
setFullObservation(null);
if (!currentBank || !observation?.id) return;
setLoadingMentalModel(true);
setLoadingObservation(true);
try {
const models = await client.listMentalModels(currentBank);
const fullModel = models.items?.find((m: any) => m.id === model.id);
setFullMentalModel(fullModel || model);
const observations = await client.listObservations(currentBank);
const fullObs = observations.items?.find((o: any) => o.id === observation.id);
setFullObservation(fullObs || observation);
} catch (error) {
console.error("Failed to load mental model:", error);
setFullMentalModel(model); // Fall back to partial data
console.error("Failed to load observation:", error);
setFullObservation(observation); // Fall back to partial data
} finally {
setLoadingMentalModel(false);
setLoadingObservation(false);
}
};
@ -436,33 +436,33 @@ export function ThinkView() {
{/* Trace View - Split Layout */}
{viewMode === "trace" && (
<div className="space-y-4">
{/* Mental Models Created */}
{result.mental_models_created && result.mental_models_created.length > 0 && (
{/* Observations Created */}
{result.observations_created && result.observations_created.length > 0 && (
<Card className="border-emerald-200 dark:border-emerald-800">
<CardHeader className="bg-emerald-50 dark:bg-emerald-950 py-3">
<CardTitle className="flex items-center gap-2 text-base">
<Brain className="w-4 h-4 text-emerald-600" />
Mental Models Created ({result.mental_models_created.length})
Observations Created ({result.observations_created.length})
</CardTitle>
<CardDescription className="text-xs">
New mental models learned during this reflection
New observations learned during this reflection
</CardDescription>
</CardHeader>
<CardContent className="pt-4">
<div className="space-y-2">
{result.mental_models_created.map((model: any, i: number) => (
{result.observations_created.map((obs: any, i: number) => (
<div
key={i}
className="p-3 bg-emerald-50 dark:bg-emerald-950/50 rounded-lg border border-emerald-200 dark:border-emerald-800"
>
<div className="font-medium text-sm text-emerald-900 dark:text-emerald-100">
{model.name}
{obs.name}
</div>
<div className="text-xs text-emerald-700 dark:text-emerald-300 mt-1">
{model.description}
{obs.description}
</div>
<div className="text-[10px] text-muted-foreground mt-2 font-mono">
ID: {model.id}
ID: {obs.id}
</div>
</div>
))}
@ -665,10 +665,10 @@ export function ThinkView() {
<CardTitle className="text-base">Based On</CardTitle>
<CardDescription className="text-xs">
{(result.based_on?.memories?.length || 0) +
(result.based_on?.mental_models?.filter(
(m: any) => m.subtype !== "directive"
(result.based_on?.observations?.filter(
(o: any) => o.subtype !== "directive"
)?.length || 0) +
(result.trace?.mental_models?.filter((m: any) => m.subtype === "directive")
(result.trace?.observations?.filter((o: any) => o.subtype === "directive")
?.length || 0)}{" "}
items used
</CardDescription>
@ -685,8 +685,7 @@ export function ThinkView() {
</div>
</div>
) : (result.based_on?.memories && result.based_on.memories.length > 0) ||
(result.based_on?.mental_models &&
result.based_on.mental_models.length > 0) ? (
(result.based_on?.observations && result.based_on.observations.length > 0) ? (
<div className="space-y-4 max-h-[500px] overflow-y-auto">
{(() => {
const memories = result.based_on?.memories || [];
@ -695,12 +694,12 @@ export function ThinkView() {
(f: any) => f.type === "experience"
);
const opinionFacts = memories.filter((f: any) => f.type === "opinion");
const mentalModels = (result.based_on?.mental_models || []).filter(
(m: any) => m.subtype !== "directive"
const observations = (result.based_on?.observations || []).filter(
(o: any) => o.subtype !== "directive"
);
const directives =
result.trace?.mental_models?.filter(
(m: any) => m.subtype === "directive"
result.trace?.observations?.filter(
(o: any) => o.subtype === "directive"
) || [];
return (
@ -742,21 +741,21 @@ export function ThinkView() {
</div>
)}
{/* Mental Models */}
{mentalModels.length > 0 && (
{/* Observations */}
{observations.length > 0 && (
<div className="space-y-1.5">
<div className="flex items-center gap-2 text-xs font-semibold text-orange-600 dark:text-orange-400">
<div className="w-2 h-2 rounded-full bg-orange-500" />
Mental Models ({mentalModels.length})
Observations ({observations.length})
</div>
<div className="space-y-1.5">
{mentalModels.map((model: any, i: number) => (
{observations.map((obs: any, i: number) => (
<div
key={i}
className="p-2 bg-muted rounded text-xs cursor-pointer hover:bg-muted/80 transition-colors"
onClick={() => handleSelectMentalModel(model)}
onClick={() => handleSelectObservation(obs)}
>
<div className="font-medium">{model.name}</div>
<div className="font-medium">{obs.name}</div>
</div>
))}
</div>
@ -999,73 +998,43 @@ export function ThinkView() {
</div>
)}
{/* Mental Model Detail Panel */}
{selectedMentalModel && (
{/* Observation Detail Panel */}
{selectedObservation && (
<div className="fixed right-0 top-0 h-screen w-[420px] bg-card border-l shadow-2xl z-50 overflow-y-auto">
<div className="p-6">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-2">
<Brain className="w-5 h-5" />
<h2 className="text-lg font-semibold">Mental Model</h2>
<h2 className="text-lg font-semibold">Observation</h2>
</div>
<Button
variant="ghost"
size="icon"
onClick={() => {
setSelectedMentalModel(null);
setFullMentalModel(null);
setSelectedObservation(null);
setFullObservation(null);
}}
>
<X className="w-4 h-4" />
</Button>
</div>
{loadingMentalModel ? (
{loadingObservation ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
) : (
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-muted-foreground">Name</h3>
<h3 className="text-sm font-medium text-muted-foreground">Text</h3>
<p className="mt-1 font-medium">
{fullMentalModel?.name || selectedMentalModel.name}
{fullObservation?.text || selectedObservation.text}
</p>
</div>
{fullMentalModel?.description && (
<div>
<h3 className="text-sm font-medium text-muted-foreground">Description</h3>
<p className="mt-1 text-sm">{fullMentalModel.description}</p>
</div>
)}
<div className="flex gap-4">
<div>
<h3 className="text-sm font-medium text-muted-foreground">Type</h3>
<p className="mt-1 text-sm">{selectedMentalModel.type}</p>
</div>
<div>
<h3 className="text-sm font-medium text-muted-foreground">Subtype</h3>
<span
className={`inline-block mt-1 text-xs px-2 py-0.5 rounded ${
selectedMentalModel.subtype === "structural"
? "bg-blue-500/10 text-blue-600"
: selectedMentalModel.subtype === "emergent"
? "bg-emerald-500/10 text-emerald-600"
: selectedMentalModel.subtype === "learned"
? "bg-violet-500/10 text-violet-600"
: selectedMentalModel.subtype === "directive"
? "bg-rose-500/10 text-rose-600"
: "bg-muted"
}`}
>
{selectedMentalModel.subtype}
</span>
</div>
</div>
{fullMentalModel?.tags && fullMentalModel.tags.length > 0 && (
{fullObservation?.tags && fullObservation.tags.length > 0 && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-1">Tags</h3>
<div className="flex flex-wrap gap-1">
{fullMentalModel.tags.map((tag: string) => (
{fullObservation.tags.map((tag: string) => (
<span
key={tag}
className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground flex items-center gap-1"
@ -1077,23 +1046,17 @@ export function ThinkView() {
</div>
</div>
)}
{fullMentalModel?.observations && fullMentalModel.observations.length > 0 && (
{fullObservation?.source_memories && fullObservation.source_memories.length > 0 && (
<div>
<h3 className="text-sm font-medium text-muted-foreground mb-2">
Observations ({fullMentalModel.observations.length})
Source Memories ({fullObservation.source_memories.length})
</h3>
<div className="space-y-2">
{fullMentalModel.observations.map((obs: any, i: number) => (
{fullObservation.source_memories.map((mem: any, i: number) => (
<div key={i} className="p-3 bg-muted rounded-lg">
{obs.title && <div className="font-medium text-sm mb-1">{obs.title}</div>}
<div className="text-sm text-muted-foreground whitespace-pre-wrap">
{obs.content || obs.text || (typeof obs === "string" ? obs : "")}
{mem.text || (typeof mem === "string" ? mem : "")}
</div>
{obs.memory_ids && obs.memory_ids.length > 0 && (
<div className="mt-2 text-xs text-muted-foreground">
Based on {obs.memory_ids.length} memories
</div>
)}
</div>
))}
</div>
@ -1102,7 +1065,7 @@ export function ThinkView() {
<div className="pt-2 border-t">
<h3 className="text-sm font-medium text-muted-foreground">ID</h3>
<p className="mt-1 font-mono text-xs text-muted-foreground">
{selectedMentalModel.id}
{selectedObservation.id}
</p>
</div>
</div>

View file

@ -51,7 +51,7 @@ export class ControlPlaneClient {
include?: {
entities?: { max_tokens: number } | null;
chunks?: { max_tokens: number } | null;
mental_models?: { max_results?: number } | null;
observations?: { max_results?: number } | null;
};
query_timestamp?: string;
tags?: string[];
@ -244,14 +244,14 @@ export class ControlPlaneClient {
}
/**
* Clear all mental models for a bank
* Clear all observations for a bank
*/
async clearMentalModels(bankId: string) {
async clearObservations(bankId: string) {
return this.fetchApi<{
success: boolean;
message: string;
deleted_count: number;
}>(`/api/banks/${bankId}/mental-models`, {
}>(`/api/banks/${bankId}/observations`, {
method: "DELETE",
});
}
@ -470,12 +470,12 @@ export class ControlPlaneClient {
});
}
// ============= MENTAL MODELS (auto-consolidated, read-only) =============
// ============= OBSERVATIONS (auto-consolidated, read-only) =============
/**
* List mental models for a bank (auto-consolidated knowledge)
* List observations for a bank (auto-consolidated knowledge)
*/
async listMentalModels(bankId: string, tags?: string[], tagsMatch?: string) {
async listObservations(bankId: string, tags?: string[], tagsMatch?: string) {
const params = new URLSearchParams();
if (tags && tags.length > 0) {
tags.forEach((t) => params.append("tags", t));
@ -508,13 +508,13 @@ export class ControlPlaneClient {
created_at: string;
updated_at: string;
}>;
}>(`/api/banks/${bankId}/mental-models${query ? `?${query}` : ""}`);
}>(`/api/banks/${bankId}/observations${query ? `?${query}` : ""}`);
}
/**
* Get a mental model with source memories
* Get an observation with source memories
*/
async getMentalModel(bankId: string, modelId: string) {
async getObservation(bankId: string, observationId: string) {
return this.fetchApi<{
id: string;
bank_id: string;
@ -537,15 +537,15 @@ export class ControlPlaneClient {
}>;
created_at: string;
updated_at: string;
}>(`/api/banks/${bankId}/mental-models/${modelId}`);
}>(`/api/banks/${bankId}/observations/${observationId}`);
}
// ============= REFLECTIONS =============
// ============= MENTAL MODELS (stored reflect responses) =============
/**
* List reflections for a bank
* List mental models for a bank
*/
async listReflections(bankId: string, tags?: string[], tagsMatch?: string) {
async listMentalModels(bankId: string, tags?: string[], tagsMatch?: string) {
const params = new URLSearchParams();
if (tags && tags.length > 0) {
tags.forEach((t) => params.append("tags", t));
@ -567,17 +567,16 @@ export class ControlPlaneClient {
reflect_response?: {
text: string;
based_on: Record<string, Array<{ id: string; text: string; type: string }>>;
mental_models?: Array<{ id: string; text: string }>;
};
}>;
}>(`/api/banks/${bankId}/reflections${query ? `?${query}` : ""}`);
}>(`/api/banks/${bankId}/mental-models${query ? `?${query}` : ""}`);
}
/**
* Create a reflection (async - content auto-generated in background)
* Create a mental model (async - content auto-generated in background)
* Returns operation_id to track progress
*/
async createReflection(
async createMentalModel(
bankId: string,
params: {
name: string;
@ -588,16 +587,16 @@ export class ControlPlaneClient {
) {
return this.fetchApi<{
operation_id: string;
}>(`/api/banks/${bankId}/reflections`, {
}>(`/api/banks/${bankId}/mental-models`, {
method: "POST",
body: JSON.stringify(params),
});
}
/**
* Get a reflection
* Get a mental model
*/
async getReflection(bankId: string, reflectionId: string) {
async getMentalModel(bankId: string, mentalModelId: string) {
return this.fetchApi<{
id: string;
bank_id: string;
@ -610,17 +609,17 @@ export class ControlPlaneClient {
reflect_response?: {
text: string;
based_on: Record<string, Array<{ id: string; text: string; type: string }>>;
mental_models?: Array<{ id: string; text: string }>;
observations?: Array<{ id: string; text: string }>;
};
}>(`/api/banks/${bankId}/reflections/${reflectionId}`);
}>(`/api/banks/${bankId}/mental-models/${mentalModelId}`);
}
/**
* Update a reflection
* Update a mental model
*/
async updateReflection(
async updateMentalModel(
bankId: string,
reflectionId: string,
mentalModelId: string,
params: {
name?: string;
}
@ -637,30 +636,30 @@ export class ControlPlaneClient {
reflect_response?: {
text: string;
based_on: Record<string, Array<{ id: string; text: string; type: string }>>;
mental_models?: Array<{ id: string; text: string }>;
observations?: Array<{ id: string; text: string }>;
};
}>(`/api/banks/${bankId}/reflections/${reflectionId}`, {
}>(`/api/banks/${bankId}/mental-models/${mentalModelId}`, {
method: "PATCH",
body: JSON.stringify(params),
});
}
/**
* Delete a reflection
* Delete a mental model
*/
async deleteReflection(bankId: string, reflectionId: string) {
return this.fetchApi(`/api/banks/${bankId}/reflections/${reflectionId}`, {
async deleteMentalModel(bankId: string, mentalModelId: string) {
return this.fetchApi(`/api/banks/${bankId}/mental-models/${mentalModelId}`, {
method: "DELETE",
});
}
/**
* Refresh a reflection (re-run source query) - async operation
* Refresh a mental model (re-run source query) - async operation
*/
async refreshReflection(bankId: string, reflectionId: string) {
async refreshMentalModel(bankId: string, mentalModelId: string) {
return this.fetchApi<{
operation_id: string;
}>(`/api/banks/${bankId}/reflections/${reflectionId}/refresh`, {
}>(`/api/banks/${bankId}/mental-models/${mentalModelId}/refresh`, {
method: "POST",
});
}
@ -673,7 +672,7 @@ export class ControlPlaneClient {
return this.fetchApi<{
api_version: string;
features: {
mental_models: boolean;
observations: boolean;
mcp: boolean;
worker: boolean;
};

View file

@ -4,7 +4,7 @@ import React, { createContext, useContext, useState, useEffect } from "react";
import { client } from "./api";
interface Features {
mental_models: boolean;
observations: boolean;
mcp: boolean;
worker: boolean;
}
@ -16,7 +16,7 @@ interface FeaturesContextType {
}
const defaultFeatures: Features = {
mental_models: false,
observations: false,
mcp: false,
worker: false,
};

View file

@ -48,7 +48,7 @@ OpenAI Assistant (analyzes, gives advice)
|
Function Call: store_memory(advice as experience)
|
Hindsight API (stores coach's advice, consolidates into mental models)
Hindsight API (stores coach's advice, consolidates into observations)
|
Personalized Answer
```
@ -126,7 +126,7 @@ retrieve_memories(query, fact_types, top_k)
search_workouts(after_date, before_date, workout_type)
get_nutrition_summary(after_date, before_date)
get_user_goals()
get_coach_insights(about) # Retrieves mental models
get_coach_insights(about) # Retrieves observations
```
Each function makes API calls to Hindsight to fetch relevant memories.
@ -192,7 +192,7 @@ The OpenAI Agent can retrieve different memory types from Hindsight:
- **World Facts** (`fact_type: "world"`): Workouts, meals, activities
- **Experience Facts** (`fact_type: "experience"`): Goals, intentions, coach advice
- **Mental Models** (`fact_type: "mental_model"`): Consolidated knowledge about user patterns
- **Observations** (`fact_type: "observation"`): Consolidated knowledge about user patterns
## Customization
@ -266,7 +266,7 @@ The key benefit: **Separation of concerns**
**Use Hindsight directly when:**
- You want a complete memory-first solution
- You want automatic memory retrieval and mental model consolidation
- You want automatic memory retrieval and observation consolidation
- You want to use different LLM providers (not just OpenAI)
- You want the `/reflect` endpoint's integrated approach

View file

@ -127,7 +127,7 @@ for r in results.results:
## Reflect: Generate Insights
The `reflect` operation performs reasoning over existing memories using the bank's disposition. It retrieves relevant facts and mental models to generate contextual responses.
The `reflect` operation performs reasoning over existing memories using the bank's disposition. It retrieves relevant facts and observations to generate contextual responses.
Example use cases:
- An AI Project Manager reflecting on what risks need to be mitigated
@ -142,11 +142,11 @@ print(response)
## Memory Types
Hindsight organizes knowledge into facts and consolidated mental models:
Hindsight organizes knowledge into facts and consolidated observations:
- **World**: Facts about the world ("The stove gets hot")
- **Experience**: Agent's own experiences ("I touched the stove and it really hurt")
- **Mental Model**: Consolidated knowledge synthesized from facts ("Always be careful around hot surfaces")
- **Observation**: Consolidated knowledge synthesized from facts ("Always be careful around hot surfaces")
## Cleanup

View file

@ -78,7 +78,7 @@ The backup includes:
- Memory banks and their configuration
- Documents and chunks
- Entities and their relationships
- Memory units (facts, experiences, mental models)
- Memory units (facts, experiences, observations)
- Entity cooccurrences and memory links
:::note Consistency

View file

@ -89,7 +89,7 @@ hindsight recall my-bank "Tell me about Alice" -v
## Reflect: Reason with Disposition
Generate disposition-aware responses using memories and mental models.
Generate disposition-aware responses using memories and observations.
<Tabs>
<TabItem value="python" label="Python">
@ -104,7 +104,7 @@ Generate disposition-aware responses using memories and mental models.
# Basic reflect
hindsight reflect my-bank "Should we adopt TypeScript for our backend?"
# Verbose output (shows sources and mental models)
# Verbose output (shows sources and observations)
hindsight reflect my-bank "What are Alice's strengths for the team lead role?" -v
# With higher reasoning budget
@ -114,7 +114,7 @@ hindsight reflect my-bank "Analyze our tech stack" --budget high
</TabItem>
</Tabs>
**What happens:** Memories and mental models are recalled, bank disposition is applied, and the LLM reasons through the evidence to generate a response.
**What happens:** Memories and observations are recalled, bank disposition is applied, and the LLM reasons through the evidence to generate a response.
**See:** [Reflect Details](./reflect) for disposition configuration.
@ -126,9 +126,9 @@ hindsight reflect my-bank "Analyze our tech stack" --budget high
|---------|--------|--------|---------|
| **Purpose** | Store information | Find information | Reason about information |
| **Input** | Raw text/documents | Search query | Question/prompt |
| **Output** | Memory IDs | Ranked facts + mental models | Reasoned response |
| **Output** | Memory IDs | Ranked facts + observations | Reasoned response |
| **Uses LLM** | Yes (extraction) | No | Yes (generation) |
| **Uses mental models** | No | Yes | Yes |
| **Uses observations** | No | Yes | Yes |
| **Disposition** | No | No | Yes |
---

View file

@ -2,7 +2,7 @@
sidebar_position: 4
---
# Reflections
# Mental Models
User-curated summaries that provide high-quality, pre-computed answers for common queries.
@ -11,23 +11,23 @@ import TabItem from '@theme/TabItem';
import CodeSnippet from '@site/src/components/CodeSnippet';
{/* Import raw source files */}
import reflectionsPy from '!!raw-loader!@site/examples/api/reflections.py';
import mentalModelsPy from '!!raw-loader!@site/examples/api/mental-models.py';
## What Are Reflections?
## What Are Mental Models?
Reflections are **saved reflect responses** that you curate for your memory bank. When you create a reflection, Hindsight runs a reflect operation with your source query and stores the result. During future reflect calls, these pre-computed summaries are checked first — providing faster, more consistent answers.
Mental models are **saved reflect responses** that you curate for your memory bank. When you create a mental model, Hindsight runs a reflect operation with your source query and stores the result. During future reflect calls, these pre-computed summaries are checked first — providing faster, more consistent answers.
```mermaid
graph LR
A[Create Reflection] --> B[Run Reflect]
A[Create Mental Model] --> B[Run Reflect]
B --> C[Store Result]
C --> D[Future Queries]
D --> E{Match Found?}
E -->|Yes| F[Return Reflection]
E -->|Yes| F[Return Mental Model]
E -->|No| G[Run Full Reflect]
```
### Why Use Reflections?
### Why Use Mental Models?
| Benefit | Description |
|---------|-------------|
@ -40,27 +40,27 @@ graph LR
During reflect, the agent checks sources in priority order:
1. **Reflections** — User-curated summaries (highest priority)
2. **Mental Models** — Consolidated knowledge
1. **Mental Models** — User-curated summaries (highest priority)
2. **Observations** — Consolidated knowledge
3. **Raw Facts** — Ground truth memories
Reflections are checked first because they represent your explicitly curated knowledge.
Mental models are checked first because they represent your explicitly curated knowledge.
---
## Create a Reflection
## Create a Mental Model
Creating a reflection runs a reflect operation in the background and saves the result:
Creating a mental model runs a reflect operation in the background and saves the result:
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={reflectionsPy} section="create-reflection" language="python" />
<CodeSnippet code={mentalModelsPy} section="create-mental-model" language="python" />
</TabItem>
<TabItem value="cli" label="CLI">
```bash
# Create a reflection (async operation)
curl -X POST "http://localhost:8888/v1/default/banks/my-bank/reflections" \
# Create a mental model (async operation)
curl -X POST "http://localhost:8888/v1/default/banks/my-bank/mental-models" \
-H "Content-Type: application/json" \
-d '{
"name": "Team Communication Preferences",
@ -79,23 +79,23 @@ curl -X POST "http://localhost:8888/v1/default/banks/my-bank/reflections" \
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Human-readable name for the reflection |
| `name` | string | Yes | Human-readable name for the mental model |
| `source_query` | string | Yes | The query to run to generate content |
| `tags` | list | No | Tags for filtering during retrieval |
| `max_tokens` | int | No | Maximum tokens for the reflection content |
| `max_tokens` | int | No | Maximum tokens for the mental model content |
---
## List Reflections
## List Mental Models
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={reflectionsPy} section="list-reflections" language="python" />
<CodeSnippet code={mentalModelsPy} section="list-mental-models" language="python" />
</TabItem>
<TabItem value="cli" label="CLI">
```bash
curl "http://localhost:8888/v1/default/banks/my-bank/reflections"
curl "http://localhost:8888/v1/default/banks/my-bank/mental-models"
```
</TabItem>
@ -103,16 +103,16 @@ curl "http://localhost:8888/v1/default/banks/my-bank/reflections"
---
## Get a Reflection
## Get a Mental Model
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={reflectionsPy} section="get-reflection" language="python" />
<CodeSnippet code={mentalModelsPy} section="get-mental-model" language="python" />
</TabItem>
<TabItem value="cli" label="CLI">
```bash
curl "http://localhost:8888/v1/default/banks/my-bank/reflections/{reflection_id}"
curl "http://localhost:8888/v1/default/banks/my-bank/mental-models/{mental_model_id}"
```
</TabItem>
@ -122,30 +122,30 @@ curl "http://localhost:8888/v1/default/banks/my-bank/reflections/{reflection_id}
| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique reflection ID |
| `id` | string | Unique mental model ID |
| `bank_id` | string | Memory bank ID |
| `name` | string | Human-readable name |
| `source_query` | string | The query used to generate content |
| `content` | string | The generated reflection text |
| `content` | string | The generated mental model text |
| `tags` | list | Tags for filtering |
| `last_refreshed_at` | string | When the reflection was last updated |
| `created_at` | string | When the reflection was created |
| `last_refreshed_at` | string | When the mental model was last updated |
| `created_at` | string | When the mental model was created |
| `reflect_response` | object | Full reflect response including `based_on` facts |
---
## Refresh a Reflection
## Refresh a Mental Model
Re-run the source query to update the reflection with current knowledge:
Re-run the source query to update the mental model with current knowledge:
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={reflectionsPy} section="refresh-reflection" language="python" />
<CodeSnippet code={mentalModelsPy} section="refresh-mental-model" language="python" />
</TabItem>
<TabItem value="cli" label="CLI">
```bash
curl -X POST "http://localhost:8888/v1/default/banks/my-bank/reflections/{reflection_id}/refresh"
curl -X POST "http://localhost:8888/v1/default/banks/my-bank/mental-models/{mental_model_id}/refresh"
```
</TabItem>
@ -153,23 +153,23 @@ curl -X POST "http://localhost:8888/v1/default/banks/my-bank/reflections/{reflec
Refreshing is useful when:
- New memories have been retained that affect the topic
- Mental models have been updated
- You want to ensure the reflection reflects current knowledge
- Observations have been updated
- You want to ensure the mental model reflects current knowledge
---
## Update a Reflection
## Update a Mental Model
Update the reflection's name:
Update the mental model's name:
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={reflectionsPy} section="update-reflection" language="python" />
<CodeSnippet code={mentalModelsPy} section="update-mental-model" language="python" />
</TabItem>
<TabItem value="cli" label="CLI">
```bash
curl -X PATCH "http://localhost:8888/v1/default/banks/my-bank/reflections/{reflection_id}" \
curl -X PATCH "http://localhost:8888/v1/default/banks/my-bank/mental-models/{mental_model_id}" \
-H "Content-Type: application/json" \
-d '{"name": "Updated Team Communication Preferences"}'
```
@ -179,16 +179,16 @@ curl -X PATCH "http://localhost:8888/v1/default/banks/my-bank/reflections/{refle
---
## Delete a Reflection
## Delete a Mental Model
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={reflectionsPy} section="delete-reflection" language="python" />
<CodeSnippet code={mentalModelsPy} section="delete-mental-model" language="python" />
</TabItem>
<TabItem value="cli" label="CLI">
```bash
curl -X DELETE "http://localhost:8888/v1/default/banks/my-bank/reflections/{reflection_id}"
curl -X DELETE "http://localhost:8888/v1/default/banks/my-bank/mental-models/{mental_model_id}"
```
</TabItem>
@ -209,6 +209,6 @@ curl -X DELETE "http://localhost:8888/v1/default/banks/my-bank/reflections/{refl
## Next Steps
- [**Reflect**](./reflect) — How the agentic loop uses reflections
- [**Mental Models**](/developer/mental-models) — How knowledge is consolidated
- [**Operations**](./operations) — Track async reflection creation
- [**Reflect**](./reflect) — How the agentic loop uses mental models
- [**Observations**](/developer/observations) — How knowledge is consolidated
- [**Operations**](./operations) — Track async mental model creation

View file

@ -25,7 +25,7 @@ Support for external streaming platforms like Kafka for scale-out processing is
| Operation | Trigger | Description |
|-----------|---------|-------------|
| **batch_retain** | `retain_batch` with `async=True` | Processes large content batches in the background |
| **consolidate** | After `retain` | Consolidates new facts into mental models |
| **consolidate** | After `retain` | Consolidates new facts into observations |
## Async Retain Example

View file

@ -42,7 +42,7 @@ Make sure you've completed the [Quick Start](./quickstart) to install the client
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `query` | string | required | Natural language query |
| `types` | list | all | Filter: `world`, `experience`, `mental_model` |
| `types` | list | all | Filter: `world`, `experience`, `observation` |
| `budget` | string | "mid" | Budget level: `low`, `mid`, `high` |
| `max_tokens` | int | 4096 | Token budget for results |
| `trace` | bool | false | Enable trace output for debugging |
@ -68,15 +68,15 @@ Recall specific memory types:
<TabItem value="python" label="Python">
<CodeSnippet code={recallPy} section="recall-world-only" language="python" />
<CodeSnippet code={recallPy} section="recall-experience-only" language="python" />
<CodeSnippet code={recallPy} section="recall-mental-models-only" language="python" />
<CodeSnippet code={recallPy} section="recall-observations-only" language="python" />
</TabItem>
<TabItem value="cli" label="CLI">
<CodeSnippet code={recallSh} section="recall-fact-type" language="bash" />
</TabItem>
</Tabs>
:::tip About Mental Models
Mental models are consolidated knowledge synthesized from multiple facts. They capture patterns, preferences, and learnings that the memory bank has built up over time. Mental models are automatically created in the background after retain operations.
:::tip About Observations
Observations are consolidated knowledge synthesized from multiple facts. They capture patterns, preferences, and learnings that the memory bank has built up over time. Observations are automatically created in the background after retain operations.
:::
## Token Budget Management

View file

@ -11,7 +11,7 @@ When you call **reflect**, Hindsight runs an **agentic loop** that:
2. **Applies** the bank's disposition traits to shape the reasoning style
3. **Generates** a grounded answer with citations to the sources used
The agent has access to hierarchical retrieval tools (reflections → mental models → raw facts) and decides what information it needs to answer your query.
The agent has access to hierarchical retrieval tools (mental models → observations → raw facts) and decides what information it needs to answer your query.
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
@ -66,7 +66,7 @@ The `budget` parameter controls how thoroughly the agent searches for informatio
| `mid` | 1x base | Balanced exploration |
| `high` | 2x base | Complex questions, comprehensive analysis |
Higher budgets allow the agent more iterations to search reflections, mental models, and raw facts before generating a response. Use `high` for questions that require synthesizing information from multiple sources.
Higher budgets allow the agent more iterations to search mental models, observations, and raw facts before generating a response. Use `high` for questions that require synthesizing information from multiple sources.
### Max Tokens
@ -78,8 +78,8 @@ The `max_tokens` parameter limits the length of the final generated response. Th
|-------|------|-------------|
| `text` | string | The generated answer text |
| `used_memory_ids` | array | Memory IDs cited by the agent |
| `used_reflection_ids` | array | Reflection IDs cited by the agent |
| `used_mental_model_ids` | array | Mental model IDs cited by the agent |
| `used_observation_ids` | array | Observation IDs cited by the agent |
| `structured_output` | object | Parsed structured output (when `response_schema` provided) |
| `iterations` | int | Number of agent loop iterations |
| `tools_called` | int | Total number of tool calls made |
@ -123,8 +123,8 @@ The bank's disposition affects reflect responses:
The agent cites which sources it used to generate the response:
- `used_memory_ids` — Raw memory facts that were retrieved and cited
- `used_reflection_ids` — User-curated reflections that were used
- `used_mental_model_ids` — Consolidated mental models that were used
- `used_mental_model_ids` — User-curated mental models that were used
- `used_observation_ids` — Consolidated observations that were used
**Important:** Only IDs that were actually retrieved during the agent loop can be cited. The agent validates citations to prevent hallucinated references.

View file

@ -13,7 +13,7 @@ AI agents forget everything between sessions. Every conversation starts from zer
- **Simple vector search isn't enough** — "What did Alice do last spring?" requires temporal reasoning, not just semantic similarity
- **Facts get disconnected** — Knowing "Alice works at Google" and "Google is in Mountain View" should let you answer "Where does Alice work?" even if you never stored that directly
- **AI Agents need to consolidate knowledge** — A coding assistant that remembers "the user prefers functional programming" should consolidate this into a mental model and weigh it when making recommendations
- **AI Agents need to consolidate knowledge** — A coding assistant that remembers "the user prefers functional programming" should consolidate this into an observation and weigh it when making recommendations
- **Context matters** — The same information means different things to different memory banks with different personalities
Hindsight solves these problems with a memory system designed specifically for AI agents.
@ -31,12 +31,12 @@ graph LR
subgraph bank["<b>Memory Bank</b>"]
direction TB
MentalModels[Mental Models]
Observations[Observations]
MemEnt[Memories & Entities]
Chunks[Chunks]
Documents[Documents]
MentalModels --> MemEnt --> Chunks --> Documents
Observations --> MemEnt --> Chunks --> Documents
end
end
@ -53,13 +53,13 @@ graph LR
### Memory Types
Hindsight organizes knowledge into facts and consolidated mental models:
Hindsight organizes knowledge into facts and consolidated observations:
| Type | What it stores | Example |
|------|----------------|---------|
| **World** | Objective facts received | "Alice works at Google" |
| **Experience** | Bank's own actions and interactions | "I recommended Python to Bob" |
| **Mental Model** | Consolidated knowledge from facts | "The user prefers functional programming patterns"
| **Observation** | Consolidated knowledge from facts | "The user prefers functional programming patterns"
### Multi-Strategy Retrieval (TEMPR)
@ -88,13 +88,13 @@ graph LR
| **Graph** | Related entities, indirect connections |
| **Temporal** | "last spring", "in June", time ranges |
### Mental Model Consolidation
### Observation Consolidation
After memories are retained, Hindsight automatically consolidates related facts into **mental models** — synthesized knowledge representations that capture patterns and learnings:
After memories are retained, Hindsight automatically consolidates related facts into **observations** — synthesized knowledge representations that capture patterns and learnings:
- **Automatic synthesis**: New facts are analyzed and consolidated into existing or new mental models
- **Evidence tracking**: Each mental model tracks which facts support it
- **Continuous refinement**: Mental models evolve as new evidence arrives
- **Automatic synthesis**: New facts are analyzed and consolidated into existing or new observations
- **Evidence tracking**: Each observation tracks which facts support it
- **Continuous refinement**: Observations evolve as new evidence arrives
### Disposition Traits

View file

@ -4,7 +4,7 @@ sidebar_position: 5
# Multilingual Support
Hindsight automatically detects the language of your input and responds in the same language. This means facts, entities, and reflections are preserved in their original language without translation to English.
Hindsight automatically detects the language of your input and responds in the same language. This means facts, entities, and reflect responses are preserved in their original language without translation to English.
## How It Works

View file

@ -5,33 +5,33 @@ sidebar_position: 5
import CodeSnippet from '@site/src/components/CodeSnippet';
import recallPy from '!!raw-loader!@site/examples/api/recall.py';
# Mental Models: Knowledge Consolidation
# Observations: Knowledge Consolidation
After memories are retained, Hindsight automatically consolidates related facts into **mental models** — synthesized knowledge representations that capture patterns and learnings.
After memories are retained, Hindsight automatically consolidates related facts into **observations** — synthesized knowledge representations that capture patterns and learnings.
```mermaid
graph LR
A[New Facts] --> B[Consolidation Engine]
B --> C{Existing Model?}
C -->|Yes| D[Refine Model]
C -->|No| E[Create Model]
D --> F[Mental Models]
B --> C{Existing Observation?}
C -->|Yes| D[Refine Observation]
C -->|No| E[Create Observation]
D --> F[Observations]
E --> F
```
---
## What Are Mental Models?
## What Are Observations?
Mental models are **consolidated knowledge** synthesized from multiple facts. Unlike raw facts which are individual pieces of information, mental models represent patterns, preferences, and learnings that emerge from accumulated evidence.
Observations are **consolidated knowledge** synthesized from multiple facts. Unlike raw facts which are individual pieces of information, observations represent patterns, preferences, and learnings that emerge from accumulated evidence.
| Raw Facts | Mental Model |
| Raw Facts | Observation |
|-----------|--------------|
| "Alice prefers Python" | "Alice is a Python-focused developer who values readability and simplicity" |
| "Alice dislikes verbose code" | |
| "Alice recommends type hints" | |
Mental models provide:
Observations provide:
- **Synthesis**: Patterns that emerge from multiple facts
- **Context**: Richer understanding than individual facts
- **Efficiency**: Condensed knowledge for faster retrieval
@ -44,87 +44,87 @@ Mental models provide:
After `retain()` completes, the consolidation engine runs automatically:
1. **New facts analyzed** — Each new fact is compared against existing mental models
1. **New facts analyzed** — Each new fact is compared against existing observations
2. **Pattern detection** — Related facts are grouped and synthesized
3. **Model creation/update** — New mental models are created or existing ones refined
4. **Evidence tracking** — Each mental model maintains references to supporting facts
3. **Observation creation/update** — New observations are created or existing ones refined
4. **Evidence tracking** — Each observation maintains references to supporting facts
### Evidence-Based Evolution
Mental models evolve as new evidence arrives:
Observations evolve as new evidence arrives:
| Event | What the bank learns | Mental model state |
| Event | What the bank learns | Observation state |
|-------|---------------------|----------------|
| **Day 1** | "Redis is open source under BSD license" | "Redis is excellent for caching — fast, reliable, and OSS-friendly" (2 supporting facts) |
| **Day 2** | "Redis has great community support" | Mental model reinforced (3 supporting facts) |
| **Day 30** | "Redis changed license to SSPL" | Mental model refined: "Redis is technically strong, but has license concerns for cloud" |
| **Day 45** | "Valkey forked Redis under BSD" | New mental model: "Consider Valkey for new projects requiring true OSS" |
| **Day 2** | "Redis has great community support" | Observation reinforced (3 supporting facts) |
| **Day 30** | "Redis changed license to SSPL" | Observation refined: "Redis is technically strong, but has license concerns for cloud" |
| **Day 45** | "Valkey forked Redis under BSD" | New observation: "Consider Valkey for new projects requiring true OSS" |
### Handling Contradictory Evidence
What happens when a new fact contradicts an existing mental model?
What happens when a new fact contradicts an existing observation?
The consolidation engine doesn't blindly overwrite — it **reconciles** the contradiction by capturing the evolution:
**Example: User preference changes**
| Time | Fact | Mental Model |
| Time | Fact | Observation |
|------|------|--------------|
| Week 1 | "User says they love React" | "User prefers React for frontend development" |
| Week 2 | "User praises React's component model" | "User is enthusiastic about React, particularly its component model" |
| Week 3 | "User says they've switched to Vue and won't use React anymore" | "User was previously a React enthusiast who appreciated its component model, but has now switched to Vue and no longer uses React" |
Notice how the final mental model captures the **full journey** — not just "User prefers Vue" but the complete evolution of their preference. This nuanced understanding means:
Notice how the final observation captures the **full journey** — not just "User prefers Vue" but the complete evolution of their preference. This nuanced understanding means:
- Your agent won't recommend React tutorials to someone who explicitly moved away from it
- Your agent understands *why* this matters (they were enthusiastic before, so this is a deliberate choice)
- Your agent can reference this history when relevant ("I know you used to work with React...")
The system:
1. **Detects the conflict** — New fact contradicts existing model
2. **Preserves history** — Incorporates the previous understanding into the new model
3. **Creates nuanced model** — Synthesizes a richer understanding that captures the change
4. **Updates freshness** — Marks the model as recently updated
1. **Detects the conflict** — New fact contradicts existing observation
2. **Preserves history** — Incorporates the previous understanding into the new observation
3. **Creates nuanced observation** — Synthesizes a richer understanding that captures the change
4. **Updates freshness** — Marks the observation as recently updated
**Example: Correcting misinformation**
| Time | Fact | Mental Model |
| Time | Fact | Observation |
|------|------|--------------|
| Day 1 | "Alice works at Google" | "Alice is a Google employee" |
| Day 10 | "Alice actually works at Meta, not Google" | "Alice works at Meta (previously thought to work at Google)" |
When a fact explicitly corrects previous information, the mental model is updated to reflect the correction while noting the previous understanding. The raw facts are always preserved, so you can trace back to see what was originally stated and when it was corrected.
When a fact explicitly corrects previous information, the observation is updated to reflect the correction while noting the previous understanding. The raw facts are always preserved, so you can trace back to see what was originally stated and when it was corrected.
---
## Mental Models in Retrieval
## Observations in Retrieval
Mental models are automatically included in both `recall()` and `reflect()` operations:
Observations are automatically included in both `recall()` and `reflect()` operations:
### In Recall
Mental models are returned alongside raw facts, filtered by the `types` parameter:
Observations are returned alongside raw facts, filtered by the `types` parameter:
<CodeSnippet code={recallPy} section="recall-with-mental-models" language="python" />
<CodeSnippet code={recallPy} section="recall-with-observations" language="python" />
### In Reflect
The reflect agent uses **hierarchical retrieval**:
1. **[Reflections](/developer/api/reflections)** — User-curated summaries (highest priority)
2. **Mental Models** — Consolidated knowledge with freshness awareness
1. **[Mental Models](/developer/api/mental-models)** — User-curated summaries (highest priority)
2. **Observations** — Consolidated knowledge with freshness awareness
3. **Raw Facts** — Ground truth for verification
The agent automatically queries mental models and uses them to inform its reasoning.
The agent automatically queries observations and uses them to inform its reasoning.
---
## Freshness Awareness
Mental models track when they were last updated. During reflect, the agent considers freshness:
Observations track when they were last updated. During reflect, the agent considers freshness:
- **Fresh models**: Used directly for reasoning
- **Stale models**: Agent verifies against current facts before relying on them
- **Fresh observations**: Used directly for reasoning
- **Stale observations**: Agent verifies against current facts before relying on them
This ensures responses stay accurate even as the underlying data changes.
@ -132,7 +132,7 @@ This ensures responses stay accurate even as the underlying data changes.
## Mission-Oriented Consolidation
The bank's **mission** directly influences what knowledge gets consolidated into mental models. When you set a mission on your memory bank, the consolidation engine focuses on extracting knowledge that serves that mission.
The bank's **mission** directly influences what knowledge gets consolidated into observations. When you set a mission on your memory bank, the consolidation engine focuses on extracting knowledge that serves that mission.
**Example:**
@ -148,11 +148,11 @@ client.create_bank(
With this mission, the consolidation engine will:
- **Prioritize** customer preferences, issue patterns, and communication styles
- **Skip** ephemeral details that don't serve support goals
- **Synthesize** mental models focused on helping customers
- **Synthesize** observations focused on helping customers
Without a mission, the engine performs general-purpose consolidation. With a mission, it becomes focused and efficient — extracting only knowledge that matters for your use case.
| Mission | Mental Models Focus |
| Mission | Observations Focus |
|---------|-------------------|
| *Customer support agent* | Customer preferences, issue patterns, resolution history |
| *Code review assistant* | Coding patterns, team conventions, common mistakes |
@ -162,13 +162,13 @@ Without a mission, the engine performs general-purpose consolidation. With a mis
## Configuration
Mental model consolidation runs automatically. You can monitor consolidation via the [Operations API](./api/operations).
Observation consolidation runs automatically. You can monitor consolidation via the [Operations API](./api/operations).
---
## Next Steps
- [**Retain**](./retain) — How facts are stored and trigger consolidation
- [**Recall**](./retrieval) — How mental models are retrieved
- [**Reflect**](./reflect) — How the agentic loop uses mental models
- [**Reflections**](./api/reflections) — User-curated summaries for common queries
- [**Recall**](./retrieval) — How observations are retrieved
- [**Reflect**](./reflect) — How the agentic loop uses observations
- [**Mental Models**](./api/mental-models) — User-curated summaries for common queries

View file

@ -14,8 +14,8 @@ graph TB
subgraph agent["Reflect Agent Loop"]
A[Query] --> B{Need more info?}
B -->|Yes| C[Call Tools]
C --> D[search_reflections]
C --> E[search_mental_models]
C --> D[search_mental_models]
C --> E[search_observations]
C --> F[recall]
C --> G[expand]
D --> B
@ -34,9 +34,9 @@ graph TB
Unlike simple retrieval, reflect is an **agentic system** that:
1. **Autonomously gathers evidence** — The agent decides what information it needs and calls appropriate tools
2. **Uses hierarchical retrieval** — Checks reflections first, then mental models, then raw facts
2. **Uses hierarchical retrieval** — Checks mental models first, then observations, then raw facts
3. **Applies disposition** — Shapes reasoning based on the bank's personality traits
4. **Cites sources** — Returns which memories and mental models were used
4. **Cites sources** — Returns which memories and observations were used
### The Agentic Loop
@ -44,8 +44,8 @@ The reflect agent runs in a loop with access to these tools:
| Tool | Purpose | Priority |
|------|---------|----------|
| `search_reflections` | User-curated summaries | Highest (check first) |
| `search_mental_models` | Consolidated knowledge | High |
| `search_mental_models` | User-curated summaries | Highest (check first) |
| `search_observations` | Consolidated knowledge | High |
| `recall` | Raw facts (ground truth) | Fallback |
| `expand` | Get more context for a memory | As needed |
| `done` | Complete with final answer | When ready |
@ -59,13 +59,13 @@ The agent:
The agent uses a smart retrieval hierarchy:
1. **[Reflections](/developer/api/reflections)** — User-curated summaries you've pre-computed for common queries
2. **[Mental Models](/developer/mental-models)** — Consolidated knowledge with freshness awareness
3. **Raw Facts** — Ground truth for verification when models are stale
1. **[Mental Models](/developer/api/mental-models)** — User-curated summaries you've pre-computed for common queries
2. **[Observations](/developer/observations)** — Consolidated knowledge with freshness awareness
3. **Raw Facts** — Ground truth for verification when observations are stale
**Reflections** are saved reflect responses that you create for frequently asked questions. They're checked first because they represent explicitly curated knowledge. See the [Reflections API](/developer/api/reflections) for how to create and manage them.
**Mental models** are saved reflect responses that you create for frequently asked questions. They're checked first because they represent explicitly curated knowledge. See the [Mental Models API](/developer/api/mental-models) for how to create and manage them.
If a mental model is marked as **stale**, the agent automatically verifies it against current facts.
If an observation is marked as **stale**, the agent automatically verifies it against current facts.
---
@ -85,7 +85,7 @@ Without reflect:
With reflect:
- **Consistent character**: A "detail-oriented, cautious" bank emphasizes risks and thorough planning
- **Evolving knowledge**: Mental models strengthen and adapt as evidence accumulates
- **Evolving knowledge**: Observations strengthen and adapt as evidence accumulates
- **Contextual reasoning**: "Based on what I know about your team's remote work success..."
- **Differentiated behavior**: Support bots sound diplomatic, code reviewers sound direct
@ -162,7 +162,7 @@ When you call `reflect()`:
**Returns:**
- **Response text** — Disposition-influenced answer from the agent
- **based_on** — Evidence used: memories that grounded the response
- **trace** — Tool calls, LLM calls, and mental models accessed (when `include.tool_calls=True`)
- **trace** — Tool calls, LLM calls, and observations accessed (when `include.tool_calls=True`)
- **structured_output** — Parsed response if `response_schema` was provided
- **usage** — Token usage metrics
@ -183,8 +183,8 @@ When you call `reflect()`:
"llm_calls": [
{"scope": "agent_1", "duration_ms": 1200}
],
"mental_models": [
{"id": "mm-789", "name": "Alice", "type": "entity", "subtype": "structural"}
"observations": [
{"id": "obs-789", "name": "Alice", "type": "entity", "subtype": "structural"}
]
},
"usage": {"input_tokens": 1500, "output_tokens": 500, "total_tokens": 2000}
@ -204,13 +204,13 @@ Without disposition, all AI assistants sound the same. With disposition:
- **Creative assistants** can be open to unconventional ideas
- **Risk analysts** can be appropriately cautious
Disposition creates **consistent character** across conversations while mental models **evolve with evidence**.
Disposition creates **consistent character** across conversations while observations **evolve with evidence**.
---
## Next Steps
- [**Mental Models**](./mental-models) — How knowledge is consolidated
- [**Observations**](./observations) — How knowledge is consolidated
- [**Retain**](./retain) — How rich facts are stored
- [**Recall**](./retrieval) — How multi-strategy search works
- [**Reflect API**](./api/reflect) — Code examples and parameters

View file

@ -63,7 +63,7 @@ Hindsight distinguishes between **world** facts (about others) and **experience*
| **experience** | Conversations and events | "I recommended Python to Alice" |
**Note:** Mental models are consolidated automatically in the background after `retain()` operations complete. This consolidation process synthesizes patterns from new facts into the bank's knowledge base.
**Note:** Observations are consolidated automatically in the background after `retain()` operations complete. This consolidation process synthesizes patterns from new facts into the bank's knowledge base.
---
@ -176,24 +176,24 @@ All stored in your isolated **memory bank**, ready for `recall()` and `reflect()
---
## Mental Model Consolidation
## Observation Consolidation
After `retain()` completes, Hindsight automatically triggers **mental model consolidation** in the background. This process:
After `retain()` completes, Hindsight automatically triggers **observation consolidation** in the background. This process:
1. Analyzes new facts against existing mental models
2. Creates new mental models when patterns emerge
3. Refines existing mental models with new evidence
4. Tracks which facts support each mental model
1. Analyzes new facts against existing observations
2. Creates new observations when patterns emerge
3. Refines existing observations with new evidence
4. Tracks which facts support each observation
This happens asynchronously — your `retain()` call returns immediately while consolidation runs in the background.
See [Mental Models](./mental-models) for details on how consolidation works.
See [Observations](./observations) for details on how consolidation works.
---
## Next Steps
- [**Mental Models**](./mental-models) — How knowledge is consolidated after retain
- [**Observations**](./observations) — How knowledge is consolidated after retain
- [**Recall**](./retrieval) — How multi-strategy search retrieves relevant memories
- [**Reflect**](./reflect) — How the agentic loop uses mental models
- [**Reflect**](./reflect) — How the agentic loop uses observations
- [**Retain API**](./api/retain) — Code examples and parameters

View file

@ -133,7 +133,7 @@ Hindsight is built for AI agents, not humans. Traditional search systems return
**Parameters you control:**
- `max_tokens`: How much memory content to return (default: 4096 tokens)
- `budget`: Search depth level (low, mid, high)
- `types`: Filter by world, experience, mental_model, or all
- `types`: Filter by world, experience, observation, or all
- `tags`: Filter memories by visibility tags
- `tags_match`: How to match tags (see [Recall API](./api/recall) for all options)

View file

@ -74,7 +74,7 @@ hindsight memory recall <bank_id> "hiking recommendations" \
--max-tokens 8192
# Filter by fact type
hindsight memory recall <bank_id> "query" --fact-type world,mental_model
hindsight memory recall <bank_id> "query" --fact-type world,observation
# Show trace information
hindsight memory recall <bank_id> "query" --trace
@ -206,7 +206,7 @@ The explorer provides an interactive terminal interface to:
- **Browse memory banks** — View all banks and their statistics
- **Search memories** — Run recall queries with real-time results
- **Inspect entities** — Explore the knowledge graph and entity relationships
- **View facts** — Browse world facts, experiences, and mental models
- **View facts** — Browse world facts, experiences, and observations
- **Navigate documents** — See source documents and their extracted memories
### Keyboard Shortcuts

View file

@ -83,7 +83,7 @@ for (const r of response.results) {
// With options
const response = await client.recall('my-bank', 'What does Alice do?', {
types: ['world', 'mental_model'], // Filter by fact type
types: ['world', 'observation'], // Filter by fact type
maxTokens: 4096,
budget: 'high', // 'low', 'mid', or 'high'
});

View file

@ -150,7 +150,7 @@ for r in results.results:
results = client.recall(
bank_id="my-bank",
query="What does Alice do?",
types=["world", "mental_model"], # Filter by fact type
types=["world", "observation"], # Filter by fact type
max_tokens=4096,
budget="high", # low, mid, or high
)

View file

@ -1,14 +1,14 @@
#!/usr/bin/env python3
"""
Reflections API examples for Hindsight.
Run: python examples/api/reflections.py
Mental Models API examples for Hindsight.
Run: python examples/api/mental-models.py
"""
import os
import time
import requests
HINDSIGHT_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
BANK_ID = "reflections-demo-bank"
BANK_ID = "mental-models-demo-bank"
# =============================================================================
# Setup (not shown in docs)
@ -18,7 +18,7 @@ from hindsight_client import Hindsight
client = Hindsight(base_url=HINDSIGHT_URL)
# Create bank and seed some data
client.create_bank(bank_id=BANK_ID, name="Reflections Demo")
client.create_bank(bank_id=BANK_ID, name="Mental Models Demo")
client.retain(bank_id=BANK_ID, content="The team prefers async communication via Slack")
client.retain(bank_id=BANK_ID, content="For urgent issues, use the #incidents channel")
client.retain(bank_id=BANK_ID, content="Weekly syncs happen every Monday at 10am")
@ -30,10 +30,10 @@ time.sleep(2)
# Doc Examples
# =============================================================================
# [docs:create-reflection]
# Create a reflection (runs reflect in background)
# [docs:create-mental-model]
# Create a mental model (runs reflect in background)
response = requests.post(
f"{HINDSIGHT_URL}/v1/default/banks/{BANK_ID}/reflections",
f"{HINDSIGHT_URL}/v1/default/banks/{BANK_ID}/mental-models",
json={
"name": "Team Communication Preferences",
"source_query": "How does the team prefer to communicate?",
@ -44,66 +44,66 @@ result = response.json()
# Returns an operation_id - check operations endpoint for completion
print(f"Operation ID: {result['operation_id']}")
# [/docs:create-reflection]
# [/docs:create-mental-model]
# Wait for the reflection to be created
# Wait for the mental model to be created
time.sleep(5)
# [docs:list-reflections]
# List all reflections in a bank
response = requests.get(f"{HINDSIGHT_URL}/v1/default/banks/{BANK_ID}/reflections")
reflections = response.json()
# [docs:list-mental-models]
# List all mental models in a bank
response = requests.get(f"{HINDSIGHT_URL}/v1/default/banks/{BANK_ID}/mental-models")
mental_models = response.json()
for reflection in reflections["items"]:
print(f"- {reflection['name']}: {reflection['source_query']}")
# [/docs:list-reflections]
for mental_model in mental_models["items"]:
print(f"- {mental_model['name']}: {mental_model['source_query']}")
# [/docs:list-mental-models]
# Get the reflection ID for subsequent examples
reflection_id = reflections["items"][0]["id"] if reflections["items"] else None
# Get the mental model ID for subsequent examples
mental_model_id = mental_models["items"][0]["id"] if mental_models["items"] else None
if reflection_id:
# [docs:get-reflection]
# Get a specific reflection
if mental_model_id:
# [docs:get-mental-model]
# Get a specific mental model
response = requests.get(
f"{HINDSIGHT_URL}/v1/default/banks/{BANK_ID}/reflections/{reflection_id}"
f"{HINDSIGHT_URL}/v1/default/banks/{BANK_ID}/mental-models/{mental_model_id}"
)
reflection = response.json()
mental_model = response.json()
print(f"Name: {reflection['name']}")
print(f"Content: {reflection['content']}")
print(f"Last refreshed: {reflection['last_refreshed_at']}")
# [/docs:get-reflection]
print(f"Name: {mental_model['name']}")
print(f"Content: {mental_model['content']}")
print(f"Last refreshed: {mental_model['last_refreshed_at']}")
# [/docs:get-mental-model]
# [docs:refresh-reflection]
# Refresh a reflection to update with current knowledge
# [docs:refresh-mental-model]
# Refresh a mental model to update with current knowledge
response = requests.post(
f"{HINDSIGHT_URL}/v1/default/banks/{BANK_ID}/reflections/{reflection_id}/refresh"
f"{HINDSIGHT_URL}/v1/default/banks/{BANK_ID}/mental-models/{mental_model_id}/refresh"
)
result = response.json()
print(f"Refresh operation ID: {result['operation_id']}")
# [/docs:refresh-reflection]
# [/docs:refresh-mental-model]
# [docs:update-reflection]
# Update a reflection's name
# [docs:update-mental-model]
# Update a mental model's name
response = requests.patch(
f"{HINDSIGHT_URL}/v1/default/banks/{BANK_ID}/reflections/{reflection_id}",
f"{HINDSIGHT_URL}/v1/default/banks/{BANK_ID}/mental-models/{mental_model_id}",
json={"name": "Updated Team Communication Preferences"}
)
updated = response.json()
print(f"Updated name: {updated['name']}")
# [/docs:update-reflection]
# [/docs:update-mental-model]
# [docs:delete-reflection]
# Delete a reflection
# [docs:delete-mental-model]
# Delete a mental model
requests.delete(
f"{HINDSIGHT_URL}/v1/default/banks/{BANK_ID}/reflections/{reflection_id}"
f"{HINDSIGHT_URL}/v1/default/banks/{BANK_ID}/mental-models/{mental_model_id}"
)
# [/docs:delete-reflection]
# [/docs:delete-mental-model]
# =============================================================================
@ -111,4 +111,4 @@ if reflection_id:
# =============================================================================
requests.delete(f"{HINDSIGHT_URL}/v1/default/banks/{BANK_ID}")
print("reflections.py: All examples passed")
print("mental-models.py: All examples passed")

View file

@ -67,31 +67,31 @@ experience = client.recall(
# [/docs:recall-experience-only]
# [docs:recall-mental-models-only]
# Only mental models (consolidated knowledge)
mental_models = client.recall(
# [docs:recall-observations-only]
# Only observations (consolidated knowledge)
observations = client.recall(
bank_id="my-bank",
query="What patterns have I learned?",
types=["mental_model"]
types=["observation"]
)
# [/docs:recall-mental-models-only]
# [/docs:recall-observations-only]
# [docs:recall-with-mental-models]
# Include mental models in recall
# [docs:recall-with-observations]
# Include observations in recall
results = client.recall(
bank_id="my-bank",
query="What programming languages does Alice prefer?",
types=["world", "experience", "mental_model"]
types=["world", "experience", "observation"]
)
# Mental models only
models = client.recall(
# Observations only
observations = client.recall(
bank_id="my-bank",
query="What patterns have I learned?",
types=["mental_model"]
types=["observation"]
)
# [/docs:recall-with-mental-models]
# [/docs:recall-with-observations]
# [docs:recall-token-budget]

View file

@ -29,7 +29,7 @@ hindsight memory recall my-bank "hiking recommendations" \
# [docs:recall-fact-type]
hindsight memory recall my-bank "query" --fact-type world,mental_model
hindsight memory recall my-bank "query" --fact-type world,observation
# [/docs:recall-fact-type]

View file

@ -83,13 +83,15 @@ const responseSchema = {
required: ['recommendation', 'confidence', 'key_factors'],
};
const structuredResponse = await client.reflect('hiring-team', 'Should we hire Alice for the ML team lead position?', {
const structuredResponse = await client.reflect('my-bank', 'What do you know about Alice and her career?', {
responseSchema: responseSchema,
});
// Structured output
console.log(structuredResponse.structuredOutput.recommendation);
console.log(structuredResponse.structuredOutput.keyFactors);
// Structured output (if returned)
if (structuredResponse.structuredOutput) {
console.log('Recommendation:', structuredResponse.structuredOutput.recommendation || 'N/A');
console.log('Key factors:', structuredResponse.structuredOutput.key_factors || []);
}
// [/docs:reflect-structured-output]

View file

@ -33,20 +33,25 @@ hindsight memory reflect my-bank "Summarize my week" --budget high
# [docs:reflect-structured-output]
# First, create a JSON schema file schema.json:
# {
# "type": "object",
# "properties": {
# "recommendation": {"type": "string"},
# "confidence": {"type": "string", "enum": ["low", "medium", "high"]},
# "key_factors": {"type": "array", "items": {"type": "string"}}
# },
# "required": ["recommendation", "confidence", "key_factors"]
# }
cat > schema.json << 'EOF'
{
"type": "object",
"properties": {
"recommendation": {"type": "string"},
"confidence": {"type": "string", "enum": ["low", "medium", "high"]},
"key_factors": {"type": "array", "items": {"type": "string"}}
},
"required": ["recommendation", "confidence", "key_factors"]
}
EOF
# Then use the --schema flag:
hindsight memory reflect hiring-team \
"Should we hire Alice for the ML team lead position?" \
--schema schema.json
# Cleanup the temporary schema file
rm -f schema.json
# [/docs:reflect-structured-output]

View file

@ -29,8 +29,8 @@ const sidebars: SidebarsConfig = {
},
{
type: 'doc',
id: 'developer/mental-models',
label: 'Mental Models',
id: 'developer/observations',
label: 'Observations',
},
{
type: 'doc',
@ -81,8 +81,8 @@ const sidebars: SidebarsConfig = {
},
{
type: 'doc',
id: 'developer/api/reflections',
label: 'Reflections',
id: 'developer/api/mental-models',
label: 'Mental Models',
},
{
type: 'doc',

View file

@ -804,14 +804,14 @@
}
}
},
"/v1/default/banks/{bank_id}/reflections": {
"/v1/default/banks/{bank_id}/mental-models": {
"get": {
"tags": [
"Reflections"
"Mental Models"
],
"summary": "List reflections",
"summary": "List mental models",
"description": "List user-curated living documents that stay current.",
"operationId": "list_reflections",
"operationId": "list_mental_models",
"parameters": [
{
"name": "bank_id",
@ -906,7 +906,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ReflectionListResponse"
"$ref": "#/components/schemas/MentalModelListResponse"
}
}
}
@ -925,11 +925,11 @@
},
"post": {
"tags": [
"Reflections"
"Mental Models"
],
"summary": "Create reflection",
"description": "Create a reflection by running reflect with the source query in the background. Returns an operation ID to track progress. The content is auto-generated by the reflect endpoint. Use the operations endpoint to check completion status.",
"operationId": "create_reflection",
"summary": "Create mental model",
"description": "Create a mental model by running reflect with the source query in the background. Returns an operation ID to track progress. The content is auto-generated by the reflect endpoint. Use the operations endpoint to check completion status.",
"operationId": "create_mental_model",
"parameters": [
{
"name": "bank_id",
@ -962,7 +962,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateReflectionRequest"
"$ref": "#/components/schemas/CreateMentalModelRequest"
}
}
}
@ -973,7 +973,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateReflectionResponse"
"$ref": "#/components/schemas/CreateMentalModelResponse"
}
}
}
@ -991,14 +991,14 @@
}
}
},
"/v1/default/banks/{bank_id}/reflections/{reflection_id}": {
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}": {
"get": {
"tags": [
"Reflections"
"Mental Models"
],
"summary": "Get reflection",
"description": "Get a specific reflection by ID.",
"operationId": "get_reflection",
"summary": "Get mental model",
"description": "Get a specific mental model by ID.",
"operationId": "get_mental_model",
"parameters": [
{
"name": "bank_id",
@ -1010,12 +1010,12 @@
}
},
{
"name": "reflection_id",
"name": "mental_model_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Reflection Id"
"title": "Mental Model Id"
}
},
{
@ -1041,7 +1041,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ReflectionResponse"
"$ref": "#/components/schemas/MentalModelResponse"
}
}
}
@ -1060,11 +1060,11 @@
},
"patch": {
"tags": [
"Reflections"
"Mental Models"
],
"summary": "Update reflection",
"description": "Update a reflection's name.",
"operationId": "update_reflection",
"summary": "Update mental model",
"description": "Update a mental model's name.",
"operationId": "update_mental_model",
"parameters": [
{
"name": "bank_id",
@ -1076,12 +1076,12 @@
}
},
{
"name": "reflection_id",
"name": "mental_model_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Reflection Id"
"title": "Mental Model Id"
}
},
{
@ -1106,7 +1106,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateReflectionRequest"
"$ref": "#/components/schemas/UpdateMentalModelRequest"
}
}
}
@ -1117,7 +1117,7 @@
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ReflectionResponse"
"$ref": "#/components/schemas/MentalModelResponse"
}
}
}
@ -1136,11 +1136,11 @@
},
"delete": {
"tags": [
"Reflections"
"Mental Models"
],
"summary": "Delete reflection",
"description": "Delete a reflection.",
"operationId": "delete_reflection",
"summary": "Delete mental model",
"description": "Delete a mental model.",
"operationId": "delete_mental_model",
"parameters": [
{
"name": "bank_id",
@ -1152,12 +1152,12 @@
}
},
{
"name": "reflection_id",
"name": "mental_model_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Reflection Id"
"title": "Mental Model Id"
}
},
{
@ -1199,14 +1199,14 @@
}
}
},
"/v1/default/banks/{bank_id}/reflections/{reflection_id}/refresh": {
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/refresh": {
"post": {
"tags": [
"Reflections"
"Mental Models"
],
"summary": "Refresh reflection",
"summary": "Refresh mental model",
"description": "Submit an async task to re-run the source query through reflect and update the content.",
"operationId": "refresh_reflection",
"operationId": "refresh_mental_model",
"parameters": [
{
"name": "bank_id",
@ -1218,12 +1218,12 @@
}
},
{
"name": "reflection_id",
"name": "mental_model_id",
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Reflection Id"
"title": "Mental Model Id"
}
},
{
@ -2690,14 +2690,14 @@
}
}
},
"/v1/default/banks/{bank_id}/mental-models": {
"/v1/default/banks/{bank_id}/observations": {
"delete": {
"tags": [
"Banks"
],
"summary": "Clear all mental models",
"description": "Delete all mental models for a memory bank. This is useful for resetting the consolidated knowledge.",
"operationId": "clear_mental_models",
"summary": "Clear all observations",
"description": "Delete all observations for a memory bank. This is useful for resetting the consolidated knowledge.",
"operationId": "clear_observations",
"parameters": [
{
"name": "bank_id",
@ -2755,7 +2755,7 @@
"Banks"
],
"summary": "Trigger consolidation",
"description": "Run memory consolidation to create/update mental models from recent memories.",
"description": "Run memory consolidation to create/update observations from recent memories.",
"operationId": "trigger_consolidation",
"parameters": [
{
@ -3260,13 +3260,13 @@
"pending_consolidation": {
"type": "integer",
"title": "Pending Consolidation",
"description": "Number of memories not yet processed into mental models",
"description": "Number of memories not yet processed into observations",
"default": 0
},
"total_mental_models": {
"total_observations": {
"type": "integer",
"title": "Total Mental Models",
"description": "Total number of mental models",
"title": "Total Observations",
"description": "Total number of observations",
"default": 0
}
},
@ -3315,8 +3315,8 @@
"pending_operations": 2,
"total_documents": 10,
"total_links": 300,
"total_mental_models": 45,
"total_nodes": 150
"total_nodes": 150,
"total_observations": 45
}
},
"Budget": {
@ -3571,12 +3571,12 @@
"title": "CreateDirectiveRequest",
"description": "Request model for creating a directive."
},
"CreateReflectionRequest": {
"CreateMentalModelRequest": {
"properties": {
"name": {
"type": "string",
"title": "Name",
"description": "Human-readable name for the reflection"
"description": "Human-readable name for the mental model"
},
"source_query": {
"type": "string",
@ -3605,8 +3605,8 @@
"name",
"source_query"
],
"title": "CreateReflectionRequest",
"description": "Request model for creating a reflection.",
"title": "CreateMentalModelRequest",
"description": "Request model for creating a mental model.",
"example": {
"max_tokens": 2048,
"name": "Team Communication Preferences",
@ -3616,7 +3616,7 @@
]
}
},
"CreateReflectionResponse": {
"CreateMentalModelResponse": {
"properties": {
"operation_id": {
"type": "string",
@ -3628,8 +3628,8 @@
"required": [
"operation_id"
],
"title": "CreateReflectionResponse",
"description": "Response model for reflection creation."
"title": "CreateMentalModelResponse",
"description": "Response model for mental model creation."
},
"DeleteDocumentResponse": {
"properties": {
@ -4192,10 +4192,10 @@
},
"FeaturesInfo": {
"properties": {
"mental_models": {
"observations": {
"type": "boolean",
"title": "Mental Models",
"description": "Whether mental models (auto-consolidation) are enabled"
"title": "Observations",
"description": "Whether observations (auto-consolidation) are enabled"
},
"mcp": {
"type": "boolean",
@ -4210,7 +4210,7 @@
},
"type": "object",
"required": [
"mental_models",
"observations",
"mcp",
"worker"
],
@ -4605,6 +4605,99 @@
"timestamp": "2024-01-15T10:30:00Z"
}
},
"MentalModelListResponse": {
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/MentalModelResponse"
},
"type": "array",
"title": "Items"
}
},
"type": "object",
"required": [
"items"
],
"title": "MentalModelListResponse",
"description": "Response model for listing mental models."
},
"MentalModelResponse": {
"properties": {
"id": {
"type": "string",
"title": "Id"
},
"bank_id": {
"type": "string",
"title": "Bank Id"
},
"name": {
"type": "string",
"title": "Name"
},
"source_query": {
"type": "string",
"title": "Source Query"
},
"content": {
"type": "string",
"title": "Content"
},
"tags": {
"items": {
"type": "string"
},
"type": "array",
"title": "Tags"
},
"last_refreshed_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Last Refreshed At"
},
"created_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Created At"
},
"reflect_response": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"title": "Reflect Response",
"description": "Full reflect API response payload including based_on facts and observations"
}
},
"type": "object",
"required": [
"id",
"bank_id",
"name",
"source_query",
"content"
],
"title": "MentalModelResponse",
"description": "Response model for a mental model (stored reflect response)."
},
"OperationResponse": {
"properties": {
"id": {
@ -4827,7 +4920,7 @@
}
],
"title": "Types",
"description": "List of fact types to recall: 'world', 'experience', 'mental_model'. Defaults to world and experience if not specified. Note: 'opinion' is accepted but ignored (opinions are excluded from recall)."
"description": "List of fact types to recall: 'world', 'experience', 'observation'. Defaults to world and experience if not specified. Note: 'opinion' is accepted but ignored (opinions are excluded from recall)."
},
"budget": {
"$ref": "#/components/schemas/Budget",
@ -5316,54 +5409,6 @@
"title": "ReflectLLMCall",
"description": "An LLM call made during reflect agent execution."
},
"ReflectMentalModel": {
"properties": {
"id": {
"type": "string",
"title": "Id",
"description": "Mental model ID"
},
"name": {
"type": "string",
"title": "Name",
"description": "Mental model name"
},
"type": {
"type": "string",
"title": "Type",
"description": "Mental model type: entity, concept, event"
},
"subtype": {
"type": "string",
"title": "Subtype",
"description": "Mental model subtype: structural, emergent, learned, directive"
},
"observations": {
"anyOf": [
{
"items": {
"type": "string"
},
"type": "array"
},
{
"type": "null"
}
],
"title": "Observations",
"description": "Observations for directive mental models (subtype='directive')"
}
},
"type": "object",
"required": [
"id",
"name",
"type",
"subtype"
],
"title": "ReflectMentalModel",
"description": "A mental model accessed during reflect."
},
"ReflectRequest": {
"properties": {
"query": {
@ -5564,9 +5609,9 @@
"scope": "agent_1"
}
],
"mental_models": [
"observations": [
{
"id": "mm-1",
"id": "obs-1",
"name": "AI Technology",
"subtype": "structural",
"type": "concept"
@ -5653,113 +5698,12 @@
"type": "array",
"title": "Llm Calls",
"description": "LLM calls made during reflection"
},
"mental_models": {
"items": {
"$ref": "#/components/schemas/ReflectMentalModel"
},
"type": "array",
"title": "Mental Models",
"description": "Mental models used during reflection (includes directives with subtype='directive')"
}
},
"type": "object",
"title": "ReflectTrace",
"description": "Execution trace of LLM and tool calls during reflection."
},
"ReflectionListResponse": {
"properties": {
"items": {
"items": {
"$ref": "#/components/schemas/ReflectionResponse"
},
"type": "array",
"title": "Items"
}
},
"type": "object",
"required": [
"items"
],
"title": "ReflectionListResponse",
"description": "Response model for listing reflections."
},
"ReflectionResponse": {
"properties": {
"id": {
"type": "string",
"title": "Id"
},
"bank_id": {
"type": "string",
"title": "Bank Id"
},
"name": {
"type": "string",
"title": "Name"
},
"source_query": {
"type": "string",
"title": "Source Query"
},
"content": {
"type": "string",
"title": "Content"
},
"tags": {
"items": {
"type": "string"
},
"type": "array",
"title": "Tags"
},
"last_refreshed_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Last Refreshed At"
},
"created_at": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"title": "Created At"
},
"reflect_response": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"title": "Reflect Response",
"description": "Full reflect API response payload including based_on facts and mental_models"
}
},
"type": "object",
"required": [
"id",
"bank_id",
"name",
"source_query",
"content"
],
"title": "ReflectionResponse",
"description": "Response model for a reflection."
},
"RetainRequest": {
"properties": {
"items": {
@ -6028,7 +5972,7 @@
"title": "UpdateDispositionRequest",
"description": "Request model for updating disposition traits."
},
"UpdateReflectionRequest": {
"UpdateMentalModelRequest": {
"properties": {
"name": {
"anyOf": [
@ -6040,12 +5984,12 @@
}
],
"title": "Name",
"description": "New name for the reflection"
"description": "New name for the mental model"
}
},
"type": "object",
"title": "UpdateReflectionRequest",
"description": "Request model for updating a reflection.",
"title": "UpdateMentalModelRequest",
"description": "Request model for updating a mental model.",
"example": {
"name": "Updated Team Communication Preferences"
}
@ -6106,7 +6050,7 @@
"api_version": "1.0.0",
"features": {
"mcp": true,
"mental_models": false,
"observations": false,
"worker": true
}
}

View file

@ -111,7 +111,7 @@ def recall(
query: The query string to search memories for
bank_id: Override the configured bank_id. For multi-user support,
use different bank_ids per user (e.g., f"user-{user_id}")
fact_types: Filter by fact types (world, experience, mental_model)
fact_types: Filter by fact types (world, experience, observation)
budget: Recall budget level (low, mid, high) - controls how many memories are returned
max_tokens: Maximum tokens for memory context
hindsight_api_url: Override the configured API URL
@ -1523,7 +1523,7 @@ def wrap_openai(
- store_conversations: Whether to store conversations (default: True)
- inject_memories: Whether to inject memories (default: True)
- budget: Recall budget level - low/mid/high (default: "mid")
- fact_types: Filter by fact types (world/experience/mental_model)
- fact_types: Filter by fact types (world/experience/observation)
- max_memories: Max memories to inject (None = no limit)
- max_memory_tokens: Max tokens for memory context (default: 4096)
- use_reflect: Use reflect API instead of recall (default: False)
@ -1623,7 +1623,7 @@ def wrap_anthropic(
- store_conversations: Whether to store conversations (default: True)
- inject_memories: Whether to inject memories (default: True)
- budget: Recall budget level - low/mid/high (default: "mid")
- fact_types: Filter by fact types (world/experience/mental_model)
- fact_types: Filter by fact types (world/experience/observation)
- max_memories: Max memories to inject (None = no limit)
- max_memory_tokens: Max tokens for memory context (default: 4096)
- use_reflect: Use reflect API instead of recall (default: False)