chore: remove dead code and stale config (#2135)

Remove unreferenced backend helpers, stale UI/docs components, and
unused imports across the API, control plane, clients, and integrations.

Drop obsolete consolidated-observation helpers and unused scoring code,
clean orphaned React/docs components, and remove stale Radix dependencies.

Align release scripts, Helm docs, lockfiles, generated clients, and
current API examples with the package and endpoint surface still in use.
This commit is contained in:
Sanderhoff-alt
2026-06-11 17:12:03 +02:00
committed by GitHub
parent 4032b27912
commit c96106cc01
69 changed files with 104 additions and 1353 deletions
Generated
-1
View File
@@ -77,7 +77,6 @@
"npm:@radix-ui/react-dropdown-menu@^2.1.16",
"npm:@radix-ui/react-label@^2.1.8",
"npm:@radix-ui/react-popover@^1.1.15",
"npm:@radix-ui/react-radio-group@^1.3.8",
"npm:@radix-ui/react-select@^2.2.6",
"npm:@radix-ui/react-slider@^1.3.6",
"npm:@radix-ui/react-slot@^1.2.4",
@@ -1,6 +1,6 @@
name: hindsight
# Docker Compose file for Hindsight with PostgreSQL and vectorchord
# docker compose -f docker/docker-compose/docker-compose.yaml down && sleep 2 && docker compose -f docker/docker-compose/docker-compose.yaml up -d
# docker compose -f docker/docker-compose/vchord/docker-compose.yaml down && sleep 2 && docker compose -f docker/docker-compose/vchord/docker-compose.yaml up -d
# Make sure to set the required environment variables before running:
# - HINDSIGHT_DB_PASSWORD: Password for the PostgreSQL user
# - Configure LLM provider variables as needed (see below in the hindsight service)
+3 -3
View File
@@ -66,13 +66,13 @@ helm install hindsight ./helm/hindsight -n hindsight --create-namespace -f value
| Parameter | Description | Default |
|-----------|-------------|---------|
| `version` | Default image tag for all components | `0.1.0` |
| `version` | Default image tag for all components | Chart `appVersion` |
| `api.enabled` | Enable the API component | `true` |
| `api.image.repository` | API image repository | `hindsight/api` |
| `api.image.repository` | API image repository | `ghcr.io/vectorize-io/hindsight-api` |
| `api.image.tag` | API image tag (defaults to `version`) | - |
| `api.service.port` | API service port | `8888` |
| `controlPlane.enabled` | Enable the control plane | `true` |
| `controlPlane.image.repository` | Control plane image repository | `hindsight/control-plane` |
| `controlPlane.image.repository` | Control plane image repository | `ghcr.io/vectorize-io/hindsight-control-plane` |
| `controlPlane.image.tag` | Control plane image tag (defaults to `version`) | - |
| `controlPlane.service.port` | Control plane service port | `3000` |
| `postgresql.enabled` | Deploy PostgreSQL as subchart | `true` |
-3
View File
@@ -13,9 +13,6 @@
# - Any other env vars you want to inject
# existingSecret: "my-hindsight-secret"
# Global settings
replicaCount: 1
# Image settings for api
api:
enabled: true
@@ -16,9 +16,7 @@ retention parameters, retrieval settings, etc.) in Python field name format.
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import context, op
from sqlalchemy.dialects.postgresql import JSONB
from hindsight_api.alembic._dialect import run_for_dialect
+10 -14
View File
@@ -12,7 +12,7 @@ import re
import uuid
from collections.abc import Awaitable
from contextlib import asynccontextmanager
from datetime import datetime, timezone
from datetime import datetime
from typing import Any, Literal, TypeVar
from fastapi import Depends, FastAPI, File, Form, Header, HTTPException, Query, Request, UploadFile
@@ -83,7 +83,7 @@ def FieldWithDefault(default_factory: Callable, **kwargs) -> Any:
from hindsight_api.config import get_config
from hindsight_api.engine.memory_engine import Budget, _current_schema, _get_tiktoken_encoding, fq_table
from hindsight_api.engine.memory_engine import Budget, _current_schema, _get_tiktoken_encoding
from hindsight_api.engine.providers.none_llm import LLMNotAvailableError
from hindsight_api.engine.response_models import VALID_RECALL_FACT_TYPES, MemoryFact, TokenUsage
from hindsight_api.engine.search.tags import TagGroup, TagsMatch
@@ -265,7 +265,7 @@ class RecallResult(BaseModel):
id: str
text: str
type: str | None = None # fact type: world, experience, opinion, observation
type: str | None = None # fact type: world, experience, observation
entities: list[str] | None = None # Entity names mentioned in this fact
context: str | None = None
occurred_start: str | None = None # ISO format date when the event started
@@ -852,7 +852,7 @@ class ReflectFact(BaseModel):
text: str = Field(
description="Fact text. When type='observation', this contains markdown-formatted consolidated knowledge"
)
type: str | None = None # fact type: world, experience, opinion, observation
type: str | None = None # fact type: world, experience, observation
context: str | None = None
occurred_start: str | None = None
occurred_end: str | None = None
@@ -2737,7 +2737,6 @@ def _make_audited_http(audit_logger_getter: Callable[[], AuditLogger | None]):
from datetime import datetime as _dt
from datetime import timezone as _tz
from functools import wraps
from typing import Callable as _Callable
def audited(action: str, *, request_param: str | None = "request"):
"""Decorator that wraps an HTTP handler with audit logging.
@@ -3081,8 +3080,6 @@ def create_app(
# Replace UUIDs and numeric IDs with placeholders
import re
from starlette.requests import Request
path = request.url.path
# Replace UUIDs
path = re.sub(r"/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", "/{id}", path)
@@ -3283,7 +3280,7 @@ def _register_routes(app: FastAPI):
"/v1/default/banks/{bank_id}/graph",
response_model=GraphDataResponse,
summary="Get memory graph data",
description="Retrieve graph data for visualization, optionally filtered by type (world/experience/opinion).",
description="Retrieve graph data for visualization, optionally filtered by type (world/experience/observation).",
operation_id="get_graph",
tags=["Memory"],
)
@@ -3350,7 +3347,7 @@ def _register_routes(app: FastAPI):
Args:
bank_id: Memory Bank ID (from path)
type: Filter by fact type (world, experience, opinion)
type: Filter by fact type (world, experience, observation)
q: Search query for full-text search (searches text and context)
consolidation_state: Filter by consolidation state for source memories
(world/experience). One of 'failed', 'pending', or 'done'.
@@ -3700,11 +3697,11 @@ def _register_routes(app: FastAPI):
"/v1/default/banks/{bank_id}/reflect",
response_model=ReflectResponse,
summary="Reflect and generate answer",
description="Reflect and formulate an answer using bank identity, world facts, and opinions.\n\n"
description="Reflect and formulate an answer using bank identity, world facts, observations, and mental models.\n\n"
"This endpoint:\n"
"1. Retrieves experience (conversations and events)\n"
"2. Retrieves world facts relevant to the query\n"
"3. Retrieves existing opinions (bank's perspectives)\n"
"3. Retrieves observations and mental models (bank's synthesized perspectives)\n"
"4. Uses LLM to formulate a contextual answer\n"
"5. Returns plain text answer and the facts used",
operation_id="reflect",
@@ -6585,7 +6582,6 @@ def _register_routes(app: FastAPI):
_validate_parsers(_resolve_parser(request_data.parser), "request-level parser")
# Prepare file items and calculate total batch size
import io
file_items = []
total_batch_size = 0
@@ -6664,14 +6660,14 @@ def _register_routes(app: FastAPI):
"/v1/default/banks/{bank_id}/memories",
response_model=DeleteResponse,
summary="Clear memory bank memories",
description="Delete memory units for a memory bank. Optionally filter by type (world, experience, opinion) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.",
description="Delete memory units for a memory bank. Optionally filter by type (world, experience, observation) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.",
operation_id="clear_bank_memories",
tags=["Memory"],
)
@audited("clear_memories", request_param=None)
async def api_clear_bank_memories(
bank_id: str,
type: str | None = Query(None, description="Optional fact type filter (world, experience, opinion)"),
type: str | None = Query(None, description="Optional fact type filter (world, experience, observation)"),
request_context: RequestContext = Depends(get_request_context),
):
"""Clear memories for a memory bank, optionally filtered by type."""
@@ -27,34 +27,21 @@ from ..config import (
DEFAULT_RERANKER_LITELLM_MODEL,
DEFAULT_RERANKER_LITELLM_SDK_MODEL,
DEFAULT_RERANKER_LOCAL_BATCH_SIZE,
DEFAULT_RERANKER_LOCAL_FORCE_CPU,
DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT,
DEFAULT_RERANKER_LOCAL_MODEL,
DEFAULT_RERANKER_LOCAL_TRUST_REMOTE_CODE,
DEFAULT_RERANKER_PROVIDER,
DEFAULT_RERANKER_SILICONFLOW_BASE_URL,
DEFAULT_RERANKER_SILICONFLOW_MODEL,
DEFAULT_RERANKER_TEI_BATCH_SIZE,
DEFAULT_RERANKER_TEI_HTTP_TIMEOUT,
DEFAULT_RERANKER_TEI_MAX_CONCURRENT,
DEFAULT_RERANKER_ZEROENTROPY_MODEL,
DEFAULT_ZEROENTROPY_BASE_URL,
ENV_RERANKER_ALIBABA_API_KEY,
ENV_RERANKER_COHERE_API_KEY,
ENV_RERANKER_COHERE_MODEL,
ENV_RERANKER_FLASHRANK_CACHE_DIR,
ENV_RERANKER_FLASHRANK_CPU_MEM_ARENA,
ENV_RERANKER_FLASHRANK_MODEL,
ENV_RERANKER_GOOGLE_PROJECT_ID,
ENV_RERANKER_LOCAL_FORCE_CPU,
ENV_RERANKER_LOCAL_MAX_CONCURRENT,
ENV_RERANKER_LOCAL_MODEL,
ENV_RERANKER_LOCAL_TRUST_REMOTE_CODE,
ENV_RERANKER_PROVIDER,
ENV_RERANKER_SILICONFLOW_API_KEY,
ENV_RERANKER_TEI_BATCH_SIZE,
ENV_RERANKER_TEI_HTTP_TIMEOUT,
ENV_RERANKER_TEI_MAX_CONCURRENT,
ENV_RERANKER_TEI_URL,
ENV_RERANKER_ZEROENTROPY_API_KEY,
)
@@ -303,7 +290,6 @@ class LocalSTCrossEncoder(CrossEncoderModel):
- bucket_batching: sort pairs by token length to reduce padding waste (36-54% speedup)
- batch_size: explicit batch size for predict() calls (MPS optimal: 32)
"""
import numpy as np
try:
if self.bucket_batching and len(pairs) > 1:
@@ -19,7 +19,6 @@ and mirrors Django's ``DatabaseOperations`` architecture.
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any
from uuid import UUID
from .base import DatabaseConnection
from .result import ResultRow
@@ -8,8 +8,6 @@ columns can't appear in GROUP BY).
import json
import uuid as uuid_mod
from datetime import UTC, datetime
from typing import Any
from uuid import UUID
from .base import DatabaseConnection
from .ops import DataAccessOps, TagListingParts
@@ -4,11 +4,6 @@ Uses unnest(), LATERAL, DISTINCT ON, and native array operations for
efficient batch operations.
"""
import json
from datetime import UTC, datetime
from typing import Any
from uuid import UUID
from .base import DatabaseConnection
from .ops import DataAccessOps, TagListingParts
from .result import ResultRow
@@ -620,7 +615,6 @@ class PostgreSQLOps(DataAccessOps):
per_entity_limit: int,
) -> tuple[list[ResultRow], list[ResultRow], list[ResultRow]]:
# v0.5.6 array ops: unnest, &&, COUNT(DISTINCT) on source_memory_ids.
from ..schema import fq_table
entity_rows = await conn.fetch(
f"""
@@ -26,11 +26,8 @@ from ..config import (
DEFAULT_EMBEDDINGS_GEMINI_MODEL,
DEFAULT_EMBEDDINGS_LITELLM_MODEL,
DEFAULT_EMBEDDINGS_LITELLM_SDK_MODEL,
DEFAULT_EMBEDDINGS_LOCAL_FORCE_CPU,
DEFAULT_EMBEDDINGS_LOCAL_MODEL,
DEFAULT_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE,
DEFAULT_EMBEDDINGS_OPENAI_MODEL,
DEFAULT_EMBEDDINGS_PROVIDER,
DEFAULT_EMBEDDINGS_ZEROENTROPY_BATCH_SIZE,
DEFAULT_EMBEDDINGS_ZEROENTROPY_DIMENSIONS,
DEFAULT_EMBEDDINGS_ZEROENTROPY_ENCODING_FORMAT,
@@ -40,13 +37,6 @@ from ..config import (
DEFAULT_ZEROENTROPY_BASE_URL,
ENV_EMBEDDINGS_COHERE_API_KEY,
ENV_EMBEDDINGS_GEMINI_API_KEY,
ENV_EMBEDDINGS_LOCAL_FORCE_CPU,
ENV_EMBEDDINGS_LOCAL_MODEL,
ENV_EMBEDDINGS_LOCAL_TRUST_REMOTE_CODE,
ENV_EMBEDDINGS_ONNX_DIMENSIONS,
ENV_EMBEDDINGS_ONNX_MODEL_ID,
ENV_EMBEDDINGS_ONNX_MODEL_PATH,
ENV_EMBEDDINGS_ONNX_TOKENIZER_NAME_OR_PATH,
ENV_EMBEDDINGS_OPENAI_API_KEY,
ENV_EMBEDDINGS_OPENAI_BASE_URL,
ENV_EMBEDDINGS_OPENAI_MODEL,
@@ -834,14 +834,12 @@ class EntityResolver:
best_candidate = None
best_score = 0.0
best_name_similarity = 0.0
nearby_entity_set = {e["text"].lower() for e in nearby_entities if e["text"] != entity_text}
for row in candidates:
candidate_id = row["id"]
canonical_name = row["canonical_name"]
metadata = row["metadata"]
last_seen = row["last_seen"]
score = 0.0
@@ -888,7 +886,6 @@ class EntityResolver:
if score > best_score:
best_score = score
best_candidate = candidate_id
best_name_similarity = name_similarity
# Threshold for considering it the same entity
threshold = 0.6
@@ -8,7 +8,7 @@ enabling support for multiple LLM backends (OpenAI, Anthropic, Gemini, Codex, et
from abc import ABC, abstractmethod
from typing import Any
from .response_models import LLMToolCallResult, TokenUsage
from .response_models import LLMToolCallResult
class LLMInterface(ABC):
@@ -11,14 +11,10 @@ import time
import uuid
from contextlib import AsyncExitStack
from pathlib import Path
from typing import Any
import httpx
from openai import APIConnectionError, APIStatusError, AsyncOpenAI, LengthFinishReasonError
from typing import TYPE_CHECKING, Any
# Vertex AI imports (conditional - for LLMProvider to pass credentials to GeminiLLM)
try:
import google.auth
from google.oauth2 import service_account
VERTEXAI_AVAILABLE = True
@@ -27,16 +23,14 @@ except ImportError:
from ..config import (
DEFAULT_LLM_MAX_CONCURRENT,
DEFAULT_LLM_TIMEOUT,
ENV_CONSOLIDATION_LLM_MAX_CONCURRENT,
ENV_LLM_GROQ_SERVICE_TIER,
ENV_LLM_MAX_CONCURRENT,
ENV_LLM_TIMEOUT,
ENV_REFLECT_LLM_MAX_CONCURRENT,
ENV_RETAIN_LLM_MAX_CONCURRENT,
)
from ..metrics import get_metrics_collector
from .response_models import TokenUsage
if TYPE_CHECKING:
from .response_models import LLMToolCallResult
# Seed applied to every Groq request for deterministic behavior.
DEFAULT_LLM_SEED = 4242
@@ -287,7 +281,6 @@ def create_llm_provider(
Returns:
LLMInterface implementation for the specified provider.
"""
from .llm_interface import LLMInterface
from .providers import (
AnthropicLLM,
ClaudeCodeLLM,
@@ -36,8 +36,6 @@ from ..config import (
HindsightConfig,
get_config,
)
from ..db_url import to_libpq_url
from ..metrics import get_metrics_collector
from ..tracing import create_operation_span
from ..utils import mask_network_location
from ..worker.exceptions import DeferOperation, RetryTaskAt
@@ -58,10 +56,7 @@ from .llm_trace import (
from .operation_metadata import (
BatchRetainChildMetadata,
BatchRetainParentMetadata,
ConsolidationMetadata,
RefreshMentalModelMetadata,
RetainExtractionErrors,
RetainMetadata,
RetainOutcomeAggregate,
RetainOutcomeMetadata,
)
@@ -334,16 +329,12 @@ def validate_sql_schema(sql: str) -> None:
)
import asyncpg
import numpy as np
from pydantic import BaseModel, Field
from .cross_encoder import CrossEncoderModel
from .embeddings import Embeddings, create_embeddings_from_env
from .interface import MemoryEngineInterface
if TYPE_CHECKING:
from hindsight_api.extensions import OperationValidatorExtension, TenantExtension
from hindsight_api.extensions import OperationValidatorExtension, TenantExtension, ValidationResult
from hindsight_api.models import RequestContext
from .audit import AuditLogListResponse, AuditLogStatsResponse
@@ -352,21 +343,17 @@ if TYPE_CHECKING:
from enum import Enum
from ..metrics import get_metrics_collector
from ..pg0 import EmbeddedPostgres, parse_pg0_url
from .entity_resolver import EntityResolver
from .llm_wrapper import LLMConfig, requires_api_key, sanitize_llm_output, sanitize_text
from .query_analyzer import QueryAnalyzer
from .reflect import run_reflect_agent
from .reflect.prompts import DELTA_SYSTEM_PROMPT, build_delta_prompt
from .reflect.tools import tool_expand, tool_recall, tool_search_mental_models, tool_search_observations
from .response_models import (
VALID_RECALL_FACT_TYPES,
EntityObservation,
EntityState,
LLMCallTrace,
MemoryFact,
ObservationRef,
ReflectResult,
TokenUsage,
ToolCallTrace,
@@ -374,7 +361,6 @@ from .response_models import (
from .response_models import RecallResult as RecallResultModel
from .retain import bank_utils, embedding_utils
from .retain.types import RetainContentDict
from .search import think_utils
from .search.reranking import CrossEncoderReranker, apply_combined_scoring
from .search.tags import TagGroup, TagsMatch, build_tag_groups_where_clause, build_tags_where_clause
from .search.types import ScoredResult
@@ -1161,7 +1147,7 @@ class MemoryEngine(MemoryEngineInterface):
if self._operation_validator is None:
return None
from hindsight_api.extensions import OperationValidationError, ValidationResult
from hindsight_api.extensions import OperationValidationError
result = await validation_coro
if not result.allowed:
@@ -3115,7 +3101,7 @@ class MemoryEngine(MemoryEngineInterface):
)
result = await self._validate_operation(self._operation_validator.validate_retain(ctx))
if result and result.contents is not None:
contents = result.contents
contents = cast(list[RetainContentDict], result.contents)
# Engine-owned copy: the orchestrator clears per-item "content" strings
# after building the document's combined text (memory pressure
@@ -3476,7 +3462,7 @@ class MemoryEngine(MemoryEngineInterface):
# Use the new modular orchestrator
from .retain import orchestrator
backend = await self._get_backend()
await self._get_backend()
# Resolve bank-specific config for this operation
resolved_config = await self._config_resolver.resolve_full_config(bank_id, request_context)
@@ -3612,7 +3598,7 @@ class MemoryEngine(MemoryEngineInterface):
parse_archive(archive_bytes)
await self._authenticate_tenant(request_context)
backend = await self._get_backend()
await self._get_backend()
# Ensure the bank (and its per-bank vector indexes) exist before inserts.
# Import has no single write transaction to join — the archive is written
# by a worker later — so the bank is created on its own connection.
@@ -4178,9 +4164,6 @@ class MemoryEngine(MemoryEngineInterface):
retrieve_all_fact_types_parallel,
)
# Track each retrieval start time
retrieval_start = time.time()
retrieval_span = tracer_otel.start_span("hindsight.recall_retrieval")
retrieval_span.set_attribute("hindsight.bank_id", bank_id)
retrieval_span.set_attribute("hindsight.fact_types", ",".join(fact_type))
@@ -4291,10 +4274,7 @@ class MemoryEngine(MemoryEngineInterface):
f"graph {pre_cap_counts[2]}->{len(graph_results)}"
)
retrieval_duration = time.time() - retrieval_start
step_duration = time.time() - step_start
total_retrievals = len(fact_type) * (4 if temporal_results else 3)
# Format per-method timings
timing_parts = [
f"semantic={len(semantic_results)}({aggregated_timings['semantic']:.3f}s)",
@@ -8081,7 +8061,7 @@ class MemoryEngine(MemoryEngineInterface):
bank_id=bank_id, operation="update_bank_disposition", request_context=request_context
)
await self._validate_operation(self._operation_validator.validate_bank_write(ctx))
backend = await self._get_backend()
await self._get_backend()
await bank_utils.update_bank_disposition(self._backend, bank_id, disposition)
async def set_bank_mission(
@@ -8108,7 +8088,7 @@ class MemoryEngine(MemoryEngineInterface):
ctx = BankWriteContext(bank_id=bank_id, operation="set_bank_mission", request_context=request_context)
await self._validate_operation(self._operation_validator.validate_bank_write(ctx))
backend = await self._get_backend()
await self._get_backend()
await bank_utils.set_bank_mission(self._backend, bank_id, mission)
return {"bank_id": bank_id, "mission": mission}
@@ -8137,7 +8117,7 @@ class MemoryEngine(MemoryEngineInterface):
ctx = BankWriteContext(bank_id=bank_id, operation="merge_bank_mission", request_context=request_context)
await self._validate_operation(self._operation_validator.validate_bank_write(ctx))
backend = await self._get_backend()
await self._get_backend()
return await bank_utils.merge_bank_mission(self._backend, self._reflect_llm_config, bank_id, new_info)
async def list_banks(
@@ -8155,7 +8135,7 @@ class MemoryEngine(MemoryEngineInterface):
List of dicts with bank_id, name, disposition, mission, created_at, updated_at
"""
await self._authenticate_tenant(request_context)
backend = await self._get_backend()
await self._get_backend()
banks = await bank_utils.list_banks(self._backend)
if self._operation_validator:
from hindsight_api.extensions import BankListContext
@@ -9432,87 +9412,6 @@ class MemoryEngine(MemoryEngineInterface):
"observations": [],
}
def _parse_observations(self, observations_raw: list):
"""Parse raw observation dicts into typed Observation models.
Returns list of Observation models with computed trend/evidence_span/evidence_count.
"""
from .reflect.observations import Observation, ObservationEvidence
observations: list[Observation] = []
for obs in observations_raw:
if not isinstance(obs, dict):
continue
try:
parsed = Observation(
title=obs.get("title", ""),
content=obs.get("content", ""),
evidence=[
ObservationEvidence(
memory_id=ev.get("memory_id", ""),
quote=ev.get("quote", ""),
relevance=ev.get("relevance", ""),
timestamp=ev.get("timestamp"),
)
for ev in obs.get("evidence", [])
if isinstance(ev, dict)
],
created_at=obs.get("created_at"),
)
observations.append(parsed)
except Exception as e:
logger.warning(f"Failed to parse observation: {e}")
continue
return observations
async def _count_memories_since(
self,
bank_id: str,
since_timestamp: str | None,
backend=None,
) -> int:
"""
Count memories created after a given timestamp.
Args:
bank_id: Bank identifier
since_timestamp: ISO timestamp string. If None, returns total count.
backend: Optional database backend (uses default if not provided)
Returns:
Number of memories created since the timestamp
"""
if backend is None:
backend = await self._get_backend()
async with acquire_with_retry(backend) as conn:
if since_timestamp:
# Parse the timestamp
from datetime import datetime
try:
ts = datetime.fromisoformat(since_timestamp.replace("Z", "+00:00"))
except ValueError:
# Invalid timestamp, return total count
ts = None
if ts:
count = await conn.fetchval(
f"SELECT COUNT(*) FROM {fq_table('memory_units')} WHERE bank_id = $1 AND created_at > $2",
bank_id,
ts,
)
return count or 0
# No timestamp or invalid, return total count
count = await conn.fetchval(
f"SELECT COUNT(*) FROM {fq_table('memory_units')} WHERE bank_id = $1",
bank_id,
)
return count or 0
async def _delete_stale_observations_for_memories(
self,
conn,
@@ -9529,149 +9428,6 @@ class MemoryEngine(MemoryEngineInterface):
return await delete_stale_observations_for_memories(conn, bank_id, fact_ids, ops=self._backend.ops)
# =========================================================================
# MENTAL MODELS (CONSOLIDATED) - Read-only access to auto-consolidated mental models
# =========================================================================
async def list_mental_models_consolidated(
self,
bank_id: str,
*,
tags: list[str] | None = None,
tags_match: str = "any",
limit: int = 100,
offset: int = 0,
request_context: "RequestContext",
) -> list[dict[str, Any]]:
"""List auto-consolidated observations for a bank.
Observations are stored in memory_units with fact_type='observation'.
They are automatically created and updated by the consolidation engine.
Args:
bank_id: Bank identifier
tags: Optional tags to filter by
tags_match: How to match tags - 'any', 'all', or 'exact'
limit: Maximum number of results
offset: Offset for pagination
request_context: Request context for authentication
Returns:
List of observation dicts
"""
await self._authenticate_tenant(request_context)
backend = await self._get_backend()
async with acquire_with_retry(backend) as conn:
# Build tag filter
tag_filter = ""
params: list[Any] = [bank_id, limit, offset]
if tags:
if tags_match == "all":
tag_filter = " AND tags @> $4::varchar[]"
elif tags_match == "exact":
tag_filter = " AND tags = $4::varchar[]"
else: # any
tag_filter = " AND tags && $4::varchar[]"
params.append(tags)
rows = await conn.fetch(
f"""
SELECT id, bank_id, text, proof_count, tags, source_memory_ids, created_at, updated_at
FROM {fq_table("memory_units")}
WHERE bank_id = $1 AND fact_type = 'observation' {tag_filter}
ORDER BY updated_at DESC NULLS LAST
LIMIT $2 OFFSET $3
""",
*params,
)
return [self._row_to_observation_consolidated(row) for row in rows]
async def get_observation_consolidated(
self,
bank_id: str,
observation_id: str,
*,
include_source_memories: bool = True,
request_context: "RequestContext",
) -> dict[str, Any] | None:
"""Get a single observation by ID.
Args:
bank_id: Bank identifier
observation_id: Observation ID
include_source_memories: Whether to include full source memory details
request_context: Request context for authentication
Returns:
Observation dict or None if not found
"""
await self._authenticate_tenant(request_context)
backend = await self._get_backend()
async with acquire_with_retry(backend) as conn:
row = await conn.fetchrow(
f"""
SELECT id, bank_id, text, proof_count, tags, source_memory_ids, created_at, updated_at
FROM {fq_table("memory_units")}
WHERE bank_id = $1 AND id = $2 AND fact_type = 'observation'
""",
bank_id,
observation_id,
)
if not row:
return None
result = self._row_to_observation_consolidated(row)
# Fetch source memories if requested and source_memory_ids exist
if include_source_memories and result.get("source_memory_ids"):
source_ids = [uuid.UUID(sid) if isinstance(sid, str) else sid for sid in result["source_memory_ids"]]
source_rows = await conn.fetch(
f"""
SELECT id, text, fact_type, context, occurred_start, mentioned_at
FROM {fq_table("memory_units")}
WHERE id = ANY($1::uuid[])
ORDER BY mentioned_at DESC NULLS LAST
""",
source_ids,
)
result["source_memories"] = [
{
"id": str(r["id"]),
"text": r["text"],
"type": r["fact_type"],
"context": r["context"],
"occurred_start": r["occurred_start"].isoformat() if r["occurred_start"] else None,
"mentioned_at": r["mentioned_at"].isoformat() if r["mentioned_at"] else None,
}
for r in source_rows
]
return result
def _row_to_observation_consolidated(self, row: Any) -> dict[str, Any]:
"""Convert a database row to an observation dict."""
# Convert source_memory_ids to strings
source_memory_ids = row.get("source_memory_ids") or []
source_memory_ids = [str(sid) for sid in source_memory_ids]
return {
"id": str(row["id"]),
"bank_id": row["bank_id"],
"text": row["text"],
"proof_count": row["proof_count"] or 1,
# Deprecated inline field — full history via GET .../{id}/history.
"history": [],
"tags": row["tags"] or [],
"source_memory_ids": source_memory_ids,
"source_memories": [], # Populated separately when fetching full details
"created_at": row["created_at"].isoformat() if row["created_at"] else None,
"updated_at": row["updated_at"].isoformat() if row["updated_at"] else None,
}
# =========================================================================
# MENTAL MODELS CRUD
# =========================================================================
@@ -11139,10 +10895,6 @@ class MemoryEngine(MemoryEngineInterface):
# Parent operations have their status updated when all children complete/fail
operation_list = []
for row in operations:
# Map DB status to API status (pending includes processing)
db_status = row["status"]
api_status = "pending" if db_status in ("pending", "processing") else db_status
result_metadata = conn.parse_json(row["result_metadata"]) or {}
next_retry_at = row["next_retry_at"]
@@ -11237,7 +10989,6 @@ class MemoryEngine(MemoryEngineInterface):
child_statuses = []
all_done = True
any_failed = False
all_completed = True
for child_row in child_rows:
raw_crm = child_row["result_metadata"]
@@ -11258,9 +11009,6 @@ class MemoryEngine(MemoryEngineInterface):
all_done = False
if child_status == "failed":
any_failed = True
if child_status != "completed":
all_completed = False
# Self-healing: if parent status is out of sync with children, update it
if all_done and api_status == "pending":
correct_status = "failed" if any_failed else "completed"
@@ -14,7 +14,7 @@ import logging
import time
from typing import Any
from hindsight_api.engine.llm_interface import LLMInterface, OutputTooLongError
from hindsight_api.engine.llm_interface import LLMInterface
from hindsight_api.engine.response_models import LLMToolCall, LLMToolCallResult, TokenUsage
from hindsight_api.metrics import get_metrics_collector
@@ -15,7 +15,7 @@ from typing import Any
from pydantic import ValidationError
from hindsight_api.engine.llm_interface import LLMInterface, OutputTooLongError
from hindsight_api.engine.llm_interface import LLMInterface
from hindsight_api.engine.response_models import LLMToolCall, LLMToolCallResult, TokenUsage
from hindsight_api.metrics import get_metrics_collector
@@ -24,7 +24,7 @@ from typing import Any
import httpx
from hindsight_api.engine.llm_interface import LLMInterface, OutputTooLongError
from hindsight_api.engine.llm_interface import LLMInterface
from hindsight_api.engine.response_models import LLMToolCall, LLMToolCallResult, TokenUsage
from hindsight_api.metrics import get_metrics_collector
@@ -397,7 +397,6 @@ class CodexLLM(LLMInterface):
}
url = f"{self.base_url}/codex/responses"
last_exception = None
# Manual attempt tracking instead of ``for attempt in range(...)`` so
# that the reactive-refresh path can retry once without consuming a
@@ -428,7 +427,6 @@ class CodexLLM(LLMInterface):
if attempt < max_retries:
backoff = min(initial_backoff * (2**attempt), max_backoff)
await asyncio.sleep(backoff)
last_exception = e
attempt += 1
continue
raise
@@ -490,7 +488,6 @@ class CodexLLM(LLMInterface):
return result
except httpx.HTTPStatusError as e:
last_exception = e
status_code = e.response.status_code
# Auth error: try one OAuth refresh + retry before giving up.
@@ -549,7 +546,6 @@ class CodexLLM(LLMInterface):
raise
except httpx.RequestError as e:
last_exception = e
if attempt < max_retries:
backoff = min(initial_backoff * (2**attempt), max_backoff)
logger.warning(f"Codex connection error (attempt {attempt + 1}/{max_retries + 1}): {e}")
@@ -564,10 +560,6 @@ class CodexLLM(LLMInterface):
logger.error(f"Unexpected Codex error: {type(e).__name__}: {e}")
raise
if last_exception:
raise last_exception
raise RuntimeError("Codex call failed after all retries")
async def _parse_sse_stream(self, response: httpx.Response) -> str:
"""
Parse Server-Sent Events (SSE) stream from Codex API.
@@ -11,7 +11,6 @@ import base64
import io
import json
import logging
import os
import time
from contextvars import ContextVar
from typing import Any
@@ -20,7 +19,7 @@ from google import genai
from google.genai import errors as genai_errors
from google.genai import types as genai_types
from hindsight_api.engine.llm_interface import LLMInterface, OutputTooLongError
from hindsight_api.engine.llm_interface import LLMInterface
from hindsight_api.engine.llm_wrapper import parse_llm_json
from hindsight_api.engine.response_models import LLMToolCall, LLMToolCallResult, TokenUsage
from hindsight_api.metrics import get_metrics_collector
@@ -36,7 +35,6 @@ _safety_settings_ctx: ContextVar[list | None] = ContextVar("gemini_safety_settin
# Vertex AI imports (optional)
try:
import google.auth
from google.oauth2 import service_account
VERTEXAI_AVAILABLE = True
@@ -4,7 +4,6 @@ bank profile utilities for disposition and mission management.
import json
import logging
import re
import uuid
from dataclasses import dataclass
from typing import TypedDict
@@ -14,7 +14,6 @@ from typing import Any, Literal, cast
from pydantic import BaseModel, ConfigDict, Field, create_model, field_validator
from ...config import get_config
from ..llm_wrapper import LLMConfig, OutputTooLongError, sanitize_llm_output
from ..operation_metadata import RetainExtractionErrors
from ..response_models import TokenUsage
@@ -1881,8 +1880,7 @@ async def extract_facts_from_contents_batch_api(
logger.info(f"Using Batch API for fact extraction ({len(contents)} contents)")
# Check config for extraction mode and causal link extraction (used throughout)
extraction_mode = config.retain_extraction_mode
# Check config for causal link extraction (used throughout)
extract_causal_links = config.retain_extract_causal_links
# Check if provider supports batch API
@@ -800,8 +800,6 @@ async def create_causal_links_batch(
try:
import time as time_mod
create_start = time_mod.time()
# Build links list
links = []
for fact_idx, causal_relations in enumerate(causal_relations_per_fact):
@@ -23,7 +23,6 @@ from ...extensions.memory_defense import (
parse_policy,
)
from ...worker.stage import set_stage
from ..db.base import DatabaseBackend
from ..db_utils import acquire_with_retry
from ..memory_engine import count_tokens, fq_table
from . import bank_utils
@@ -2,8 +2,6 @@
Helper functions for hybrid search (semantic + BM25 + graph).
"""
from typing import Any
from .types import MergedCandidate, RetrievalResult
@@ -156,39 +154,3 @@ def interleave_fusion(result_lists: list[list[RetrievalResult]]) -> list[MergedC
)
for pos, doc_id in enumerate(ordered_ids)
]
def normalize_scores_on_deltas(results: list[dict[str, Any]], score_keys: list[str]) -> list[dict[str, Any]]:
"""
Normalize scores based on deltas (min-max normalization within result set).
This ensures all scores are in [0, 1] range based on the spread in THIS result set.
Args:
results: List of result dicts
score_keys: Keys to normalize (e.g., ["recency", "frequency"])
Returns:
Results with normalized scores added as "{key}_normalized"
"""
for key in score_keys:
values = [r.get(key, 0.0) for r in results if key in r]
if not values:
continue
min_val = min(values)
max_val = max(values)
delta = max_val - min_val
if delta > 0:
for r in results:
if key in r:
r[f"{key}_normalized"] = (r[key] - min_val) / delta
else:
# All values are the same, set to 0.5
for r in results:
if key in r:
r[f"{key}_normalized"] = 0.5
return results
@@ -13,7 +13,7 @@ import logging
import re
from dataclasses import dataclass, field
from datetime import UTC, datetime
from typing import Any, Optional
from typing import TYPE_CHECKING, Any, Optional
from ...config import get_config
from ..db_utils import acquire_with_retry
@@ -24,6 +24,9 @@ from .link_expansion_retrieval import LinkExpansionRetriever
from .tags import TagGroup, TagsMatch, build_tag_groups_where_clause, build_tags_where_clause_simple
from .types import GraphRetrievalTimings, RetrievalResult
if TYPE_CHECKING:
from ..query_analyzer import QueryAnalyzer
logger = logging.getLogger(__name__)
@@ -2,7 +2,7 @@
import logging
import os
from datetime import datetime, timedelta, timezone
from datetime import timedelta, timezone
import obstore as obs
from obstore.store import GCSStore
@@ -3,7 +3,7 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING
from hindsight_api.extensions.base import Extension
@@ -16,7 +16,7 @@ import time
import traceback
from collections import Counter
from collections.abc import Awaitable, Callable, Iterable
from dataclasses import dataclass, field
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
from ..engine.schema import fq_table_explicit as fq_table
@@ -824,7 +824,6 @@ class WorkerPoller:
recovered = 0
for row in rows:
operation_id = str(row["operation_id"])
task_payload = row["task_payload"]
result_metadata = row["result_metadata"]
# Parse metadata
@@ -838,12 +837,6 @@ class WorkerPoller:
f"Recovering batch operation: operation_id={operation_id}, batch_id={batch_id}, provider={batch_provider}"
)
# Parse task_payload
if isinstance(task_payload, str):
task_dict = json.loads(task_payload)
else:
task_dict = task_payload
# Mark operation as ready for re-processing
# Reset to pending with task_payload intact so worker picks it up again
async with self._backend.acquire() as conn:
+7 -7
View File
@@ -55,7 +55,7 @@ paths:
/v1/default/banks/{bank_id}/graph:
get:
description: "Retrieve graph data for visualization, optionally filtered by\
\ type (world/experience/opinion)."
\ type (world/experience/observation)."
operationId: get_graph
parameters:
- explode: false
@@ -442,12 +442,12 @@ paths:
/v1/default/banks/{bank_id}/reflect:
post:
description: |-
Reflect and formulate an answer using bank identity, world facts, and opinions.
Reflect and formulate an answer using bank identity, world facts, observations, and mental models.
This endpoint:
1. Retrieves experience (conversations and events)
2. Retrieves world facts relevant to the query
3. Retrieves existing opinions (bank's perspectives)
3. Retrieves observations and mental models (bank's synthesized perspectives)
4. Uses LLM to formulate a contextual answer
5. Returns plain text answer and the facts used
operationId: reflect
@@ -3331,9 +3331,9 @@ paths:
/v1/default/banks/{bank_id}/memories:
delete:
description: "Delete memory units for a memory bank. Optionally filter by type\
\ (world, experience, opinion) to delete only specific types. This is a destructive\
\ operation that cannot be undone. The bank profile (disposition and background)\
\ will be preserved."
\ (world, experience, observation) to delete only specific types. This is\
\ a destructive operation that cannot be undone. The bank profile (disposition\
\ and background) will be preserved."
operationId: clear_bank_memories
parameters:
- explode: false
@@ -3344,7 +3344,7 @@ paths:
title: Bank Id
type: string
style: simple
- description: "Optional fact type filter (world, experience, opinion)"
- description: "Optional fact type filter (world, experience, observation)"
explode: true
in: query
name: type
+5 -5
View File
@@ -32,7 +32,7 @@ type ApiClearBankMemoriesRequest struct {
authorization *string
}
// Optional fact type filter (world, experience, opinion)
// Optional fact type filter (world, experience, observation)
func (r ApiClearBankMemoriesRequest) Type_(type_ string) ApiClearBankMemoriesRequest {
r.type_ = &type_
return r
@@ -50,7 +50,7 @@ func (r ApiClearBankMemoriesRequest) Execute() (*DeleteResponse, *http.Response,
/*
ClearBankMemories Clear memory bank memories
Delete memory units for a memory bank. Optionally filter by type (world, experience, opinion) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.
Delete memory units for a memory bank. Optionally filter by type (world, experience, observation) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@@ -343,7 +343,7 @@ func (r ApiGetGraphRequest) Execute() (*GraphDataResponse, *http.Response, error
/*
GetGraph Get memory graph data
Retrieve graph data for visualization, optionally filtered by type (world/experience/opinion).
Retrieve graph data for visualization, optionally filtered by type (world/experience/observation).
@param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background().
@param bankId
@@ -1257,12 +1257,12 @@ func (r ApiReflectRequest) Execute() (*ReflectResponse, *http.Response, error) {
/*
Reflect Reflect and generate answer
Reflect and formulate an answer using bank identity, world facts, and opinions.
Reflect and formulate an answer using bank identity, world facts, observations, and mental models.
This endpoint:
1. Retrieves experience (conversations and events)
2. Retrieves world facts relevant to the query
3. Retrieves existing opinions (bank's perspectives)
3. Retrieves observations and mental models (bank's synthesized perspectives)
4. Uses LLM to formulate a contextual answer
5. Returns plain text answer and the facts used
@@ -44,7 +44,6 @@ from hindsight_client_api.models.bank_profile_response import BankProfileRespons
from hindsight_client_api.models.file_retain_response import FileRetainResponse
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse
from hindsight_client_api.models.recall_response import RecallResponse
from hindsight_client_api.models.recall_result import RecallResult
from hindsight_client_api.models.reflect_response import ReflectResponse
from hindsight_client_api.models.retain_response import RetainResponse
@@ -54,7 +54,7 @@ class MemoryApi:
async def clear_bank_memories(
self,
bank_id: StrictStr,
type: Annotated[Optional[StrictStr], Field(description="Optional fact type filter (world, experience, opinion)")] = None,
type: Annotated[Optional[StrictStr], Field(description="Optional fact type filter (world, experience, observation)")] = None,
authorization: Optional[StrictStr] = None,
_request_timeout: Union[
None,
@@ -71,11 +71,11 @@ class MemoryApi:
) -> DeleteResponse:
"""Clear memory bank memories
Delete memory units for a memory bank. Optionally filter by type (world, experience, opinion) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.
Delete memory units for a memory bank. Optionally filter by type (world, experience, observation) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.
:param bank_id: (required)
:type bank_id: str
:param type: Optional fact type filter (world, experience, opinion)
:param type: Optional fact type filter (world, experience, observation)
:type type: str
:param authorization:
:type authorization: str
@@ -130,7 +130,7 @@ class MemoryApi:
async def clear_bank_memories_with_http_info(
self,
bank_id: StrictStr,
type: Annotated[Optional[StrictStr], Field(description="Optional fact type filter (world, experience, opinion)")] = None,
type: Annotated[Optional[StrictStr], Field(description="Optional fact type filter (world, experience, observation)")] = None,
authorization: Optional[StrictStr] = None,
_request_timeout: Union[
None,
@@ -147,11 +147,11 @@ class MemoryApi:
) -> ApiResponse[DeleteResponse]:
"""Clear memory bank memories
Delete memory units for a memory bank. Optionally filter by type (world, experience, opinion) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.
Delete memory units for a memory bank. Optionally filter by type (world, experience, observation) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.
:param bank_id: (required)
:type bank_id: str
:param type: Optional fact type filter (world, experience, opinion)
:param type: Optional fact type filter (world, experience, observation)
:type type: str
:param authorization:
:type authorization: str
@@ -206,7 +206,7 @@ class MemoryApi:
async def clear_bank_memories_without_preload_content(
self,
bank_id: StrictStr,
type: Annotated[Optional[StrictStr], Field(description="Optional fact type filter (world, experience, opinion)")] = None,
type: Annotated[Optional[StrictStr], Field(description="Optional fact type filter (world, experience, observation)")] = None,
authorization: Optional[StrictStr] = None,
_request_timeout: Union[
None,
@@ -223,11 +223,11 @@ class MemoryApi:
) -> RESTResponseType:
"""Clear memory bank memories
Delete memory units for a memory bank. Optionally filter by type (world, experience, opinion) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.
Delete memory units for a memory bank. Optionally filter by type (world, experience, observation) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.
:param bank_id: (required)
:type bank_id: str
:param type: Optional fact type filter (world, experience, opinion)
:param type: Optional fact type filter (world, experience, observation)
:type type: str
:param authorization:
:type authorization: str
@@ -665,7 +665,7 @@ class MemoryApi:
) -> GraphDataResponse:
"""Get memory graph data
Retrieve graph data for visualization, optionally filtered by type (world/experience/opinion).
Retrieve graph data for visualization, optionally filtered by type (world/experience/observation).
:param bank_id: (required)
:type bank_id: str
@@ -765,7 +765,7 @@ class MemoryApi:
) -> ApiResponse[GraphDataResponse]:
"""Get memory graph data
Retrieve graph data for visualization, optionally filtered by type (world/experience/opinion).
Retrieve graph data for visualization, optionally filtered by type (world/experience/observation).
:param bank_id: (required)
:type bank_id: str
@@ -865,7 +865,7 @@ class MemoryApi:
) -> RESTResponseType:
"""Get memory graph data
Retrieve graph data for visualization, optionally filtered by type (world/experience/opinion).
Retrieve graph data for visualization, optionally filtered by type (world/experience/observation).
:param bank_id: (required)
:type bank_id: str
@@ -2692,7 +2692,7 @@ class MemoryApi:
) -> ReflectResponse:
"""Reflect and generate answer
Reflect and formulate an answer using bank identity, world facts, and opinions. This endpoint: 1. Retrieves experience (conversations and events) 2. Retrieves world facts relevant to the query 3. Retrieves existing opinions (bank's perspectives) 4. Uses LLM to formulate a contextual answer 5. Returns plain text answer and the facts used
Reflect and formulate an answer using bank identity, world facts, observations, and mental models. This endpoint: 1. Retrieves experience (conversations and events) 2. Retrieves world facts relevant to the query 3. Retrieves observations and mental models (bank's synthesized perspectives) 4. Uses LLM to formulate a contextual answer 5. Returns plain text answer and the facts used
:param bank_id: (required)
:type bank_id: str
@@ -2768,7 +2768,7 @@ class MemoryApi:
) -> ApiResponse[ReflectResponse]:
"""Reflect and generate answer
Reflect and formulate an answer using bank identity, world facts, and opinions. This endpoint: 1. Retrieves experience (conversations and events) 2. Retrieves world facts relevant to the query 3. Retrieves existing opinions (bank's perspectives) 4. Uses LLM to formulate a contextual answer 5. Returns plain text answer and the facts used
Reflect and formulate an answer using bank identity, world facts, observations, and mental models. This endpoint: 1. Retrieves experience (conversations and events) 2. Retrieves world facts relevant to the query 3. Retrieves observations and mental models (bank's synthesized perspectives) 4. Uses LLM to formulate a contextual answer 5. Returns plain text answer and the facts used
:param bank_id: (required)
:type bank_id: str
@@ -2844,7 +2844,7 @@ class MemoryApi:
) -> RESTResponseType:
"""Reflect and generate answer
Reflect and formulate an answer using bank identity, world facts, and opinions. This endpoint: 1. Retrieves experience (conversations and events) 2. Retrieves world facts relevant to the query 3. Retrieves existing opinions (bank's perspectives) 4. Uses LLM to formulate a contextual answer 5. Returns plain text answer and the facts used
Reflect and formulate an answer using bank identity, world facts, observations, and mental models. This endpoint: 1. Retrieves experience (conversations and events) 2. Retrieves world facts relevant to the query 3. Retrieves observations and mental models (bank's synthesized perspectives) 4. Uses LLM to formulate a contextual answer 5. Returns plain text answer and the facts used
:param bank_id: (required)
:type bank_id: str
@@ -284,7 +284,7 @@ export const metricsEndpointMetricsGet = <ThrowOnError extends boolean = false>(
/**
* Get memory graph data
*
* Retrieve graph data for visualization, optionally filtered by type (world/experience/opinion).
* Retrieve graph data for visualization, optionally filtered by type (world/experience/observation).
*/
export const getGraph = <ThrowOnError extends boolean = false>(
options: Options<GetGraphData, ThrowOnError>
@@ -375,12 +375,12 @@ export const recallMemories = <ThrowOnError extends boolean = false>(
/**
* Reflect and generate answer
*
* Reflect and formulate an answer using bank identity, world facts, and opinions.
* Reflect and formulate an answer using bank identity, world facts, observations, and mental models.
*
* This endpoint:
* 1. Retrieves experience (conversations and events)
* 2. Retrieves world facts relevant to the query
* 3. Retrieves existing opinions (bank's perspectives)
* 3. Retrieves observations and mental models (bank's synthesized perspectives)
* 4. Uses LLM to formulate a contextual answer
* 5. Returns plain text answer and the facts used
*/
@@ -1243,7 +1243,7 @@ export const listWebhookDeliveries = <ThrowOnError extends boolean = false>(
/**
* Clear memory bank memories
*
* Delete memory units for a memory bank. Optionally filter by type (world, experience, opinion) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.
* Delete memory units for a memory bank. Optionally filter by type (world, experience, observation) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.
*/
export const clearBankMemories = <ThrowOnError extends boolean = false>(
options: Options<ClearBankMemoriesData, ThrowOnError>
@@ -6603,7 +6603,7 @@ export type ClearBankMemoriesData = {
/**
* Type
*
* Optional fact type filter (world, experience, opinion)
* Optional fact type filter (world, experience, observation)
*/
type?: string | null;
};
-1
View File
@@ -37,7 +37,6 @@
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
@@ -1425,7 +1425,6 @@ function MapFieldsEditor({
};
const isRoot = depth === 0;
const indent = `${(depth + 1) * 12}px`;
return (
<div
@@ -7,20 +7,11 @@ import remarkGfm from "remark-gfm";
import { useRouter } from "next/navigation";
import { client } from "@/lib/api";
import { useBank } from "@/lib/bank-context";
import { useFeatures } from "@/lib/features-context";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Dialog,
DialogContent,
@@ -39,24 +30,10 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
RefreshCw,
Save,
Brain,
Clock,
AlertCircle,
CheckCircle,
Database,
Link2,
FolderOpen,
Activity,
Trash2,
Target,
AlertTriangle,
@@ -64,7 +41,6 @@ import {
Tag,
Loader2,
X,
MoreVertical,
Pencil,
} from "lucide-react";
@@ -104,16 +80,6 @@ interface BankStats {
total_observations: number;
}
interface Operation {
id: string;
task_type: string;
items_count: number;
document_id: string | null;
created_at: string;
status: string;
error_message: string | null;
}
interface Directive {
id: string;
bank_id: string;
@@ -168,14 +134,9 @@ export function BankProfileView({ hideReflectFields = false }: { hideReflectFiel
const tBank = useTranslations("bank");
const traitLabels = useTraitLabels();
const { currentBank, setCurrentBank, loadBanks } = useBank();
const { features } = useFeatures();
const observationsEnabled = features?.observations ?? false;
const [profile, setProfile] = useState<BankProfile | null>(null);
const [stats, setStats] = useState<BankStats | null>(null);
const [operations, setOperations] = useState<Operation[]>([]);
const [totalOperations, setTotalOperations] = useState(0);
const [directives, setDirectives] = useState<Directive[]>([]);
const [mentalModelsCount, setMentalModelsCount] = useState(0);
const [loading, setLoading] = useState(false);
const [showDispositionDialog, setShowDispositionDialog] = useState(false);
const [showMissionDialog, setShowMissionDialog] = useState(false);
@@ -197,49 +158,19 @@ export function BankProfileView({ hideReflectFields = false }: { hideReflectFiel
const [showClearObservationsDialog, setShowClearObservationsDialog] = useState(false);
const [isClearingObservations, setIsClearingObservations] = useState(false);
// Consolidation state
const [isConsolidating, setIsConsolidating] = useState(false);
// Operations filter/pagination state
const [opsStatusFilter, setOpsStatusFilter] = useState<string | null>(null);
const [opsLimit] = useState(10);
const [opsOffset, setOpsOffset] = useState(0);
const [cancellingOpId, setCancellingOpId] = useState<string | null>(null);
const loadOperations = async (
statusFilter: string | null = opsStatusFilter,
offset: number = opsOffset
) => {
if (!currentBank) return;
try {
const opsData = await client.listOperations(currentBank, {
status: statusFilter || undefined,
limit: opsLimit,
offset,
});
setOperations(opsData.operations || []);
setTotalOperations(opsData.total || 0);
} catch (error) {
console.error("Error loading operations:", error);
}
};
const loadData = async (isPolling = false) => {
if (!currentBank) return;
// During polling, only refresh stats (not operations to avoid interfering with filters)
// During polling, only refresh read-only profile data to avoid overwriting form edits.
// Use ref to get current value (avoids stale closure in setInterval)
if (isPolling) {
try {
const [statsData, directivesData, mentalModelsData] = await Promise.all([
const [statsData, directivesData] = await Promise.all([
client.getBankStats(currentBank),
client.listDirectives(currentBank),
client.listMentalModels(currentBank),
]);
setStats(statsData as BankStats);
setDirectives(directivesData.items || []);
setMentalModelsCount(mentalModelsData.items?.length || 0);
// Skip operations refresh during polling to not interfere with filter/pagination state
} catch (error) {
console.error("Error refreshing stats:", error);
}
@@ -248,17 +179,14 @@ export function BankProfileView({ hideReflectFields = false }: { hideReflectFiel
setLoading(true);
try {
const [profileData, statsData, directivesData, mentalModelsData] = await Promise.all([
const [profileData, statsData, directivesData] = await Promise.all([
client.getBankProfile(currentBank),
client.getBankStats(currentBank),
client.listDirectives(currentBank),
client.listMentalModels(currentBank),
]);
setProfile(profileData);
setStats(statsData as BankStats);
setDirectives(directivesData.items || []);
setMentalModelsCount(mentalModelsData.items?.length || 0);
await loadOperations();
} catch (error) {
// Error toast is shown automatically by the API client interceptor
} finally {
@@ -301,47 +229,6 @@ export function BankProfileView({ hideReflectFields = false }: { hideReflectFiel
}
};
const handleTriggerConsolidation = async () => {
if (!currentBank) return;
setIsConsolidating(true);
try {
await client.triggerConsolidation(currentBank);
// Reload to show the new operation in the list
await loadData();
await loadOperations();
} catch (error) {
// Error toast is shown automatically by the API client interceptor
} finally {
setIsConsolidating(false);
}
};
const handleOpsFilterChange = (newFilter: string | null) => {
setOpsStatusFilter(newFilter);
setOpsOffset(0); // Reset to first page when filter changes
loadOperations(newFilter, 0);
};
const handleOpsPageChange = (newOffset: number) => {
setOpsOffset(newOffset);
loadOperations(opsStatusFilter, newOffset);
};
const handleCancelOperation = async (operationId: string) => {
if (!currentBank) return;
setCancellingOpId(operationId);
try {
await client.cancelOperation(currentBank, operationId);
await loadOperations();
} catch (error) {
// Error toast is shown automatically by the API client interceptor
} finally {
setCancellingOpId(null);
}
};
const handleDeleteDirective = async () => {
if (!currentBank || !directiveDeleteTarget) return;
@@ -361,7 +248,7 @@ export function BankProfileView({ hideReflectFields = false }: { hideReflectFiel
useEffect(() => {
if (currentBank) {
loadData();
// Refresh stats/operations every 5 seconds (isPolling=true to avoid overwriting form)
// Refresh read-only profile data every 5 seconds without overwriting form state.
const interval = setInterval(() => loadData(true), 5000);
return () => clearInterval(interval);
}
@@ -188,39 +188,6 @@ function ChartTooltip({ active, payload, label, valueLabel }: ChartTooltipProps)
);
}
function HeroCard({
icon: Icon,
label,
value,
pulse,
}: {
icon: typeof Database;
label: string;
value: number;
pulse?: boolean;
}) {
return (
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className="p-2 rounded-md bg-muted">
<Icon
className={`w-4 h-4 ${pulse ? "animate-pulse text-amber-500" : "text-muted-foreground"}`}
/>
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-muted-foreground font-medium">{label}</p>
<CompactNumber
value={value}
className="text-2xl font-semibold text-foreground leading-tight tabular-nums block"
/>
</div>
</div>
</CardContent>
</Card>
);
}
function SectionHeading({ children }: { children: React.ReactNode }) {
return (
<h3 className="text-[11px] font-semibold text-muted-foreground uppercase tracking-[0.08em] mb-3">
@@ -23,12 +23,6 @@ interface PreparedNode {
linkCount: number;
}
interface ScreenNode {
idx: number;
sx: number;
sy: number;
}
// ============================================================================
// Props
// ============================================================================
@@ -82,19 +76,6 @@ function lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}
function easeOutCubic(t: number): number {
return 1 - Math.pow(1 - t, 3);
}
function hexToRgba(hex: string, alpha: number): string {
// Handle non-hex formats
if (!hex.startsWith("#")) return hex;
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return `rgba(${r},${g},${b},${alpha})`;
}
/**
* Map a 0..1 value to a perceptually monotonic coolwarm ramp.
* 0 = cool blue (low / older / few links), 1 = warm orange-red (high / newer / many).
@@ -163,7 +144,6 @@ function useIsDarkMode() {
// Constants
// ============================================================================
const FONT = '12px Inter, -apple-system, "Segoe UI", sans-serif';
const FONT_SMALL = '11px Inter, -apple-system, "Segoe UI", sans-serif';
const FONT_BOLD = '600 10px Inter, -apple-system, "Segoe UI", sans-serif';
const MONO = '11px "SF Mono", "Fira Code", Consolas, monospace';
@@ -1,38 +0,0 @@
"use client";
import * as React from "react";
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
import { Circle } from "lucide-react";
import { cn } from "@/lib/utils";
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return <RadioGroupPrimitive.Root className={cn("grid gap-2", className)} {...props} ref={ref} />;
});
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-current text-current" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
);
});
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
export { RadioGroup, RadioGroupItem };
@@ -1,122 +0,0 @@
"use client";
import * as React from "react";
import * as SheetPrimitive from "@radix-ui/react-dialog";
import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
const Sheet = SheetPrimitive.Root;
const SheetTrigger = SheetPrimitive.Trigger;
const SheetClose = SheetPrimitive.Close;
const SheetPortal = SheetPrimitive.Portal;
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
));
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName;
const sheetVariants = cva(
"fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
{
variants: {
side: {
top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
bottom:
"inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
right:
"inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
},
},
defaultVariants: {
side: "right",
},
}
);
interface SheetContentProps
extends
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
SheetContentProps
>(({ side = "right", className, children, ...props }, ref) => (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Content ref={ref} className={cn(sheetVariants({ side }), className)} {...props}>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
));
SheetContent.displayName = SheetPrimitive.Content.displayName;
const SheetHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div className={cn("flex flex-col space-y-2 text-center sm:text-left", className)} {...props} />
);
SheetHeader.displayName = "SheetHeader";
const SheetFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)}
{...props}
/>
);
SheetFooter.displayName = "SheetFooter";
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
));
SheetTitle.displayName = SheetPrimitive.Title.displayName;
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
SheetDescription.displayName = SheetPrimitive.Description.displayName;
export {
Sheet,
SheetPortal,
SheetOverlay,
SheetTrigger,
SheetClose,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
};
@@ -1,32 +0,0 @@
"use client";
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
const TooltipProvider = TooltipPrimitive.Provider;
const Tooltip = TooltipPrimitive.Root;
const TooltipTrigger = TooltipPrimitive.Trigger;
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</TooltipPrimitive.Portal>
));
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
@@ -1,47 +0,0 @@
"use client";
import React, { createContext, useContext, useState, useEffect } from "react";
import { client } from "./api";
interface AgentContextType {
currentAgent: string | null;
setCurrentAgent: (agent: string | null) => void;
agents: string[];
loadAgents: () => Promise<void>;
}
const AgentContext = createContext<AgentContextType | undefined>(undefined);
export function AgentProvider({ children }: { children: React.ReactNode }) {
const [currentAgent, setCurrentAgent] = useState<string | null>(null);
const [agents, setAgents] = useState<string[]>([]);
const loadAgents = async () => {
try {
const data = await client.listBanks();
// Extract bank_id from each bank object
const agentIds = data.banks?.map((agent: any) => agent.bank_id) || [];
setAgents(agentIds);
} catch (error) {
console.error("Error loading agents:", error);
}
};
useEffect(() => {
loadAgents();
}, []);
return (
<AgentContext.Provider value={{ currentAgent, setCurrentAgent, agents, loadAgents }}>
{children}
</AgentContext.Provider>
);
}
export function useAgent() {
const context = useContext(AgentContext);
if (context === undefined) {
throw new Error("useAgent must be used within an AgentProvider");
}
return context;
}
@@ -6,7 +6,6 @@ This script imports the FastAPI app and exports its OpenAPI schema to a JSON fil
"""
import json
import os
import sys
from pathlib import Path
@@ -17,7 +17,6 @@ Conventions in cookbook repo:
"""
import json
import os
import re
import shutil
import subprocess
-27
View File
@@ -246,33 +246,6 @@ response = client.recall(
# [/docs:recall-tags-all-strict]
# =============================================================================
# Legacy snippets for v0.3 docs (kept for backward compatibility)
# =============================================================================
# [docs:recall-opinions-only]
# Legacy: opinions replaced by observations in v0.4+
# Only retrieve opinions (beliefs and preferences)
opinions = client.recall(
bank_id="my-bank",
query="What are my preferences?",
types=["opinion"]
)
# [/docs:recall-opinions-only]
# [docs:recall-include-entities]
# Legacy: entity summaries replaced by observations in v0.4+
# Include entity summaries in recall results
response = client.recall(
bank_id="my-bank",
query="What do I know about Alice?",
include_entities=True,
max_entity_tokens=500
)
# [/docs:recall-include-entities]
# =============================================================================
# Cleanup (not shown in docs)
# =============================================================================
@@ -1,162 +0,0 @@
import React, { useState, useCallback } from 'react';
import styles from './styles.module.css';
export default function CopyPageButton(): JSX.Element | null {
const [copied, setCopied] = useState(false);
const copyPageAsMarkdown = useCallback(async () => {
try {
// Get the page content
const contentElement = document.querySelector('.markdown');
if (!contentElement) return;
// Convert HTML to markdown-like text
let markdown = '';
// Add title
const title = document.querySelector('h1')?.textContent;
if (title) {
markdown += `# ${title}\n\n`;
}
// Extract text content from the markdown container
const extractMarkdown = (element: Element): string => {
let text = '';
const processNode = (node: Node): string => {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent || '';
}
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as Element;
const tagName = el.tagName.toLowerCase();
const children = Array.from(el.childNodes).map(processNode).join('');
switch (tagName) {
case 'h1':
return `# ${children}\n\n`;
case 'h2':
return `## ${children}\n\n`;
case 'h3':
return `### ${children}\n\n`;
case 'h4':
return `#### ${children}\n\n`;
case 'h5':
return `##### ${children}\n\n`;
case 'h6':
return `###### ${children}\n\n`;
case 'p':
return `${children}\n\n`;
case 'ul':
return `${children}\n`;
case 'ol':
return `${children}\n`;
case 'li':
const parent = el.parentElement;
const isOrdered = parent?.tagName.toLowerCase() === 'ol';
if (isOrdered) {
const index = Array.from(parent?.children || []).indexOf(el) + 1;
return `${index}. ${children}\n`;
}
return `- ${children}\n`;
case 'code':
const isBlock = el.parentElement?.tagName.toLowerCase() === 'pre';
if (isBlock) {
const lang = el.className.replace('language-', '');
return `\`\`\`${lang}\n${children}\n\`\`\`\n\n`;
}
return `\`${children}\``;
case 'pre':
return children; // Already handled by code block
case 'blockquote':
return children.split('\n').map(line => `> ${line}`).join('\n') + '\n\n';
case 'a':
const href = el.getAttribute('href') || '';
return `[${children}](${href})`;
case 'strong':
case 'b':
return `**${children}**`;
case 'em':
case 'i':
return `*${children}*`;
case 'br':
return '\n';
case 'hr':
return '---\n\n';
case 'table':
return `${children}\n`;
case 'thead':
case 'tbody':
return children;
case 'tr':
return `${children}|\n`;
case 'th':
case 'td':
return `| ${children} `;
case 'img':
const src = el.getAttribute('src') || '';
const alt = el.getAttribute('alt') || '';
return `![${alt}](${src})`;
default:
return children;
}
}
return '';
};
Array.from(element.childNodes).forEach(node => {
text += processNode(node);
});
return text;
};
// Skip the title h1 if it's already added
const contentToCopy = Array.from(contentElement.children)
.filter(child => !(child.tagName === 'H1' && child.textContent === title))
.map(child => extractMarkdown(child))
.join('');
markdown += contentToCopy;
// Clean up excessive newlines
markdown = markdown.replace(/\n{3,}/g, '\n\n').trim();
// Copy to clipboard
await navigator.clipboard.writeText(markdown);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error('Failed to copy page content:', error);
}
}, []);
return (
<button
className={`${styles.copyPageButton} ${copied ? styles.copied : ''}`}
onClick={copyPageAsMarkdown}
aria-label="Copy page as markdown"
title="Copy page as markdown"
>
{copied ? (
<>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M12.736 3.97a.733.733 0 0 1 1.047 0c.286.289.29.756.01 1.05L7.88 12.01a.733.733 0 0 1-1.065.02L3.217 8.384a.757.757 0 0 1 0-1.06.733.733 0 0 1 1.047 0l3.052 3.093 5.4-6.425a.247.247 0 0 1 .02-.022Z"/>
</svg>
<span className={styles.buttonText}>Copied!</span>
</>
) : (
<>
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M4 2a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V2zm2-1a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1V2a1 1 0 0 0-1-1H6z"/>
<path d="M2 5a1 1 0 0 0-1 1v8a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1v-1h1v1a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h1v1H2z"/>
</svg>
<span className={styles.buttonText}>Copy page</span>
</>
)}
</button>
);
}
@@ -1,55 +0,0 @@
.copyPageButton {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: transparent;
border: 1px solid var(--ifm-color-emphasis-300);
border-radius: 6px;
color: var(--ifm-font-color-base);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.copyPageButton:hover {
background-color: var(--ifm-color-emphasis-100);
border-color: var(--ifm-color-emphasis-400);
}
.copyPageButton:active {
transform: translateY(1px);
}
.copyPageButton.copied {
background-color: var(--ifm-color-success-contrast-background);
border-color: var(--ifm-color-success);
color: var(--ifm-color-success-darkest);
}
.copyPageButton.copied:hover {
background-color: var(--ifm-color-success-contrast-background);
border-color: var(--ifm-color-success);
}
.buttonText {
margin: 0 4px;
}
/* Dark mode adjustments */
[data-theme='dark'] .copyPageButton {
border-color: var(--ifm-color-emphasis-400);
}
[data-theme='dark'] .copyPageButton:hover {
background-color: var(--ifm-color-emphasis-200);
border-color: var(--ifm-color-emphasis-500);
}
[data-theme='dark'] .copyPageButton.copied {
background-color: var(--ifm-color-success-dark);
border-color: var(--ifm-color-success);
color: var(--ifm-color-success-contrast-foreground);
}
@@ -1,124 +0,0 @@
.toastContainer {
position: fixed;
bottom: 24px;
right: 24px;
z-index: 9999;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.3s ease, transform 0.3s ease;
pointer-events: none;
}
.toastContainer.show {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.toast {
background: linear-gradient(135deg, #0074d9 0%, #005db0 100%);
color: white;
border-radius: 12px;
padding: 16px 20px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(255, 255, 255, 0.1);
display: flex;
align-items: flex-start;
gap: 12px;
max-width: 420px;
min-width: 320px;
backdrop-filter: blur(10px);
}
.icon {
font-size: 24px;
flex-shrink: 0;
line-height: 1;
}
.content {
flex: 1;
display: flex;
flex-direction: column;
gap: 6px;
}
.title {
font-weight: 600;
font-size: 15px;
line-height: 1.4;
}
.message {
font-size: 13px;
line-height: 1.5;
opacity: 0.95;
}
.command {
background: rgba(0, 0, 0, 0.25);
padding: 8px 12px;
border-radius: 6px;
font-size: 12px;
font-family: 'JetBrains Mono', 'Fira Code', 'SF Mono', Monaco, Consolas, monospace;
display: block;
margin-top: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
color: #e6f7f8;
overflow-x: auto;
}
.closeButton {
background: none;
border: none;
color: white;
font-size: 24px;
line-height: 1;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s ease;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
border-radius: 4px;
}
.closeButton:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.1);
}
.closeButton:active {
background: rgba(255, 255, 255, 0.15);
}
/* Mobile responsive */
@media (max-width: 768px) {
.toastContainer {
bottom: 16px;
right: 16px;
left: 16px;
}
.toast {
min-width: unset;
max-width: unset;
width: 100%;
padding: 14px 16px;
}
.command {
font-size: 11px;
padding: 6px 10px;
}
}
/* Dark mode adjustments (if needed) */
html[data-theme='dark'] .toast {
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.15);
}
@@ -1,58 +0,0 @@
import React, { useState, useEffect } from 'react';
import styles from './SkillToast.module.css';
const STORAGE_KEY = 'hindsight-skill-toast-dismissed';
export default function SkillToast(): JSX.Element | null {
const [isVisible, setIsVisible] = useState(false);
const [isAnimating, setIsAnimating] = useState(false);
useEffect(() => {
// Check if user has already dismissed the toast
const dismissed = localStorage.getItem(STORAGE_KEY);
if (!dismissed) {
// Show toast after a short delay
const timer = setTimeout(() => {
setIsVisible(true);
setIsAnimating(true);
}, 1500);
return () => clearTimeout(timer);
}
}, []);
const handleDismiss = () => {
setIsAnimating(false);
setTimeout(() => {
setIsVisible(false);
localStorage.setItem(STORAGE_KEY, 'true');
}, 300); // Match animation duration
};
if (!isVisible) return null;
return (
<div className={`${styles.toastContainer} ${isAnimating ? styles.show : ''}`}>
<div className={styles.toast}>
<div className={styles.icon}>🤖</div>
<div className={styles.content}>
<div className={styles.title}>Building with a coding agent?</div>
<div className={styles.message}>
Install the Hindsight documentation skill for faster development:
</div>
<code className={styles.command}>
curl -fsSL https://hindsight.vectorize.io/get-skill | bash
</code>
</div>
<button
className={styles.closeButton}
onClick={handleDismiss}
aria-label="Dismiss notification"
>
×
</button>
</div>
</div>
);
}
+11 -11
View File
@@ -6,13 +6,13 @@ Hindsight is an agent memory system that gives AI agents persistent, structured
## Links
- [Full Documentation (llms-full.txt)](https://vectorize-io.github.io/hindsight/llms-full.txt): Complete documentation for LLM consumption
- [Quick Start](https://vectorize-io.github.io/hindsight/developer/api/quickstart): Get started in 60 seconds
- [Python SDK](https://vectorize-io.github.io/hindsight/sdks/python): Python client library
- [TypeScript SDK](https://vectorize-io.github.io/hindsight/sdks/nodejs): Node.js/TypeScript client
- [API Reference](https://vectorize-io.github.io/hindsight/api-reference): REST API documentation
- [OpenAPI Spec](https://vectorize-io.github.io/hindsight/openapi.json): Machine-readable API specification
- [MCP Server](https://vectorize-io.github.io/hindsight/sdks/mcp): Model Context Protocol integration
- [Full Documentation (llms-full.txt)](https://hindsight.vectorize.io/llms-full.txt): Complete documentation for LLM consumption
- [Quick Start](https://hindsight.vectorize.io/developer/api/quickstart): Get started in 60 seconds
- [Python SDK](https://hindsight.vectorize.io/sdks/python): Python client library
- [TypeScript SDK](https://hindsight.vectorize.io/sdks/nodejs): Node.js/TypeScript client
- [API Reference](https://hindsight.vectorize.io/api-reference): REST API documentation
- [OpenAPI Spec](https://hindsight.vectorize.io/openapi.json): Machine-readable API specification
- [MCP Server](https://hindsight.vectorize.io/sdks/mcp): Model Context Protocol integration
- [GitHub](https://github.com/vectorize-io/hindsight): Source code and issues
## Core Operations
@@ -69,8 +69,8 @@ Base URL: `http://localhost:8888`
| Method | Endpoint | Description |
|--------|----------|-------------|
| POST | `/v1/default/banks/{bank_id}/retain` | Store memories |
| POST | `/v1/default/banks/{bank_id}/recall` | Retrieve memories |
| POST | `/v1/default/banks/{bank_id}/memories` | Store memories |
| POST | `/v1/default/banks/{bank_id}/memories/recall` | Retrieve memories |
| POST | `/v1/default/banks/{bank_id}/reflect` | Analyze and form opinions |
| GET | `/v1/default/banks/{bank_id}/profile` | Get bank profile |
| PUT | `/v1/default/banks/{bank_id}/profile` | Update bank profile |
@@ -81,11 +81,11 @@ Base URL: `http://localhost:8888`
### Per-User Memory
One bank per user. Simplest pattern for chatbots and assistants.
[Guide](https://vectorize-io.github.io/hindsight/cookbook/per-user-memory)
[Guide](https://hindsight.vectorize.io/cookbook/per-user-memory)
### Support Agent + Shared Knowledge
User bank + shared docs bank. Client orchestrates queries to both banks and merges results.
[Guide](https://vectorize-io.github.io/hindsight/cookbook/support-agent-with-shared-knowledge)
[Guide](https://hindsight.vectorize.io/cookbook/support-agent-with-shared-knowledge)
## Installation
+5 -5
View File
@@ -81,7 +81,7 @@
"Memory"
],
"summary": "Get memory graph data",
"description": "Retrieve graph data for visualization, optionally filtered by type (world/experience/opinion).",
"description": "Retrieve graph data for visualization, optionally filtered by type (world/experience/observation).",
"operationId": "get_graph",
"parameters": [
{
@@ -677,7 +677,7 @@
"Memory"
],
"summary": "Reflect and generate answer",
"description": "Reflect and formulate an answer using bank identity, world facts, and opinions.\n\nThis endpoint:\n1. Retrieves experience (conversations and events)\n2. Retrieves world facts relevant to the query\n3. Retrieves existing opinions (bank's perspectives)\n4. Uses LLM to formulate a contextual answer\n5. Returns plain text answer and the facts used",
"description": "Reflect and formulate an answer using bank identity, world facts, observations, and mental models.\n\nThis endpoint:\n1. Retrieves experience (conversations and events)\n2. Retrieves world facts relevant to the query\n3. Retrieves observations and mental models (bank's synthesized perspectives)\n4. Uses LLM to formulate a contextual answer\n5. Returns plain text answer and the facts used",
"operationId": "reflect",
"parameters": [
{
@@ -4955,7 +4955,7 @@
"Memory"
],
"summary": "Clear memory bank memories",
"description": "Delete memory units for a memory bank. Optionally filter by type (world, experience, opinion) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.",
"description": "Delete memory units for a memory bank. Optionally filter by type (world, experience, observation) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.",
"operationId": "clear_bank_memories",
"parameters": [
{
@@ -4980,10 +4980,10 @@
"type": "null"
}
],
"description": "Optional fact type filter (world, experience, opinion)",
"description": "Optional fact type filter (world, experience, observation)",
"title": "Type"
},
"description": "Optional fact type filter (world, experience, opinion)"
"description": "Optional fact type filter (world, experience, observation)"
},
{
"name": "authorization",
@@ -24,7 +24,7 @@ from rich.panel import Panel
from rich.text import Text
from .embed_manager import EmbedManager
from .profile_manager import UI_PORT_OFFSET, ProfileManager, lock_file, resolve_active_profile, unlock_file
from .profile_manager import UI_PORT_OFFSET, ProfileManager, lock_file, unlock_file
logger = logging.getLogger(__name__)
console = Console(stderr=True)
@@ -614,7 +614,7 @@ class DaemonEmbedManager(EmbedManager):
console.print()
return False
except FileNotFoundError as e:
except FileNotFoundError:
error_msg = (
f"Command not found: {cmd[0]}\nFull command: {' '.join(cmd)}\n\n"
"Install hindsight-api with: pip install hindsight-api"
@@ -15,7 +15,6 @@ import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from lib.client import HindsightClient
from lib.config import debug_log, load_config
from lib.daemon import get_api_url, prestart_daemon_background
@@ -42,7 +41,6 @@ def main():
try:
api_url = get_api_url(config, debug_fn=_dbg, allow_daemon_start=False)
client = HindsightClient(api_url, config.get("hindsightApiToken"))
debug_log(config, f"Hindsight server reachable at {api_url}")
except (RuntimeError, ValueError) as e:
# Server not running — kick off background pre-start so it's ready
@@ -17,7 +17,7 @@ import urllib.request
from .client import USER_AGENT
from .llm import detect_llm_config, get_llm_env_vars
from .state import read_state, write_state
from .state import write_state
DAEMON_STATE_FILE = "daemon.json"
PROFILE_NAME = "codex"
@@ -9,7 +9,6 @@ import urllib.error
import urllib.parse
import urllib.request
from pathlib import Path
from typing import Optional
DEFAULT_TIMEOUT = 15
HEALTH_CHECK_RETRIES = 3
@@ -20,7 +20,7 @@ import urllib.request
from .client import USER_AGENT
from .llm import detect_llm_config, get_llm_env_vars
from .state import read_state, write_state
from .state import write_state
DAEMON_STATE_FILE = "daemon.json"
PROFILE_NAME = "cursor-cli"
@@ -2,7 +2,6 @@
import argparse
import json
import os
import shutil
import sys
from pathlib import Path
@@ -1519,7 +1519,6 @@ def hindsight_memory(
# Save previous state
was_enabled = is_enabled()
previous_config = get_config()
previous_defaults = get_defaults()
try:
# Configure and enable
@@ -8,7 +8,6 @@ Provides automatic memory for LlamaIndex agents:
import logging
import re
import time
import uuid
from typing import Any, Optional
@@ -5,7 +5,6 @@ LlamaIndex-compatible tools backed by Hindsight's retain/recall/reflect APIs.
"""
import logging
import time
import uuid
from typing import Any, Optional
@@ -2,7 +2,7 @@
import { existsSync, realpathSync } from "fs";
import { dirname, join, resolve } from "path";
import { createRequire } from "module";
import { fileURLToPath, pathToFileURL } from "url";
import { fileURLToPath } from "url";
import { HindsightServer } from "@vectorize-io/hindsight-all";
import { HindsightClient } from "@vectorize-io/hindsight-client";
@@ -12,7 +12,6 @@ import { RetainQueue } from "./retain-queue.js";
import { compileSessionPatterns, matchesSessionPattern } from "./session-patterns.js";
import { createHash } from "crypto";
import { dirname, join } from "path";
import { fileURLToPath } from "url";
import * as log from "./logger.js";
import { configureLogger, setApiLogger, stopLogger } from "./logger.js";
import { mkdirSync } from "fs";
@@ -458,10 +457,6 @@ if (typeof global !== "undefined") {
};
}
// Get directory of current module
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Default bank name (fallback when channel context not available)
const DEFAULT_BANK_NAME = "openclaw";
@@ -30,7 +30,6 @@ import {
applyApiMode,
applyCloudMode,
applyEmbeddedMode,
defaultApiKeyEnvVar,
ensurePluginConfig,
isValidEnvVarName,
loadConfig,
@@ -275,9 +274,6 @@ export async function runNonInteractive(
// Interactive (TUI) execution
// ---------------------------------------------------------------------------
const validateEnvVar = (value: string | undefined): string | undefined =>
isValidEnvVarName(value) ? undefined : "Must be an UPPER_SNAKE_CASE env var name";
const validateRequired =
(msg: string) =>
(value: string | undefined): string | undefined =>
@@ -14,7 +14,6 @@ import { Logger } from "./logger.js";
import {
formatMemories,
formatCurrentTime,
stripMemoryTags,
composeRecallQuery,
truncateRecallQuery,
prepareRetentionTranscript,
-33
View File
@@ -368,7 +368,6 @@
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slider": "^1.3.6",
"@radix-ui/react-slot": "^1.2.4",
@@ -8667,38 +8666,6 @@
}
}
},
"node_modules/@radix-ui/react-radio-group": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
"integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.11",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+1 -1
View File
@@ -10,7 +10,7 @@ BLUE='\033[0;34m'
DIM='\033[2m'
NC='\033[0m'
ALL_INTEGRATIONS=("litellm" "pydantic-ai" "crewai" "ag2" "ai-sdk" "chat" "openclaw" "langgraph" "llamaindex" "nemoclaw" "strands" "claude-code" "codex" "autogen" "paperclip" "opencode" "cloudflare-oauth-proxy")
ALL_INTEGRATIONS=("ag2" "agentcore" "agno" "ai-sdk" "autogen" "chat" "claude-agent-sdk" "claude-code" "cline" "cloudflare-oauth-proxy" "codex" "crewai" "cursor" "cursor-cli" "dify" "flowise" "gemini-spark" "google-adk" "haystack" "langgraph" "litellm" "llamaindex" "n8n" "nemoclaw" "obsidian" "omo" "openai-agents" "openclaw" "opencode" "paperclip" "pipecat" "pydantic-ai" "roo-code" "smolagents" "strands" "superagent" "vapi")
usage() {
echo "Usage: $0 [integration]"
+1 -1
View File
@@ -13,7 +13,7 @@ print_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
VALID_INTEGRATIONS=("litellm" "pydantic-ai" "crewai" "ag2" "ai-sdk" "chat" "openclaw" "langgraph" "llamaindex" "nemoclaw" "strands" "claude-code" "codex" "cline" "cursor-cli" "hermes" "autogen" "paperclip" "opencode" "cursor" "cloudflare-oauth-proxy" "openai-agents" "pipecat" "agentcore" "smolagents" "n8n" "dify" "gemini-spark" "vapi" "roo-code" "flowise" "google-adk" "haystack" "claude-agent-sdk" "superagent" "omo" "obsidian")
VALID_INTEGRATIONS=("ag2" "agentcore" "agno" "ai-sdk" "autogen" "chat" "claude-agent-sdk" "claude-code" "cline" "cloudflare-oauth-proxy" "codex" "crewai" "cursor" "cursor-cli" "dify" "flowise" "gemini-spark" "google-adk" "haystack" "langgraph" "litellm" "llamaindex" "n8n" "nemoclaw" "obsidian" "omo" "openai-agents" "openclaw" "opencode" "paperclip" "pipecat" "pydantic-ai" "roo-code" "smolagents" "strands" "superagent" "vapi")
usage() {
print_error "Usage: $0 <integration> <version>"
@@ -81,7 +81,7 @@
"Memory"
],
"summary": "Get memory graph data",
"description": "Retrieve graph data for visualization, optionally filtered by type (world/experience/opinion).",
"description": "Retrieve graph data for visualization, optionally filtered by type (world/experience/observation).",
"operationId": "get_graph",
"parameters": [
{
@@ -677,7 +677,7 @@
"Memory"
],
"summary": "Reflect and generate answer",
"description": "Reflect and formulate an answer using bank identity, world facts, and opinions.\n\nThis endpoint:\n1. Retrieves experience (conversations and events)\n2. Retrieves world facts relevant to the query\n3. Retrieves existing opinions (bank's perspectives)\n4. Uses LLM to formulate a contextual answer\n5. Returns plain text answer and the facts used",
"description": "Reflect and formulate an answer using bank identity, world facts, observations, and mental models.\n\nThis endpoint:\n1. Retrieves experience (conversations and events)\n2. Retrieves world facts relevant to the query\n3. Retrieves observations and mental models (bank's synthesized perspectives)\n4. Uses LLM to formulate a contextual answer\n5. Returns plain text answer and the facts used",
"operationId": "reflect",
"parameters": [
{
@@ -4955,7 +4955,7 @@
"Memory"
],
"summary": "Clear memory bank memories",
"description": "Delete memory units for a memory bank. Optionally filter by type (world, experience, opinion) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.",
"description": "Delete memory units for a memory bank. Optionally filter by type (world, experience, observation) to delete only specific types. This is a destructive operation that cannot be undone. The bank profile (disposition and background) will be preserved.",
"operationId": "clear_bank_memories",
"parameters": [
{
@@ -4980,10 +4980,10 @@
"type": "null"
}
],
"description": "Optional fact type filter (world, experience, opinion)",
"description": "Optional fact type filter (world, experience, observation)",
"title": "Type"
},
"description": "Optional fact type filter (world, experience, opinion)"
"description": "Optional fact type filter (world, experience, observation)"
},
{
"name": "authorization",