Compare commits
7
Commits
embed-fixes
...
drop-ll
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7fc38e2ae3 | ||
|
|
7bdb8fc2e3 | ||
|
|
5b52a84fff | ||
|
|
f3c5a9c1c2 | ||
|
|
5832b907c6 | ||
|
|
50fa2ed090 | ||
|
|
522b71aab8 |
@@ -875,6 +875,66 @@ jobs:
|
||||
echo "=== API Server Logs ==="
|
||||
cat /tmp/api-server.log || echo "No API server log found"
|
||||
|
||||
test-upgrade:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
HINDSIGHT_API_LLM_PROVIDER: groq
|
||||
HINDSIGHT_API_LLM_API_KEY: ${{ secrets.GROQ_API_KEY }}
|
||||
HINDSIGHT_API_LLM_MODEL: openai/gpt-oss-20b
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
UV_INDEX: pytorch=https://download.pytorch.org/whl/cpu
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Full history needed for git clone of tags
|
||||
|
||||
- name: Fetch tags
|
||||
run: git fetch --tags
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v5
|
||||
with:
|
||||
enable-cache: true
|
||||
prune-cache: false
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version-file: ".python-version"
|
||||
|
||||
- name: Cache HuggingFace models
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/huggingface
|
||||
key: ${{ runner.os }}-huggingface-${{ hashFiles('hindsight-api/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-huggingface-
|
||||
|
||||
- name: Install hindsight-dev dependencies
|
||||
working-directory: ./hindsight-dev
|
||||
run: uv sync --frozen --extra test --index-strategy unsafe-best-match
|
||||
|
||||
- name: Install current hindsight-api
|
||||
working-directory: ./hindsight-api
|
||||
run: uv sync --frozen --index-strategy unsafe-best-match
|
||||
|
||||
- name: Pre-download models
|
||||
working-directory: ./hindsight-api
|
||||
run: |
|
||||
uv run python -c "
|
||||
from sentence_transformers import SentenceTransformer, CrossEncoder
|
||||
print('Downloading embedding model...')
|
||||
SentenceTransformer('BAAI/bge-small-en-v1.5')
|
||||
print('Downloading cross-encoder model...')
|
||||
CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
|
||||
print('Models downloaded successfully')
|
||||
"
|
||||
|
||||
- name: Run upgrade tests
|
||||
working-directory: ./hindsight-dev
|
||||
run: uv run pytest upgrade_tests/ -v --tb=short
|
||||
|
||||
verify-generated-files:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
||||
@@ -7,8 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
Hindsight is an agent memory system that provides long-term memory for AI agents using biomimetic data structures. Memories are organized as:
|
||||
- **World facts**: General knowledge ("The sky is blue")
|
||||
- **Experience facts**: Personal experiences ("I visited Paris in 2023")
|
||||
- **Opinion facts**: Beliefs with confidence scores ("Paris is beautiful" - 0.9 confidence)
|
||||
- **Observations**: Complex mental models derived from reflection
|
||||
- **Mental models**: Consolidated knowledge synthesized from facts ("User prefers functional programming patterns")
|
||||
|
||||
## Development Commands
|
||||
|
||||
@@ -101,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**: Deep analysis forming new opinions/observations (disposition-aware)
|
||||
- **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.
|
||||
|
||||
+134
@@ -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'
|
||||
""")
|
||||
@@ -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
|
||||
@@ -554,18 +554,6 @@ class ReflectLLMCall(BaseModel):
|
||||
duration_ms: int = Field(description="Execution time in milliseconds")
|
||||
|
||||
|
||||
class ReflectMentalModel(BaseModel):
|
||||
"""A mental model 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, directive")
|
||||
observations: list[str] | None = Field(
|
||||
default=None, description="Observations for directive mental models (subtype='directive')"
|
||||
)
|
||||
|
||||
|
||||
class ReflectBasedOn(BaseModel):
|
||||
"""Evidence the response is based on: memories and mental models."""
|
||||
|
||||
@@ -577,10 +565,6 @@ class ReflectTrace(BaseModel):
|
||||
|
||||
tool_calls: list[ReflectToolCall] = Field(default_factory=list, description="Tool calls made during reflection")
|
||||
llm_calls: list[ReflectLLMCall] = Field(default_factory=list, description="LLM calls made during reflection")
|
||||
mental_models: list[ReflectMentalModel] = Field(
|
||||
default_factory=list,
|
||||
description="Mental models used during reflection (includes directives with subtype='directive')",
|
||||
)
|
||||
|
||||
|
||||
class ReflectResponse(BaseModel):
|
||||
@@ -595,15 +579,6 @@ class ReflectResponse(BaseModel):
|
||||
{"id": "123", "text": "AI is used in healthcare", "type": "world"},
|
||||
{"id": "456", "text": "I discussed AI applications last week", "type": "experience"},
|
||||
],
|
||||
"mental_models": [
|
||||
{
|
||||
"id": "mm-1",
|
||||
"name": "AI Technology",
|
||||
"type": "concept",
|
||||
"subtype": "structural",
|
||||
"description": "Understanding of AI capabilities",
|
||||
}
|
||||
],
|
||||
},
|
||||
"structured_output": {
|
||||
"summary": "AI is transformative",
|
||||
@@ -613,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",
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1016,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,
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1033,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
|
||||
@@ -1095,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
|
||||
@@ -1112,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={
|
||||
@@ -1136,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={
|
||||
@@ -1159,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):
|
||||
@@ -1288,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")
|
||||
|
||||
@@ -1301,7 +1284,7 @@ class VersionResponse(BaseModel):
|
||||
"example": {
|
||||
"api_version": "1.0.0",
|
||||
"features": {
|
||||
"mental_models": False,
|
||||
"observations": False,
|
||||
"mcp": True,
|
||||
"worker": True,
|
||||
},
|
||||
@@ -1548,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,
|
||||
),
|
||||
@@ -1862,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 = []
|
||||
@@ -1880,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
|
||||
@@ -1895,33 +1878,9 @@ def _register_routes(app: FastAPI):
|
||||
for tc in core_result.tool_trace
|
||||
]
|
||||
llm_calls = [ReflectLLMCall(scope=lc.scope, duration_ms=lc.duration_ms) for lc in core_result.llm_trace]
|
||||
# Build map of directive observations by id
|
||||
directive_observations = {d.id: d.rules for d in core_result.directives_applied}
|
||||
# Build mental models from tool trace (get_mental_model outputs)
|
||||
trace_mental_models: list[ReflectMentalModel] = []
|
||||
seen_model_ids: set[str] = set()
|
||||
for tc in core_result.tool_trace:
|
||||
if tc.tool == "get_mental_model" and tc.output.get("found") and "model" in tc.output:
|
||||
model = tc.output["model"]
|
||||
model_id = model.get("id")
|
||||
if model_id and model_id not in seen_model_ids:
|
||||
seen_model_ids.add(model_id)
|
||||
model_subtype = model.get("subtype", "structural")
|
||||
trace_mental_models.append(
|
||||
ReflectMentalModel(
|
||||
id=model_id,
|
||||
name=model.get("name", ""),
|
||||
type=model.get("type", "concept"),
|
||||
subtype=model_subtype,
|
||||
observations=directive_observations.get(model_id)
|
||||
if model_subtype == "directive"
|
||||
else None,
|
||||
)
|
||||
)
|
||||
trace_result = ReflectTrace(
|
||||
tool_calls=tool_calls,
|
||||
llm_calls=llm_calls,
|
||||
mental_models=trace_mental_models,
|
||||
)
|
||||
|
||||
return ReflectResponse(
|
||||
@@ -2069,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}
|
||||
@@ -2111,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):
|
||||
@@ -2218,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"),
|
||||
@@ -2237,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,
|
||||
@@ -2247,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,
|
||||
@@ -2314,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):
|
||||
@@ -2323,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")
|
||||
@@ -2356,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
|
||||
@@ -2422,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))
|
||||
|
||||
# =========================================================================
|
||||
@@ -3146,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):
|
||||
@@ -3168,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"],
|
||||
)
|
||||
|
||||
@@ -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))
|
||||
),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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"}}]"""
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,17 +4,15 @@ 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)
|
||||
4. Expand memories (get chunk/document context)
|
||||
3. Expand memories (get chunk/document context)
|
||||
"""
|
||||
|
||||
from .agent import ReflectAgentResult, run_reflect_agent
|
||||
from .models import MentalModelInput, ReflectAction, ReflectActionBatch
|
||||
from .models import ReflectAction, ReflectActionBatch
|
||||
|
||||
__all__ = [
|
||||
"run_reflect_agent",
|
||||
"ReflectAgentResult",
|
||||
"ReflectAction",
|
||||
"ReflectActionBatch",
|
||||
"MentalModelInput",
|
||||
]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -7,51 +7,28 @@ 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")
|
||||
|
||||
|
||||
class MentalModelInput(BaseModel):
|
||||
"""Input for the learn tool to create a mental model placeholder.
|
||||
|
||||
The agent only specifies name and description - the actual content/observations
|
||||
are generated during refresh, similar to pinned models.
|
||||
"""
|
||||
|
||||
name: str = Field(description="Human-readable name for the mental model")
|
||||
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")
|
||||
|
||||
|
||||
class AnswerSection(BaseModel):
|
||||
"""A section of the answer with its supporting evidence (DEPRECATED)."""
|
||||
|
||||
title: str = Field(description="Section header/title")
|
||||
text: str = Field(description="Section content")
|
||||
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")
|
||||
model_ids: list[str] = Field(default_factory=list, description="Mental model IDs supporting this section")
|
||||
|
||||
|
||||
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", "expand", "done"] = Field(
|
||||
description="Tool to invoke: list_observations, get_observation, recall, 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")
|
||||
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)")
|
||||
@@ -73,7 +50,7 @@ class ReflectActionBatch(BaseModel):
|
||||
class ToolCall(BaseModel):
|
||||
"""A single tool call made during reflect."""
|
||||
|
||||
tool: str = Field(description="Tool name: lookup, recall, learn, expand")
|
||||
tool: str = Field(description="Tool name: lookup, recall, expand")
|
||||
input: dict = Field(description="Tool input parameters")
|
||||
output: dict = Field(description="Tool output/result")
|
||||
duration_ms: int = Field(description="Execution time in milliseconds")
|
||||
@@ -120,12 +97,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"
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
"""
|
||||
Scoring functions for memory search and retrieval.
|
||||
|
||||
Includes recency weighting, frequency weighting, temporal proximity,
|
||||
and similarity calculations used in memory activation and ranking.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def cosine_similarity(vec1: list[float], vec2: list[float]) -> float:
|
||||
"""
|
||||
Calculate cosine similarity between two vectors.
|
||||
|
||||
Args:
|
||||
vec1: First vector
|
||||
vec2: Second vector
|
||||
|
||||
Returns:
|
||||
Similarity score between 0 and 1
|
||||
"""
|
||||
if len(vec1) != len(vec2):
|
||||
raise ValueError("Vectors must have same dimension")
|
||||
|
||||
dot_product = sum(a * b for a, b in zip(vec1, vec2))
|
||||
magnitude1 = sum(a * a for a in vec1) ** 0.5
|
||||
magnitude2 = sum(b * b for b in vec2) ** 0.5
|
||||
|
||||
if magnitude1 == 0 or magnitude2 == 0:
|
||||
return 0.0
|
||||
|
||||
return dot_product / (magnitude1 * magnitude2)
|
||||
|
||||
|
||||
def calculate_recency_weight(days_since: float, half_life_days: float = 365.0) -> float:
|
||||
"""
|
||||
Calculate recency weight using logarithmic decay.
|
||||
|
||||
This provides much better differentiation over long time periods compared to
|
||||
exponential decay. Uses a log-based decay where the half-life parameter controls
|
||||
when memories reach 50% weight.
|
||||
|
||||
Examples:
|
||||
- Today (0 days): 1.0
|
||||
- 1 year (365 days): ~0.5 (with default half_life=365)
|
||||
- 2 years (730 days): ~0.33
|
||||
- 5 years (1825 days): ~0.17
|
||||
- 10 years (3650 days): ~0.09
|
||||
|
||||
This ensures that 2-year-old and 5-year-old memories have meaningfully
|
||||
different weights, unlike exponential decay which makes them both ~0.
|
||||
|
||||
Args:
|
||||
days_since: Number of days since the memory was created
|
||||
half_life_days: Number of days for weight to reach 0.5 (default: 1 year)
|
||||
|
||||
Returns:
|
||||
Weight between 0 and 1
|
||||
"""
|
||||
import math
|
||||
|
||||
# Logarithmic decay: 1 / (1 + log(1 + days_since/half_life))
|
||||
# This decays much slower than exponential, giving better long-term differentiation
|
||||
normalized_age = days_since / half_life_days
|
||||
return 1.0 / (1.0 + math.log1p(normalized_age))
|
||||
|
||||
|
||||
def calculate_temporal_anchor(occurred_start: datetime, occurred_end: datetime) -> datetime:
|
||||
"""
|
||||
Calculate a single temporal anchor point from a temporal range.
|
||||
|
||||
Used for spreading activation - we need a single representative date
|
||||
to calculate temporal proximity between facts. This simplifies the
|
||||
range-to-range distance problem.
|
||||
|
||||
Strategy: Use midpoint of the range for balanced representation.
|
||||
|
||||
Args:
|
||||
occurred_start: Start of temporal range
|
||||
occurred_end: End of temporal range
|
||||
|
||||
Returns:
|
||||
Single datetime representing the temporal anchor (midpoint)
|
||||
|
||||
Examples:
|
||||
- Point event (July 14): start=July 14, end=July 14 → anchor=July 14
|
||||
- Month range (February): start=Feb 1, end=Feb 28 → anchor=Feb 14
|
||||
- Year range (2023): start=Jan 1, end=Dec 31 → anchor=July 1
|
||||
"""
|
||||
# Calculate midpoint
|
||||
time_delta = occurred_end - occurred_start
|
||||
midpoint = occurred_start + (time_delta / 2)
|
||||
return midpoint
|
||||
|
||||
|
||||
def calculate_temporal_proximity(anchor_a: datetime, anchor_b: datetime, half_life_days: float = 30.0) -> float:
|
||||
"""
|
||||
Calculate temporal proximity between two temporal anchors.
|
||||
|
||||
Used for spreading activation to determine how "close" two facts are
|
||||
in time. Uses logarithmic decay so that temporal similarity doesn't
|
||||
drop off too quickly.
|
||||
|
||||
Args:
|
||||
anchor_a: Temporal anchor of first fact
|
||||
anchor_b: Temporal anchor of second fact
|
||||
half_life_days: Number of days for proximity to reach 0.5
|
||||
(default: 30 days = 1 month)
|
||||
|
||||
Returns:
|
||||
Proximity score in [0, 1] where:
|
||||
- 1.0 = same day
|
||||
- 0.5 = ~half_life days apart
|
||||
- 0.0 = very distant in time
|
||||
|
||||
Examples:
|
||||
- Same day: 1.0
|
||||
- 1 week apart (half_life=30): ~0.7
|
||||
- 1 month apart (half_life=30): ~0.5
|
||||
- 1 year apart (half_life=30): ~0.2
|
||||
"""
|
||||
import math
|
||||
|
||||
days_apart = abs((anchor_a - anchor_b).days)
|
||||
|
||||
if days_apart == 0:
|
||||
return 1.0
|
||||
|
||||
# Logarithmic decay: 1 / (1 + log(1 + days_apart/half_life))
|
||||
# Similar to calculate_recency_weight but for proximity between events
|
||||
normalized_distance = days_apart / half_life_days
|
||||
proximity = 1.0 / (1.0 + math.log1p(normalized_distance))
|
||||
|
||||
return proximity
|
||||
@@ -65,129 +65,3 @@ async def extract_facts(
|
||||
return [], chunks
|
||||
|
||||
return facts, chunks
|
||||
|
||||
|
||||
def cosine_similarity(vec1: list[float], vec2: list[float]) -> float:
|
||||
"""
|
||||
Calculate cosine similarity between two vectors.
|
||||
|
||||
Args:
|
||||
vec1: First vector
|
||||
vec2: Second vector
|
||||
|
||||
Returns:
|
||||
Similarity score between 0 and 1
|
||||
"""
|
||||
if len(vec1) != len(vec2):
|
||||
raise ValueError("Vectors must have same dimension")
|
||||
|
||||
dot_product = sum(a * b for a, b in zip(vec1, vec2))
|
||||
magnitude1 = sum(a * a for a in vec1) ** 0.5
|
||||
magnitude2 = sum(b * b for b in vec2) ** 0.5
|
||||
|
||||
if magnitude1 == 0 or magnitude2 == 0:
|
||||
return 0.0
|
||||
|
||||
return dot_product / (magnitude1 * magnitude2)
|
||||
|
||||
|
||||
def calculate_recency_weight(days_since: float, half_life_days: float = 365.0) -> float:
|
||||
"""
|
||||
Calculate recency weight using logarithmic decay.
|
||||
|
||||
This provides much better differentiation over long time periods compared to
|
||||
exponential decay. Uses a log-based decay where the half-life parameter controls
|
||||
when memories reach 50% weight.
|
||||
|
||||
Examples:
|
||||
- Today (0 days): 1.0
|
||||
- 1 year (365 days): ~0.5 (with default half_life=365)
|
||||
- 2 years (730 days): ~0.33
|
||||
- 5 years (1825 days): ~0.17
|
||||
- 10 years (3650 days): ~0.09
|
||||
|
||||
This ensures that 2-year-old and 5-year-old memories have meaningfully
|
||||
different weights, unlike exponential decay which makes them both ~0.
|
||||
|
||||
Args:
|
||||
days_since: Number of days since the memory was created
|
||||
half_life_days: Number of days for weight to reach 0.5 (default: 1 year)
|
||||
|
||||
Returns:
|
||||
Weight between 0 and 1
|
||||
"""
|
||||
import math
|
||||
|
||||
# Logarithmic decay: 1 / (1 + log(1 + days_since/half_life))
|
||||
# This decays much slower than exponential, giving better long-term differentiation
|
||||
normalized_age = days_since / half_life_days
|
||||
return 1.0 / (1.0 + math.log1p(normalized_age))
|
||||
|
||||
|
||||
def calculate_temporal_anchor(occurred_start: datetime, occurred_end: datetime) -> datetime:
|
||||
"""
|
||||
Calculate a single temporal anchor point from a temporal range.
|
||||
|
||||
Used for spreading activation - we need a single representative date
|
||||
to calculate temporal proximity between facts. This simplifies the
|
||||
range-to-range distance problem.
|
||||
|
||||
Strategy: Use midpoint of the range for balanced representation.
|
||||
|
||||
Args:
|
||||
occurred_start: Start of temporal range
|
||||
occurred_end: End of temporal range
|
||||
|
||||
Returns:
|
||||
Single datetime representing the temporal anchor (midpoint)
|
||||
|
||||
Examples:
|
||||
- Point event (July 14): start=July 14, end=July 14 → anchor=July 14
|
||||
- Month range (February): start=Feb 1, end=Feb 28 → anchor=Feb 14
|
||||
- Year range (2023): start=Jan 1, end=Dec 31 → anchor=July 1
|
||||
"""
|
||||
# Calculate midpoint
|
||||
time_delta = occurred_end - occurred_start
|
||||
midpoint = occurred_start + (time_delta / 2)
|
||||
return midpoint
|
||||
|
||||
|
||||
def calculate_temporal_proximity(anchor_a: datetime, anchor_b: datetime, half_life_days: float = 30.0) -> float:
|
||||
"""
|
||||
Calculate temporal proximity between two temporal anchors.
|
||||
|
||||
Used for spreading activation to determine how "close" two facts are
|
||||
in time. Uses logarithmic decay so that temporal similarity doesn't
|
||||
drop off too quickly.
|
||||
|
||||
Args:
|
||||
anchor_a: Temporal anchor of first fact
|
||||
anchor_b: Temporal anchor of second fact
|
||||
half_life_days: Number of days for proximity to reach 0.5
|
||||
(default: 30 days = 1 month)
|
||||
|
||||
Returns:
|
||||
Proximity score in [0, 1] where:
|
||||
- 1.0 = same day
|
||||
- 0.5 = ~half_life days apart
|
||||
- 0.0 = very distant in time
|
||||
|
||||
Examples:
|
||||
- Same day: 1.0
|
||||
- 1 week apart (half_life=30): ~0.7
|
||||
- 1 month apart (half_life=30): ~0.5
|
||||
- 1 year apart (half_life=30): ~0.2
|
||||
"""
|
||||
import math
|
||||
|
||||
days_apart = abs((anchor_a - anchor_b).days)
|
||||
|
||||
if days_apart == 0:
|
||||
return 1.0
|
||||
|
||||
# Logarithmic decay: 1 / (1 + log(1 + days_apart/half_life))
|
||||
# Similar to calculate_recency_weight but for proximity between events
|
||||
normalized_distance = days_apart / half_life_days
|
||||
proximity = 1.0 / (1.0 + math.log1p(normalized_distance))
|
||||
|
||||
return proximity
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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": []}),
|
||||
}
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -22,7 +22,7 @@ TABLES = [
|
||||
"chunks",
|
||||
"async_operations",
|
||||
"directives",
|
||||
"reflections",
|
||||
"mental_models",
|
||||
]
|
||||
|
||||
# Files to scan for SQL queries
|
||||
|
||||
+18
-18
@@ -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())
|
||||
})
|
||||
}
|
||||
|
||||
+50
-50
@@ -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!();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
+33
-33
@@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+194
-194
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
+7
-7
@@ -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
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+4
-4
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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;
|
||||
|
||||
+9
-9
@@ -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 });
|
||||
}
|
||||
}
|
||||
+116
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
-113
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
+109
-102
@@ -57,10 +57,9 @@ interface ReflectResponseBasedOnFact {
|
||||
interface ReflectResponse {
|
||||
text: string;
|
||||
based_on: Record<string, ReflectResponseBasedOnFact[]>;
|
||||
mental_models?: Array<{ id: string; text: string }>;
|
||||
}
|
||||
|
||||
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">"{deleteTarget?.name}"</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,14 +622,15 @@ function ReflectionDetailPanel({
|
||||
})}`;
|
||||
};
|
||||
|
||||
// Extract all memories from based_on
|
||||
const basedOnFacts = reflection.reflect_response?.based_on
|
||||
? Object.entries(reflection.reflect_response.based_on).flatMap(([factType, facts]) =>
|
||||
facts.map((fact) => ({ ...fact, factType }))
|
||||
)
|
||||
// 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 })))
|
||||
: [];
|
||||
|
||||
const mentalModels = reflection.reflect_response?.mental_models || [];
|
||||
// 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">
|
||||
@@ -647,7 +654,7 @@ function ReflectionDetailPanel({
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setEditName(reflection.name);
|
||||
setEditName(mentalModel.name);
|
||||
setIsEditing(false);
|
||||
}}
|
||||
>
|
||||
@@ -658,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"
|
||||
@@ -668,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>
|
||||
@@ -699,7 +706,7 @@ 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>
|
||||
|
||||
@@ -743,32 +750,32 @@ function ReflectionDetailPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mental Models Used Section */}
|
||||
{mentalModels.length > 0 && (
|
||||
{/* 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">
|
||||
Mental Models Used ({mentalModels.length})
|
||||
Observations Used ({observations.length})
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{mentalModels.map((model, i) => (
|
||||
{observations.map((obs, i) => (
|
||||
<div
|
||||
key={model.id || i}
|
||||
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">
|
||||
mental_model
|
||||
observation
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-6 text-xs"
|
||||
onClick={() => setViewMemoryId(model.id)}
|
||||
onClick={() => setViewMemoryId(obs.id)}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm text-foreground leading-relaxed">{model.text}</p>
|
||||
<p className="text-sm text-foreground leading-relaxed">{obs.text}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -776,7 +783,7 @@ function ReflectionDetailPanel({
|
||||
)}
|
||||
|
||||
{/* No based_on data yet */}
|
||||
{!reflection.reflect_response && (
|
||||
{!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">
|
||||
@@ -786,13 +793,13 @@ function ReflectionDetailPanel({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{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"
|
||||
@@ -805,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">
|
||||
@@ -814,7 +821,7 @@ function ReflectionDetailPanel({
|
||||
ID
|
||||
</div>
|
||||
<code className="text-sm font-mono break-all text-muted-foreground">
|
||||
{reflection.id}
|
||||
{mentalModel.id}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -16,8 +16,15 @@ dependencies = [
|
||||
"pydantic>=2.0.0",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
"pytest>=8.0.0",
|
||||
"httpx>=0.27.0",
|
||||
"python-dotenv>=1.0.0",
|
||||
]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["hindsight_dev", "benchmarks"]
|
||||
packages = ["hindsight_dev", "benchmarks", "upgrade_tests"]
|
||||
|
||||
[tool.uv.sources]
|
||||
hindsight-api = { workspace = true }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# Upgrade and backwards compatibility tests
|
||||
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Pytest configuration and fixtures for upgrade tests.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# Configure logging for tests
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
|
||||
# Reduce noise from httpx
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
logging.getLogger("httpcore").setLevel(logging.WARNING)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Load environment variables before running tests."""
|
||||
# Look for .env in the workspace root
|
||||
env_file = Path(__file__).parent.parent.parent / ".env"
|
||||
if env_file.exists():
|
||||
load_dotenv(env_file)
|
||||
|
||||
|
||||
_pg0_instance = None
|
||||
_pg0_url = None
|
||||
|
||||
|
||||
def _get_or_create_pg0():
|
||||
"""Get or create the shared pg0 instance for upgrade tests."""
|
||||
global _pg0_instance, _pg0_url
|
||||
from hindsight_api.pg0 import EmbeddedPostgres
|
||||
|
||||
if _pg0_instance is None:
|
||||
_pg0_instance = EmbeddedPostgres(name="hindsight-upgrade-test", port=5560)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
try:
|
||||
_pg0_url = loop.run_until_complete(_pg0_instance.ensure_running())
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
return _pg0_url
|
||||
|
||||
|
||||
def _clean_database(db_url: str):
|
||||
"""Drop all tables in the database to reset state for next test."""
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
engine = create_engine(db_url)
|
||||
with engine.connect() as conn:
|
||||
# Drop all tables in public schema (cascade to handle foreign keys)
|
||||
tables = conn.execute(
|
||||
text("""
|
||||
SELECT tablename FROM pg_tables
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename NOT LIKE 'pg_%'
|
||||
""")
|
||||
).fetchall()
|
||||
for table in tables:
|
||||
conn.execute(text(f'DROP TABLE IF EXISTS public."{table[0]}" CASCADE'))
|
||||
conn.commit()
|
||||
engine.dispose()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def db_url():
|
||||
"""
|
||||
Provide a PostgreSQL connection URL for upgrade tests.
|
||||
|
||||
Uses pg0 (embedded PostgreSQL) for a clean, isolated test database.
|
||||
The database is cleaned between tests to ensure fresh state for migrations.
|
||||
"""
|
||||
url = _get_or_create_pg0()
|
||||
|
||||
# Clean database before each test
|
||||
_clean_database(url)
|
||||
|
||||
yield url
|
||||
|
||||
# No cleanup after - database is cleaned at start of next test
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def llm_config():
|
||||
"""
|
||||
Provide LLM configuration from environment.
|
||||
|
||||
Returns a dict with provider, api_key, and model.
|
||||
"""
|
||||
return {
|
||||
"provider": os.getenv("HINDSIGHT_API_LLM_PROVIDER", "groq"),
|
||||
"api_key": os.getenv("HINDSIGHT_API_LLM_API_KEY") or os.getenv("GROQ_API_KEY"),
|
||||
"model": os.getenv("HINDSIGHT_API_LLM_MODEL", "llama-3.3-70b-versatile"),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unique_bank_id():
|
||||
"""Generate a unique bank ID for each test."""
|
||||
import uuid
|
||||
|
||||
return f"upgrade_test_{uuid.uuid4().hex[:8]}"
|
||||
@@ -0,0 +1,303 @@
|
||||
"""
|
||||
Upgrade and backwards compatibility tests.
|
||||
|
||||
These tests verify that:
|
||||
1. Data stored in older versions is accessible after upgrade
|
||||
2. Database migrations run correctly
|
||||
3. API behavior remains compatible
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from .version_runner import VersionRunner
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Version upgrade paths to test
|
||||
# Format: (old_version, new_version)
|
||||
UPGRADE_PATHS = [
|
||||
("v0.3.0", "HEAD"),
|
||||
]
|
||||
|
||||
|
||||
class TestUpgrade:
|
||||
"""Tests for version upgrades."""
|
||||
|
||||
@pytest.mark.parametrize("old_version,new_version", UPGRADE_PATHS)
|
||||
def test_upgrade_preserves_memories(self, db_url, llm_config, unique_bank_id, old_version, new_version):
|
||||
"""
|
||||
Verify memories stored in old version are accessible after upgrade.
|
||||
|
||||
Workflow:
|
||||
1. Start old version
|
||||
2. Store memories via retain
|
||||
3. Verify recall works on old version
|
||||
4. Stop old version
|
||||
5. Start new version (same database - migrations run)
|
||||
6. Verify recall returns same data
|
||||
7. Verify reflect works
|
||||
"""
|
||||
bank_id = unique_bank_id
|
||||
|
||||
# Test data to store
|
||||
test_memories = [
|
||||
{"content": "Alice is a software engineer at TechCorp.", "context": "team introduction"},
|
||||
{"content": "Bob manages the infrastructure team and loves Kubernetes.", "context": "team introduction"},
|
||||
{"content": "The project deadline is next Friday.", "context": "project planning"},
|
||||
]
|
||||
|
||||
# Phase 1: Store data with old version
|
||||
logger.info(f"=== Phase 1: Setting up data with {old_version} ===")
|
||||
|
||||
with VersionRunner(
|
||||
old_version,
|
||||
db_url,
|
||||
port=8891,
|
||||
llm_provider=llm_config["provider"],
|
||||
llm_api_key=llm_config["api_key"],
|
||||
llm_model=llm_config["model"],
|
||||
) as old:
|
||||
server = old.start()
|
||||
client = httpx.Client(base_url=server.url, timeout=60)
|
||||
|
||||
# Store memories
|
||||
resp = client.post(
|
||||
f"/v1/default/banks/{bank_id}/memories",
|
||||
json={"items": test_memories},
|
||||
)
|
||||
assert resp.status_code == 200, f"Failed to store memories: {resp.text}"
|
||||
result = resp.json()
|
||||
assert result["success"] is True
|
||||
assert result["items_count"] == len(test_memories)
|
||||
|
||||
# Verify recall works on old version
|
||||
resp = client.post(
|
||||
f"/v1/default/banks/{bank_id}/memories/recall",
|
||||
json={"query": "Who works at TechCorp?"},
|
||||
)
|
||||
assert resp.status_code == 200, f"Recall failed on old version: {resp.text}"
|
||||
old_results = resp.json()["results"]
|
||||
assert len(old_results) > 0, "No results from recall on old version"
|
||||
|
||||
# Get stats for comparison
|
||||
resp = client.get(f"/v1/default/banks/{bank_id}/stats")
|
||||
assert resp.status_code == 200
|
||||
old_stats = resp.json()
|
||||
logger.info(f"Old version stats: {old_stats}")
|
||||
|
||||
client.close()
|
||||
|
||||
# Phase 2: Verify data with new version
|
||||
logger.info(f"=== Phase 2: Verifying data with {new_version} ===")
|
||||
|
||||
with VersionRunner(
|
||||
new_version,
|
||||
db_url,
|
||||
port=8892,
|
||||
llm_provider=llm_config["provider"],
|
||||
llm_api_key=llm_config["api_key"],
|
||||
llm_model=llm_config["model"],
|
||||
) as new:
|
||||
server = new.start()
|
||||
client = httpx.Client(base_url=server.url, timeout=60)
|
||||
|
||||
# Verify recall returns data
|
||||
resp = client.post(
|
||||
f"/v1/default/banks/{bank_id}/memories/recall",
|
||||
json={"query": "Who works at TechCorp?"},
|
||||
)
|
||||
assert resp.status_code == 200, f"Recall failed on new version: {resp.text}"
|
||||
new_results = resp.json()["results"]
|
||||
assert len(new_results) > 0, f"No results from recall after upgrade. Bank: {bank_id}"
|
||||
|
||||
# Verify Alice is found
|
||||
found_alice = any("Alice" in r.get("text", "") for r in new_results)
|
||||
assert found_alice, f"Alice not found in results after upgrade: {new_results}"
|
||||
|
||||
# Verify reflect works
|
||||
resp = client.post(
|
||||
f"/v1/default/banks/{bank_id}/reflect",
|
||||
json={"query": "Tell me about the team members"},
|
||||
)
|
||||
assert resp.status_code == 200, f"Reflect failed after upgrade: {resp.text}"
|
||||
reflect_result = resp.json()
|
||||
assert len(reflect_result.get("text", "")) > 0, "Empty reflect response after upgrade"
|
||||
|
||||
# Verify stats are preserved
|
||||
resp = client.get(f"/v1/default/banks/{bank_id}/stats")
|
||||
assert resp.status_code == 200
|
||||
new_stats = resp.json()
|
||||
logger.info(f"New version stats: {new_stats}")
|
||||
|
||||
# Stats should be similar (might have small differences due to re-indexing)
|
||||
assert new_stats["total_nodes"] >= old_stats["total_nodes"], (
|
||||
f"Lost nodes after upgrade: {old_stats['total_nodes']} -> {new_stats['total_nodes']}"
|
||||
)
|
||||
|
||||
# Cleanup - delete test bank
|
||||
resp = client.delete(f"/v1/default/banks/{bank_id}")
|
||||
assert resp.status_code == 200
|
||||
|
||||
client.close()
|
||||
|
||||
@pytest.mark.parametrize("old_version,new_version", UPGRADE_PATHS)
|
||||
def test_upgrade_preserves_documents(self, db_url, llm_config, unique_bank_id, old_version, new_version):
|
||||
"""
|
||||
Verify documents stored in old version are accessible after upgrade.
|
||||
"""
|
||||
bank_id = unique_bank_id
|
||||
doc_id = "test-document-001"
|
||||
|
||||
# Phase 1: Store document with old version
|
||||
logger.info(f"=== Phase 1: Storing document with {old_version} ===")
|
||||
|
||||
with VersionRunner(
|
||||
old_version,
|
||||
db_url,
|
||||
port=8893,
|
||||
llm_provider=llm_config["provider"],
|
||||
llm_api_key=llm_config["api_key"],
|
||||
llm_model=llm_config["model"],
|
||||
) as old:
|
||||
server = old.start()
|
||||
client = httpx.Client(base_url=server.url, timeout=60)
|
||||
|
||||
# Store memory with document
|
||||
resp = client.post(
|
||||
f"/v1/default/banks/{bank_id}/memories",
|
||||
json={
|
||||
"items": [
|
||||
{
|
||||
"content": "The quarterly report shows 25% revenue growth.",
|
||||
"context": "Q1 financial review",
|
||||
"document_id": doc_id,
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200, f"Failed to store document: {resp.text}"
|
||||
|
||||
# Verify document exists
|
||||
resp = client.get(f"/v1/default/banks/{bank_id}/documents")
|
||||
assert resp.status_code == 200
|
||||
docs = resp.json()["items"]
|
||||
doc_ids = [d["id"] for d in docs]
|
||||
assert doc_id in doc_ids, f"Document not found in old version: {doc_ids}"
|
||||
|
||||
client.close()
|
||||
|
||||
# Phase 2: Verify document with new version
|
||||
logger.info(f"=== Phase 2: Verifying document with {new_version} ===")
|
||||
|
||||
with VersionRunner(
|
||||
new_version,
|
||||
db_url,
|
||||
port=8894,
|
||||
llm_provider=llm_config["provider"],
|
||||
llm_api_key=llm_config["api_key"],
|
||||
llm_model=llm_config["model"],
|
||||
) as new:
|
||||
server = new.start()
|
||||
client = httpx.Client(base_url=server.url, timeout=60)
|
||||
|
||||
# Verify document still exists
|
||||
resp = client.get(f"/v1/default/banks/{bank_id}/documents")
|
||||
assert resp.status_code == 200
|
||||
docs = resp.json()["items"]
|
||||
doc_ids = [d["id"] for d in docs]
|
||||
assert doc_id in doc_ids, f"Document not found after upgrade: {doc_ids}"
|
||||
|
||||
# Verify document details
|
||||
resp = client.get(f"/v1/default/banks/{bank_id}/documents/{doc_id}")
|
||||
assert resp.status_code == 200
|
||||
doc_info = resp.json()
|
||||
assert doc_info["id"] == doc_id
|
||||
assert doc_info["memory_unit_count"] > 0
|
||||
|
||||
# Cleanup
|
||||
resp = client.delete(f"/v1/default/banks/{bank_id}")
|
||||
assert resp.status_code == 200
|
||||
|
||||
client.close()
|
||||
|
||||
@pytest.mark.parametrize("old_version,new_version", UPGRADE_PATHS)
|
||||
def test_upgrade_preserves_bank_profile(self, db_url, llm_config, unique_bank_id, old_version, new_version):
|
||||
"""
|
||||
Verify bank profile (disposition) is preserved after upgrade.
|
||||
"""
|
||||
bank_id = unique_bank_id
|
||||
|
||||
# Phase 1: Create bank with custom disposition
|
||||
logger.info(f"=== Phase 1: Creating bank profile with {old_version} ===")
|
||||
|
||||
with VersionRunner(
|
||||
old_version,
|
||||
db_url,
|
||||
port=8895,
|
||||
llm_provider=llm_config["provider"],
|
||||
llm_api_key=llm_config["api_key"],
|
||||
llm_model=llm_config["model"],
|
||||
) as old:
|
||||
server = old.start()
|
||||
client = httpx.Client(base_url=server.url, timeout=60)
|
||||
|
||||
# Create bank by storing a memory
|
||||
resp = client.post(
|
||||
f"/v1/default/banks/{bank_id}/memories",
|
||||
json={"items": [{"content": "Test memory", "context": "test"}]},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Set custom disposition
|
||||
resp = client.put(
|
||||
f"/v1/default/banks/{bank_id}/profile",
|
||||
json={
|
||||
"disposition": {
|
||||
"skepticism": 4,
|
||||
"literalism": 2,
|
||||
"empathy": 5,
|
||||
}
|
||||
},
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
|
||||
# Verify profile
|
||||
resp = client.get(f"/v1/default/banks/{bank_id}/profile")
|
||||
assert resp.status_code == 200
|
||||
old_profile = resp.json()
|
||||
assert old_profile["disposition"]["skepticism"] == 4
|
||||
assert old_profile["disposition"]["literalism"] == 2
|
||||
assert old_profile["disposition"]["empathy"] == 5
|
||||
|
||||
client.close()
|
||||
|
||||
# Phase 2: Verify profile with new version
|
||||
logger.info(f"=== Phase 2: Verifying profile with {new_version} ===")
|
||||
|
||||
with VersionRunner(
|
||||
new_version,
|
||||
db_url,
|
||||
port=8896,
|
||||
llm_provider=llm_config["provider"],
|
||||
llm_api_key=llm_config["api_key"],
|
||||
llm_model=llm_config["model"],
|
||||
) as new:
|
||||
server = new.start()
|
||||
client = httpx.Client(base_url=server.url, timeout=60)
|
||||
|
||||
# Verify profile is preserved
|
||||
resp = client.get(f"/v1/default/banks/{bank_id}/profile")
|
||||
assert resp.status_code == 200
|
||||
new_profile = resp.json()
|
||||
assert new_profile["disposition"]["skepticism"] == 4, "Skepticism not preserved"
|
||||
assert new_profile["disposition"]["literalism"] == 2, "Literalism not preserved"
|
||||
assert new_profile["disposition"]["empathy"] == 5, "Empathy not preserved"
|
||||
|
||||
# Cleanup
|
||||
resp = client.delete(f"/v1/default/banks/{bank_id}")
|
||||
assert resp.status_code == 200
|
||||
|
||||
client.close()
|
||||
@@ -0,0 +1,275 @@
|
||||
"""
|
||||
Version runner for upgrade tests.
|
||||
|
||||
Manages running different git versions of the Hindsight API for upgrade testing.
|
||||
Handles git checkout, venv creation, dependency installation, and server lifecycle.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServerInfo:
|
||||
"""Information about a running server."""
|
||||
|
||||
url: str
|
||||
port: int
|
||||
version: str
|
||||
|
||||
|
||||
class VersionRunner:
|
||||
"""
|
||||
Manages running a specific git version of the Hindsight API.
|
||||
|
||||
For "HEAD" or "current", uses the current working directory.
|
||||
For git tags (e.g., "v0.3.0"), clones the repo at that tag to a temp directory.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
version: str,
|
||||
db_url: str,
|
||||
port: int = 8890,
|
||||
llm_provider: str | None = None,
|
||||
llm_api_key: str | None = None,
|
||||
llm_model: str | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize a version runner.
|
||||
|
||||
Args:
|
||||
version: Git tag (e.g., "v0.3.0") or "HEAD"/"current" for current code
|
||||
db_url: PostgreSQL connection URL
|
||||
port: Port to run the API on
|
||||
llm_provider: LLM provider (defaults to env var)
|
||||
llm_api_key: LLM API key (defaults to env var)
|
||||
llm_model: LLM model (defaults to env var)
|
||||
"""
|
||||
self.version = version
|
||||
self.db_url = db_url
|
||||
self.port = port
|
||||
self.llm_provider = llm_provider or os.getenv("HINDSIGHT_API_LLM_PROVIDER", "groq")
|
||||
self.llm_api_key = llm_api_key or os.getenv("HINDSIGHT_API_LLM_API_KEY") or os.getenv("GROQ_API_KEY")
|
||||
self.llm_model = llm_model or os.getenv("HINDSIGHT_API_LLM_MODEL", "llama-3.3-70b-versatile")
|
||||
|
||||
self.work_dir: Path | None = None
|
||||
self.process: subprocess.Popen | None = None
|
||||
self._temp_dir: str | None = None
|
||||
self._is_current = version.lower() in ("head", "current")
|
||||
|
||||
def _find_repo_root(self) -> Path:
|
||||
"""Find the git repository root."""
|
||||
result = subprocess.run(
|
||||
["git", "rev-parse", "--show-toplevel"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
return Path(result.stdout.strip())
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Checkout version and install dependencies."""
|
||||
if self._is_current:
|
||||
# Use current working directory
|
||||
self.work_dir = self._find_repo_root()
|
||||
logger.info(f"Using current code at {self.work_dir}")
|
||||
return
|
||||
|
||||
# Create temp dir and checkout specific version
|
||||
self._temp_dir = tempfile.mkdtemp(prefix=f"hindsight-{self.version}-")
|
||||
self.work_dir = Path(self._temp_dir)
|
||||
|
||||
repo_root = self._find_repo_root()
|
||||
logger.info(f"Cloning {repo_root} at {self.version} to {self.work_dir}")
|
||||
|
||||
# Shallow clone at specific tag
|
||||
subprocess.run(
|
||||
["git", "clone", "--depth", "1", "--branch", self.version, str(repo_root), str(self.work_dir)],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
|
||||
# Create venv and install
|
||||
venv_path = self.work_dir / ".venv-upgrade-test"
|
||||
logger.info(f"Creating venv at {venv_path}")
|
||||
|
||||
subprocess.run(["uv", "venv", str(venv_path)], check=True, capture_output=True)
|
||||
|
||||
api_path = self.work_dir / "hindsight-api"
|
||||
logger.info(f"Installing hindsight-api from {api_path}")
|
||||
|
||||
# Install with uv pip - use --index-strategy for pytorch
|
||||
subprocess.run(
|
||||
[
|
||||
"uv",
|
||||
"pip",
|
||||
"install",
|
||||
"-e",
|
||||
str(api_path),
|
||||
"--python",
|
||||
str(venv_path / "bin" / "python"),
|
||||
"--index-strategy",
|
||||
"unsafe-best-match",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
env={**os.environ, "UV_INDEX": "pytorch=https://download.pytorch.org/whl/cpu"},
|
||||
)
|
||||
|
||||
logger.info(f"Version {self.version} setup complete")
|
||||
|
||||
def _get_venv_path(self) -> Path:
|
||||
"""Get the path to the venv for this version."""
|
||||
if self._is_current:
|
||||
# For current code, the venv is at the workspace root (uv workspace layout)
|
||||
# Check both possible locations
|
||||
workspace_venv = self.work_dir / ".venv"
|
||||
api_venv = self.work_dir / "hindsight-api" / ".venv"
|
||||
|
||||
if (workspace_venv / "bin" / "hindsight-api").exists():
|
||||
return workspace_venv
|
||||
elif (api_venv / "bin" / "hindsight-api").exists():
|
||||
return api_venv
|
||||
else:
|
||||
# Default to workspace root
|
||||
return workspace_venv
|
||||
return self.work_dir / ".venv-upgrade-test"
|
||||
|
||||
def start(self) -> ServerInfo:
|
||||
"""
|
||||
Start the API server.
|
||||
|
||||
Returns:
|
||||
ServerInfo with the URL and port
|
||||
"""
|
||||
venv_path = self._get_venv_path()
|
||||
hindsight_api_bin = venv_path / "bin" / "hindsight-api"
|
||||
|
||||
if not hindsight_api_bin.exists():
|
||||
raise RuntimeError(f"hindsight-api binary not found at {hindsight_api_bin}")
|
||||
|
||||
env = os.environ.copy()
|
||||
env.update(
|
||||
{
|
||||
"HINDSIGHT_API_PORT": str(self.port),
|
||||
"HINDSIGHT_API_DATABASE_URL": self.db_url,
|
||||
"HINDSIGHT_API_HOST": "127.0.0.1",
|
||||
"HINDSIGHT_API_LLM_PROVIDER": self.llm_provider,
|
||||
"HINDSIGHT_API_LLM_API_KEY": self.llm_api_key or "",
|
||||
"HINDSIGHT_API_LLM_MODEL": self.llm_model,
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
}
|
||||
)
|
||||
|
||||
logger.info(f"Starting {self.version} API on port {self.port}")
|
||||
logger.info(f"Database URL: {self.db_url}")
|
||||
|
||||
# Determine working directory
|
||||
# For HEAD/current, use a temp directory to avoid .env file from workspace root
|
||||
# (hindsight-api loads .env with override=True which would override our env vars)
|
||||
if self._is_current:
|
||||
# Create a temp directory for HEAD to avoid workspace .env
|
||||
self._head_cwd = tempfile.mkdtemp(prefix="hindsight-head-cwd-")
|
||||
cwd = self._head_cwd
|
||||
else:
|
||||
cwd = str(self.work_dir)
|
||||
self._head_cwd = None
|
||||
|
||||
# Start the server
|
||||
self.process = subprocess.Popen(
|
||||
[str(hindsight_api_bin)],
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
cwd=cwd,
|
||||
)
|
||||
|
||||
self._wait_healthy()
|
||||
|
||||
url = f"http://127.0.0.1:{self.port}"
|
||||
logger.info(f"Server {self.version} ready at {url}")
|
||||
|
||||
return ServerInfo(url=url, port=self.port, version=self.version)
|
||||
|
||||
def _wait_healthy(self, timeout: int = 120) -> None:
|
||||
"""Wait for /health endpoint to respond."""
|
||||
url = f"http://127.0.0.1:{self.port}/health"
|
||||
deadline = time.time() + timeout
|
||||
|
||||
while time.time() < deadline:
|
||||
# Check if process is still alive
|
||||
if self.process and self.process.poll() is not None:
|
||||
stdout = self.process.stdout.read().decode() if self.process.stdout else ""
|
||||
raise RuntimeError(f"Server {self.version} exited unexpectedly.\nLogs:\n{stdout}")
|
||||
|
||||
try:
|
||||
resp = httpx.get(url, timeout=2)
|
||||
if resp.status_code == 200:
|
||||
return
|
||||
except httpx.RequestError:
|
||||
pass
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
# Timeout - dump logs
|
||||
if self.process:
|
||||
self.process.terminate()
|
||||
try:
|
||||
stdout, _ = self.process.communicate(timeout=5)
|
||||
logs = stdout.decode() if stdout else ""
|
||||
except Exception:
|
||||
logs = "(failed to read logs)"
|
||||
raise TimeoutError(f"Server {self.version} not healthy after {timeout}s.\nLogs:\n{logs}")
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the server and cleanup temp directory."""
|
||||
if self.process:
|
||||
logger.info(f"Stopping {self.version} server")
|
||||
self.process.terminate()
|
||||
try:
|
||||
self.process.wait(timeout=10)
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning(f"Server {self.version} did not stop gracefully, killing")
|
||||
self.process.kill()
|
||||
self.process.wait()
|
||||
self.process = None
|
||||
|
||||
if self._temp_dir and os.path.exists(self._temp_dir):
|
||||
logger.info(f"Cleaning up {self._temp_dir}")
|
||||
shutil.rmtree(self._temp_dir, ignore_errors=True)
|
||||
self._temp_dir = None
|
||||
|
||||
# Clean up HEAD's temp cwd
|
||||
if hasattr(self, "_head_cwd") and self._head_cwd and os.path.exists(self._head_cwd):
|
||||
shutil.rmtree(self._head_cwd, ignore_errors=True)
|
||||
self._head_cwd = None
|
||||
|
||||
def get_logs(self) -> str:
|
||||
"""Get current server logs (if process is running)."""
|
||||
if self.process and self.process.stdout:
|
||||
# Non-blocking read of available output
|
||||
import select
|
||||
|
||||
if hasattr(select, "select"):
|
||||
readable, _, _ = select.select([self.process.stdout], [], [], 0)
|
||||
if readable:
|
||||
return self.process.stdout.read(4096).decode()
|
||||
return ""
|
||||
|
||||
def __enter__(self) -> "VersionRunner":
|
||||
self.setup()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args) -> None:
|
||||
self.stop()
|
||||
@@ -22,7 +22,7 @@ This example showcases:
|
||||
- **Function calling** to bridge them together
|
||||
- **Streaming responses** for real-time interaction (enabled by default)
|
||||
- **Bidirectional memory** - both user data AND coach observations stored
|
||||
- **System-level post-processing** - automatic opinion storage for reliability
|
||||
- **System-level post-processing** - automatic knowledge consolidation
|
||||
- **Temporal-semantic memory** queries via function tools
|
||||
- **Enhanced preference learning** - coach learns and respects user likes/dislikes
|
||||
- **Real-world integration pattern** for adding memory to AI agents
|
||||
@@ -46,9 +46,9 @@ Hindsight API (returns workouts + preferences)
|
||||
|
|
||||
OpenAI Assistant (analyzes, gives advice)
|
||||
|
|
||||
Function Call: store_memory(advice as opinion)
|
||||
Function Call: store_memory(advice as experience)
|
||||
|
|
||||
Hindsight API (stores coach's observation)
|
||||
Hindsight API (stores coach's advice, consolidates into observations)
|
||||
|
|
||||
Personalized Answer
|
||||
```
|
||||
@@ -57,10 +57,10 @@ Personalized Answer
|
||||
|
||||
| Component | Standard Demo | OpenAI Integration |
|
||||
|-----------|---------------|-------------------|
|
||||
| **Conversation** | Hindsight `/think` endpoint | OpenAI Assistant API |
|
||||
| **Conversation** | Hindsight `/reflect` endpoint | OpenAI Assistant API |
|
||||
| **Memory** | Hindsight (built-in) | Hindsight (via function calling) |
|
||||
| **LLM** | Configured in Hindsight | OpenAI GPT-4 |
|
||||
| **Opinion Formation** | Automatic in `/think` | Explicit via `store_memory(type="opinion")` |
|
||||
| **Knowledge Consolidation** | Automatic after retain | Automatic after retain |
|
||||
| **Best For** | Hindsight-native apps | Integrating memory into existing OpenAI agents |
|
||||
|
||||
## Quick Start
|
||||
@@ -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_opinions(about)
|
||||
get_coach_insights(about) # Retrieves observations
|
||||
```
|
||||
|
||||
Each function makes API calls to Hindsight to fetch relevant memories.
|
||||
@@ -191,8 +191,8 @@ The agent will automatically:
|
||||
The OpenAI Agent can retrieve different memory types from Hindsight:
|
||||
|
||||
- **World Facts** (`fact_type: "world"`): Workouts, meals, activities
|
||||
- **Agent Facts** (`fact_type: "agent"`): Goals, intentions
|
||||
- **Opinions** (`fact_type: "opinion"`): Coach's observations about patterns
|
||||
- **Experience Facts** (`fact_type: "experience"`): Goals, intentions, coach advice
|
||||
- **Observations** (`fact_type: "observation"`): Consolidated knowledge about user patterns
|
||||
|
||||
## Customization
|
||||
|
||||
@@ -266,9 +266,9 @@ The key benefit: **Separation of concerns**
|
||||
|
||||
**Use Hindsight directly when:**
|
||||
- You want a complete memory-first solution
|
||||
- You want automatic memory retrieval and opinion formation
|
||||
- You want automatic memory retrieval and observation consolidation
|
||||
- You want to use different LLM providers (not just OpenAI)
|
||||
- You want the `/think` endpoint's integrated approach
|
||||
- You want the `/reflect` endpoint's integrated approach
|
||||
|
||||
## Learning Points
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ for r in results.results:
|
||||
|
||||
## Reflect: Generate Insights
|
||||
|
||||
The `reflect` operation performs a more thorough analysis of existing memories. This allows the agent to form new connections between memories which are then persisted as opinions and/or observations.
|
||||
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,12 +142,11 @@ print(response)
|
||||
|
||||
## Memory Types
|
||||
|
||||
Hindsight organizes memory into four networks to mimic human memory:
|
||||
Hindsight organizes knowledge into facts and consolidated observations:
|
||||
|
||||
- **World**: Facts about the world ("The stove gets hot")
|
||||
- **Experiences**: Agent's own experiences ("I touched the stove and it really hurt")
|
||||
- **Opinion**: Beliefs with confidence scores ("I shouldn't touch the stove again" - .99 confidence)
|
||||
- **Observation**: Complex mental models derived by reflecting on facts and experiences
|
||||
- **Experience**: Agent's own experiences ("I touched the stove and it really hurt")
|
||||
- **Observation**: Consolidated knowledge synthesized from facts ("Always be careful around hot surfaces")
|
||||
|
||||
## Cleanup
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ The backup includes:
|
||||
- Memory banks and their configuration
|
||||
- Documents and chunks
|
||||
- Entities and their relationships
|
||||
- Memory units (facts, experiences, opinions, observations)
|
||||
- Memory units (facts, experiences, observations)
|
||||
- Entity cooccurrences and memory links
|
||||
|
||||
:::note Consistency
|
||||
|
||||
@@ -147,6 +147,5 @@ Deleting a document permanently removes all memories extracted from it. This act
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Entities**](./entities) — Track people, places, and concepts
|
||||
- [**Operations**](./operations) — Monitor background tasks
|
||||
- [**Memory Banks**](./memory-banks) — Configure bank settings
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
---
|
||||
sidebar_position: 7
|
||||
---
|
||||
|
||||
# Entities
|
||||
|
||||
Entities are the people, organizations, places, and concepts that Hindsight automatically extracts and tracks across your memory bank.
|
||||
|
||||
:::info Automatic Feature
|
||||
You don't need to do anything to use entities—Hindsight extracts them automatically when you call `retain`. However, understanding how entities work is important because they power key features in [recall](./recall) and [reflect](./reflect).
|
||||
:::
|
||||
|
||||
## Why Entities Matter
|
||||
|
||||
Entities improve recall quality in two ways:
|
||||
|
||||
1. **Co-occurrence tracking** — When entities appear together in facts, Hindsight builds a graph of relationships. This enables graph-based recall to find indirect connections.
|
||||
|
||||
2. **Observations** — Hindsight synthesizes high-level summaries about each entity from multiple facts. Including entity observations in recall provides richer context.
|
||||
|
||||
## What Gets Extracted?
|
||||
|
||||
When you retain information, the LLM extracts named entities from each fact:
|
||||
|
||||
- **People** — Names like "Alice", "Dr. Smith", "CEO John"
|
||||
- **Organizations** — Companies, teams, institutions
|
||||
- **Places** — Cities, countries, specific locations
|
||||
- **Products/Objects** — Software, tools, significant items
|
||||
- **Concepts** — Abstract themes like "career growth", "friendship"
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
Content: "Alice works at Google in Mountain View. She specializes in TensorFlow."
|
||||
|
||||
Entities extracted:
|
||||
- Alice (person)
|
||||
- Google (organization)
|
||||
- Mountain View (location)
|
||||
- TensorFlow (product)
|
||||
```
|
||||
|
||||
## Entity Resolution
|
||||
|
||||
When the same entity is mentioned multiple times (possibly with different names), Hindsight resolves them to a single canonical entity using a scoring algorithm:
|
||||
|
||||
### Resolution Factors
|
||||
|
||||
1. **Name similarity (50%)** — How closely the text matches existing entity names. Handles variations like "Alice" vs "Alice Chen" or partial matches.
|
||||
|
||||
2. **Co-occurrence (30%)** — Entities that frequently appear together are more likely to be the same. If "Alice" always appears with "Google" and "TensorFlow", a new mention of "Alice" near those entities scores higher for matching.
|
||||
|
||||
3. **Temporal proximity (20%)** — Recent mentions are weighted more heavily. If an entity was seen in the last 7 days, new similar mentions are more likely to match.
|
||||
|
||||
### Resolution Threshold
|
||||
|
||||
A match requires a combined score above **0.6** (60%). Below this threshold, Hindsight creates a new entity rather than risk merging distinct entities.
|
||||
|
||||
This means:
|
||||
- Exact name matches with recent co-occurring entities → strong match
|
||||
- Partial name matches without context → likely creates new entity
|
||||
- Same name in completely different contexts → may create separate entities
|
||||
|
||||
## Entity Observations
|
||||
|
||||
Observations are **derived state**—high-level summaries that Hindsight automatically synthesizes from the facts associated with an entity. They provide a condensed view of what the system knows about important entities.
|
||||
|
||||
**Example:**
|
||||
|
||||
Facts about Alice:
|
||||
- "Alice works at Google"
|
||||
- "Alice is a software engineer"
|
||||
- "Alice specializes in ML"
|
||||
- "Alice joined Google in 2020"
|
||||
- "Alice leads the search team"
|
||||
|
||||
Observation created:
|
||||
- "Alice is a software engineer at Google who joined in 2020, specializes in ML, and leads the search team"
|
||||
|
||||
### How Observations Work
|
||||
|
||||
Observations are **not generated for every entity**. When you retain new documents:
|
||||
|
||||
1. **Top entities selected** — Hindsight identifies the top 5 most-mentioned entities in the batch
|
||||
2. **Threshold check** — Only entities with at least 5 facts get observations
|
||||
3. **Regeneration** — Observations are regenerated using the entity's most recent 50 facts
|
||||
4. **Old observations replaced** — Previous observations are deleted and new ones created
|
||||
|
||||
This means:
|
||||
- Frequently mentioned entities get observations; rarely mentioned ones don't
|
||||
- Observations stay up-to-date as new information is retained
|
||||
- The system prioritizes entities that matter most to your memory bank
|
||||
|
||||
### Observations vs Opinions
|
||||
|
||||
Observations are **objective summaries**—they synthesize facts without any bias or perspective. This is different from [opinions](./opinions), which are influenced by the memory bank's disposition.
|
||||
|
||||
| | Observations | Opinions |
|
||||
|---|---|---|
|
||||
| **Purpose** | Summarize what's known about an entity | Express the bank's perspective on a topic |
|
||||
| **Disposition influence** | No | Yes |
|
||||
| **Scope** | Per-entity | Any topic |
|
||||
| **Generation** | Automatic (top entities) | On-demand via reflect |
|
||||
|
||||
### Using Observations
|
||||
|
||||
Observations are included in recall results when you set `include_entities=True`. They provide quick context about key entities without retrieving all underlying facts.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Recall**](./recall) — Use entities in memory retrieval
|
||||
- [**Reflect**](./reflect) — Get entity-aware responses
|
||||
@@ -89,7 +89,7 @@ hindsight recall my-bank "Tell me about Alice" -v
|
||||
|
||||
## Reflect: Reason with Disposition
|
||||
|
||||
Generate disposition-aware responses that form opinions based on evidence.
|
||||
Generate disposition-aware responses using memories and observations.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
@@ -104,7 +104,7 @@ Generate disposition-aware responses that form opinions based on evidence.
|
||||
# Basic reflect
|
||||
hindsight reflect my-bank "Should we adopt TypeScript for our backend?"
|
||||
|
||||
# Verbose output (shows sources and opinions)
|
||||
# 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 are recalled, bank disposition is loaded, LLM reasons through evidence, new opinions are formed and stored.
|
||||
**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 | Reasoned response + opinions |
|
||||
| **Output** | Memory IDs | Ranked facts + observations | Reasoned response |
|
||||
| **Uses LLM** | Yes (extraction) | No | Yes (generation) |
|
||||
| **Forms opinions** | No | No | Yes |
|
||||
| **Uses observations** | No | Yes | Yes |
|
||||
| **Disposition** | No | No | Yes |
|
||||
|
||||
---
|
||||
@@ -137,5 +137,5 @@ hindsight reflect my-bank "Analyze our tech stack" --budget high
|
||||
|
||||
- [**Retain**](./retain) — Advanced options for storing memories
|
||||
- [**Recall**](./recall) — Tuning search quality and performance
|
||||
- [**Reflect**](./reflect) — Configuring disposition and opinions
|
||||
- [**Reflect**](./reflect) — Configuring disposition
|
||||
- [**Memory Banks**](./memory-banks) — Managing memory bank disposition
|
||||
|
||||
@@ -43,8 +43,8 @@ Make sure you've completed the [Quick Start](./quickstart) to install the client
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# Set background
|
||||
hindsight bank background my-bank "I am a research assistant specializing in ML"
|
||||
# Set mission
|
||||
hindsight bank mission my-bank "I am a research assistant specializing in ML"
|
||||
|
||||
# Set disposition
|
||||
hindsight bank disposition my-bank \
|
||||
@@ -56,30 +56,30 @@ hindsight bank disposition my-bank \
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Background and Disposition
|
||||
## Mission and Disposition
|
||||
|
||||
Background and disposition are optional settings that influence how the bank forms opinions during [reflect](./reflect) operations.
|
||||
Mission and disposition are optional settings that influence how the bank reasons during [reflect](./reflect) operations.
|
||||
|
||||
:::info
|
||||
Background and disposition only affect the `reflect` operation (opinion formation). They do not impact `retain`, `recall`, or other memory operations.
|
||||
Mission and disposition only affect the `reflect` operation. They do not impact `retain`, `recall`, or other memory operations.
|
||||
:::
|
||||
|
||||
### Background
|
||||
### Mission
|
||||
|
||||
The background is a first-person narrative providing context for opinion formation:
|
||||
The mission is a first-person narrative providing context for reasoning:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={memoryBanksPy} section="bank-background" language="python" />
|
||||
<CodeSnippet code={memoryBanksPy} section="bank-mission" language="python" />
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
<CodeSnippet code={memoryBanksMjs} section="bank-background" language="javascript" />
|
||||
<CodeSnippet code={memoryBanksMjs} section="bank-mission" language="javascript" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Disposition Traits
|
||||
|
||||
Disposition traits influence how opinions are formed during reflection. Each trait is scored 1 to 5:
|
||||
Disposition traits influence how reasoning is performed during reflection. Each trait is scored 1 to 5:
|
||||
|
||||
| Trait | Low (1) | High (5) |
|
||||
|-------|---------|----------|
|
||||
|
||||
@@ -0,0 +1,214 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Mental Models
|
||||
|
||||
User-curated summaries that provide high-quality, pre-computed answers for common queries.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeSnippet from '@site/src/components/CodeSnippet';
|
||||
|
||||
{/* Import raw source files */}
|
||||
import mentalModelsPy from '!!raw-loader!@site/examples/api/mental-models.py';
|
||||
|
||||
## What Are Mental Models?
|
||||
|
||||
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 Mental Model] --> B[Run Reflect]
|
||||
B --> C[Store Result]
|
||||
C --> D[Future Queries]
|
||||
D --> E{Match Found?}
|
||||
E -->|Yes| F[Return Mental Model]
|
||||
E -->|No| G[Run Full Reflect]
|
||||
```
|
||||
|
||||
### Why Use Mental Models?
|
||||
|
||||
| Benefit | Description |
|
||||
|---------|-------------|
|
||||
| **Consistency** | Same answer every time for common questions |
|
||||
| **Speed** | Pre-computed responses are returned instantly |
|
||||
| **Quality** | Manually curated summaries you've reviewed |
|
||||
| **Control** | Define exactly how key topics should be answered |
|
||||
|
||||
### Hierarchical Retrieval
|
||||
|
||||
During reflect, the agent checks sources in priority order:
|
||||
|
||||
1. **Mental Models** — User-curated summaries (highest priority)
|
||||
2. **Observations** — Consolidated knowledge
|
||||
3. **Raw Facts** — Ground truth memories
|
||||
|
||||
Mental models are checked first because they represent your explicitly curated knowledge.
|
||||
|
||||
---
|
||||
|
||||
## Create a Mental Model
|
||||
|
||||
Creating a mental model runs a reflect operation in the background and saves the result:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={mentalModelsPy} section="create-mental-model" language="python" />
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# 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",
|
||||
"source_query": "How does the team prefer to communicate?",
|
||||
"tags": ["team"]
|
||||
}'
|
||||
|
||||
# Response: {"operation_id": "op-123"}
|
||||
# Use the operations endpoint to check completion
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `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 mental model content |
|
||||
|
||||
---
|
||||
|
||||
## List Mental Models
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="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/mental-models"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Get a Mental Model
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="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/mental-models/{mental_model_id}"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Response Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `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 mental model text |
|
||||
| `tags` | list | Tags for filtering |
|
||||
| `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 Mental Model
|
||||
|
||||
Re-run the source query to update the mental model with current knowledge:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="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/mental-models/{mental_model_id}/refresh"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Refreshing is useful when:
|
||||
- New memories have been retained that affect the topic
|
||||
- Observations have been updated
|
||||
- You want to ensure the mental model reflects current knowledge
|
||||
|
||||
---
|
||||
|
||||
## Update a Mental Model
|
||||
|
||||
Update the mental model's name:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="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/mental-models/{mental_model_id}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "Updated Team Communication Preferences"}'
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Delete a Mental Model
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="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/mental-models/{mental_model_id}"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
|
||||
## Use Cases
|
||||
|
||||
| Use Case | Example |
|
||||
|----------|---------|
|
||||
| **FAQ Answers** | Pre-compute answers to common customer questions |
|
||||
| **Onboarding Summaries** | "What should new team members know?" |
|
||||
| **Status Reports** | "What's the current project status?" refreshed weekly |
|
||||
| **Policy Summaries** | "What are our security policies?" |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Reflect**](./reflect) — How the agentic loop uses mental models
|
||||
- [**Observations**](/developer/observations) — How knowledge is consolidated
|
||||
- [**Operations**](./operations) — Track async mental model creation
|
||||
@@ -25,9 +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 |
|
||||
| **form_opinion** | After each `reflect` call | Extracts and stores new opinions formed during reflection |
|
||||
| **reinforce_opinion** | After `retain` | Updates opinion confidence based on new supporting evidence |
|
||||
| **regenerate_observations** | Bank profile update | Regenerates entity observations when disposition changes |
|
||||
| **consolidate** | After `retain` | Consolidates new facts into observations |
|
||||
|
||||
## Async Retain Example
|
||||
|
||||
@@ -93,5 +91,4 @@ Response:
|
||||
## Next Steps
|
||||
|
||||
- [**Documents**](./documents) — Track document sources
|
||||
- [**Entities**](./entities) — Monitor entity tracking
|
||||
- [**Memory Banks**](./memory-banks) — Configure bank settings
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Opinions
|
||||
|
||||
How memory banks form, store, and evolve beliefs.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
import CodeSnippet from '@site/src/components/CodeSnippet';
|
||||
|
||||
{/* Import raw source files */}
|
||||
import opinionsPy from '!!raw-loader!@site/examples/api/opinions.py';
|
||||
import opinionsMjs from '!!raw-loader!@site/examples/api/opinions.mjs';
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) to install the client and start the server.
|
||||
:::
|
||||
|
||||
## What Are Opinions?
|
||||
|
||||
Opinions are beliefs formed by the memory bank based on evidence and disposition. Unlike world facts (objective information received) or experience (conversations and events), opinions are **judgments** with confidence scores.
|
||||
|
||||
| Type | Example | Confidence |
|
||||
|------|---------|------------|
|
||||
| World Fact | "Python was created in 1991" | — |
|
||||
| Experience | "I recommended Python to Bob" | — |
|
||||
| Opinion | "Python is the best language for data science" | 0.85 |
|
||||
|
||||
## How Opinions Form
|
||||
|
||||
Opinions are created during `reflect` operations when the memory bank:
|
||||
1. Retrieves relevant facts
|
||||
2. Applies disposition traits
|
||||
3. Forms a judgment
|
||||
4. Assigns a confidence score
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
F[Facts] --> D[Disposition Filter]
|
||||
D --> J[Judgment]
|
||||
J --> O[Opinion + Confidence]
|
||||
O --> S[(Store)]
|
||||
```
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={opinionsPy} section="opinion-form" language="python" />
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
<CodeSnippet code={opinionsMjs} section="opinion-form" language="javascript" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Searching Opinions
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={opinionsPy} section="opinion-search" language="python" />
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
<CodeSnippet code={opinionsMjs} section="opinion-search" language="javascript" />
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight recall my-bank "programming" --types opinion
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Opinion Evolution
|
||||
|
||||
Opinions change as new evidence arrives:
|
||||
|
||||
| Evidence Type | Effect |
|
||||
|---------------|--------|
|
||||
| **Reinforcing** | Confidence increases (+0.1) |
|
||||
| **Weakening** | Confidence decreases (-0.15) |
|
||||
| **Contradicting** | Opinion revised, confidence reset |
|
||||
|
||||
**Example evolution:**
|
||||
|
||||
```
|
||||
t=0: "Python is best for data science" (0.70)
|
||||
↓ New evidence: Python dominates ML libraries
|
||||
t=1: "Python is best for data science" (0.85)
|
||||
↓ New evidence: Julia is 10x faster for numerical computing
|
||||
t=2: "Python is best for data science, though Julia is faster" (0.75)
|
||||
↓ New evidence: Most teams still use Python
|
||||
t=3: "Python is best for data science" (0.82)
|
||||
```
|
||||
|
||||
## Disposition Influence
|
||||
|
||||
Different dispositions form different opinions from the same facts:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={opinionsPy} section="opinion-disposition" language="python" />
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
<CodeSnippet code={opinionsMjs} section="opinion-disposition" language="javascript" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Opinions in Reflect Responses
|
||||
|
||||
When `reflect` uses opinions, they appear in `based_on`:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={opinionsPy} section="opinion-in-reflect" language="python" />
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
<CodeSnippet code={opinionsMjs} section="opinion-in-reflect" language="javascript" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Confidence Thresholds
|
||||
|
||||
Opinions below a confidence threshold may be:
|
||||
- Excluded from responses
|
||||
- Marked as uncertain
|
||||
- Revised more easily
|
||||
|
||||
```python
|
||||
# Low confidence opinions are held loosely
|
||||
# "I think Python might be good for this" (0.45)
|
||||
|
||||
# High confidence opinions are stated firmly
|
||||
# "Python is definitely the right choice" (0.92)
|
||||
```
|
||||
@@ -105,5 +105,5 @@ curl -fsSL https://hindsight.vectorize.io/get-cli | bash
|
||||
- [**Retain**](./retain) — Advanced options for storing memories
|
||||
- [**Recall**](./recall) — Search and retrieval strategies
|
||||
- [**Reflect**](./reflect) — Disposition-aware reasoning
|
||||
- [**Memory Banks**](./memory-banks) — Configure disposition and background
|
||||
- [**Memory Banks**](./memory-banks) — Configure disposition and mission
|
||||
- [**Server Deployment**](/developer/installation) — Docker Compose, Helm, and production setup
|
||||
|
||||
@@ -42,12 +42,12 @@ 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`, `opinion` |
|
||||
| `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 |
|
||||
| `include_entities` | bool | false | Include entity observations |
|
||||
| `max_entity_tokens` | int | 500 | Token budget for entity observations |
|
||||
| `include_chunks` | bool | false | Include raw text chunks that generated the memories |
|
||||
| `max_chunk_tokens` | int | 500 | Token budget for chunks |
|
||||
| `tags` | list | None | Filter memories by tags (see [Tag Filtering](#filter-by-tags)) |
|
||||
| `tags_match` | string | "any" | How to match tags: `any`, `all`, `any_strict`, `all_strict` |
|
||||
|
||||
@@ -68,18 +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-opinions-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>
|
||||
|
||||
:::warning About Opinions
|
||||
Opinions are beliefs formed during [reflect](/developer/api/reflect) operations. Unlike world facts and experience, opinions are subjective interpretations and may not represent objective truth. Depending on your use case:
|
||||
- **Exclude opinions** (`types=["world", "experience"]`) when you need factual, verifiable information
|
||||
- **Include opinions** when you want the agent's perspective or formed beliefs
|
||||
- **Use opinions alone** (`types=["opinion"]`) only when specifically asking about the agent's views
|
||||
:::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
|
||||
@@ -96,23 +93,6 @@ The `max_tokens` parameter lets you control how much of your agent's context bud
|
||||
|
||||
This design means you never have to guess whether 10 results or 50 results will fit your context. Just specify the token budget and Hindsight returns as many relevant memories as will fit.
|
||||
|
||||
## Include Related Context
|
||||
|
||||
Beyond the core memory results, you can optionally retrieve additional context—each with its own token budget:
|
||||
|
||||
| Option | Parameter | Description |
|
||||
|--------|-----------|-------------|
|
||||
| **Chunks** | `include_chunks`, `max_chunk_tokens` | Raw text chunks that generated the memories |
|
||||
| **Entity Observations** | `include_entities`, `max_entity_tokens` | Related observations about entities mentioned in results |
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={recallPy} section="recall-include-entities" language="python" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
This gives your agent richer context while maintaining precise control over total token consumption.
|
||||
|
||||
## Budget Levels
|
||||
|
||||
The `budget` parameter controls graph traversal depth:
|
||||
|
||||
@@ -4,15 +4,14 @@ sidebar_position: 3
|
||||
|
||||
# Reflect
|
||||
|
||||
Generate disposition-aware responses using retrieved memories.
|
||||
Generate disposition-aware responses using an agentic reasoning loop.
|
||||
|
||||
When you call **reflect**, Hindsight performs a multi-step reasoning process:
|
||||
1. **Recalls** relevant memories from the bank based on your query
|
||||
When you call **reflect**, Hindsight runs an **agentic loop** that:
|
||||
1. **Autonomously searches** for relevant information using multiple tools
|
||||
2. **Applies** the bank's disposition traits to shape the reasoning style
|
||||
3. **Generates** a contextual answer grounded in the retrieved facts
|
||||
4. **Forms opinions** in the background based on the reasoning (available in subsequent calls)
|
||||
3. **Generates** a grounded answer with citations to the sources used
|
||||
|
||||
The response includes the generated answer along with the facts that were used, providing full transparency into how the answer was derived.
|
||||
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';
|
||||
@@ -24,7 +23,7 @@ import reflectMjs from '!!raw-loader!@site/examples/api/reflect.mjs';
|
||||
import reflectSh from '!!raw-loader!@site/examples/api/reflect.sh';
|
||||
|
||||
:::info How Reflect Works
|
||||
Learn about disposition-driven reasoning and opinion formation in the [Reflect Architecture](/developer/reflect) guide.
|
||||
Learn about disposition-driven reasoning in the [Reflect Architecture](/developer/reflect) guide.
|
||||
:::
|
||||
|
||||
:::tip Prerequisites
|
||||
@@ -50,21 +49,41 @@ Make sure you've completed the [Quick Start](./quickstart) to install the client
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `query` | string | required | Question or prompt |
|
||||
| `budget` | string | "low" | Budget level: `low`, `mid`, `high` |
|
||||
| `context` | string | None | Additional context for the query |
|
||||
| `max_tokens` | int | 4096 | Maximum tokens for the response |
|
||||
| `budget` | string | "low" | Budget level: `low`, `mid`, `high` (see below) |
|
||||
| `max_tokens` | int | 4096 | Maximum tokens for the final response |
|
||||
| `response_schema` | object | None | JSON Schema for [structured output](#structured-output) |
|
||||
| `tags` | list | None | Filter memories by tags during reflection |
|
||||
| `tags_match` | string | "any" | How to match tags: `any`, `all`, `any_strict`, `all_strict` |
|
||||
| `trace` | bool | false | Include detailed agent trace in response |
|
||||
|
||||
### Budget
|
||||
|
||||
The `budget` parameter controls how thoroughly the agent searches for information:
|
||||
|
||||
| Budget | Iterations | Use Case |
|
||||
|--------|------------|----------|
|
||||
| `low` | 0.5x base | Quick answers, simple lookups |
|
||||
| `mid` | 1x base | Balanced exploration |
|
||||
| `high` | 2x base | Complex questions, comprehensive analysis |
|
||||
|
||||
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
|
||||
|
||||
The `max_tokens` parameter limits the length of the final generated response. This does not affect how much the agent can retrieve during the agentic loop — only the final answer length.
|
||||
|
||||
### Response Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `text` | string | The generated answer text |
|
||||
| `based_on` | array | Facts used to generate the response |
|
||||
| `used_memory_ids` | array | Memory 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) |
|
||||
| `usage` | TokenUsage | Token usage metrics for the LLM call |
|
||||
| `iterations` | int | Number of agent loop iterations |
|
||||
| `tools_called` | int | Total number of tool calls made |
|
||||
| `usage` | TokenUsage | Token usage metrics |
|
||||
|
||||
The `usage` field contains:
|
||||
- `input_tokens`: Number of input/prompt tokens consumed
|
||||
@@ -80,35 +99,6 @@ The `usage` field contains:
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## The Role of Context
|
||||
|
||||
The `context` parameter steers how the reflection is performed without impacting the memory recall. It provides situational information that helps shape the reasoning and response.
|
||||
|
||||
**How context is used:**
|
||||
- **Shapes reasoning**: Helps understand the situation when formulating an answer
|
||||
- **Disambiguates intent**: Clarifies what aspect of the query matters most
|
||||
- **Does not affect recall**: The same memories are retrieved regardless of context
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={reflectPy} section="reflect-with-context" language="python" />
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
<CodeSnippet code={reflectMjs} section="reflect-with-context" language="javascript" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Opinion Formation
|
||||
|
||||
When reflect reasons about a question, it may form new **opinions** based on the evidence in the memory bank. These opinions are created in the background and become available in subsequent `reflect` and `recall` calls.
|
||||
|
||||
**Why opinions matter:**
|
||||
- **Consistent thinking**: Opinions ensure the memory bank maintains a coherent perspective over time
|
||||
- **Evolving viewpoints**: As more information is retained, opinions can be refined or updated
|
||||
- **Grounded reasoning**: Opinions are always derived from factual evidence in the memory bank
|
||||
|
||||
Opinions are stored as a special memory type and are automatically retrieved when relevant to future queries. This creates a natural evolution of the bank's perspective, similar to how humans form and refine their views based on accumulated experience.
|
||||
|
||||
## Disposition Influence
|
||||
|
||||
The bank's disposition affects reflect responses:
|
||||
@@ -128,23 +118,20 @@ The bank's disposition affects reflect responses:
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Using Sources
|
||||
## Citations
|
||||
|
||||
The `based_on` field shows which memories informed the response:
|
||||
The agent cites which sources it used to generate the response:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={reflectPy} section="reflect-sources" language="python" />
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
<CodeSnippet code={reflectMjs} section="reflect-sources" language="javascript" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
- `used_memory_ids` — Raw memory facts that were retrieved and cited
|
||||
- `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.
|
||||
|
||||
This enables:
|
||||
- **Transparency** — users see why the bank said something
|
||||
- **Verification** — check if the response is grounded in facts
|
||||
- **Debugging** — understand retrieval quality
|
||||
- **Transparency** — users see exactly which sources informed the answer
|
||||
- **Verification** — check if the response is grounded in actual memories
|
||||
- **Debugging** — use `trace=True` for detailed tool call logs
|
||||
|
||||
## Structured Output
|
||||
|
||||
@@ -154,86 +141,13 @@ The easiest way to define a schema is using **Pydantic models**:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
# Define your response structure with Pydantic
|
||||
class HiringRecommendation(BaseModel):
|
||||
recommendation: str
|
||||
confidence: str # "low", "medium", "high"
|
||||
key_factors: list[str]
|
||||
risks: list[str] = []
|
||||
|
||||
with Hindsight() as client:
|
||||
response = client.reflect(
|
||||
bank_id="hiring-team",
|
||||
query="Should we hire Alice for the ML team lead position?",
|
||||
response_schema=HiringRecommendation.model_json_schema(),
|
||||
)
|
||||
|
||||
# Parse structured output into Pydantic model
|
||||
result = HiringRecommendation.model_validate(response.structured_output)
|
||||
print(f"Recommendation: {result.recommendation}")
|
||||
print(f"Confidence: {result.confidence}")
|
||||
print(f"Key factors: {result.key_factors}")
|
||||
```
|
||||
|
||||
<CodeSnippet code={reflectPy} section="reflect-structured-output" language="python" />
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```javascript
|
||||
import { Hindsight } from "@anthropic-ai/hindsight";
|
||||
|
||||
const client = new Hindsight();
|
||||
|
||||
// Define JSON schema directly
|
||||
const responseSchema = {
|
||||
type: "object",
|
||||
properties: {
|
||||
recommendation: { type: "string" },
|
||||
confidence: { type: "string", enum: ["low", "medium", "high"] },
|
||||
key_factors: { type: "array", items: { type: "string" } },
|
||||
risks: { type: "array", items: { type: "string" } },
|
||||
},
|
||||
required: ["recommendation", "confidence", "key_factors"],
|
||||
};
|
||||
|
||||
const response = await client.reflect({
|
||||
bankId: "hiring-team",
|
||||
query: "Should we hire Alice for the ML team lead position?",
|
||||
responseSchema: responseSchema,
|
||||
});
|
||||
|
||||
// Structured output
|
||||
console.log(response.structuredOutput.recommendation);
|
||||
console.log(response.structuredOutput.keyFactors);
|
||||
```
|
||||
|
||||
<CodeSnippet code={reflectMjs} section="reflect-structured-output" language="javascript" />
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
First, create a JSON schema file `schema.json`:
|
||||
```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"]
|
||||
}
|
||||
```
|
||||
|
||||
Then use the `--schema` flag:
|
||||
```bash
|
||||
hindsight memory reflect hiring-team \
|
||||
"Should we hire Alice for the ML team lead position?" \
|
||||
--schema schema.json
|
||||
```
|
||||
|
||||
<CodeSnippet code={reflectSh} section="reflect-structured-output" language="bash" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
|
||||
@@ -169,15 +169,10 @@ Use consistent naming patterns for tags:
|
||||
|
||||
Use the list tags API to discover existing tags, useful for UI autocomplete or wildcard expansion:
|
||||
|
||||
```python
|
||||
# List all tags in a bank
|
||||
tags = client.list_tags(bank_id="my-bank")
|
||||
for tag in tags.items:
|
||||
print(f"{tag.tag}: {tag.count} memories")
|
||||
|
||||
# Search with wildcards (* matches any characters)
|
||||
user_tags = client.list_tags(bank_id="my-bank", q="user:*")
|
||||
admin_tags = client.list_tags(bank_id="my-bank", q="*-admin")
|
||||
```
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
<CodeSnippet code={retainPy} section="retain-list-tags" language="python" />
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
See [Recall API](./recall#filter-by-tags) for filtering memories by tags during retrieval.
|
||||
|
||||
@@ -301,15 +301,6 @@ For advanced authentication (JWT, OAuth, multi-tenant schemas), implement a cust
|
||||
- **`mpfp`**: Multi-Path Fact Propagation - iterative graph traversal with activation spreading. More thorough but slower.
|
||||
- **`bfs`**: Breadth-first search from seed facts. Simple but less effective for large graphs.
|
||||
|
||||
### Entity Observations
|
||||
|
||||
Controls when the system generates entity observations (summaries about entities mentioned in retained content).
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HINDSIGHT_API_OBSERVATION_MIN_FACTS` | Minimum facts about an entity before generating observations | `5` |
|
||||
| `HINDSIGHT_API_OBSERVATION_TOP_ENTITIES` | Max entities to process per retain batch | `5` |
|
||||
|
||||
### Retain
|
||||
|
||||
Controls the retain (memory ingestion) pipeline.
|
||||
@@ -320,7 +311,6 @@ Controls the retain (memory ingestion) pipeline.
|
||||
| `HINDSIGHT_API_RETAIN_CHUNK_SIZE` | Max characters per chunk for fact extraction. Larger chunks extract fewer LLM calls but may lose context. | `3000` |
|
||||
| `HINDSIGHT_API_RETAIN_EXTRACTION_MODE` | Fact extraction mode: `concise` (selective, fewer high-quality facts) or `verbose` (detailed, more facts) | `concise` |
|
||||
| `HINDSIGHT_API_RETAIN_EXTRACT_CAUSAL_LINKS` | Extract causal relationships between facts | `true` |
|
||||
| `HINDSIGHT_API_RETAIN_OBSERVATIONS_ASYNC` | Run entity observation generation asynchronously (after retain completes) | `false` |
|
||||
|
||||
#### Extraction Modes
|
||||
|
||||
|
||||
@@ -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 needs to form opinions** — A coding assistant that remembers "the user prefers functional programming" should weigh that 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.
|
||||
@@ -21,7 +21,7 @@ Hindsight solves these problems with a memory system designed specifically for A
|
||||
## What Hindsight Does
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
graph LR
|
||||
subgraph app["<b>Your Application</b>"]
|
||||
Agent[AI Agent]
|
||||
end
|
||||
@@ -30,9 +30,13 @@ graph TB
|
||||
API[API Server]
|
||||
|
||||
subgraph bank["<b>Memory Bank</b>"]
|
||||
direction TB
|
||||
Observations[Observations]
|
||||
MemEnt[Memories & Entities]
|
||||
Chunks[Chunks]
|
||||
Documents[Documents]
|
||||
Memories[Memories]
|
||||
Entities[Entities]
|
||||
|
||||
Observations --> MemEnt --> Chunks --> Documents
|
||||
end
|
||||
end
|
||||
|
||||
@@ -40,24 +44,22 @@ graph TB
|
||||
Agent -->|recall| API
|
||||
Agent -->|reflect| API
|
||||
|
||||
API --> Documents
|
||||
API --> Memories
|
||||
API --> Entities
|
||||
API --> bank
|
||||
```
|
||||
|
||||
**Your AI agent** stores information via `retain()`, searches with `recall()`, and reasons with `reflect()` — all interactions with its dedicated **memory bank**
|
||||
|
||||
## Key Components
|
||||
|
||||
### Three Memory Types
|
||||
### Memory Types
|
||||
|
||||
Hindsight separates memories by type for epistemic clarity:
|
||||
Hindsight organizes knowledge into facts and consolidated observations:
|
||||
|
||||
| Type | What it stores | Example |
|
||||
|------|----------------|---------|
|
||||
| **World** | Objective facts received | "Alice works at Google" |
|
||||
| **Bank** | Bank's own actions | "I recommended Python to Bob" |
|
||||
| **Opinion** | Formed beliefs + confidence | "Python is best for ML" (0.85) |
|
||||
| **Experience** | Bank's own actions and interactions | "I recommended Python to Bob" |
|
||||
| **Observation** | Consolidated knowledge from facts | "The user prefers functional programming patterns"
|
||||
|
||||
### Multi-Strategy Retrieval (TEMPR)
|
||||
|
||||
@@ -86,9 +88,17 @@ graph LR
|
||||
| **Graph** | Related entities, indirect connections |
|
||||
| **Temporal** | "last spring", "in June", time ranges |
|
||||
|
||||
### Observation Consolidation
|
||||
|
||||
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 observations
|
||||
- **Evidence tracking**: Each observation tracks which facts support it
|
||||
- **Continuous refinement**: Observations evolve as new evidence arrives
|
||||
|
||||
### Disposition Traits
|
||||
|
||||
Memory banks have disposition traits that influence how opinions are formed during Reflect:
|
||||
Memory banks have disposition traits that influence reasoning during Reflect:
|
||||
|
||||
| Trait | Scale | Low (1) | High (5) |
|
||||
|-------|-------|---------|----------|
|
||||
@@ -107,14 +117,13 @@ These traits only affect the `reflect` operation, not `recall`.
|
||||
### Core Concepts
|
||||
- [**Retain**](/developer/retain) — How memories are stored with multi-dimensional facts
|
||||
- [**Recall**](/developer/retrieval) — How TEMPR's 4-way search retrieves memories
|
||||
- [**Reflect**](/developer/reflect) — How disposition influences reasoning and opinion formation
|
||||
- [**Reflect**](/developer/reflect) — How disposition influences reasoning
|
||||
|
||||
### API Methods
|
||||
- [**Retain**](/developer/api/retain) — Store information in memory banks
|
||||
- [**Recall**](/developer/api/recall) — Search and retrieve memories
|
||||
- [**Reflect**](/developer/api/reflect) — Reason with disposition
|
||||
- [**Memory Banks**](/developer/api/memory-banks) — Configure disposition and background
|
||||
- [**Entities**](/developer/api/entities) — Track people, places, and concepts
|
||||
- [**Memory Banks**](/developer/api/memory-banks) — Configure disposition and mission
|
||||
- [**Documents**](/developer/api/documents) — Manage document sources
|
||||
- [**Operations**](/developer/api/operations) — Monitor async tasks
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ All local models (embedding, cross-encoder) are automatically downloaded from Hu
|
||||
|
||||
## LLM
|
||||
|
||||
Used for fact extraction, entity resolution, opinion generation, and answer synthesis.
|
||||
Used for fact extraction, entity resolution, mental model consolidation, and answer synthesis.
|
||||
|
||||
**Supported providers:** OpenAI, Anthropic, Gemini, Groq, Ollama, LM Studio, and **any OpenAI-compatible API**
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ The `source` label allows distinguishing between:
|
||||
**Labels:**
|
||||
- `provider`: LLM provider (`openai`, `anthropic`, `gemini`, `groq`, `ollama`, `lmstudio`)
|
||||
- `model`: Model name (e.g., `gpt-4`, `claude-3-sonnet`)
|
||||
- `scope`: What the LLM call is for (`memory`, `reflect`, `entity_observation`, `answer`)
|
||||
- `scope`: What the LLM call is for (`memory`, `reflect`, `consolidation`, `answer`)
|
||||
- `success`: Whether the call succeeded (`true`, `false`)
|
||||
- `token_bucket`: Token count bucket for cardinality control (`0-100`, `100-500`, `500-1k`, `1k-5k`, `5k-10k`, `10k-50k`, `50k+`)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
import CodeSnippet from '@site/src/components/CodeSnippet';
|
||||
import recallPy from '!!raw-loader!@site/examples/api/recall.py';
|
||||
|
||||
# Observations: Knowledge Consolidation
|
||||
|
||||
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 Observation?}
|
||||
C -->|Yes| D[Refine Observation]
|
||||
C -->|No| E[Create Observation]
|
||||
D --> F[Observations]
|
||||
E --> F
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Are Observations?
|
||||
|
||||
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 | Observation |
|
||||
|-----------|--------------|
|
||||
| "Alice prefers Python" | "Alice is a Python-focused developer who values readability and simplicity" |
|
||||
| "Alice dislikes verbose code" | |
|
||||
| "Alice recommends type hints" | |
|
||||
|
||||
Observations provide:
|
||||
- **Synthesis**: Patterns that emerge from multiple facts
|
||||
- **Context**: Richer understanding than individual facts
|
||||
- **Efficiency**: Condensed knowledge for faster retrieval
|
||||
|
||||
---
|
||||
|
||||
## How Consolidation Works
|
||||
|
||||
### Automatic Background Processing
|
||||
|
||||
After `retain()` completes, the consolidation engine runs automatically:
|
||||
|
||||
1. **New facts analyzed** — Each new fact is compared against existing observations
|
||||
2. **Pattern detection** — Related facts are grouped and synthesized
|
||||
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
|
||||
|
||||
Observations evolve as new evidence arrives:
|
||||
|
||||
| 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" | 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 observation?
|
||||
|
||||
The consolidation engine doesn't blindly overwrite — it **reconciles** the contradiction by capturing the evolution:
|
||||
|
||||
**Example: User preference changes**
|
||||
|
||||
| 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 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 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 | 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 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.
|
||||
|
||||
---
|
||||
|
||||
## Observations in Retrieval
|
||||
|
||||
Observations are automatically included in both `recall()` and `reflect()` operations:
|
||||
|
||||
### In Recall
|
||||
|
||||
Observations are returned alongside raw facts, filtered by the `types` parameter:
|
||||
|
||||
<CodeSnippet code={recallPy} section="recall-with-observations" language="python" />
|
||||
|
||||
### In Reflect
|
||||
|
||||
The reflect agent uses **hierarchical retrieval**:
|
||||
|
||||
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 observations and uses them to inform its reasoning.
|
||||
|
||||
---
|
||||
|
||||
## Freshness Awareness
|
||||
|
||||
Observations track when they were last updated. During reflect, the agent considers freshness:
|
||||
|
||||
- **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.
|
||||
|
||||
---
|
||||
|
||||
## Mission-Oriented Consolidation
|
||||
|
||||
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:**
|
||||
|
||||
```python
|
||||
# A support agent bank
|
||||
client.create_bank(
|
||||
bank_id="support-agent",
|
||||
mission="You're a customer support agent - you need to keep track of "
|
||||
"customer preferences, past issues, and communication styles."
|
||||
)
|
||||
```
|
||||
|
||||
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** 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 | Observations Focus |
|
||||
|---------|-------------------|
|
||||
| *Customer support agent* | Customer preferences, issue patterns, resolution history |
|
||||
| *Code review assistant* | Coding patterns, team conventions, common mistakes |
|
||||
| *Research assistant* | Topic expertise, source reliability, methodology preferences |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
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 observations are retrieved
|
||||
- [**Reflect**](./reflect) — How the agentic loop uses observations
|
||||
- [**Mental Models**](./api/mental-models) — User-curated summaries for common queries
|
||||
@@ -13,8 +13,8 @@ Traditional RAG (Retrieval-Augmented Generation) retrieves documents similar to
|
||||
| **Search strategy** | Semantic similarity only | Semantic + keyword + graph + temporal |
|
||||
| **Multi-hop reasoning** | Limited to retrieved chunks | Graph traversal across entity relationships |
|
||||
| **Temporal queries** | Keyword matching ("spring") | Date parsing and range filtering |
|
||||
| **Entity understanding** | None | Entity resolution, observations, co-occurrence |
|
||||
| **Belief formation** | Stateless | Opinions with confidence scores that evolve |
|
||||
| **Entity understanding** | None | Entity resolution, co-occurrence tracking |
|
||||
| **Knowledge consolidation** | Stateless | Mental models that synthesize and evolve |
|
||||
| **Disposition** | None | 3 traits (skepticism, literalism, empathy) influence interpretation |
|
||||
|
||||
## Architecture Comparison
|
||||
@@ -86,9 +86,9 @@ Multiple retrieval strategies. Persistent state across sessions.
|
||||
| System | Result |
|
||||
|--------|--------|
|
||||
| RAG | Lists disconnected facts |
|
||||
| Hindsight | Returns synthesized entity observations: subscription status, billing, known issues |
|
||||
| Hindsight | Returns connected facts via entity graph: subscription status, billing, known issues |
|
||||
|
||||
### Belief Evolution
|
||||
### Knowledge Evolution
|
||||
|
||||
**Week 1:** User struggles with async Python, succeeds with threads
|
||||
**Week 3:** User asks about asyncio, implements async database calls
|
||||
@@ -96,7 +96,7 @@ Multiple retrieval strategies. Persistent state across sessions.
|
||||
| System | Behavior |
|
||||
|--------|----------|
|
||||
| RAG | No memory of progression |
|
||||
| Hindsight | Forms opinion "user prefers sync" (0.7) → updates to "user growing comfortable with async" (0.6) |
|
||||
| Hindsight | Consolidates mental model "user prefers sync" → refines to "user growing comfortable with async" |
|
||||
|
||||
## When to Use Each
|
||||
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Reflect: How Hindsight Reasons with Disposition
|
||||
|
||||
When you call `reflect()`, Hindsight doesn't just retrieve facts — it **reasons** about them through the lens of the bank's unique disposition, forming new opinions and generating contextual responses.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[Query] --> B[Recall Memories]
|
||||
B --> C[Load Disposition]
|
||||
C --> D[Reason]
|
||||
D --> E[Form Opinions]
|
||||
E --> F[Response]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Why Reflect?
|
||||
|
||||
Most AI systems can retrieve facts, but they can't **reason** about them in a consistent way. Every response is generated fresh without a stable perspective or evolving beliefs.
|
||||
|
||||
### The Problem
|
||||
|
||||
Without reflect:
|
||||
- **No consistent character**: "Should we adopt remote work?" gets a different answer each time based on the LLM's randomness
|
||||
- **No opinion formation**: The system never develops beliefs based on accumulated evidence
|
||||
- **No reasoning context**: Responses don't reflect what the bank has learned or its perspective
|
||||
- **Generic responses**: Every AI sounds the same — no disposition, no point of view
|
||||
|
||||
### The Value
|
||||
|
||||
With reflect:
|
||||
- **Consistent character**: A bank configured as "detail-oriented, cautious" will consistently emphasize risks and thorough planning
|
||||
- **Evolving opinions**: As the bank learns more about a topic, its opinions strengthen, weaken, or change — just like a real expert
|
||||
- **Contextual reasoning**: Responses reflect the bank's accumulated knowledge and perspective: "Based on what I know about your team's remote work success..."
|
||||
- **Differentiated behavior**: Customer support bots sound diplomatic, code reviewers sound direct, creative assistants sound open-minded
|
||||
|
||||
### When to Use Reflect
|
||||
|
||||
| Use `recall()` when... | Use `reflect()` when... |
|
||||
|------------------------|-------------------------|
|
||||
| You need raw facts | You need reasoned interpretation |
|
||||
| You're building your own reasoning | You want disposition-consistent responses |
|
||||
| You need maximum control | You want the bank to "think" for itself |
|
||||
| Simple fact lookup | Forming recommendations or opinions |
|
||||
|
||||
**Example:**
|
||||
- `recall("Alice")` → Returns all Alice facts
|
||||
- `reflect("Should we hire Alice?")` → Reasons about Alice's fit based on accumulated knowledge, weighs evidence, forms opinion
|
||||
|
||||
---
|
||||
|
||||
## Disposition Traits
|
||||
|
||||
When you create a memory bank, you can configure its disposition using three traits. These traits influence how the bank interprets information and forms opinions during `reflect()`:
|
||||
|
||||
| Trait | Scale | Low (1) | High (5) |
|
||||
|-------|-------|---------|----------|
|
||||
| **Skepticism** | 1-5 | Trusting, accepts information at face value | Skeptical, questions and doubts claims |
|
||||
| **Literalism** | 1-5 | Flexible interpretation, reads between the lines | Literal interpretation, takes things at face value |
|
||||
| **Empathy** | 1-5 | Detached, focuses on facts | Empathetic, considers emotional context |
|
||||
|
||||
### Background: Natural Language Identity
|
||||
|
||||
Beyond numeric traits, you can provide a natural language **background** that describes the bank's identity:
|
||||
|
||||
```python
|
||||
client.create_bank(
|
||||
bank_id="my-bank",
|
||||
background="I am a senior software architect with 15 years of distributed "
|
||||
"systems experience. I prefer simplicity over cutting-edge technology.",
|
||||
disposition={
|
||||
"skepticism": 4, # Questions new technologies
|
||||
"literalism": 4, # Focuses on concrete specs
|
||||
"empathy": 2 # Prioritizes technical facts
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
The background provides context that shapes how disposition traits are applied:
|
||||
- "I prefer simplicity" + high skepticism → questions complex solutions
|
||||
- "15 years experience" → responses reference this expertise
|
||||
- First-person perspective → creates consistent voice
|
||||
|
||||
---
|
||||
|
||||
## Opinion Formation
|
||||
|
||||
When `reflect()` encounters a question that warrants forming an opinion, disposition shapes the response.
|
||||
|
||||
### Same Facts, Different Opinions
|
||||
|
||||
Two banks with different dispositions, given identical facts about remote work:
|
||||
|
||||
**Bank A** (low skepticism, high empathy):
|
||||
> "Remote work enables flexibility and work-life balance. The team seems happier and more productive when they can choose their environment."
|
||||
|
||||
**Bank B** (high skepticism, low empathy):
|
||||
> "Remote work claims need verification. What are the actual productivity metrics? The anecdotal benefits may not translate to measurable outcomes."
|
||||
|
||||
**Same facts → Different conclusions** because disposition shapes interpretation.
|
||||
|
||||
---
|
||||
|
||||
## Opinion Evolution
|
||||
|
||||
Opinions aren't static — they evolve as new evidence arrives. Here's a real-world example with a database library:
|
||||
|
||||
| Event | What the bank learns | Opinion formed |
|
||||
|-------|---------------------|----------------|
|
||||
| **Day 1** | "Redis is open source under BSD license" | "Redis is excellent for caching — fast, reliable, and OSS-friendly" (confidence: 0.85) |
|
||||
| **Day 2** | "Redis has great community support and documentation" | Opinion reinforced (confidence: 0.90) |
|
||||
| **Day 30** | "Redis changed license to SSPL, restricting cloud usage" | "Redis is still technically strong, but license concerns for cloud deployments" (confidence: 0.65) |
|
||||
| **Day 45** | "Valkey forked Redis under BSD license with Linux Foundation backing" | "Consider Valkey for new projects requiring true OSS; Redis for existing deployments" (confidence: 0.80) |
|
||||
|
||||
**Before the license change:**
|
||||
> "Should we use Redis for our caching layer?"
|
||||
> → "Yes, Redis is the industry standard — fast, battle-tested, and fully open source."
|
||||
|
||||
**After the license change:**
|
||||
> "Should we use Redis for our caching layer?"
|
||||
> → "It depends. For cloud deployments, consider Valkey (the BSD-licensed fork). For on-premise, Redis remains excellent technically."
|
||||
|
||||
This **continuous learning** ensures recommendations stay current with real-world changes.
|
||||
|
||||
---
|
||||
|
||||
## Disposition Presets by Use Case
|
||||
|
||||
Different use cases benefit from different disposition configurations:
|
||||
|
||||
| Use Case | Recommended Traits | Why |
|
||||
|----------|-------------------|-----|
|
||||
| **Customer Support** | skepticism: 2, literalism: 2, empathy: 5 | Trusting, flexible, understanding |
|
||||
| **Code Review** | skepticism: 4, literalism: 5, empathy: 2 | Questions assumptions, precise, direct |
|
||||
| **Legal Analysis** | skepticism: 5, literalism: 5, empathy: 2 | Highly skeptical, exact interpretation |
|
||||
| **Therapist/Coach** | skepticism: 2, literalism: 2, empathy: 5 | Supportive, reads between lines |
|
||||
| **Research Assistant** | skepticism: 4, literalism: 3, empathy: 3 | Questions claims, balanced interpretation |
|
||||
|
||||
---
|
||||
|
||||
## What You Get from Reflect
|
||||
|
||||
When you call `reflect()`:
|
||||
|
||||
**Returns:**
|
||||
- **Response text** — Disposition-influenced answer
|
||||
- **Based on** — Which memories were used (with relevance scores)
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"text": "Based on Alice's ML expertise and her work at Google, she'd be an excellent fit for the research team lead position...",
|
||||
"based_on": {
|
||||
"world": [
|
||||
{"text": "Alice works at Google...", "weight": 0.95},
|
||||
{"text": "Alice specializes in ML...", "weight": 0.88}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** New opinions are formed asynchronously in the background. They'll influence future `reflect()` calls but aren't returned directly.
|
||||
|
||||
---
|
||||
|
||||
## Why Disposition Matters
|
||||
|
||||
Without disposition, all AI assistants sound the same. With disposition:
|
||||
|
||||
- **Customer support bots** can be diplomatic and empathetic
|
||||
- **Code review assistants** can be direct and thorough
|
||||
- **Creative assistants** can be open to unconventional ideas
|
||||
- **Risk analysts** can be appropriately cautious
|
||||
|
||||
Disposition creates **consistent character** across conversations while allowing opinions to **evolve with evidence**.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Retain**](./retain) — How rich facts are stored
|
||||
- [**Recall**](./retrieval) — How multi-strategy search works
|
||||
- [**Reflect API**](./api/reflect) — Code examples, parameters, and tag filtering
|
||||
@@ -0,0 +1,216 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
import CodeSnippet from '@site/src/components/CodeSnippet';
|
||||
import memoryBanksPy from '!!raw-loader!@site/examples/api/memory-banks.py';
|
||||
|
||||
# Reflect: Agentic Reasoning with Disposition
|
||||
|
||||
When you call `reflect()`, Hindsight runs an **agentic loop** that autonomously gathers evidence and reasons through the lens of the bank's disposition to generate contextual responses.
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph agent["Reflect Agent Loop"]
|
||||
A[Query] --> B{Need more info?}
|
||||
B -->|Yes| C[Call Tools]
|
||||
C --> D[search_mental_models]
|
||||
C --> E[search_observations]
|
||||
C --> F[recall]
|
||||
C --> G[expand]
|
||||
D --> B
|
||||
E --> B
|
||||
F --> B
|
||||
G --> B
|
||||
B -->|No| H[Generate Response]
|
||||
end
|
||||
H --> I[Response + Citations]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
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 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 observations were used
|
||||
|
||||
### The Agentic Loop
|
||||
|
||||
The reflect agent runs in a loop with access to these tools:
|
||||
|
||||
| Tool | Purpose | Priority |
|
||||
|------|---------|----------|
|
||||
| `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 |
|
||||
|
||||
The agent:
|
||||
- **Must gather evidence** before answering (guardrail prevents empty responses)
|
||||
- **Runs up to 10 iterations** to find relevant information
|
||||
- **Validates citations** — only IDs that were actually retrieved can be cited
|
||||
|
||||
### Hierarchical Retrieval Strategy
|
||||
|
||||
The agent uses a smart retrieval hierarchy:
|
||||
|
||||
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
|
||||
|
||||
**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 an observation is marked as **stale**, the agent automatically verifies it against current facts.
|
||||
|
||||
---
|
||||
|
||||
## Why Reflect?
|
||||
|
||||
Most AI systems can retrieve facts, but they can't **reason** about them in a consistent way.
|
||||
|
||||
### The Problem
|
||||
|
||||
Without reflect:
|
||||
- **No consistent character**: Same question gets different answers each time
|
||||
- **No knowledge synthesis**: System never connects related facts
|
||||
- **No reasoning context**: Responses don't reflect accumulated knowledge
|
||||
- **Generic responses**: Every AI sounds the same
|
||||
|
||||
### The Value
|
||||
|
||||
With reflect:
|
||||
- **Consistent character**: A "detail-oriented, cautious" bank emphasizes risks and thorough planning
|
||||
- **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
|
||||
|
||||
### When to Use Reflect
|
||||
|
||||
| Use `recall()` when... | Use `reflect()` when... |
|
||||
|------------------------|-------------------------|
|
||||
| You need raw facts | You need reasoned interpretation |
|
||||
| You're building your own reasoning | You want disposition-consistent responses |
|
||||
| You need maximum control | You want the bank to "think" for itself |
|
||||
| Simple fact lookup | Forming recommendations |
|
||||
|
||||
**Example:**
|
||||
- `recall("Alice")` → Returns all Alice facts and relevant mental models
|
||||
- `reflect("Should we hire Alice?")` → Agent gathers evidence about Alice, reasons about fit, returns answer with citations
|
||||
|
||||
---
|
||||
|
||||
## Disposition Traits
|
||||
|
||||
When you create a memory bank, you can configure its disposition using three traits. These traits influence how the bank interprets information and reasons during `reflect()`:
|
||||
|
||||
| Trait | Scale | Low (1) | High (5) |
|
||||
|-------|-------|---------|----------|
|
||||
| **Skepticism** | 1-5 | Trusting, accepts information at face value | Skeptical, questions and doubts claims |
|
||||
| **Literalism** | 1-5 | Flexible interpretation, reads between the lines | Literal interpretation, takes things at face value |
|
||||
| **Empathy** | 1-5 | Detached, focuses on facts | Empathetic, considers emotional context |
|
||||
|
||||
### Mission: Natural Language Identity
|
||||
|
||||
Beyond numeric traits, you can provide a natural language **mission** that describes the bank's identity:
|
||||
|
||||
<CodeSnippet code={memoryBanksPy} section="bank-with-disposition" language="python" />
|
||||
|
||||
The mission tells Hindsight what knowledge to prioritize and shapes how disposition traits are applied:
|
||||
- "keep track of system designs" → focuses consolidation on architectural decisions
|
||||
- "prefer simplicity over cutting-edge" + high skepticism → questions complex solutions
|
||||
- Explicit guidance → consistent memory focus across conversations
|
||||
|
||||
---
|
||||
|
||||
## Disposition Shapes Reasoning
|
||||
|
||||
Two banks with different dispositions, given identical facts about remote work:
|
||||
|
||||
**Bank A** (low skepticism, high empathy):
|
||||
> "Remote work enables flexibility and work-life balance. The team seems happier and more productive when they can choose their environment."
|
||||
|
||||
**Bank B** (high skepticism, low empathy):
|
||||
> "Remote work claims need verification. What are the actual productivity metrics? The anecdotal benefits may not translate to measurable outcomes."
|
||||
|
||||
**Same facts → Different conclusions** because disposition shapes interpretation.
|
||||
|
||||
---
|
||||
|
||||
## Disposition Presets by Use Case
|
||||
|
||||
Different use cases benefit from different disposition configurations:
|
||||
|
||||
| Use Case | Recommended Traits | Why |
|
||||
|----------|-------------------|-----|
|
||||
| **Customer Support** | skepticism: 2, literalism: 2, empathy: 5 | Trusting, flexible, understanding |
|
||||
| **Code Review** | skepticism: 4, literalism: 5, empathy: 2 | Questions assumptions, precise, direct |
|
||||
| **Legal Analysis** | skepticism: 5, literalism: 5, empathy: 2 | Highly skeptical, exact interpretation |
|
||||
| **Therapist/Coach** | skepticism: 2, literalism: 2, empathy: 5 | Supportive, reads between lines |
|
||||
| **Research Assistant** | skepticism: 4, literalism: 3, empathy: 3 | Questions claims, balanced interpretation |
|
||||
|
||||
---
|
||||
|
||||
## What You Get from Reflect
|
||||
|
||||
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 observations accessed (when `include.tool_calls=True`)
|
||||
- **structured_output** — Parsed response if `response_schema` was provided
|
||||
- **usage** — Token usage metrics
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"text": "Based on Alice's ML expertise and her work at Google, she'd be an excellent fit for the research team lead position...",
|
||||
"based_on": {
|
||||
"memories": [
|
||||
{"id": "mem-123", "text": "Alice has 5 years of ML experience", "type": "world"},
|
||||
{"id": "mem-456", "text": "Alice worked at Google on search ranking", "type": "experience"}
|
||||
]
|
||||
},
|
||||
"trace": {
|
||||
"tool_calls": [
|
||||
{"tool": "recall", "input": {"query": "Alice"}, "duration_ms": 150}
|
||||
],
|
||||
"llm_calls": [
|
||||
{"scope": "agent_1", "duration_ms": 1200}
|
||||
],
|
||||
"observations": [
|
||||
{"id": "obs-789", "name": "Alice", "type": "entity", "subtype": "structural"}
|
||||
]
|
||||
},
|
||||
"usage": {"input_tokens": 1500, "output_tokens": 500, "total_tokens": 2000}
|
||||
}
|
||||
```
|
||||
|
||||
The agent automatically gathers evidence, validates citations, and generates a grounded response.
|
||||
|
||||
---
|
||||
|
||||
## Why Disposition Matters
|
||||
|
||||
Without disposition, all AI assistants sound the same. With disposition:
|
||||
|
||||
- **Customer support bots** can be diplomatic and empathetic
|
||||
- **Code review assistants** can be direct and thorough
|
||||
- **Creative assistants** can be open to unconventional ideas
|
||||
- **Risk analysts** can be appropriately cautious
|
||||
|
||||
Disposition creates **consistent character** across conversations while observations **evolve with evidence**.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**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
|
||||
@@ -63,8 +63,7 @@ Hindsight distinguishes between **world** facts (about others) and **experience*
|
||||
| **experience** | Conversations and events | "I recommended Python to Alice" |
|
||||
|
||||
|
||||
**Note:** Opinions aren't created during `retain()` — only during `reflect()` when the bank forms beliefs.
|
||||
This separation is important for `reflect()` — the bank can reason about what it knows versus what happened in conversations.
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
@@ -151,22 +150,6 @@ Without this distinction, old information would either be unsearchable by date o
|
||||
|
||||
---
|
||||
|
||||
## Entity Observations
|
||||
|
||||
As facts accumulate about an entity, Hindsight synthesizes **observations** — high-level summaries that capture what's known:
|
||||
|
||||
**From multiple facts:**
|
||||
- "Alice works at Google"
|
||||
- "Alice is a software engineer"
|
||||
- "Alice specializes in ML"
|
||||
|
||||
**Hindsight creates:**
|
||||
- "Alice is a software engineer at Google specializing in ML"
|
||||
|
||||
**Why it helps:** You can quickly understand an entity without reading through dozens of individual facts.
|
||||
|
||||
---
|
||||
|
||||
## Tagging Memories
|
||||
|
||||
Tags enable visibility scoping—useful when one memory bank serves multiple users but each should only see relevant memories.
|
||||
@@ -187,15 +170,30 @@ After `retain()` completes:
|
||||
- **Unified entities** that resolve different name variations
|
||||
- **Knowledge graph** with entity, temporal, semantic, and causal links
|
||||
- **Temporal grounding** for both historical and recency-based queries
|
||||
- **Background processing** that generates entity summaries
|
||||
- **Optional tags** for filtering during recall
|
||||
|
||||
All stored in your isolated **memory bank**, ready for `recall()` and `reflect()`.
|
||||
|
||||
---
|
||||
|
||||
## Observation Consolidation
|
||||
|
||||
After `retain()` completes, Hindsight automatically triggers **observation consolidation** in the background. This process:
|
||||
|
||||
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 [Observations](./observations) for details on how consolidation works.
|
||||
|
||||
---
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Observations**](./observations) — How knowledge is consolidated after retain
|
||||
- [**Recall**](./retrieval) — How multi-strategy search retrieves relevant memories
|
||||
- [**Reflect**](./reflect) — How disposition influences reasoning and opinion formation
|
||||
- [**Reflect**](./reflect) — How the agentic loop uses observations
|
||||
- [**Retain API**](./api/retain) — Code examples and parameters
|
||||
|
||||
@@ -110,11 +110,11 @@ After the four strategies run, results are **fused together**:
|
||||
|
||||
## Why Multiple Strategies?
|
||||
|
||||
Consider the query: **"What did Alice think about Python last spring?"**
|
||||
Consider the query: **"What did Alice say about Python last spring?"**
|
||||
|
||||
- **Semantic** finds facts about Alice's opinions on programming
|
||||
- **Semantic** finds facts about Alice's views on programming
|
||||
- **Keyword** ensures "Python" is actually mentioned
|
||||
- **Graph** connects Alice → opinions → programming languages
|
||||
- **Graph** connects Alice → programming languages → related entities
|
||||
- **Temporal** filters to "last spring" timeframe
|
||||
|
||||
The **fusion** of all four gives you exactly what you're looking for, even though no single strategy would suffice.
|
||||
@@ -133,18 +133,13 @@ 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, opinion, 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)
|
||||
|
||||
### Expanding Context: Chunks and Entity Observations
|
||||
### Expanding Context: Chunks
|
||||
|
||||
Memories are distilled facts—concise but sometimes missing nuance. When your agent needs deeper context, you can optionally retrieve the source material and related knowledge:
|
||||
|
||||
| Option | Parameters | When to Use |
|
||||
|--------|------------|-------------|
|
||||
| **Chunks** | `include_chunks`, `max_chunk_tokens` | Need exact quotes, original phrasing, or surrounding context |
|
||||
| **Entity Observations** | `include_entities`, `max_entity_tokens` | Need broader knowledge about people/things mentioned in results |
|
||||
Memories are distilled facts—concise but sometimes missing nuance. When your agent needs deeper context, you can optionally retrieve the source material:
|
||||
|
||||
**Chunks** return the raw text that generated each memory—useful when the distilled fact loses important nuance:
|
||||
|
||||
@@ -155,22 +150,7 @@ Chunk: "Alice mentioned she prefers Python over JavaScript, mainly because
|
||||
frontend work and she's been learning TypeScript lately."
|
||||
```
|
||||
|
||||
**Entity Observations** pull in related facts about entities mentioned in your results. If a memory mentions "Alice", you automatically get her role, skills, and other relevant context—without needing a separate query:
|
||||
|
||||
```
|
||||
Query: "What programming languages does Alice like?"
|
||||
Memory: "Alice prefers Python over JavaScript"
|
||||
Entity Observations (Alice):
|
||||
- "Alice is a senior data scientist at Google"
|
||||
- "Alice specializes in machine learning"
|
||||
- "Alice has been learning TypeScript"
|
||||
```
|
||||
|
||||
**When to include them:**
|
||||
- **Chunks**: When generating responses that need verbatim quotes or when context matters (e.g., "What exactly did Alice say about the project?")
|
||||
- **Entity Observations**: When building complete profiles or when the conversation might reference multiple aspects of an entity (e.g., "Tell me about Alice's work")
|
||||
|
||||
Each has its own token budget, giving you precise control over total context size.
|
||||
Use `include_chunks=True` with `max_chunk_tokens` to control the token budget for chunks. This is useful when generating responses that need verbatim quotes or when context matters (e.g., "What exactly did Alice say about the project?").
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ hindsight-api # Default port: 8888
|
||||
|
||||
The API service is stateless and can be horizontally scaled behind a load balancer. All state is stored in PostgreSQL.
|
||||
|
||||
By default, the API also processes background tasks (opinion formation, entity observations) internally. For high-throughput deployments, you can disable this and run dedicated workers instead.
|
||||
By default, the API also processes background tasks (mental model consolidation) internally. For high-throughput deployments, you can disable this and run dedicated workers instead.
|
||||
|
||||
## Worker Service
|
||||
|
||||
|
||||
@@ -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,opinion
|
||||
hindsight memory recall <bank_id> "query" --fact-type world,observation
|
||||
|
||||
# Show trace information
|
||||
hindsight memory recall <bank_id> "query" --trace
|
||||
@@ -120,13 +120,13 @@ hindsight bank stats <bank_id>
|
||||
hindsight bank name <bank_id> "My Assistant"
|
||||
```
|
||||
|
||||
### Set Background
|
||||
### Set Mission
|
||||
|
||||
```bash
|
||||
hindsight bank background <bank_id> "I am a helpful AI assistant interested in technology"
|
||||
hindsight bank mission <bank_id> "I am a helpful AI assistant interested in technology"
|
||||
|
||||
# Skip automatic disposition inference
|
||||
hindsight bank background <bank_id> "Background text" --no-update-disposition
|
||||
hindsight bank mission <bank_id> "Mission text" --no-update-disposition
|
||||
```
|
||||
|
||||
## Document Management
|
||||
@@ -150,9 +150,6 @@ hindsight entity list <bank_id>
|
||||
|
||||
# Get entity details
|
||||
hindsight entity get <bank_id> <entity_id>
|
||||
|
||||
# Regenerate entity observations
|
||||
hindsight entity regenerate <bank_id> <entity_id>
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
@@ -209,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 opinions
|
||||
- **View facts** — Browse world facts, experiences, and observations
|
||||
- **Navigate documents** — See source documents and their extracted memories
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
@@ -73,7 +73,7 @@ hindsight_litellm.configure(
|
||||
|
||||
# Optional - Bank Configuration
|
||||
bank_name="My Agent", # Human-readable display name for the memory bank
|
||||
background="This agent...", # Instructions guiding what Hindsight should remember
|
||||
mission="This agent...", # Instructions guiding what Hindsight should remember
|
||||
|
||||
# Optional - Advanced
|
||||
injection_mode="system_message", # or "prepend_user"
|
||||
@@ -84,16 +84,16 @@ hindsight_litellm.configure(
|
||||
|
||||
### Bank Configuration
|
||||
|
||||
The `background` and `bank_name` parameters configure the memory bank itself. When provided, `configure()` will automatically create or update the bank with these settings.
|
||||
The `mission` and `bank_name` parameters configure the memory bank itself. When provided, `configure()` will automatically create or update the bank with these settings.
|
||||
|
||||
```python
|
||||
hindsight_litellm.configure(
|
||||
hindsight_api_url="http://localhost:8888",
|
||||
bank_id="support-router",
|
||||
bank_name="Customer Support Router",
|
||||
background="""This agent routes customer support requests to the appropriate team.
|
||||
Remember which types of issues should go to which teams (billing, technical, sales).
|
||||
Track customer preferences for communication channels and past issue resolutions.""",
|
||||
mission="""You're a customer support router - keep track of which types of issues
|
||||
should go to which teams (billing, technical, sales), customer preferences for
|
||||
communication channels, and past issue resolutions.""",
|
||||
)
|
||||
```
|
||||
|
||||
@@ -108,7 +108,7 @@ hindsight_litellm.configure(
|
||||
bank_id="my-agent",
|
||||
use_reflect=False, # Default
|
||||
)
|
||||
# Injects: "1. [WORLD] User prefers Python\n2. [OPINION] User dislikes Java..."
|
||||
# Injects: "1. [WORLD] User prefers Python\n2. [MENTAL MODEL] User prefers simple code..."
|
||||
|
||||
# Reflect mode - synthesized context
|
||||
hindsight_litellm.configure(
|
||||
|
||||
@@ -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', 'opinion'], // Filter by fact type
|
||||
types: ['world', 'observation'], // Filter by fact type
|
||||
maxTokens: 4096,
|
||||
budget: 'high', // 'low', 'mid', or 'high'
|
||||
});
|
||||
@@ -107,7 +107,7 @@ console.log(answer.text); // Generated response
|
||||
```typescript
|
||||
await client.createBank('my-bank', {
|
||||
name: 'Assistant',
|
||||
background: 'I am a helpful AI assistant',
|
||||
mission: "You're a helpful AI assistant - keep track of user preferences and conversation history.",
|
||||
disposition: {
|
||||
skepticism: 3, // 1-5: trusting to skeptical
|
||||
literalism: 3, // 1-5: flexible to literal
|
||||
|
||||
@@ -150,7 +150,7 @@ for r in results.results:
|
||||
results = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
types=["world", "opinion"], # Filter by fact type
|
||||
types=["world", "observation"], # Filter by fact type
|
||||
max_tokens=4096,
|
||||
budget="high", # low, mid, or high
|
||||
)
|
||||
@@ -201,7 +201,7 @@ print(answer.text) # Generated response
|
||||
client.create_bank(
|
||||
bank_id="my-bank",
|
||||
name="Assistant",
|
||||
background="I am a helpful AI assistant",
|
||||
mission="You're a helpful AI assistant - keep track of user preferences and conversation history.",
|
||||
disposition={
|
||||
"skepticism": 3, # 1-5: trusting to skeptical
|
||||
"literalism": 3, # 1-5: flexible to literal
|
||||
|
||||
@@ -181,7 +181,7 @@ const config: Config = {
|
||||
items: [
|
||||
{
|
||||
type: 'doc',
|
||||
docId: 'developer/index',
|
||||
docId: 'developer/installation',
|
||||
position: 'left',
|
||||
label: 'Developer',
|
||||
className: 'navbar-item-developer',
|
||||
@@ -223,6 +223,11 @@ const config: Config = {
|
||||
type: 'docsVersionDropdown',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
href: 'https://join.slack.com/t/hindsight-space/shared_invite/zt-3nhbm4w29-LeSJ5Ixi6j8PdiYOCPlOgg',
|
||||
position: 'right',
|
||||
label: 'Community',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/vectorize-io/hindsight',
|
||||
position: 'right',
|
||||
@@ -241,6 +246,10 @@ const config: Config = {
|
||||
label: 'Introduction',
|
||||
to: '/',
|
||||
},
|
||||
{
|
||||
label: 'Developer Guide',
|
||||
to: '/developer/installation',
|
||||
},
|
||||
{
|
||||
label: 'SDKs',
|
||||
to: '/sdks/python',
|
||||
@@ -252,16 +261,37 @@ const config: Config = {
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'More',
|
||||
title: 'Resources',
|
||||
items: [
|
||||
{
|
||||
label: 'Cookbook',
|
||||
to: '/cookbook',
|
||||
},
|
||||
{
|
||||
label: 'Changelog',
|
||||
to: '/changelog',
|
||||
},
|
||||
{
|
||||
label: 'Hindsight Cloud',
|
||||
href: 'https://vectorize.io/hindsight/cloud',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
items: [
|
||||
{
|
||||
label: 'GitHub',
|
||||
href: 'https://github.com/vectorize-io/hindsight',
|
||||
},
|
||||
{
|
||||
label: 'Slack',
|
||||
href: 'https://join.slack.com/t/hindsight-space/shared_invite/zt-3nhbm4w29-LeSJ5Ixi6j8PdiYOCPlOgg',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Hindsight.`,
|
||||
copyright: `Copyright © ${new Date().getFullYear()} Vectorize, Inc.`,
|
||||
},
|
||||
prism: {
|
||||
theme: prismThemes.github,
|
||||
|
||||
@@ -18,7 +18,7 @@ This directory contains runnable example scripts that serve as the source of tru
|
||||
| `reflect.py/mjs/sh` | reflect.md | AI reflection examples |
|
||||
| `memory-banks.py/mjs` | memory-banks.md | Bank management examples |
|
||||
| `documents.py/mjs` | documents.md | Document CRUD examples |
|
||||
| `opinions.py` | opinions.md | Opinion management examples |
|
||||
| `reflections.py` | reflections.md | Reflections CRUD examples |
|
||||
| `main-methods.py` | main-methods.md | Core method examples |
|
||||
| `cli-reference.sh` | cli.md | CLI command examples |
|
||||
|
||||
@@ -37,6 +37,10 @@ for f in *.sh; do bash "$f"; done
|
||||
|
||||
Requires a running Hindsight server at `http://localhost:8888` (or set `HINDSIGHT_API_URL`).
|
||||
|
||||
## Legacy Examples
|
||||
|
||||
The `legacy/` folder contains deprecated example files kept only for backward compatibility with older documentation versions. These files are **not runnable** and are skipped by CI tests.
|
||||
|
||||
## What's NOT Covered
|
||||
|
||||
### 1. OpenAPI Auto-Generated Docs (`/api-reference/*`)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Opinions API examples for Hindsight (deprecated - kept for versioned docs).
|
||||
* This file is preserved for backward compatibility with v0.3 documentation.
|
||||
* Opinions have been replaced by Mental Models in v0.4+.
|
||||
*/
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const HINDSIGHT_URL = process.env.HINDSIGHT_API_URL || 'http://localhost:8888';
|
||||
const client = new HindsightClient({ baseUrl: HINDSIGHT_URL });
|
||||
|
||||
// [docs:opinion-form]
|
||||
// Opinions are automatically formed when the bank encounters
|
||||
// claims, preferences, or judgments in retained content
|
||||
await client.retain('my-bank',
|
||||
"I think Python is excellent for data science because of its libraries"
|
||||
);
|
||||
|
||||
// The bank forms an opinion with confidence based on evidence
|
||||
// [/docs:opinion-form]
|
||||
|
||||
|
||||
// [docs:opinion-search]
|
||||
// Search for opinions on a topic
|
||||
const response = await client.recall('my-bank', 'What do you think about Python?', {
|
||||
types: ['opinion']
|
||||
});
|
||||
|
||||
for (const opinion of response.results) {
|
||||
console.log(`Opinion: ${opinion.text}`);
|
||||
console.log(`Confidence: ${opinion.confidence}`);
|
||||
}
|
||||
// [/docs:opinion-search]
|
||||
|
||||
|
||||
// [docs:opinion-disposition]
|
||||
// Bank disposition affects how opinions are formed
|
||||
// High skepticism = lower confidence, requires more evidence
|
||||
// Low skepticism = higher confidence, accepts claims more readily
|
||||
|
||||
await client.createBank('skeptical-bank', {
|
||||
disposition: { skepticism: 5, literalism: 3, empathy: 2 }
|
||||
});
|
||||
|
||||
// Same content, different confidence due to disposition
|
||||
await client.retain('skeptical-bank', 'Python is the best language');
|
||||
// [/docs:opinion-disposition]
|
||||
|
||||
|
||||
// [docs:opinion-in-reflect]
|
||||
// Opinions influence reflect responses
|
||||
const reflectResponse = await client.reflect('my-bank',
|
||||
'Should I use Python for my data project?'
|
||||
);
|
||||
|
||||
// The response incorporates the bank's opinions with appropriate confidence
|
||||
console.log(reflectResponse.text);
|
||||
// [/docs:opinion-in-reflect]
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Cleanup (not shown in docs)
|
||||
// =============================================================================
|
||||
await fetch(`${HINDSIGHT_URL}/v1/default/banks/my-bank`, { method: 'DELETE' });
|
||||
await fetch(`${HINDSIGHT_URL}/v1/default/banks/skeptical-bank`, { method: 'DELETE' });
|
||||
|
||||
console.log('opinions.mjs: All examples passed');
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Opinions API examples for Hindsight (deprecated - kept for versioned docs).
|
||||
This file is preserved for backward compatibility with v0.3 documentation.
|
||||
Opinions have been replaced by Mental Models in v0.4+.
|
||||
"""
|
||||
import os
|
||||
import requests
|
||||
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
HINDSIGHT_URL = os.getenv("HINDSIGHT_API_URL", "http://localhost:8888")
|
||||
client = Hindsight(base_url=HINDSIGHT_URL)
|
||||
|
||||
# [docs:opinion-form]
|
||||
# Opinions are automatically formed when the bank encounters
|
||||
# claims, preferences, or judgments in retained content
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="I think Python is excellent for data science because of its libraries"
|
||||
)
|
||||
|
||||
# The bank forms an opinion with confidence based on evidence
|
||||
# [/docs:opinion-form]
|
||||
|
||||
|
||||
# [docs:opinion-search]
|
||||
# Search for opinions on a topic
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What do you think about Python?",
|
||||
types=["opinion"]
|
||||
)
|
||||
|
||||
for opinion in response.results:
|
||||
print(f"Opinion: {opinion.text}")
|
||||
print(f"Confidence: {opinion.confidence}")
|
||||
# [/docs:opinion-search]
|
||||
|
||||
|
||||
# [docs:opinion-disposition]
|
||||
# Bank disposition affects how opinions are formed
|
||||
# High skepticism = lower confidence, requires more evidence
|
||||
# Low skepticism = higher confidence, accepts claims more readily
|
||||
|
||||
client.create_bank(
|
||||
bank_id="skeptical-bank",
|
||||
disposition={"skepticism": 5, "literalism": 3, "empathy": 2}
|
||||
)
|
||||
|
||||
# Same content, different confidence due to disposition
|
||||
client.retain(bank_id="skeptical-bank", content="Python is the best language")
|
||||
# [/docs:opinion-disposition]
|
||||
|
||||
|
||||
# [docs:opinion-in-reflect]
|
||||
# Opinions influence reflect responses
|
||||
response = client.reflect(
|
||||
bank_id="my-bank",
|
||||
query="Should I use Python for my data project?"
|
||||
)
|
||||
|
||||
# The response incorporates the bank's opinions with appropriate confidence
|
||||
print(response.text)
|
||||
# [/docs:opinion-in-reflect]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Cleanup (not shown in docs)
|
||||
# =============================================================================
|
||||
requests.delete(f"{HINDSIGHT_URL}/v1/default/banks/my-bank")
|
||||
requests.delete(f"{HINDSIGHT_URL}/v1/default/banks/skeptical-bank")
|
||||
|
||||
print("opinions.py: All examples passed")
|
||||
@@ -19,7 +19,7 @@ const client = new HindsightClient({ baseUrl: HINDSIGHT_URL });
|
||||
// [docs:create-bank]
|
||||
await client.createBank('my-bank', {
|
||||
name: 'Research Assistant',
|
||||
background: 'I am a research assistant specializing in machine learning',
|
||||
mission: 'I am a research assistant specializing in machine learning',
|
||||
disposition: {
|
||||
skepticism: 4,
|
||||
literalism: 3,
|
||||
@@ -29,14 +29,14 @@ await client.createBank('my-bank', {
|
||||
// [/docs:create-bank]
|
||||
|
||||
|
||||
// [docs:bank-background]
|
||||
// [docs:bank-mission]
|
||||
await client.createBank('financial-advisor', {
|
||||
name: 'Financial Advisor',
|
||||
background: `I am a conservative financial advisor with 20 years of experience.
|
||||
mission: `I am a conservative financial advisor with 20 years of experience.
|
||||
I prioritize capital preservation over aggressive growth.
|
||||
I have seen multiple market crashes and believe in diversification.`
|
||||
});
|
||||
// [/docs:bank-background]
|
||||
// [/docs:bank-mission]
|
||||
|
||||
|
||||
// =============================================================================
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user