feat: mental model history tracking and UI diff view (#516)
* feat: mental model refresh history tracking and UI diff view
- DB migration: add history JSONB column to mental_models table
- Track previous content on each refresh in update_mental_model
- Add get_mental_model_history() engine method
- New GET /mental-models/{id}/history endpoint
- Control plane proxy route and getMentalModelHistory() in api.ts
- MentalModelDetailModal: add History tab with lazy loading, carousel
navigation (left=older, right=newer), word-level content diff view
* fix: resolve alembic migration head conflict for mental model history
* feat: mental model history tracking, side-by-side diff UI, and config flag
- Track content changes on every mental model update/refresh (persisted in JSONB history column)
- New GET /mental-models/{id}/history endpoint returning changes most-recent-first
- Side-by-side diff view in History tab (Before/After columns, line-level highlights)
- Actions dropdown in detail panel (Edit, Refresh, View History, Delete)
- HINDSIGHT_API_ENABLE_MENTAL_MODEL_HISTORY config flag (default: true)
- Also adds missing HINDSIGHT_API_ENABLE_OBSERVATION_HISTORY to configuration docs
- Python client wrapper method get_mental_model_history()
- Tests for history persistence (recorded, ordered, name-only skipped, missing returns None)
- Fix NameError: timezone not imported in update_mental_model
* fix: call get_mental_model_history before delete in doc example
This commit is contained in:
parent
576473b6aa
commit
e2baca8bfe
20 changed files with 1214 additions and 35 deletions
|
|
@ -0,0 +1,30 @@
|
||||||
|
"""Add history column to mental_models
|
||||||
|
|
||||||
|
Revision ID: c3d4e5f6g7h8
|
||||||
|
Revises: a2b3c4d5e6f7, a2b3c4d5e6f8
|
||||||
|
Create Date: 2026-03-06
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from alembic import context, op
|
||||||
|
|
||||||
|
revision: str = "c3d4e5f6g7h8"
|
||||||
|
down_revision: str | Sequence[str] | None = ("a2b3c4d5e6f7", "a2b3c4d5e6f8")
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_schema_prefix() -> str:
|
||||||
|
schema = context.config.get_main_option("target_schema")
|
||||||
|
return f'"{schema}".' if schema else ""
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
schema = _get_schema_prefix()
|
||||||
|
op.execute(f"ALTER TABLE {schema}mental_models ADD COLUMN IF NOT EXISTS history JSONB DEFAULT '[]'::jsonb")
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
schema = _get_schema_prefix()
|
||||||
|
op.execute(f"ALTER TABLE {schema}mental_models DROP COLUMN IF EXISTS history")
|
||||||
|
|
@ -2748,6 +2748,41 @@ def _register_routes(app: FastAPI):
|
||||||
logger.error(f"Error in GET /v1/default/banks/{bank_id}/mental-models/{mental_model_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))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/history",
|
||||||
|
summary="Get mental model history",
|
||||||
|
description="Get the refresh history of a mental model, showing content changes over time.",
|
||||||
|
operation_id="get_mental_model_history",
|
||||||
|
tags=["Mental Models"],
|
||||||
|
)
|
||||||
|
async def api_get_mental_model_history(
|
||||||
|
bank_id: str,
|
||||||
|
mental_model_id: str,
|
||||||
|
request_context: RequestContext = Depends(get_request_context),
|
||||||
|
):
|
||||||
|
"""Get the refresh history of a mental model."""
|
||||||
|
try:
|
||||||
|
data = await app.state.memory.get_mental_model_history(
|
||||||
|
bank_id=bank_id,
|
||||||
|
mental_model_id=mental_model_id,
|
||||||
|
request_context=request_context,
|
||||||
|
)
|
||||||
|
if data is None:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Mental model '{mental_model_id}' not found")
|
||||||
|
return data
|
||||||
|
except (AuthenticationError, HTTPException):
|
||||||
|
raise
|
||||||
|
except OperationValidationError as e:
|
||||||
|
raise HTTPException(status_code=e.status_code, detail=e.reason)
|
||||||
|
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}/mental-models/{mental_model_id}/history: {error_detail}"
|
||||||
|
)
|
||||||
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
@app.post(
|
@app.post(
|
||||||
"/v1/default/banks/{bank_id}/mental-models",
|
"/v1/default/banks/{bank_id}/mental-models",
|
||||||
response_model=CreateMentalModelResponse,
|
response_model=CreateMentalModelResponse,
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,7 @@ ENV_CONSOLIDATION_SOURCE_FACTS_MAX_TOKENS_PER_OBSERVATION = (
|
||||||
)
|
)
|
||||||
ENV_OBSERVATIONS_MISSION = "HINDSIGHT_API_OBSERVATIONS_MISSION"
|
ENV_OBSERVATIONS_MISSION = "HINDSIGHT_API_OBSERVATIONS_MISSION"
|
||||||
ENV_ENABLE_OBSERVATION_HISTORY = "HINDSIGHT_API_ENABLE_OBSERVATION_HISTORY"
|
ENV_ENABLE_OBSERVATION_HISTORY = "HINDSIGHT_API_ENABLE_OBSERVATION_HISTORY"
|
||||||
|
ENV_ENABLE_MENTAL_MODEL_HISTORY = "HINDSIGHT_API_ENABLE_MENTAL_MODEL_HISTORY"
|
||||||
|
|
||||||
# Webhook configuration (global, static - server-level only)
|
# Webhook configuration (global, static - server-level only)
|
||||||
ENV_WEBHOOK_URL = "HINDSIGHT_API_WEBHOOK_URL"
|
ENV_WEBHOOK_URL = "HINDSIGHT_API_WEBHOOK_URL"
|
||||||
|
|
@ -451,6 +452,7 @@ DEFAULT_FILE_DELETE_AFTER_RETAIN = True # Delete file bytes after retain (saves
|
||||||
# Observations defaults (consolidated knowledge from facts)
|
# Observations defaults (consolidated knowledge from facts)
|
||||||
DEFAULT_ENABLE_OBSERVATIONS = True # Observations enabled by default
|
DEFAULT_ENABLE_OBSERVATIONS = True # Observations enabled by default
|
||||||
DEFAULT_ENABLE_OBSERVATION_HISTORY = True # Observation history tracking enabled by default
|
DEFAULT_ENABLE_OBSERVATION_HISTORY = True # Observation history tracking enabled by default
|
||||||
|
DEFAULT_ENABLE_MENTAL_MODEL_HISTORY = True # Mental model history tracking enabled by default
|
||||||
DEFAULT_CONSOLIDATION_BATCH_SIZE = 50 # Memories to load per batch (internal memory optimization)
|
DEFAULT_CONSOLIDATION_BATCH_SIZE = 50 # Memories to load per batch (internal memory optimization)
|
||||||
DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 8 # Facts per LLM call (1 = no batching; >1 = batch mode)
|
DEFAULT_CONSOLIDATION_LLM_BATCH_SIZE = 8 # Facts per LLM call (1 = no batching; >1 = batch mode)
|
||||||
DEFAULT_CONSOLIDATION_MAX_TOKENS = 512 # Max tokens for recall when finding related observations
|
DEFAULT_CONSOLIDATION_MAX_TOKENS = 512 # Max tokens for recall when finding related observations
|
||||||
|
|
@ -730,6 +732,7 @@ class HindsightConfig:
|
||||||
# Observations settings (consolidated knowledge from facts)
|
# Observations settings (consolidated knowledge from facts)
|
||||||
enable_observations: bool
|
enable_observations: bool
|
||||||
enable_observation_history: bool
|
enable_observation_history: bool
|
||||||
|
enable_mental_model_history: bool
|
||||||
consolidation_batch_size: int
|
consolidation_batch_size: int
|
||||||
consolidation_llm_batch_size: int
|
consolidation_llm_batch_size: int
|
||||||
consolidation_max_tokens: int
|
consolidation_max_tokens: int
|
||||||
|
|
@ -1186,6 +1189,10 @@ class HindsightConfig:
|
||||||
ENV_ENABLE_OBSERVATION_HISTORY, str(DEFAULT_ENABLE_OBSERVATION_HISTORY)
|
ENV_ENABLE_OBSERVATION_HISTORY, str(DEFAULT_ENABLE_OBSERVATION_HISTORY)
|
||||||
).lower()
|
).lower()
|
||||||
== "true",
|
== "true",
|
||||||
|
enable_mental_model_history=os.getenv(
|
||||||
|
ENV_ENABLE_MENTAL_MODEL_HISTORY, str(DEFAULT_ENABLE_MENTAL_MODEL_HISTORY)
|
||||||
|
).lower()
|
||||||
|
== "true",
|
||||||
consolidation_batch_size=int(
|
consolidation_batch_size=int(
|
||||||
os.getenv(ENV_CONSOLIDATION_BATCH_SIZE, str(DEFAULT_CONSOLIDATION_BATCH_SIZE))
|
os.getenv(ENV_CONSOLIDATION_BATCH_SIZE, str(DEFAULT_CONSOLIDATION_BATCH_SIZE))
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import logging
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
from datetime import UTC, datetime, timedelta
|
from datetime import UTC, datetime, timedelta, timezone
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import asyncpg
|
import asyncpg
|
||||||
|
|
@ -5999,6 +5999,39 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
async def get_mental_model_history(
|
||||||
|
self,
|
||||||
|
bank_id: str,
|
||||||
|
mental_model_id: str,
|
||||||
|
*,
|
||||||
|
request_context: "RequestContext",
|
||||||
|
) -> list[dict] | None:
|
||||||
|
"""Get the refresh history of a mental model.
|
||||||
|
|
||||||
|
Returns None if the mental model is not found.
|
||||||
|
Returns a list of history entries (most recent first), each with previous_content and changed_at.
|
||||||
|
"""
|
||||||
|
await self._authenticate_tenant(request_context)
|
||||||
|
pool = await self._get_pool()
|
||||||
|
async with acquire_with_retry(pool) as conn:
|
||||||
|
row = await conn.fetchrow(
|
||||||
|
f"""
|
||||||
|
SELECT history
|
||||||
|
FROM {fq_table("mental_models")}
|
||||||
|
WHERE bank_id = $1 AND id = $2
|
||||||
|
""",
|
||||||
|
bank_id,
|
||||||
|
mental_model_id,
|
||||||
|
)
|
||||||
|
if row is None:
|
||||||
|
return None
|
||||||
|
raw_history = row["history"]
|
||||||
|
if isinstance(raw_history, str):
|
||||||
|
raw_history = json.loads(raw_history)
|
||||||
|
if not raw_history:
|
||||||
|
return []
|
||||||
|
return list(reversed(raw_history))
|
||||||
|
|
||||||
async def create_mental_model(
|
async def create_mental_model(
|
||||||
self,
|
self,
|
||||||
bank_id: str,
|
bank_id: str,
|
||||||
|
|
@ -6219,6 +6252,17 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
pool = await self._get_pool()
|
pool = await self._get_pool()
|
||||||
|
|
||||||
async with acquire_with_retry(pool) as conn:
|
async with acquire_with_retry(pool) as conn:
|
||||||
|
# If content is changing, fetch current content first to record history
|
||||||
|
previous_content: str | None = None
|
||||||
|
if content is not None:
|
||||||
|
current_row = await conn.fetchrow(
|
||||||
|
f"SELECT content FROM {fq_table('mental_models')} WHERE bank_id = $1 AND id = $2",
|
||||||
|
bank_id,
|
||||||
|
mental_model_id,
|
||||||
|
)
|
||||||
|
if current_row:
|
||||||
|
previous_content = current_row["content"]
|
||||||
|
|
||||||
# Build dynamic update
|
# Build dynamic update
|
||||||
updates = []
|
updates = []
|
||||||
params: list[Any] = [bank_id, mental_model_id]
|
params: list[Any] = [bank_id, mental_model_id]
|
||||||
|
|
@ -6234,6 +6278,14 @@ class MemoryEngine(MemoryEngineInterface):
|
||||||
params.append(content)
|
params.append(content)
|
||||||
param_idx += 1
|
param_idx += 1
|
||||||
updates.append("last_refreshed_at = NOW()")
|
updates.append("last_refreshed_at = NOW()")
|
||||||
|
# Record history entry with the previous content
|
||||||
|
if get_config().enable_mental_model_history:
|
||||||
|
history_entry = json.dumps(
|
||||||
|
[{"previous_content": previous_content, "changed_at": datetime.now(timezone.utc).isoformat()}]
|
||||||
|
)
|
||||||
|
updates.append(f"history = COALESCE(history, '[]'::jsonb) || ${param_idx}::jsonb")
|
||||||
|
params.append(history_entry)
|
||||||
|
param_idx += 1
|
||||||
# Also update embedding (convert to string for asyncpg vector type)
|
# Also update embedding (convert to string for asyncpg vector type)
|
||||||
embedding_text = f"{name or ''} {content}"
|
embedding_text = f"{name or ''} {content}"
|
||||||
embedding = await embedding_utils.generate_embeddings_batch(self.embeddings, [embedding_text])
|
embedding = await embedding_utils.generate_embeddings_batch(self.embeddings, [embedding_text])
|
||||||
|
|
|
||||||
|
|
@ -277,6 +277,7 @@ def main():
|
||||||
file_delete_after_retain=config.file_delete_after_retain,
|
file_delete_after_retain=config.file_delete_after_retain,
|
||||||
enable_observations=config.enable_observations,
|
enable_observations=config.enable_observations,
|
||||||
enable_observation_history=config.enable_observation_history,
|
enable_observation_history=config.enable_observation_history,
|
||||||
|
enable_mental_model_history=config.enable_mental_model_history,
|
||||||
consolidation_batch_size=config.consolidation_batch_size,
|
consolidation_batch_size=config.consolidation_batch_size,
|
||||||
consolidation_llm_batch_size=config.consolidation_llm_batch_size,
|
consolidation_llm_batch_size=config.consolidation_llm_batch_size,
|
||||||
consolidation_max_tokens=config.consolidation_max_tokens,
|
consolidation_max_tokens=config.consolidation_max_tokens,
|
||||||
|
|
|
||||||
|
|
@ -656,6 +656,113 @@ class TestDirectivesPromptInjection:
|
||||||
assert directives_pos < critical_rules_pos
|
assert directives_pos < critical_rules_pos
|
||||||
|
|
||||||
|
|
||||||
|
class TestMentalModelHistory:
|
||||||
|
"""Test mental model history persistence."""
|
||||||
|
|
||||||
|
async def test_history_recorded_on_content_update(self, memory: MemoryEngine, request_context):
|
||||||
|
"""Test that updating content records a history entry."""
|
||||||
|
bank_id = f"test-mm-history-{uuid.uuid4().hex[:8]}"
|
||||||
|
await memory.get_bank_profile(bank_id, request_context=request_context)
|
||||||
|
|
||||||
|
mm = await memory.create_mental_model(
|
||||||
|
bank_id=bank_id,
|
||||||
|
name="Test Model",
|
||||||
|
source_query="What is the test?",
|
||||||
|
content="Original content",
|
||||||
|
request_context=request_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
# No history yet
|
||||||
|
history = await memory.get_mental_model_history(bank_id, mm["id"], request_context=request_context)
|
||||||
|
assert history == []
|
||||||
|
|
||||||
|
# Update content
|
||||||
|
await memory.update_mental_model(
|
||||||
|
bank_id=bank_id,
|
||||||
|
mental_model_id=mm["id"],
|
||||||
|
content="Updated content",
|
||||||
|
request_context=request_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
history = await memory.get_mental_model_history(bank_id, mm["id"], request_context=request_context)
|
||||||
|
assert len(history) == 1
|
||||||
|
assert history[0]["previous_content"] == "Original content"
|
||||||
|
assert "changed_at" in history[0]
|
||||||
|
|
||||||
|
await memory.delete_bank(bank_id, request_context=request_context)
|
||||||
|
|
||||||
|
async def test_history_ordered_most_recent_first(self, memory: MemoryEngine, request_context):
|
||||||
|
"""Test that history is returned most recent first."""
|
||||||
|
bank_id = f"test-mm-history-order-{uuid.uuid4().hex[:8]}"
|
||||||
|
await memory.get_bank_profile(bank_id, request_context=request_context)
|
||||||
|
|
||||||
|
mm = await memory.create_mental_model(
|
||||||
|
bank_id=bank_id,
|
||||||
|
name="Test Model",
|
||||||
|
source_query="What is the test?",
|
||||||
|
content="v1",
|
||||||
|
request_context=request_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
await memory.update_mental_model(
|
||||||
|
bank_id=bank_id,
|
||||||
|
mental_model_id=mm["id"],
|
||||||
|
content="v2",
|
||||||
|
request_context=request_context,
|
||||||
|
)
|
||||||
|
await memory.update_mental_model(
|
||||||
|
bank_id=bank_id,
|
||||||
|
mental_model_id=mm["id"],
|
||||||
|
content="v3",
|
||||||
|
request_context=request_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
history = await memory.get_mental_model_history(bank_id, mm["id"], request_context=request_context)
|
||||||
|
assert len(history) == 2
|
||||||
|
# Most recent first: second update recorded "v2" as previous, first recorded "v1"
|
||||||
|
assert history[0]["previous_content"] == "v2"
|
||||||
|
assert history[1]["previous_content"] == "v1"
|
||||||
|
|
||||||
|
await memory.delete_bank(bank_id, request_context=request_context)
|
||||||
|
|
||||||
|
async def test_history_not_recorded_on_name_only_update(self, memory: MemoryEngine, request_context):
|
||||||
|
"""Test that updating only name does not record history."""
|
||||||
|
bank_id = f"test-mm-history-name-{uuid.uuid4().hex[:8]}"
|
||||||
|
await memory.get_bank_profile(bank_id, request_context=request_context)
|
||||||
|
|
||||||
|
mm = await memory.create_mental_model(
|
||||||
|
bank_id=bank_id,
|
||||||
|
name="Original Name",
|
||||||
|
source_query="What is the test?",
|
||||||
|
content="Content",
|
||||||
|
request_context=request_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
await memory.update_mental_model(
|
||||||
|
bank_id=bank_id,
|
||||||
|
mental_model_id=mm["id"],
|
||||||
|
name="Updated Name",
|
||||||
|
request_context=request_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
history = await memory.get_mental_model_history(bank_id, mm["id"], request_context=request_context)
|
||||||
|
assert history == []
|
||||||
|
|
||||||
|
await memory.delete_bank(bank_id, request_context=request_context)
|
||||||
|
|
||||||
|
async def test_history_returns_none_for_missing_model(self, memory: MemoryEngine, request_context):
|
||||||
|
"""Test that history returns None when mental model doesn't exist."""
|
||||||
|
bank_id = f"test-mm-history-missing-{uuid.uuid4().hex[:8]}"
|
||||||
|
await memory.get_bank_profile(bank_id, request_context=request_context)
|
||||||
|
|
||||||
|
result = await memory.get_mental_model_history(
|
||||||
|
bank_id, "nonexistent-id", request_context=request_context
|
||||||
|
)
|
||||||
|
assert result is None
|
||||||
|
|
||||||
|
await memory.delete_bank(bank_id, request_context=request_context)
|
||||||
|
|
||||||
|
|
||||||
class TestMentalModelRefreshTagSecurity:
|
class TestMentalModelRefreshTagSecurity:
|
||||||
"""Test that mental model refresh respects tag-based security boundaries."""
|
"""Test that mental model refresh respects tag-based security boundaries."""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -884,6 +884,51 @@ paths:
|
||||||
summary: Update mental model
|
summary: Update mental model
|
||||||
tags:
|
tags:
|
||||||
- Mental Models
|
- Mental Models
|
||||||
|
/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/history:
|
||||||
|
get:
|
||||||
|
description: "Get the refresh history of a mental model, showing content changes\
|
||||||
|
\ over time."
|
||||||
|
operationId: get_mental_model_history
|
||||||
|
parameters:
|
||||||
|
- explode: false
|
||||||
|
in: path
|
||||||
|
name: bank_id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
title: Bank Id
|
||||||
|
type: string
|
||||||
|
style: simple
|
||||||
|
- explode: false
|
||||||
|
in: path
|
||||||
|
name: mental_model_id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
title: Mental Model Id
|
||||||
|
type: string
|
||||||
|
style: simple
|
||||||
|
- explode: false
|
||||||
|
in: header
|
||||||
|
name: authorization
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
nullable: true
|
||||||
|
type: string
|
||||||
|
style: simple
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema: {}
|
||||||
|
description: Successful Response
|
||||||
|
"422":
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/HTTPValidationError'
|
||||||
|
description: Validation Error
|
||||||
|
summary: Get mental model history
|
||||||
|
tags:
|
||||||
|
- Mental Models
|
||||||
/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/refresh:
|
/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/refresh:
|
||||||
post:
|
post:
|
||||||
description: Submit an async task to re-run the source query through reflect
|
description: Submit an async task to re-run the source query through reflect
|
||||||
|
|
|
||||||
|
|
@ -409,6 +409,132 @@ func (a *MentalModelsAPIService) GetMentalModelExecute(r ApiGetMentalModelReques
|
||||||
return localVarReturnValue, localVarHTTPResponse, nil
|
return localVarReturnValue, localVarHTTPResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ApiGetMentalModelHistoryRequest struct {
|
||||||
|
ctx context.Context
|
||||||
|
ApiService *MentalModelsAPIService
|
||||||
|
bankId string
|
||||||
|
mentalModelId string
|
||||||
|
authorization *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiGetMentalModelHistoryRequest) Authorization(authorization string) ApiGetMentalModelHistoryRequest {
|
||||||
|
r.authorization = &authorization
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ApiGetMentalModelHistoryRequest) Execute() (interface{}, *http.Response, error) {
|
||||||
|
return r.ApiService.GetMentalModelHistoryExecute(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
GetMentalModelHistory Get mental model history
|
||||||
|
|
||||||
|
Get the refresh history of a mental model, showing content changes over time.
|
||||||
|
|
||||||
|
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
|
||||||
|
@param bankId
|
||||||
|
@param mentalModelId
|
||||||
|
@return ApiGetMentalModelHistoryRequest
|
||||||
|
*/
|
||||||
|
func (a *MentalModelsAPIService) GetMentalModelHistory(ctx context.Context, bankId string, mentalModelId string) ApiGetMentalModelHistoryRequest {
|
||||||
|
return ApiGetMentalModelHistoryRequest{
|
||||||
|
ApiService: a,
|
||||||
|
ctx: ctx,
|
||||||
|
bankId: bankId,
|
||||||
|
mentalModelId: mentalModelId,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute executes the request
|
||||||
|
// @return interface{}
|
||||||
|
func (a *MentalModelsAPIService) GetMentalModelHistoryExecute(r ApiGetMentalModelHistoryRequest) (interface{}, *http.Response, error) {
|
||||||
|
var (
|
||||||
|
localVarHTTPMethod = http.MethodGet
|
||||||
|
localVarPostBody interface{}
|
||||||
|
formFiles []formFile
|
||||||
|
localVarReturnValue interface{}
|
||||||
|
)
|
||||||
|
|
||||||
|
localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "MentalModelsAPIService.GetMentalModelHistory")
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarPath := localBasePath + "/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/history"
|
||||||
|
localVarPath = strings.Replace(localVarPath, "{"+"bank_id"+"}", url.PathEscape(parameterValueToString(r.bankId, "bankId")), -1)
|
||||||
|
localVarPath = strings.Replace(localVarPath, "{"+"mental_model_id"+"}", url.PathEscape(parameterValueToString(r.mentalModelId, "mentalModelId")), -1)
|
||||||
|
|
||||||
|
localVarHeaderParams := make(map[string]string)
|
||||||
|
localVarQueryParams := url.Values{}
|
||||||
|
localVarFormParams := url.Values{}
|
||||||
|
|
||||||
|
// to determine the Content-Type header
|
||||||
|
localVarHTTPContentTypes := []string{}
|
||||||
|
|
||||||
|
// set Content-Type header
|
||||||
|
localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes)
|
||||||
|
if localVarHTTPContentType != "" {
|
||||||
|
localVarHeaderParams["Content-Type"] = localVarHTTPContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
// to determine the Accept header
|
||||||
|
localVarHTTPHeaderAccepts := []string{"application/json"}
|
||||||
|
|
||||||
|
// set Accept header
|
||||||
|
localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts)
|
||||||
|
if localVarHTTPHeaderAccept != "" {
|
||||||
|
localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept
|
||||||
|
}
|
||||||
|
if r.authorization != nil {
|
||||||
|
parameterAddToHeaderOrQuery(localVarHeaderParams, "authorization", r.authorization, "simple", "")
|
||||||
|
}
|
||||||
|
req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles)
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarHTTPResponse, err := a.client.callAPI(req)
|
||||||
|
if err != nil || localVarHTTPResponse == nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localVarBody, err := io.ReadAll(localVarHTTPResponse.Body)
|
||||||
|
localVarHTTPResponse.Body.Close()
|
||||||
|
localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody))
|
||||||
|
if err != nil {
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if localVarHTTPResponse.StatusCode >= 300 {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: localVarHTTPResponse.Status,
|
||||||
|
}
|
||||||
|
if localVarHTTPResponse.StatusCode == 422 {
|
||||||
|
var v HTTPValidationError
|
||||||
|
err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
newErr.error = err.Error()
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v)
|
||||||
|
newErr.model = v
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type"))
|
||||||
|
if err != nil {
|
||||||
|
newErr := &GenericOpenAPIError{
|
||||||
|
body: localVarBody,
|
||||||
|
error: err.Error(),
|
||||||
|
}
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, newErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return localVarReturnValue, localVarHTTPResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
type ApiListMentalModelsRequest struct {
|
type ApiListMentalModelsRequest struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
ApiService *MentalModelsAPIService
|
ApiService *MentalModelsAPIService
|
||||||
|
|
|
||||||
|
|
@ -913,6 +913,19 @@ class Hindsight:
|
||||||
"""
|
"""
|
||||||
return _run_async(self._mental_models_api.delete_mental_model(bank_id, mental_model_id, _request_timeout=self._timeout))
|
return _run_async(self._mental_models_api.delete_mental_model(bank_id, mental_model_id, _request_timeout=self._timeout))
|
||||||
|
|
||||||
|
def get_mental_model_history(self, bank_id: str, mental_model_id: str):
|
||||||
|
"""
|
||||||
|
Get the content change history of a mental model.
|
||||||
|
|
||||||
|
Returns a list of history entries (most recent first), each with
|
||||||
|
``previous_content`` and ``changed_at`` fields.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
bank_id: The memory bank ID
|
||||||
|
mental_model_id: The mental model ID
|
||||||
|
"""
|
||||||
|
return _run_async(self._mental_models_api.get_mental_model_history(bank_id, mental_model_id, _request_timeout=self._timeout))
|
||||||
|
|
||||||
# Directives methods
|
# Directives methods
|
||||||
|
|
||||||
def create_directive(
|
def create_directive(
|
||||||
|
|
|
||||||
|
|
@ -936,6 +936,299 @@ class MentalModelsApi:
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@validate_call
|
||||||
|
async def get_mental_model_history(
|
||||||
|
self,
|
||||||
|
bank_id: StrictStr,
|
||||||
|
mental_model_id: StrictStr,
|
||||||
|
authorization: Optional[StrictStr] = None,
|
||||||
|
_request_timeout: Union[
|
||||||
|
None,
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Tuple[
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Annotated[StrictFloat, Field(gt=0)]
|
||||||
|
]
|
||||||
|
] = None,
|
||||||
|
_request_auth: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_content_type: Optional[StrictStr] = None,
|
||||||
|
_headers: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
|
||||||
|
) -> object:
|
||||||
|
"""Get mental model history
|
||||||
|
|
||||||
|
Get the refresh history of a mental model, showing content changes over time.
|
||||||
|
|
||||||
|
:param bank_id: (required)
|
||||||
|
:type bank_id: str
|
||||||
|
:param mental_model_id: (required)
|
||||||
|
:type mental_model_id: str
|
||||||
|
:param authorization:
|
||||||
|
:type authorization: str
|
||||||
|
:param _request_timeout: timeout setting for this request. If one
|
||||||
|
number provided, it will be total request
|
||||||
|
timeout. It can also be a pair (tuple) of
|
||||||
|
(connection, read) timeouts.
|
||||||
|
:type _request_timeout: int, tuple(int, int), optional
|
||||||
|
:param _request_auth: set to override the auth_settings for an a single
|
||||||
|
request; this effectively ignores the
|
||||||
|
authentication in the spec for a single request.
|
||||||
|
:type _request_auth: dict, optional
|
||||||
|
:param _content_type: force content-type for the request.
|
||||||
|
:type _content_type: str, Optional
|
||||||
|
:param _headers: set to override the headers for a single
|
||||||
|
request; this effectively ignores the headers
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _headers: dict, optional
|
||||||
|
:param _host_index: set to override the host_index for a single
|
||||||
|
request; this effectively ignores the host_index
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _host_index: int, optional
|
||||||
|
:return: Returns the result object.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
_param = self._get_mental_model_history_serialize(
|
||||||
|
bank_id=bank_id,
|
||||||
|
mental_model_id=mental_model_id,
|
||||||
|
authorization=authorization,
|
||||||
|
_request_auth=_request_auth,
|
||||||
|
_content_type=_content_type,
|
||||||
|
_headers=_headers,
|
||||||
|
_host_index=_host_index
|
||||||
|
)
|
||||||
|
|
||||||
|
_response_types_map: Dict[str, Optional[str]] = {
|
||||||
|
'200': "object",
|
||||||
|
'422': "HTTPValidationError",
|
||||||
|
}
|
||||||
|
response_data = await self.api_client.call_api(
|
||||||
|
*_param,
|
||||||
|
_request_timeout=_request_timeout
|
||||||
|
)
|
||||||
|
await response_data.read()
|
||||||
|
return self.api_client.response_deserialize(
|
||||||
|
response_data=response_data,
|
||||||
|
response_types_map=_response_types_map,
|
||||||
|
).data
|
||||||
|
|
||||||
|
|
||||||
|
@validate_call
|
||||||
|
async def get_mental_model_history_with_http_info(
|
||||||
|
self,
|
||||||
|
bank_id: StrictStr,
|
||||||
|
mental_model_id: StrictStr,
|
||||||
|
authorization: Optional[StrictStr] = None,
|
||||||
|
_request_timeout: Union[
|
||||||
|
None,
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Tuple[
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Annotated[StrictFloat, Field(gt=0)]
|
||||||
|
]
|
||||||
|
] = None,
|
||||||
|
_request_auth: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_content_type: Optional[StrictStr] = None,
|
||||||
|
_headers: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
|
||||||
|
) -> ApiResponse[object]:
|
||||||
|
"""Get mental model history
|
||||||
|
|
||||||
|
Get the refresh history of a mental model, showing content changes over time.
|
||||||
|
|
||||||
|
:param bank_id: (required)
|
||||||
|
:type bank_id: str
|
||||||
|
:param mental_model_id: (required)
|
||||||
|
:type mental_model_id: str
|
||||||
|
:param authorization:
|
||||||
|
:type authorization: str
|
||||||
|
:param _request_timeout: timeout setting for this request. If one
|
||||||
|
number provided, it will be total request
|
||||||
|
timeout. It can also be a pair (tuple) of
|
||||||
|
(connection, read) timeouts.
|
||||||
|
:type _request_timeout: int, tuple(int, int), optional
|
||||||
|
:param _request_auth: set to override the auth_settings for an a single
|
||||||
|
request; this effectively ignores the
|
||||||
|
authentication in the spec for a single request.
|
||||||
|
:type _request_auth: dict, optional
|
||||||
|
:param _content_type: force content-type for the request.
|
||||||
|
:type _content_type: str, Optional
|
||||||
|
:param _headers: set to override the headers for a single
|
||||||
|
request; this effectively ignores the headers
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _headers: dict, optional
|
||||||
|
:param _host_index: set to override the host_index for a single
|
||||||
|
request; this effectively ignores the host_index
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _host_index: int, optional
|
||||||
|
:return: Returns the result object.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
_param = self._get_mental_model_history_serialize(
|
||||||
|
bank_id=bank_id,
|
||||||
|
mental_model_id=mental_model_id,
|
||||||
|
authorization=authorization,
|
||||||
|
_request_auth=_request_auth,
|
||||||
|
_content_type=_content_type,
|
||||||
|
_headers=_headers,
|
||||||
|
_host_index=_host_index
|
||||||
|
)
|
||||||
|
|
||||||
|
_response_types_map: Dict[str, Optional[str]] = {
|
||||||
|
'200': "object",
|
||||||
|
'422': "HTTPValidationError",
|
||||||
|
}
|
||||||
|
response_data = await self.api_client.call_api(
|
||||||
|
*_param,
|
||||||
|
_request_timeout=_request_timeout
|
||||||
|
)
|
||||||
|
await response_data.read()
|
||||||
|
return self.api_client.response_deserialize(
|
||||||
|
response_data=response_data,
|
||||||
|
response_types_map=_response_types_map,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@validate_call
|
||||||
|
async def get_mental_model_history_without_preload_content(
|
||||||
|
self,
|
||||||
|
bank_id: StrictStr,
|
||||||
|
mental_model_id: StrictStr,
|
||||||
|
authorization: Optional[StrictStr] = None,
|
||||||
|
_request_timeout: Union[
|
||||||
|
None,
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Tuple[
|
||||||
|
Annotated[StrictFloat, Field(gt=0)],
|
||||||
|
Annotated[StrictFloat, Field(gt=0)]
|
||||||
|
]
|
||||||
|
] = None,
|
||||||
|
_request_auth: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_content_type: Optional[StrictStr] = None,
|
||||||
|
_headers: Optional[Dict[StrictStr, Any]] = None,
|
||||||
|
_host_index: Annotated[StrictInt, Field(ge=0, le=0)] = 0,
|
||||||
|
) -> RESTResponseType:
|
||||||
|
"""Get mental model history
|
||||||
|
|
||||||
|
Get the refresh history of a mental model, showing content changes over time.
|
||||||
|
|
||||||
|
:param bank_id: (required)
|
||||||
|
:type bank_id: str
|
||||||
|
:param mental_model_id: (required)
|
||||||
|
:type mental_model_id: str
|
||||||
|
:param authorization:
|
||||||
|
:type authorization: str
|
||||||
|
:param _request_timeout: timeout setting for this request. If one
|
||||||
|
number provided, it will be total request
|
||||||
|
timeout. It can also be a pair (tuple) of
|
||||||
|
(connection, read) timeouts.
|
||||||
|
:type _request_timeout: int, tuple(int, int), optional
|
||||||
|
:param _request_auth: set to override the auth_settings for an a single
|
||||||
|
request; this effectively ignores the
|
||||||
|
authentication in the spec for a single request.
|
||||||
|
:type _request_auth: dict, optional
|
||||||
|
:param _content_type: force content-type for the request.
|
||||||
|
:type _content_type: str, Optional
|
||||||
|
:param _headers: set to override the headers for a single
|
||||||
|
request; this effectively ignores the headers
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _headers: dict, optional
|
||||||
|
:param _host_index: set to override the host_index for a single
|
||||||
|
request; this effectively ignores the host_index
|
||||||
|
in the spec for a single request.
|
||||||
|
:type _host_index: int, optional
|
||||||
|
:return: Returns the result object.
|
||||||
|
""" # noqa: E501
|
||||||
|
|
||||||
|
_param = self._get_mental_model_history_serialize(
|
||||||
|
bank_id=bank_id,
|
||||||
|
mental_model_id=mental_model_id,
|
||||||
|
authorization=authorization,
|
||||||
|
_request_auth=_request_auth,
|
||||||
|
_content_type=_content_type,
|
||||||
|
_headers=_headers,
|
||||||
|
_host_index=_host_index
|
||||||
|
)
|
||||||
|
|
||||||
|
_response_types_map: Dict[str, Optional[str]] = {
|
||||||
|
'200': "object",
|
||||||
|
'422': "HTTPValidationError",
|
||||||
|
}
|
||||||
|
response_data = await self.api_client.call_api(
|
||||||
|
*_param,
|
||||||
|
_request_timeout=_request_timeout
|
||||||
|
)
|
||||||
|
return response_data.response
|
||||||
|
|
||||||
|
|
||||||
|
def _get_mental_model_history_serialize(
|
||||||
|
self,
|
||||||
|
bank_id,
|
||||||
|
mental_model_id,
|
||||||
|
authorization,
|
||||||
|
_request_auth,
|
||||||
|
_content_type,
|
||||||
|
_headers,
|
||||||
|
_host_index,
|
||||||
|
) -> RequestSerialized:
|
||||||
|
|
||||||
|
_host = None
|
||||||
|
|
||||||
|
_collection_formats: Dict[str, str] = {
|
||||||
|
}
|
||||||
|
|
||||||
|
_path_params: Dict[str, str] = {}
|
||||||
|
_query_params: List[Tuple[str, str]] = []
|
||||||
|
_header_params: Dict[str, Optional[str]] = _headers or {}
|
||||||
|
_form_params: List[Tuple[str, str]] = []
|
||||||
|
_files: Dict[
|
||||||
|
str, Union[str, bytes, List[str], List[bytes], List[Tuple[str, bytes]]]
|
||||||
|
] = {}
|
||||||
|
_body_params: Optional[bytes] = None
|
||||||
|
|
||||||
|
# process the path parameters
|
||||||
|
if bank_id is not None:
|
||||||
|
_path_params['bank_id'] = bank_id
|
||||||
|
if mental_model_id is not None:
|
||||||
|
_path_params['mental_model_id'] = mental_model_id
|
||||||
|
# process the query parameters
|
||||||
|
# process the header parameters
|
||||||
|
if authorization is not None:
|
||||||
|
_header_params['authorization'] = authorization
|
||||||
|
# process the form parameters
|
||||||
|
# process the body parameter
|
||||||
|
|
||||||
|
|
||||||
|
# set the HTTP header `Accept`
|
||||||
|
if 'Accept' not in _header_params:
|
||||||
|
_header_params['Accept'] = self.api_client.select_header_accept(
|
||||||
|
[
|
||||||
|
'application/json'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# authentication setting
|
||||||
|
_auth_settings: List[str] = [
|
||||||
|
]
|
||||||
|
|
||||||
|
return self.api_client.param_serialize(
|
||||||
|
method='GET',
|
||||||
|
resource_path='/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/history',
|
||||||
|
path_params=_path_params,
|
||||||
|
query_params=_query_params,
|
||||||
|
header_params=_header_params,
|
||||||
|
body=_body_params,
|
||||||
|
post_params=_form_params,
|
||||||
|
files=_files,
|
||||||
|
auth_settings=_auth_settings,
|
||||||
|
collection_formats=_collection_formats,
|
||||||
|
_host=_host,
|
||||||
|
_request_auth=_request_auth
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@validate_call
|
@validate_call
|
||||||
async def list_mental_models(
|
async def list_mental_models(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,9 @@ import type {
|
||||||
GetMemoryResponses,
|
GetMemoryResponses,
|
||||||
GetMentalModelData,
|
GetMentalModelData,
|
||||||
GetMentalModelErrors,
|
GetMentalModelErrors,
|
||||||
|
GetMentalModelHistoryData,
|
||||||
|
GetMentalModelHistoryErrors,
|
||||||
|
GetMentalModelHistoryResponses,
|
||||||
GetMentalModelResponses,
|
GetMentalModelResponses,
|
||||||
GetObservationHistoryData,
|
GetObservationHistoryData,
|
||||||
GetObservationHistoryErrors,
|
GetObservationHistoryErrors,
|
||||||
|
|
@ -503,6 +506,23 @@ export const updateMentalModel = <ThrowOnError extends boolean = false>(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get mental model history
|
||||||
|
*
|
||||||
|
* Get the refresh history of a mental model, showing content changes over time.
|
||||||
|
*/
|
||||||
|
export const getMentalModelHistory = <ThrowOnError extends boolean = false>(
|
||||||
|
options: Options<GetMentalModelHistoryData, ThrowOnError>,
|
||||||
|
) =>
|
||||||
|
(options.client ?? client).get<
|
||||||
|
GetMentalModelHistoryResponses,
|
||||||
|
GetMentalModelHistoryErrors,
|
||||||
|
ThrowOnError
|
||||||
|
>({
|
||||||
|
url: "/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/history",
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Refresh mental model
|
* Refresh mental model
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -3082,6 +3082,45 @@ export type UpdateMentalModelResponses = {
|
||||||
export type UpdateMentalModelResponse =
|
export type UpdateMentalModelResponse =
|
||||||
UpdateMentalModelResponses[keyof UpdateMentalModelResponses];
|
UpdateMentalModelResponses[keyof UpdateMentalModelResponses];
|
||||||
|
|
||||||
|
export type GetMentalModelHistoryData = {
|
||||||
|
body?: never;
|
||||||
|
headers?: {
|
||||||
|
/**
|
||||||
|
* Authorization
|
||||||
|
*/
|
||||||
|
authorization?: string | null;
|
||||||
|
};
|
||||||
|
path: {
|
||||||
|
/**
|
||||||
|
* Bank Id
|
||||||
|
*/
|
||||||
|
bank_id: string;
|
||||||
|
/**
|
||||||
|
* Mental Model Id
|
||||||
|
*/
|
||||||
|
mental_model_id: string;
|
||||||
|
};
|
||||||
|
query?: never;
|
||||||
|
url: "/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/history";
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetMentalModelHistoryErrors = {
|
||||||
|
/**
|
||||||
|
* Validation Error
|
||||||
|
*/
|
||||||
|
422: HttpValidationError;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetMentalModelHistoryError =
|
||||||
|
GetMentalModelHistoryErrors[keyof GetMentalModelHistoryErrors];
|
||||||
|
|
||||||
|
export type GetMentalModelHistoryResponses = {
|
||||||
|
/**
|
||||||
|
* Successful Response
|
||||||
|
*/
|
||||||
|
200: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
export type RefreshMentalModelData = {
|
export type RefreshMentalModelData = {
|
||||||
body?: never;
|
body?: never;
|
||||||
headers?: {
|
headers?: {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
import { DATAPLANE_URL, getDataplaneHeaders } from "@/lib/hindsight-client";
|
||||||
|
|
||||||
|
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}/history`,
|
||||||
|
{ method: "GET", headers: getDataplaneHeaders() }
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
return NextResponse.json({ error: "Mental model not found" }, { status: 404 });
|
||||||
|
}
|
||||||
|
throw new Error(`API returned ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return NextResponse.json(data, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching mental model history:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to fetch mental model history" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,8 +4,10 @@ import { useState, useEffect } from "react";
|
||||||
import { client, MentalModel } from "@/lib/api";
|
import { client, MentalModel } from "@/lib/api";
|
||||||
import { useBank } from "@/lib/bank-context";
|
import { useBank } from "@/lib/bank-context";
|
||||||
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog";
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
import { VisuallyHidden } from "@radix-ui/react-visually-hidden";
|
||||||
import { Loader2, Zap } from "lucide-react";
|
import { Loader2, Zap, FileText, History, ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
|
|
@ -100,20 +102,197 @@ export function MentalModelDetailContent({ mentalModel }: MentalModelDetailConte
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type HistoryEntry = { previous_content: string | null; changed_at: string };
|
||||||
|
|
||||||
|
type LineDiff = { type: "same" | "removed" | "added"; text: string };
|
||||||
|
|
||||||
|
function diffLines(a: string, b: string): { left: LineDiff[]; right: LineDiff[] } {
|
||||||
|
const aLines = a.split("\n");
|
||||||
|
const bLines = b.split("\n");
|
||||||
|
const m = aLines.length;
|
||||||
|
const n = bLines.length;
|
||||||
|
const dp: number[][] = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
|
||||||
|
for (let i = 1; i <= m; i++)
|
||||||
|
for (let j = 1; j <= n; j++)
|
||||||
|
dp[i][j] =
|
||||||
|
aLines[i - 1] === bLines[j - 1]
|
||||||
|
? dp[i - 1][j - 1] + 1
|
||||||
|
: Math.max(dp[i - 1][j], dp[i][j - 1]);
|
||||||
|
|
||||||
|
const ops: LineDiff[] = [];
|
||||||
|
let i = m,
|
||||||
|
j = n;
|
||||||
|
while (i > 0 || j > 0) {
|
||||||
|
if (i > 0 && j > 0 && aLines[i - 1] === bLines[j - 1]) {
|
||||||
|
ops.push({ type: "same", text: aLines[i - 1] });
|
||||||
|
i--;
|
||||||
|
j--;
|
||||||
|
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
|
||||||
|
ops.push({ type: "added", text: bLines[j - 1] });
|
||||||
|
j--;
|
||||||
|
} else {
|
||||||
|
ops.push({ type: "removed", text: aLines[i - 1] });
|
||||||
|
i--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ops.reverse();
|
||||||
|
|
||||||
|
// Pair removed/added lines side-by-side; same lines appear on both sides
|
||||||
|
const left: LineDiff[] = [];
|
||||||
|
const right: LineDiff[] = [];
|
||||||
|
let k = 0;
|
||||||
|
while (k < ops.length) {
|
||||||
|
const op = ops[k];
|
||||||
|
if (op.type === "same") {
|
||||||
|
left.push(op);
|
||||||
|
right.push(op);
|
||||||
|
k++;
|
||||||
|
} else {
|
||||||
|
// collect a block of removed/added and align them
|
||||||
|
const removed: string[] = [];
|
||||||
|
const added: string[] = [];
|
||||||
|
while (k < ops.length && ops[k].type !== "same") {
|
||||||
|
if (ops[k].type === "removed") removed.push(ops[k].text);
|
||||||
|
else added.push(ops[k].text);
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
const maxLen = Math.max(removed.length, added.length);
|
||||||
|
for (let r = 0; r < maxLen; r++) {
|
||||||
|
left.push(
|
||||||
|
r < removed.length ? { type: "removed", text: removed[r] } : { type: "same", text: "" }
|
||||||
|
);
|
||||||
|
right.push(
|
||||||
|
r < added.length ? { type: "added", text: added[r] } : { type: "same", text: "" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { left, right };
|
||||||
|
}
|
||||||
|
|
||||||
|
function SideBySideDiff({ before, after }: { before: string; after: string }) {
|
||||||
|
const { left, right } = diffLines(before, after);
|
||||||
|
const hasChanges = left.some((l) => l.type !== "same") || right.some((r) => r.type !== "same");
|
||||||
|
if (!hasChanges) return <span className="text-sm text-muted-foreground italic">unchanged</span>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-2 divide-x divide-border border border-border rounded-md overflow-hidden text-xs font-mono">
|
||||||
|
<div>
|
||||||
|
<div className="px-3 py-1.5 bg-muted text-muted-foreground font-sans font-semibold text-xs uppercase tracking-wide border-b border-border">
|
||||||
|
Before
|
||||||
|
</div>
|
||||||
|
{left.map((line, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`px-3 py-0.5 whitespace-pre-wrap leading-5 min-h-[1.25rem] ${
|
||||||
|
line.type === "removed"
|
||||||
|
? "bg-red-500/10 text-red-700 dark:text-red-400"
|
||||||
|
: "text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{line.text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="px-3 py-1.5 bg-muted text-muted-foreground font-sans font-semibold text-xs uppercase tracking-wide border-b border-border">
|
||||||
|
After
|
||||||
|
</div>
|
||||||
|
{right.map((line, idx) => (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
className={`px-3 py-0.5 whitespace-pre-wrap leading-5 min-h-[1.25rem] ${
|
||||||
|
line.type === "added"
|
||||||
|
? "bg-green-500/10 text-green-700 dark:text-green-400"
|
||||||
|
: "text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{line.text}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MentalModelHistoryView({
|
||||||
|
history,
|
||||||
|
currentContent,
|
||||||
|
}: {
|
||||||
|
history: HistoryEntry[];
|
||||||
|
currentContent: string;
|
||||||
|
}) {
|
||||||
|
const [idx, setIdx] = useState(0);
|
||||||
|
const entry = history[idx];
|
||||||
|
const afterContent = idx === 0 ? currentContent : (history[idx - 1].previous_content ?? "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Navigation header */}
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
Change <span className="font-semibold text-foreground">{history.length - idx}</span> of{" "}
|
||||||
|
{history.length} · {new Date(entry.changed_at).toLocaleString()}
|
||||||
|
</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
disabled={idx === history.length - 1}
|
||||||
|
onClick={() => setIdx(idx + 1)}
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-7 w-7 p-0"
|
||||||
|
disabled={idx === 0}
|
||||||
|
onClick={() => setIdx(idx - 1)}
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Change card */}
|
||||||
|
{entry.previous_content !== null ? (
|
||||||
|
<SideBySideDiff before={entry.previous_content} after={afterContent} />
|
||||||
|
) : (
|
||||||
|
<div className="border border-border rounded-lg p-3">
|
||||||
|
<span className="text-sm text-muted-foreground italic">
|
||||||
|
Previous content not available
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface MentalModelDetailModalProps {
|
interface MentalModelDetailModalProps {
|
||||||
mentalModelId: string | null;
|
mentalModelId: string | null;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
initialTab?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal wrapper for MentalModelDetailContent.
|
* Modal wrapper for MentalModelDetailContent.
|
||||||
* Fetches the mental model by ID and displays it in a dialog.
|
* Fetches the mental model by ID and displays it in a dialog.
|
||||||
*/
|
*/
|
||||||
export function MentalModelDetailModal({ mentalModelId, onClose }: MentalModelDetailModalProps) {
|
export function MentalModelDetailModal({
|
||||||
|
mentalModelId,
|
||||||
|
onClose,
|
||||||
|
initialTab,
|
||||||
|
}: MentalModelDetailModalProps) {
|
||||||
const { currentBank } = useBank();
|
const { currentBank } = useBank();
|
||||||
const [mentalModel, setMentalModel] = useState<MentalModel | null>(null);
|
const [mentalModel, setMentalModel] = useState<MentalModel | null>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [activeTab, setActiveTab] = useState(initialTab ?? "model");
|
||||||
|
|
||||||
|
const [history, setHistory] = useState<HistoryEntry[] | null>(null);
|
||||||
|
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!mentalModelId || !currentBank) return;
|
if (!mentalModelId || !currentBank) return;
|
||||||
|
|
@ -122,6 +301,8 @@ export function MentalModelDetailModal({ mentalModelId, onClose }: MentalModelDe
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
setMentalModel(null);
|
setMentalModel(null);
|
||||||
|
setHistory(null);
|
||||||
|
setActiveTab(initialTab ?? "model");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await client.getMentalModel(currentBank, mentalModelId);
|
const data = await client.getMentalModel(currentBank, mentalModelId);
|
||||||
|
|
@ -137,6 +318,26 @@ export function MentalModelDetailModal({ mentalModelId, onClose }: MentalModelDe
|
||||||
loadMentalModel();
|
loadMentalModel();
|
||||||
}, [mentalModelId, currentBank]);
|
}, [mentalModelId, currentBank]);
|
||||||
|
|
||||||
|
// Load history lazily when history tab is selected
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab !== "history" || !mentalModel || !currentBank || history !== null) return;
|
||||||
|
|
||||||
|
const loadHistory = async () => {
|
||||||
|
setLoadingHistory(true);
|
||||||
|
try {
|
||||||
|
const data = await client.getMentalModelHistory(currentBank, mentalModel.id);
|
||||||
|
setHistory(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Error loading mental model history:", err);
|
||||||
|
setHistory([]);
|
||||||
|
} finally {
|
||||||
|
setLoadingHistory(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadHistory();
|
||||||
|
}, [activeTab, mentalModel, currentBank, history]);
|
||||||
|
|
||||||
const isOpen = mentalModelId !== null;
|
const isOpen = mentalModelId !== null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -156,9 +357,41 @@ export function MentalModelDetailModal({ mentalModelId, onClose }: MentalModelDe
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : mentalModel ? (
|
) : mentalModel ? (
|
||||||
<div className="flex-1 overflow-y-auto">
|
<Tabs
|
||||||
<MentalModelDetailContent mentalModel={mentalModel} />
|
value={activeTab}
|
||||||
</div>
|
onValueChange={setActiveTab}
|
||||||
|
className="flex-1 flex flex-col overflow-hidden"
|
||||||
|
>
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="model" className="flex items-center gap-1.5">
|
||||||
|
<FileText className="w-3.5 h-3.5" />
|
||||||
|
Mental Model
|
||||||
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="history" className="flex items-center gap-1.5">
|
||||||
|
<History className="w-3.5 h-3.5" />
|
||||||
|
History
|
||||||
|
{history && history.length > 0 ? ` (${history.length})` : ""}
|
||||||
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto mt-4">
|
||||||
|
<TabsContent value="model" className="mt-0">
|
||||||
|
<MentalModelDetailContent mentalModel={mentalModel} />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="history" className="mt-0">
|
||||||
|
{loadingHistory ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : history && history.length > 0 ? (
|
||||||
|
<MentalModelHistoryView history={history} currentContent={mentalModel.content} />
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground italic">No history recorded yet.</p>
|
||||||
|
)}
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
|
</Tabs>
|
||||||
) : null}
|
) : null}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,19 @@ import {
|
||||||
Pencil,
|
Pencil,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
List,
|
List,
|
||||||
|
History,
|
||||||
|
MoreVertical,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { MemoryDetailModal } from "./memory-detail-modal";
|
import { MemoryDetailModal } from "./memory-detail-modal";
|
||||||
import { DirectiveDetailModal } from "./directive-detail-modal";
|
import { DirectiveDetailModal } from "./directive-detail-modal";
|
||||||
|
import { MentalModelDetailModal } from "./mental-model-detail-modal";
|
||||||
|
|
||||||
interface ReflectResponseBasedOnFact {
|
interface ReflectResponseBasedOnFact {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -927,6 +937,7 @@ function MentalModelDetailPanel({
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [viewMemoryId, setViewMemoryId] = useState<string | null>(null);
|
const [viewMemoryId, setViewMemoryId] = useState<string | null>(null);
|
||||||
const [viewDirectiveId, setViewDirectiveId] = useState<string | null>(null);
|
const [viewDirectiveId, setViewDirectiveId] = useState<string | null>(null);
|
||||||
|
const [showHistoryModal, setShowHistoryModal] = useState(false);
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
if (!currentBank) return;
|
if (!currentBank) return;
|
||||||
|
|
@ -1031,27 +1042,44 @@ function MentalModelDetailPanel({
|
||||||
<div className="flex-1 mr-4">
|
<div className="flex-1 mr-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h3 className="text-xl font-bold text-foreground">{mentalModel.name}</h3>
|
<h3 className="text-xl font-bold text-foreground">{mentalModel.name}</h3>
|
||||||
<Button variant="ghost" size="sm" onClick={onEdit} className="h-7 w-7 p-0">
|
|
||||||
<Pencil className="h-3.5 w-3.5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted-foreground mt-1">{mentalModel.source_query}</p>
|
<p className="text-sm text-muted-foreground mt-1">{mentalModel.source_query}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<DropdownMenu>
|
||||||
variant="outline"
|
<DropdownMenuTrigger asChild>
|
||||||
size="sm"
|
<Button variant="ghost" size="sm" className="h-8 px-2 gap-1" disabled={refreshing}>
|
||||||
onClick={handleRefresh}
|
{refreshing ? (
|
||||||
disabled={refreshing}
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
className="h-8"
|
) : (
|
||||||
>
|
<MoreVertical className="h-4 w-4" />
|
||||||
{refreshing ? (
|
)}
|
||||||
<Loader2 className="w-4 h-4 animate-spin mr-1" />
|
<span className="text-xs">Actions</span>
|
||||||
) : (
|
</Button>
|
||||||
<RefreshCw className="w-4 h-4 mr-1" />
|
</DropdownMenuTrigger>
|
||||||
)}
|
<DropdownMenuContent align="end">
|
||||||
Refresh
|
<DropdownMenuItem onClick={onEdit}>
|
||||||
</Button>
|
<Pencil className="h-4 w-4 mr-2" />
|
||||||
|
Edit
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={handleRefresh}>
|
||||||
|
<RefreshCw className="h-4 w-4 mr-2" />
|
||||||
|
Refresh
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => setShowHistoryModal(true)}>
|
||||||
|
<History className="h-4 w-4 mr-2" />
|
||||||
|
View History
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={onDelete}
|
||||||
|
className="text-destructive focus:text-destructive"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
Delete
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
<Button variant="ghost" size="sm" onClick={onClose} className="h-8 w-8 p-0">
|
<Button variant="ghost" size="sm" onClick={onClose} className="h-8 w-8 p-0">
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -1222,18 +1250,6 @@ function MentalModelDetailPanel({
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="pt-4 border-t border-border">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
|
||||||
onClick={onDelete}
|
|
||||||
className="text-muted-foreground hover:text-destructive hover:border-destructive"
|
|
||||||
>
|
|
||||||
<Trash2 className="h-4 w-4 mr-2" />
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -1249,6 +1265,13 @@ function MentalModelDetailPanel({
|
||||||
onClose={() => setViewDirectiveId(null)}
|
onClose={() => setViewDirectiveId(null)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Mental Model History Modal */}
|
||||||
|
<MentalModelDetailModal
|
||||||
|
mentalModelId={showHistoryModal ? mentalModel.id : null}
|
||||||
|
onClose={() => setShowHistoryModal(false)}
|
||||||
|
initialTab="history"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -813,6 +813,18 @@ export class ControlPlaneClient {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the refresh history of a mental model
|
||||||
|
*/
|
||||||
|
async getMentalModelHistory(bankId: string, mentalModelId: string) {
|
||||||
|
return this.fetchApi<
|
||||||
|
{
|
||||||
|
previous_content: string | null;
|
||||||
|
changed_at: string;
|
||||||
|
}[]
|
||||||
|
>(`/api/banks/${bankId}/mental-models/${mentalModelId}/history`);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get API version and feature flags
|
* Get API version and feature flags
|
||||||
* Use this to check which capabilities are available in the dataplane
|
* Use this to check which capabilities are available in the dataplane
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,33 @@ For more details on tag matching modes (`any`, `any_strict`, `all`, `all_strict`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## History
|
||||||
|
|
||||||
|
Every time a mental model's content changes (via refresh or manual update), the previous version is saved with a timestamp. You can retrieve the full change log with the history endpoint:
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<TabItem value="python" label="Python">
|
||||||
|
<CodeSnippet code={mentalModelsPy} section="get-mental-model-history" language="python" />
|
||||||
|
</TabItem>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
The endpoint returns a list of history entries, most recent first:
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| `previous_content` | string \| null | The content before this change (`null` if not available) |
|
||||||
|
| `changed_at` | string | ISO 8601 timestamp of when the change occurred |
|
||||||
|
|
||||||
|
Each entry captures the **content before the change** and when it happened. The current content is returned by the standard [Get a Mental Model](#get-a-mental-model) endpoint.
|
||||||
|
|
||||||
|
:::note
|
||||||
|
History tracking is enabled by default. Set `HINDSIGHT_API_ENABLE_MENTAL_MODEL_HISTORY=false` to disable it.
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Use Cases
|
## Use Cases
|
||||||
|
|
||||||
| Use Case | Example |
|
| Use Case | Example |
|
||||||
|
|
|
||||||
|
|
@ -531,6 +531,7 @@ For advanced authentication (JWT, OAuth, multi-tenant schemas), implement a cust
|
||||||
| `HINDSIGHT_API_RERANKER_MAX_CANDIDATES` | Max candidates to rerank per recall (RRF pre-filters the rest) | `300` |
|
| `HINDSIGHT_API_RERANKER_MAX_CANDIDATES` | Max candidates to rerank per recall (RRF pre-filters the rest) | `300` |
|
||||||
| `HINDSIGHT_API_MPFP_TOP_K_NEIGHBORS` | Fan-out limit per node in MPFP graph traversal | `20` |
|
| `HINDSIGHT_API_MPFP_TOP_K_NEIGHBORS` | Fan-out limit per node in MPFP graph traversal | `20` |
|
||||||
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_CONCURRENCY` | Max concurrent mental model refreshes | `8` |
|
| `HINDSIGHT_API_MENTAL_MODEL_REFRESH_CONCURRENCY` | Max concurrent mental model refreshes | `8` |
|
||||||
|
| `HINDSIGHT_API_ENABLE_MENTAL_MODEL_HISTORY` | Track history of content changes to each mental model (previous content + timestamp). Disable to reduce storage if audit trails are not needed. | `true` |
|
||||||
|
|
||||||
#### Graph Retrieval Algorithms
|
#### Graph Retrieval Algorithms
|
||||||
|
|
||||||
|
|
@ -762,6 +763,7 @@ Observations are consolidated knowledge synthesized from facts.
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|----------|-------------|---------|
|
|----------|-------------|---------|
|
||||||
| `HINDSIGHT_API_ENABLE_OBSERVATIONS` | Enable observation consolidation | `true` |
|
| `HINDSIGHT_API_ENABLE_OBSERVATIONS` | Enable observation consolidation | `true` |
|
||||||
|
| `HINDSIGHT_API_ENABLE_OBSERVATION_HISTORY` | Track history of changes to each observation (previous content + timestamp). Disable to reduce storage if audit trails are not needed. | `true` |
|
||||||
| `HINDSIGHT_API_CONSOLIDATION_BATCH_SIZE` | Memories to load per batch (internal optimization) | `50` |
|
| `HINDSIGHT_API_CONSOLIDATION_BATCH_SIZE` | Memories to load per batch (internal optimization) | `50` |
|
||||||
| `HINDSIGHT_API_CONSOLIDATION_MAX_TOKENS` | Max tokens for recall when finding related observations during consolidation | `1024` |
|
| `HINDSIGHT_API_CONSOLIDATION_MAX_TOKENS` | Max tokens for recall when finding related observations during consolidation | `1024` |
|
||||||
| `HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE` | Number of facts sent to the LLM in a single consolidation call. Higher values reduce LLM calls and improve throughput at the cost of larger prompts. Set to `1` to disable batching. Configurable per bank. | `8` |
|
| `HINDSIGHT_API_CONSOLIDATION_LLM_BATCH_SIZE` | Number of facts sent to the LLM in a single consolidation call. Higher values reduce LLM calls and improve throughput at the cost of larger prompts. Set to `1` to disable batching. Configurable per bank. | `8` |
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,18 @@ if mental_model_id:
|
||||||
# [/docs:update-mental-model]
|
# [/docs:update-mental-model]
|
||||||
|
|
||||||
|
|
||||||
|
# [docs:get-mental-model-history]
|
||||||
|
# Get the change history of a mental model
|
||||||
|
history = client.get_mental_model_history(
|
||||||
|
bank_id=BANK_ID,
|
||||||
|
mental_model_id=mental_model_id
|
||||||
|
)
|
||||||
|
|
||||||
|
for entry in history:
|
||||||
|
print(f"Changed at: {entry['changed_at']}")
|
||||||
|
print(f"Previous content: {entry['previous_content']}")
|
||||||
|
# [/docs:get-mental-model-history]
|
||||||
|
|
||||||
# [docs:delete-mental-model]
|
# [docs:delete-mental-model]
|
||||||
# Delete a mental model
|
# Delete a mental model
|
||||||
client.delete_mental_model(
|
client.delete_mental_model(
|
||||||
|
|
|
||||||
|
|
@ -1310,6 +1310,72 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/history": {
|
||||||
|
"get": {
|
||||||
|
"tags": [
|
||||||
|
"Mental Models"
|
||||||
|
],
|
||||||
|
"summary": "Get mental model history",
|
||||||
|
"description": "Get the refresh history of a mental model, showing content changes over time.",
|
||||||
|
"operationId": "get_mental_model_history",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "bank_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Bank Id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mental_model_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Mental Model Id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "authorization",
|
||||||
|
"in": "header",
|
||||||
|
"required": false,
|
||||||
|
"schema": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": "Authorization"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/refresh": {
|
"/v1/default/banks/{bank_id}/mental-models/{mental_model_id}/refresh": {
|
||||||
"post": {
|
"post": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue