* feat: introduce hindsight-api-slim and hindsight-all-slim packages Closes #552 - Move all source code from hindsight-api/ to new hindsight-api-slim/ - hindsight-api-slim has heavy ML deps (torch, sentence-transformers, transformers, einops, flashrank, mlx, mlx-lm, safetensors) and pg0-embedded as optional extras: [local-ml], [embedded-db], [all] - hindsight-api becomes a zero-code meta-package depending on hindsight-api-slim[all] for full backward compatibility - Add hindsight-all-slim meta-package: hindsight-api-slim + client + embed - hindsight-all updated to depend on hindsight-api-slim[all] - pg0.py: lazy-import pg0 with clear ImportError pointing to [embedded-db] - Dockerfile: replace sed hack with proper uv sync --extra flags - Update release.yml, test.yml, lint.sh, release.sh, CLAUDE.md and all path references throughout the repo * refactor: rename hindsight/ directory to hindsight-all/ * docs: document hindsight-api-slim and hindsight-all-slim package variants Add package variants table and extras explanation to installation.md * docs: remove emojis from installation.md, use professional tone * docs: link Docker slim variant to pip package variants section * docs: consolidate Docker image variants into single table * ci: fix working-directory paths after package restructure - Replace all hindsight-api → hindsight-api-slim in test.yml - Replace hindsight → hindsight-all in test.yml - Add --extra embedded-db to test-embed API install step * ci: add local-ml and embedded-db extras to API sync steps These extras were previously implicit in the old hindsight-api package (which bundled everything). Now that hindsight-api-slim uses optional extras, we must explicitly request local-ml and embedded-db in CI. * ci: add API install step with embedded-db to test-embed smoke test The smoke test starts hindsight-api as a daemon, which requires pg0-embedded. Add a dedicated install step for hindsight-api-slim with embedded-db extra so the daemon can start successfully. * ci: remove --no-install-project when using optional extras When --no-install-project is combined with --extra, the optional deps are not installed because extras require the project to be active. Remove --no-install-project from steps that need local-ml or embedded-db. * ci: fix ordering of uv sync steps to preserve optional extras When uv sync runs for a different workspace member, it removes optional extras installed for other members. Fix by always running extra-requiring API sync last, after other workspace member syncs. Also remove --no-install-project from embedded-db sync in test-embed, as --no-install-project prevents optional extras from being active. * ci: add local-ml extra to test-embed API install for smoke test The smoke test starts the full API server which needs sentence-transformers for local embeddings (default provider). Add local-ml extra to the install. * ci: simplify extras with --all-extras and add slim pip smoke test - Replace explicit --extra local-ml --extra embedded-db with --all-extras for cleaner, more maintainable sync steps - Add test-pip-slim job: tests hindsight-api-slim[embedded-db] without local ML models, using Cohere for embeddings/reranking (mirrors Docker slim smoke test approach) * ci: simplify slim smoke test to health check only (mirrors Docker test)
585 lines
16 KiB
Python
585 lines
16 KiB
Python
"""Abstract interface for MemoryEngine public methods.
|
|
|
|
This module defines the public API that HTTP endpoints and extensions should use
|
|
to interact with the memory system. All methods require a RequestContext for
|
|
authentication when a TenantExtension is configured.
|
|
"""
|
|
|
|
from abc import ABC, abstractmethod
|
|
from datetime import datetime
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
if TYPE_CHECKING:
|
|
from hindsight_api.engine.memory_engine import Budget
|
|
from hindsight_api.engine.response_models import RecallResult, ReflectResult
|
|
from hindsight_api.engine.search.tags import TagsMatch
|
|
from hindsight_api.models import RequestContext
|
|
|
|
|
|
class MemoryEngineInterface(ABC):
|
|
"""
|
|
Abstract interface for the Memory Engine.
|
|
|
|
This defines the public API that should be used by HTTP endpoints and extensions.
|
|
All methods require a RequestContext for authentication.
|
|
"""
|
|
|
|
# =========================================================================
|
|
# Health & Status
|
|
# =========================================================================
|
|
|
|
@abstractmethod
|
|
async def health_check(self) -> dict:
|
|
"""
|
|
Check the health of the memory system.
|
|
|
|
Returns:
|
|
Dict with 'status' key ('healthy' or 'unhealthy') and additional info.
|
|
"""
|
|
...
|
|
|
|
# =========================================================================
|
|
# Core Memory Operations
|
|
# =========================================================================
|
|
|
|
@abstractmethod
|
|
async def retain_batch_async(
|
|
self,
|
|
bank_id: str,
|
|
contents: list[dict[str, Any]],
|
|
*,
|
|
request_context: "RequestContext",
|
|
document_tags: list[str] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Retain a batch of memory items.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
contents: List of content dicts with 'content', optional 'event_date',
|
|
'context', 'metadata', 'document_id', and per-item 'tags'.
|
|
request_context: Request context for authentication.
|
|
document_tags: Optional tags applied to all items in the batch.
|
|
|
|
Returns:
|
|
Dict with processing results.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def recall_async(
|
|
self,
|
|
bank_id: str,
|
|
query: str,
|
|
*,
|
|
budget: "Budget | None" = None,
|
|
max_tokens: int = 4096,
|
|
enable_trace: bool = False,
|
|
fact_type: list[str] | None = None,
|
|
question_date: datetime | None = None,
|
|
include_entities: bool = False,
|
|
max_entity_tokens: int = 500,
|
|
include_chunks: bool = False,
|
|
max_chunk_tokens: int = 8192,
|
|
request_context: "RequestContext",
|
|
) -> "RecallResult":
|
|
"""
|
|
Recall memories relevant to a query.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
query: The search query.
|
|
budget: Search budget (LOW, MID, HIGH).
|
|
max_tokens: Maximum tokens in response.
|
|
enable_trace: Include trace information.
|
|
fact_type: Filter by fact types.
|
|
question_date: Context date for temporal relevance.
|
|
include_entities: Include entity observations.
|
|
max_entity_tokens: Max tokens for entity observations.
|
|
include_chunks: Include raw chunks.
|
|
max_chunk_tokens: Max tokens for chunks.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
RecallResult with matching memories.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def reflect_async(
|
|
self,
|
|
bank_id: str,
|
|
query: str,
|
|
*,
|
|
budget: "Budget | None" = None,
|
|
context: str | None = None,
|
|
max_tokens: int = 4096,
|
|
response_schema: dict | None = None,
|
|
request_context: "RequestContext",
|
|
) -> "ReflectResult":
|
|
"""
|
|
Reflect on a query and generate a thoughtful response.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
query: The question to reflect on.
|
|
budget: Search budget for retrieving context.
|
|
context: Additional context for the reflection.
|
|
max_tokens: Maximum tokens for the response.
|
|
response_schema: Optional JSON Schema for structured output.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
ReflectResult with generated response and supporting facts.
|
|
"""
|
|
...
|
|
|
|
# =========================================================================
|
|
# Bank Management
|
|
# =========================================================================
|
|
|
|
@abstractmethod
|
|
async def list_banks(
|
|
self,
|
|
*,
|
|
request_context: "RequestContext",
|
|
) -> list[dict[str, Any]]:
|
|
"""
|
|
List all memory banks.
|
|
|
|
Args:
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
List of bank info dicts.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def get_bank_profile(
|
|
self,
|
|
bank_id: str,
|
|
*,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Get bank profile including disposition and mission.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Bank profile dict with bank_id, name, disposition, and mission.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def update_bank_disposition(
|
|
self,
|
|
bank_id: str,
|
|
disposition: dict[str, int],
|
|
*,
|
|
request_context: "RequestContext",
|
|
) -> None:
|
|
"""
|
|
Update bank disposition traits.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
disposition: Dict with trait values.
|
|
request_context: Request context for authentication.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def merge_bank_mission(
|
|
self,
|
|
bank_id: str,
|
|
new_info: str,
|
|
*,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Merge new mission information into bank profile.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
new_info: New mission information to merge.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Updated mission info.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def set_bank_mission(
|
|
self,
|
|
bank_id: str,
|
|
mission: str,
|
|
*,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Set the bank's mission (replaces existing).
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
mission: The mission text.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Dict with bank_id and mission.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def delete_bank(
|
|
self,
|
|
bank_id: str,
|
|
*,
|
|
fact_type: str | None = None,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, int]:
|
|
"""
|
|
Delete a bank or its memories.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
fact_type: If specified, only delete memories of this type.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Dict with deletion counts.
|
|
"""
|
|
...
|
|
|
|
# =========================================================================
|
|
# Memory Units
|
|
# =========================================================================
|
|
|
|
@abstractmethod
|
|
async def list_memory_units(
|
|
self,
|
|
bank_id: str,
|
|
*,
|
|
fact_type: str | None = None,
|
|
search_query: str | None = None,
|
|
limit: int = 100,
|
|
offset: int = 0,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
List memory units with pagination.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
fact_type: Filter by fact type.
|
|
search_query: Full-text search query.
|
|
limit: Maximum results.
|
|
offset: Pagination offset.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Dict with 'items', 'total', 'limit', 'offset'.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def delete_memory_unit(
|
|
self,
|
|
unit_id: str,
|
|
*,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Delete a specific memory unit.
|
|
|
|
Args:
|
|
unit_id: The memory unit ID.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Deletion result.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def get_graph_data(
|
|
self,
|
|
bank_id: str,
|
|
*,
|
|
fact_type: str | None = None,
|
|
limit: int = 1000,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Get graph data for visualization.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
fact_type: Filter by fact type.
|
|
limit: Maximum number of items to return (default: 1000).
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Dict with nodes, edges, table_rows, total_units, limit.
|
|
"""
|
|
...
|
|
|
|
# =========================================================================
|
|
# Documents
|
|
# =========================================================================
|
|
|
|
@abstractmethod
|
|
async def list_documents(
|
|
self,
|
|
bank_id: str,
|
|
*,
|
|
search_query: str | None = None,
|
|
tags: list[str] | None = None,
|
|
tags_match: "TagsMatch" = "any_strict",
|
|
limit: int = 100,
|
|
offset: int = 0,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
List documents with pagination.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
search_query: Case-insensitive substring filter on document ID.
|
|
tags: Filter by tags.
|
|
tags_match: How to match tags (any, all, any_strict, all_strict).
|
|
limit: Maximum results.
|
|
offset: Pagination offset.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Dict with 'items', 'total', 'limit', 'offset'.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def get_document(
|
|
self,
|
|
document_id: str,
|
|
bank_id: str,
|
|
*,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any] | None:
|
|
"""
|
|
Get a specific document.
|
|
|
|
Args:
|
|
document_id: The document ID.
|
|
bank_id: The memory bank ID.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Document dict or None if not found.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def delete_document(
|
|
self,
|
|
document_id: str,
|
|
bank_id: str,
|
|
*,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, int]:
|
|
"""
|
|
Delete a document and its memory units.
|
|
|
|
Args:
|
|
document_id: The document ID.
|
|
bank_id: The memory bank ID.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Dict with deletion counts.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def get_chunk(
|
|
self,
|
|
chunk_id: str,
|
|
*,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any] | None:
|
|
"""
|
|
Get a specific chunk.
|
|
|
|
Args:
|
|
chunk_id: The chunk ID.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Chunk dict or None if not found.
|
|
"""
|
|
...
|
|
|
|
# =========================================================================
|
|
# Entities
|
|
# =========================================================================
|
|
|
|
@abstractmethod
|
|
async def list_entities(
|
|
self,
|
|
bank_id: str,
|
|
*,
|
|
limit: int = 100,
|
|
offset: int = 0,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
List entities for a bank with pagination.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
limit: Maximum results.
|
|
offset: Offset for pagination.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Dict with items, total, limit, offset.
|
|
"""
|
|
...
|
|
|
|
# =========================================================================
|
|
# Statistics & Operations
|
|
# =========================================================================
|
|
|
|
@abstractmethod
|
|
async def get_bank_stats(
|
|
self,
|
|
bank_id: str,
|
|
*,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Get statistics about memory nodes and links for a bank.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Dict with node_counts, link_counts, link_counts_by_fact_type,
|
|
link_breakdown, and operations stats.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def get_entity(
|
|
self,
|
|
bank_id: str,
|
|
entity_id: str,
|
|
*,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any] | None:
|
|
"""
|
|
Get entity details including metadata and observations.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
entity_id: The entity ID.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Entity dict with id, canonical_name, mention_count, first_seen,
|
|
last_seen, metadata, and observations. None if not found.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def list_operations(
|
|
self,
|
|
bank_id: str,
|
|
*,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
List async operations for a bank.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Dict with 'total' (int) and 'operations' (list of operation dicts).
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def cancel_operation(
|
|
self,
|
|
bank_id: str,
|
|
operation_id: str,
|
|
*,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Cancel a pending async operation.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
operation_id: The operation ID to cancel.
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Dict with success status and message.
|
|
|
|
Raises:
|
|
ValueError: If operation not found.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def update_bank(
|
|
self,
|
|
bank_id: str,
|
|
*,
|
|
name: str | None = None,
|
|
mission: str | None = None,
|
|
request_context: "RequestContext",
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Update bank name and/or mission.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
name: New bank name (optional).
|
|
mission: New mission text (optional, replaces existing).
|
|
request_context: Request context for authentication.
|
|
|
|
Returns:
|
|
Updated bank profile dict.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def submit_async_retain(
|
|
self,
|
|
bank_id: str,
|
|
contents: list[dict[str, Any]],
|
|
*,
|
|
request_context: "RequestContext",
|
|
document_tags: list[str] | None = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
Submit a batch retain operation to run asynchronously.
|
|
|
|
Args:
|
|
bank_id: The memory bank ID.
|
|
contents: List of content dicts to retain.
|
|
request_context: Request context for authentication.
|
|
document_tags: Optional tags applied to all items in the async batch.
|
|
|
|
Returns:
|
|
Dict with operation_id and items_count.
|
|
"""
|
|
...
|