Compare commits

...
Author SHA1 Message Date
Nicolò Boschi 09d56ec2ea style(retain): format long function call arguments one-per-line 2026-03-26 16:05:22 +01:00
Nicolò Boschi 599a29b01b docs(python-client): improve pydoc strings for async-first usage and low-level API access
- Class docstring now clearly documents async-first pattern: a* methods
  preferred, sync wrappers for scripts/REPLs only
- Every sync method docstring points to its async counterpart
- Every async method docstring says "preferred"
- Expose 10 low-level API properties (documents, entities, operations,
  webhooks, monitoring, etc.) so agents/users can discover the full API
  surface without guessing at _-prefixed internals
- Add missing API parameters: tag_groups (recall/reflect), fact_types,
  exclude_mental_models, exclude_mental_model_ids (reflect),
  observation_scopes/strategy (retain items), background (create_bank)
- Fix areflect missing include_facts param that sync reflect already had
- Sync recall/reflect now delegate to async counterparts (no logic duplication)
2026-03-26 16:00:21 +01:00
3 changed files with 444 additions and 158 deletions
@@ -127,9 +127,7 @@ async def _insert_facts_and_links(
if unit_ids:
# Process entities
step_start = time.time()
user_entities_per_content = {
idx: content.entities for idx, content in enumerate(contents) if content.entities
}
user_entities_per_content = {idx: content.entities for idx, content in enumerate(contents) if content.entities}
entity_links = await entity_processing.process_entities_batch(
entity_resolver,
conn,
@@ -217,9 +215,7 @@ async def _extract_and_embed(
embeddings = await embedding_processing.generate_embeddings_batch(embeddings_model, augmented_texts)
log_buffer.append(f" Generate embeddings: {len(embeddings)} embeddings in {time.time() - step_start:.3f}s")
processed_facts = [
ProcessedFact.from_extracted_fact(ef, emb) for ef, emb in zip(extracted_facts, embeddings)
]
processed_facts = [ProcessedFact.from_extracted_fact(ef, emb) for ef, emb in zip(extracted_facts, embeddings)]
return extracted_facts, processed_facts, chunks, usage
@@ -268,23 +264,56 @@ async def retain_batch(
# --- Delta retain: check if we can skip unchanged chunks ---
if is_first_batch:
delta_result = await _try_delta_retain(
pool, embeddings_model, llm_config, entity_resolver, format_date_fn,
bank_id, contents_dicts, contents, config, document_id, fact_type_override,
document_tags, agent_name, log_buffer, start_time, operation_id, schema, outbox_callback,
pool,
embeddings_model,
llm_config,
entity_resolver,
format_date_fn,
bank_id,
contents_dicts,
contents,
config,
document_id,
fact_type_override,
document_tags,
agent_name,
log_buffer,
start_time,
operation_id,
schema,
outbox_callback,
)
if delta_result is not None:
return delta_result
# --- Full retain path ---
extracted_facts, processed_facts, chunks, usage = await _extract_and_embed(
contents, llm_config, agent_name, config, embeddings_model, format_date_fn,
fact_type_override, log_buffer, pool, operation_id, schema,
contents,
llm_config,
agent_name,
config,
embeddings_model,
format_date_fn,
fact_type_override,
log_buffer,
pool,
operation_id,
schema,
)
if not extracted_facts:
await _handle_zero_facts_documents(
pool, bank_id, contents_dicts, contents, config, document_id,
is_first_batch, document_tags, chunks, log_buffer, start_time,
pool,
bank_id,
contents_dicts,
contents,
config,
document_id,
is_first_batch,
document_tags,
chunks,
log_buffer,
start_time,
)
return [[] for _ in contents], usage
@@ -336,8 +365,13 @@ async def retain_batch(
contents_dicts, document_tags, doc_contents=doc_contents
)
await fact_storage.handle_document_tracking(
conn, bank_id, actual_doc_id, combined_content,
is_first_batch, retain_params, merged_tags,
conn,
bank_id,
actual_doc_id,
combined_content,
is_first_batch,
retain_params,
merged_tags,
)
document_ids_added.append(actual_doc_id)
@@ -382,8 +416,15 @@ async def retain_batch(
# Insert facts and create all links (shared pipeline)
result_unit_ids = await _insert_facts_and_links(
conn, entity_resolver, bank_id, contents, extracted_facts,
processed_facts, config, log_buffer, outbox_callback,
conn,
entity_resolver,
bank_id,
contents,
extracted_facts,
processed_facts,
config,
log_buffer,
outbox_callback,
)
await entity_resolver.flush_pending_stats()
@@ -406,9 +447,24 @@ async def retain_batch(
async def _try_delta_retain(
pool, embeddings_model, llm_config, entity_resolver, format_date_fn,
bank_id, contents_dicts, contents, config, document_id, fact_type_override,
document_tags, agent_name, log_buffer, start_time, operation_id, schema, outbox_callback,
pool,
embeddings_model,
llm_config,
entity_resolver,
format_date_fn,
bank_id,
contents_dicts,
contents,
config,
document_id,
fact_type_override,
document_tags,
agent_name,
log_buffer,
start_time,
operation_id,
schema,
outbox_callback,
):
"""
Attempt delta retain for a document upsert. Returns result tuple if delta
@@ -472,8 +528,15 @@ async def _try_delta_retain(
# Nothing changed — just update document metadata/tags
log_buffer.append("[delta] No chunk changes detected — updating document metadata only")
return await _delta_metadata_only(
pool, bank_id, contents_dicts, contents, effective_doc_id,
document_tags, log_buffer, start_time, outbox_callback,
pool,
bank_id,
contents_dicts,
contents,
effective_doc_id,
document_tags,
log_buffer,
start_time,
outbox_callback,
)
# Build content items for only the changed/new chunks
@@ -481,14 +544,30 @@ async def _try_delta_retain(
if not delta_contents:
return await _delta_metadata_only(
pool, bank_id, contents_dicts, contents, effective_doc_id,
document_tags, log_buffer, start_time, outbox_callback,
pool,
bank_id,
contents_dicts,
contents,
effective_doc_id,
document_tags,
log_buffer,
start_time,
outbox_callback,
)
# Extract facts and generate embeddings (shared pipeline)
extracted_facts, processed_facts, new_chunk_metadata, usage = await _extract_and_embed(
delta_contents, llm_config, agent_name, config, embeddings_model, format_date_fn,
fact_type_override, log_buffer, pool, operation_id, schema,
delta_contents,
llm_config,
agent_name,
config,
embeddings_model,
format_date_fn,
fact_type_override,
log_buffer,
pool,
operation_id,
schema,
)
# Database transaction
@@ -510,7 +589,12 @@ async def _try_delta_retain(
combined_content = "\n".join([c.get("content", "") for c in contents_dicts])
retain_params, merged_tags = _build_retain_params(contents_dicts, document_tags)
await fact_storage.upsert_document_metadata(
conn, bank_id, effective_doc_id, combined_content, retain_params, merged_tags,
conn,
bank_id,
effective_doc_id,
combined_content,
retain_params,
merged_tags,
)
log_buffer.append(f" Document metadata update in {time.time() - step_start:.3f}s")
@@ -570,8 +654,15 @@ async def _try_delta_retain(
# Insert facts and create all links (shared pipeline)
result_unit_ids = await _insert_facts_and_links(
conn, entity_resolver, bank_id, contents, extracted_facts,
processed_facts, config, log_buffer, outbox_callback,
conn,
entity_resolver,
bank_id,
contents,
extracted_facts,
processed_facts,
config,
log_buffer,
outbox_callback,
)
await entity_resolver.flush_pending_stats()
@@ -591,8 +682,15 @@ async def _try_delta_retain(
async def _delta_metadata_only(
pool, bank_id, contents_dicts, contents, document_id, document_tags,
log_buffer, start_time, outbox_callback,
pool,
bank_id,
contents_dicts,
contents,
document_id,
document_tags,
log_buffer,
start_time,
outbox_callback,
):
"""Handle the case where no chunks changed — just update document metadata and tags."""
async with acquire_with_retry(pool) as conn:
@@ -600,7 +698,12 @@ async def _delta_metadata_only(
combined_content = "\n".join([c.get("content", "") for c in contents_dicts])
retain_params, merged_tags = _build_retain_params(contents_dicts, document_tags)
await fact_storage.upsert_document_metadata(
conn, bank_id, document_id, combined_content, retain_params, merged_tags,
conn,
bank_id,
document_id,
combined_content,
retain_params,
merged_tags,
)
await fact_storage.update_memory_units_tags(conn, bank_id, document_id, merged_tags)
if outbox_callback:
@@ -645,8 +748,17 @@ def _build_contents(contents_dicts: list[RetainContentDict], document_tags: list
async def _handle_zero_facts_documents(
pool, bank_id, contents_dicts, contents, config, document_id,
is_first_batch, document_tags, chunks, log_buffer, start_time,
pool,
bank_id,
contents_dicts,
contents,
config,
document_id,
is_first_batch,
document_tags,
chunks,
log_buffer,
start_time,
):
"""Handle document tracking when zero facts were extracted."""
docs_tracked = 0
@@ -677,8 +789,13 @@ async def _handle_zero_facts_documents(
contents_dicts, document_tags, doc_contents=doc_contents
)
await fact_storage.handle_document_tracking(
conn, bank_id, actual_doc_id, combined_content,
is_first_batch, retain_params, merged_tags,
conn,
bank_id,
actual_doc_id,
combined_content,
is_first_batch,
retain_params,
merged_tags,
)
docs_tracked += 1
@@ -1,18 +1,24 @@
"""
Hindsight Client - Clean, pythonic wrapper for the Hindsight API.
This package provides a high-level interface for common Hindsight operations.
For advanced use cases, use the auto-generated API client directly.
This package provides a high-level ``Hindsight`` class with simplified methods
for the most common operations (retain, recall, reflect, banks, mental models,
directives).
For operations not available as convenience methods — such as documents,
entities, async operations, webhooks, and monitoring — use the low-level API
clients exposed as properties on the ``Hindsight`` instance (e.g.
``client.documents``, ``client.entities``, ``client.operations``).
All low-level methods are async.
Quick start::
Example:
```python
from hindsight_client import Hindsight
client = Hindsight(base_url="http://localhost:8888")
# Store a memory
result = client.retain(bank_id="alice", content="Alice loves AI")
print(result.success)
client.retain(bank_id="alice", content="Alice loves AI")
# Search memories
response = client.recall(bank_id="alice", query="What does Alice like?")
@@ -22,7 +28,19 @@ Example:
# Generate contextual answer
answer = client.reflect(bank_id="alice", query="What are my interests?")
print(answer.text)
```
Low-level API access::
import asyncio
# List documents
docs = asyncio.run(client.documents.list_documents("alice"))
# Check operation status
status = asyncio.run(client.operations.get_operation_status("alice", "op-id"))
# List entities
entities = asyncio.run(client.entities.list_entities("alice"))
"""
from hindsight_client_api.models.bank_profile_response import BankProfileResponse
@@ -12,7 +12,18 @@ from pathlib import Path
from typing import Any, Literal
import hindsight_client_api
from hindsight_client_api.api import banks_api, directives_api, files_api, memory_api, mental_models_api
from hindsight_client_api.api import (
banks_api,
directives_api,
documents_api,
entities_api,
files_api,
memory_api,
mental_models_api,
monitoring_api,
operations_api,
webhooks_api,
)
from hindsight_client_api.models import (
memory_item,
recall_request,
@@ -44,27 +55,68 @@ class Hindsight:
"""
High-level, easy-to-use Hindsight API client.
Example:
```python
This class provides simplified methods for the most common operations:
retain, recall, reflect, bank management, mental models, and directives.
**Async vs sync:** Every convenience method has an async counterpart
prefixed with ``a`` (e.g. ``aretain``, ``arecall``, ``areflect``).
**Prefer the async variants** (``aretain``, ``arecall``, ``areflect``, etc.)
whenever you are inside an async context (``async def``, event loops,
frameworks like FastAPI/LangGraph/CrewAI). The sync versions (``retain``,
``recall``, ``reflect``) are convenience wrappers that call
``asyncio.run_until_complete`` under the hood — they exist for scripts and
REPLs but will raise errors if an event loop is already running.
For operations not covered here (documents, entities, operations/async jobs,
webhooks, file uploads, monitoring), use the low-level API clients exposed
as properties on this class. These are auto-generated from the OpenAPI spec
and cover the full API surface. **All low-level methods are async-only.**
Example — async (preferred)::
from hindsight_client import Hindsight
# Without authentication
client = Hindsight(base_url="http://localhost:8888")
# With API key authentication
client = Hindsight(base_url="http://localhost:8888", api_key="your-api-key")
# Store a memory
# Inside an async function — use the a* methods
await client.aretain(bank_id="alice", content="Alice loves AI")
response = await client.arecall(bank_id="alice", query="What does Alice like?")
answer = await client.areflect(bank_id="alice", query="What are my interests?")
Example — sync (scripts / REPLs only)::
# Outside an async context — sync wrappers are available
client.retain(bank_id="alice", content="Alice loves AI")
# Recall memories
response = client.recall(bank_id="alice", query="What does Alice like?")
for r in response.results:
print(r.text)
# Generate contextual answer
answer = client.reflect(bank_id="alice", query="What are my interests?")
```
Example — low-level API for advanced operations::
# Access documents, entities, operations, webhooks, etc.
# All low-level methods are async-only — use 'await' or asyncio.run().
# List documents in a bank
docs = await client.documents.list_documents("alice")
# Delete a specific document
await client.documents.delete_document("alice", "doc-123")
# Check async operation status
status = await client.operations.get_operation_status("alice", "op-456")
# List entities
entities = await client.entities.list_entities("alice")
Available low-level API properties:
- ``client.memory``: Core memory operations (MemoryApi)
- ``client.banks``: Bank management (BanksApi)
- ``client.documents``: Document CRUD (DocumentsApi)
- ``client.entities``: Entity browsing (EntitiesApi)
- ``client.mental_models``: Mental model management (MentalModelsApi)
- ``client.directives``: Directive management (DirectivesApi)
- ``client.operations``: Async operation tracking (OperationsApi)
- ``client.webhooks``: Webhook management (WebhooksApi)
- ``client.files``: File upload (FilesApi)
- ``client.monitoring``: Health/version checks (MonitoringApi)
"""
def __init__(self, base_url: str, api_key: str | None = None, timeout: float = 300.0):
@@ -88,6 +140,66 @@ class Hindsight:
self._mental_models_api = mental_models_api.MentalModelsApi(self._api_client)
self._directives_api = directives_api.DirectivesApi(self._api_client)
self._files_api = files_api.FilesApi(self._api_client)
self._documents_api = documents_api.DocumentsApi(self._api_client)
self._entities_api = entities_api.EntitiesApi(self._api_client)
self._operations_api = operations_api.OperationsApi(self._api_client)
self._webhooks_api = webhooks_api.WebhooksApi(self._api_client)
self._monitoring_api = monitoring_api.MonitoringApi(self._api_client)
# -- Low-level API accessors ------------------------------------------------
# These expose the full, auto-generated API surface for operations not
# covered by the convenience methods above. All methods on these objects
# are async — use ``await`` or ``asyncio.run()`` to call them.
@property
def memory(self) -> memory_api.MemoryApi:
"""Low-level Memory API — retain, recall, reflect, list/clear memories, tags, and graph."""
return self._memory_api
@property
def banks(self) -> banks_api.BanksApi:
"""Low-level Banks API — create, update, delete banks; stats; consolidation; config."""
return self._banks_api
@property
def documents(self) -> documents_api.DocumentsApi:
"""Low-level Documents API — list, get, update, delete documents and chunks."""
return self._documents_api
@property
def entities(self) -> entities_api.EntitiesApi:
"""Low-level Entities API — list, get, and regenerate entity observations."""
return self._entities_api
@property
def mental_models(self) -> mental_models_api.MentalModelsApi:
"""Low-level Mental Models API — create, list, get, update, refresh, delete, history."""
return self._mental_models_api
@property
def directives(self) -> directives_api.DirectivesApi:
"""Low-level Directives API — create, list, get, update, delete."""
return self._directives_api
@property
def operations(self) -> operations_api.OperationsApi:
"""Low-level Operations API — get status, list, cancel, retry async operations."""
return self._operations_api
@property
def webhooks(self) -> webhooks_api.WebhooksApi:
"""Low-level Webhooks API — create, list, update, delete webhooks and deliveries."""
return self._webhooks_api
@property
def files(self) -> files_api.FilesApi:
"""Low-level Files API — upload and retain files."""
return self._files_api
@property
def monitoring(self) -> monitoring_api.MonitoringApi:
"""Low-level Monitoring API — health check, version, metrics."""
return self._monitoring_api
def __enter__(self):
"""Context manager entry."""
@@ -128,7 +240,7 @@ class Hindsight:
tags: list[str] | None = None,
) -> RetainResponse:
"""
Store a single memory (simplified interface).
Store a single memory (sync wrapper — prefer :meth:`aretain` in async code).
Args:
bank_id: The memory bank ID
@@ -167,11 +279,13 @@ class Hindsight:
retain_async: bool = False,
) -> RetainResponse:
"""
Store multiple memories in batch.
Store multiple memories in batch (sync wrapper — prefer :meth:`aretain_batch` in async code).
Args:
bank_id: The memory bank ID
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id', 'entities', 'tags'
items: List of memory items, each a dict with 'content' (required) and optional keys:
'timestamp', 'context', 'metadata', 'document_id', 'entities', 'tags',
'observation_scopes' (str or list[list[str]]), 'strategy'.
document_id: Optional document ID for grouping memories (applied to items that don't have their own)
document_tags: Optional list of tags applied to all items in this batch (merged with per-item tags)
retain_async: If True, process asynchronously in background (default: False)
@@ -179,37 +293,16 @@ class Hindsight:
Returns:
RetainResponse with success status and item count
"""
from hindsight_client_api.models.entity_input import EntityInput
from hindsight_client_api.models.timestamp import Timestamp
memory_items = []
for item in items:
entities = None
if item.get("entities"):
entities = [EntityInput(text=e["text"], type=e.get("type")) for e in item["entities"]]
raw_ts = item.get("timestamp")
timestamp_val = Timestamp(actual_instance=raw_ts) if raw_ts is not None else None
memory_items.append(
memory_item.MemoryItem(
content=item["content"],
timestamp=timestamp_val,
context=item.get("context"),
metadata=item.get("metadata"),
# Use item's document_id if provided, otherwise fall back to batch-level document_id
document_id=item.get("document_id") or document_id,
entities=entities,
tags=item.get("tags"),
)
return _run_async(
self.aretain_batch(
bank_id=bank_id,
items=items,
document_id=document_id,
document_tags=document_tags,
retain_async=retain_async,
)
request_obj = retain_request.RetainRequest(
items=memory_items,
var_async=retain_async,
document_tags=document_tags,
)
return _run_async(self._memory_api.retain_memories(bank_id, request_obj, _request_timeout=self._timeout))
def retain_files(
self,
bank_id: str,
@@ -218,7 +311,7 @@ class Hindsight:
files_metadata: list[dict[str, Any]] | None = None,
) -> FileRetainResponse:
"""
Upload files and retain their contents as memories.
Upload files and retain their contents as memories (sync wrapper).
Files are automatically converted to text (PDF, DOCX, images via OCR, audio via
transcription, and more) and ingested as memories. Processing is always asynchronous
@@ -262,9 +355,10 @@ class Hindsight:
max_source_facts_tokens: int = 4096,
tags: list[str] | None = None,
tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any",
tag_groups: list[dict[str, Any]] | None = None,
) -> RecallResponse:
"""
Recall memories using semantic similarity.
Recall memories using semantic similarity (sync wrapper — prefer :meth:`arecall` in async code).
Args:
bank_id: The memory bank ID
@@ -283,41 +377,32 @@ class Hindsight:
tags: Optional list of tags to filter memories by
tags_match: How to match tags - "any" (OR, includes untagged), "all" (AND, includes untagged),
"any_strict" (OR, excludes untagged), "all_strict" (AND, excludes untagged). Default: "any"
tag_groups: Optional list of tag group filters for advanced boolean tag matching.
Returns:
RecallResponse with results, optional entities, optional chunks, optional source_facts, and optional trace
"""
from hindsight_client_api.models import (
chunk_include_options,
entity_include_options,
include_options,
source_facts_include_options,
return _run_async(
self.arecall(
bank_id=bank_id,
query=query,
types=types,
max_tokens=max_tokens,
budget=budget,
trace=trace,
query_timestamp=query_timestamp,
include_entities=include_entities,
max_entity_tokens=max_entity_tokens,
include_chunks=include_chunks,
max_chunk_tokens=max_chunk_tokens,
include_source_facts=include_source_facts,
max_source_facts_tokens=max_source_facts_tokens,
tags=tags,
tags_match=tags_match,
tag_groups=tag_groups,
)
)
include_opts = include_options.IncludeOptions(
entities=entity_include_options.EntityIncludeOptions(max_tokens=max_entity_tokens)
if include_entities
else None,
chunks=chunk_include_options.ChunkIncludeOptions(max_tokens=max_chunk_tokens) if include_chunks else None,
source_facts=source_facts_include_options.SourceFactsIncludeOptions(max_tokens=max_source_facts_tokens)
if include_source_facts
else None,
)
request_obj = recall_request.RecallRequest(
query=query,
types=types,
budget=budget,
max_tokens=max_tokens,
trace=trace,
query_timestamp=query_timestamp,
include=include_opts,
tags=tags,
tags_match=tags_match,
)
return _run_async(self._memory_api.recall_memories(bank_id, request_obj, _request_timeout=self._timeout))
def reflect(
self,
bank_id: str,
@@ -329,9 +414,13 @@ class Hindsight:
tags: list[str] | None = None,
tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any",
include_facts: bool = False,
tag_groups: list[dict[str, Any]] | None = None,
fact_types: list[str] | None = None,
exclude_mental_models: bool = False,
exclude_mental_model_ids: list[str] | None = None,
) -> ReflectResponse:
"""
Generate a contextual answer based on bank identity and memories.
Generate a contextual answer based on bank identity and memories (sync wrapper — prefer :meth:`areflect` in async code).
Args:
bank_id: The memory bank ID
@@ -347,25 +436,33 @@ class Hindsight:
"any_strict" (OR, excludes untagged), "all_strict" (AND, excludes untagged). Default: "any"
include_facts: If True, the response will include a 'based_on' field listing
the memories, mental models, and directives used to construct the answer.
tag_groups: Optional list of tag group filters for advanced boolean tag matching.
fact_types: Optional list of fact types to include (world, experience, observation).
exclude_mental_models: If True, exclude all mental models from reflection (default: False).
exclude_mental_model_ids: Optional list of specific mental model IDs to exclude.
Returns:
ReflectResponse with answer text, optionally facts used, and optionally
structured_output if response_schema was provided
"""
include = ReflectIncludeOptions(facts={}) if include_facts else None
request_obj = reflect_request.ReflectRequest(
query=query,
budget=budget,
context=context,
max_tokens=max_tokens,
response_schema=response_schema,
tags=tags,
tags_match=tags_match,
include=include,
return _run_async(
self.areflect(
bank_id=bank_id,
query=query,
budget=budget,
context=context,
max_tokens=max_tokens,
response_schema=response_schema,
tags=tags,
tags_match=tags_match,
include_facts=include_facts,
tag_groups=tag_groups,
fact_types=fact_types,
exclude_mental_models=exclude_mental_models,
exclude_mental_model_ids=exclude_mental_model_ids,
)
)
return _run_async(self._memory_api.reflect(bank_id, request_obj, _request_timeout=self._timeout))
def list_memories(
self,
bank_id: str,
@@ -374,7 +471,7 @@ class Hindsight:
limit: int = 100,
offset: int = 0,
) -> ListMemoryUnitsResponse:
"""List memory units with pagination."""
"""List memory units with pagination (sync wrapper — use ``await client.memory.list_memories(...)`` in async code)."""
return _run_async(
self._memory_api.list_memories(
bank_id=bank_id,
@@ -402,8 +499,9 @@ class Hindsight:
enable_observations: bool | None = None,
observations_mission: str | None = None,
reflect_mission: str | None = None,
background: str | None = None,
) -> BankProfileResponse:
"""Create or update a memory bank.
"""Create or update a memory bank (sync wrapper — prefer :meth:`acreate_bank` in async code).
Args:
bank_id: Unique identifier for the bank
@@ -420,6 +518,7 @@ class Hindsight:
enable_observations: Toggle automatic observation consolidation after retain().
observations_mission: Controls what gets synthesised into observations. Replaces built-in rules.
reflect_mission: Mission/context for Reflect operations.
background: Optional background context for the bank.
"""
return _run_async(
self._acreate_bank(
@@ -437,6 +536,7 @@ class Hindsight:
retain_chunk_size=retain_chunk_size,
enable_observations=enable_observations,
observations_mission=observations_mission,
background=background,
)
)
@@ -456,6 +556,7 @@ class Hindsight:
retain_chunk_size: int | None = None,
enable_observations: bool | None = None,
observations_mission: str | None = None,
background: str | None = None,
) -> BankProfileResponse:
import aiohttp
@@ -466,6 +567,8 @@ class Hindsight:
body["mission"] = mission
if reflect_mission is not None:
body["reflect_mission"] = reflect_mission
if background is not None:
body["background"] = background
# Individual disposition fields take priority over legacy disposition dict
if disposition_skepticism is not None:
body["disposition_skepticism"] = disposition_skepticism
@@ -528,8 +631,9 @@ class Hindsight:
enable_observations: bool | None = None,
observations_mission: str | None = None,
reflect_mission: str | None = None,
background: str | None = None,
) -> BankProfileResponse:
"""Create or update a memory bank (async).
"""Create or update a memory bank (async — preferred over :meth:`create_bank`).
Args:
bank_id: Unique identifier for the bank
@@ -546,6 +650,7 @@ class Hindsight:
enable_observations: Toggle automatic observation consolidation after retain().
observations_mission: Controls what gets synthesised into observations. Replaces built-in rules.
reflect_mission: Mission/context for Reflect operations.
background: Optional background context for the bank.
"""
return await self._acreate_bank(
bank_id,
@@ -562,6 +667,7 @@ class Hindsight:
retain_chunk_size=retain_chunk_size,
enable_observations=enable_observations,
observations_mission=observations_mission,
background=background,
)
async def aset_mission(self, bank_id: str, mission: str) -> dict[str, Any]:
@@ -581,11 +687,13 @@ class Hindsight:
retain_async: bool = False,
) -> RetainResponse:
"""
Store multiple memories in batch (async).
Store multiple memories in batch (async — preferred over :meth:`retain_batch`).
Args:
bank_id: The memory bank ID
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id', 'entities', 'tags'
items: List of memory items, each a dict with 'content' (required) and optional keys:
'timestamp', 'context', 'metadata', 'document_id', 'entities', 'tags',
'observation_scopes' (str or list[list[str]]), 'strategy'.
document_id: Optional document ID for grouping memories (applied to items that don't have their own)
document_tags: Optional list of tags applied to all items in this batch (merged with per-item tags)
retain_async: If True, process asynchronously in background (default: False)
@@ -594,6 +702,7 @@ class Hindsight:
RetainResponse with success status and item count
"""
from hindsight_client_api.models.entity_input import EntityInput
from hindsight_client_api.models.observation_scopes import ObservationScopes
from hindsight_client_api.models.timestamp import Timestamp
memory_items = []
@@ -603,6 +712,9 @@ class Hindsight:
entities = [EntityInput(text=e["text"], type=e.get("type")) for e in item["entities"]]
raw_ts = item.get("timestamp")
timestamp_val = Timestamp(actual_instance=raw_ts) if raw_ts is not None else None
obs_scopes = None
if item.get("observation_scopes") is not None:
obs_scopes = ObservationScopes(actual_instance=item["observation_scopes"])
memory_items.append(
memory_item.MemoryItem(
content=item["content"],
@@ -613,6 +725,8 @@ class Hindsight:
document_id=item.get("document_id") or document_id,
entities=entities,
tags=item.get("tags"),
observation_scopes=obs_scopes,
strategy=item.get("strategy"),
)
)
@@ -636,7 +750,7 @@ class Hindsight:
tags: list[str] | None = None,
) -> RetainResponse:
"""
Store a single memory (async).
Store a single memory (async — preferred over :meth:`retain`).
Args:
bank_id: The memory bank ID
@@ -683,9 +797,10 @@ class Hindsight:
max_source_facts_tokens: int = 4096,
tags: list[str] | None = None,
tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any",
tag_groups: list[dict[str, Any]] | None = None,
) -> RecallResponse:
"""
Recall memories using semantic similarity (async).
Recall memories using semantic similarity (async — preferred over :meth:`recall`).
Args:
bank_id: The memory bank ID
@@ -704,6 +819,11 @@ class Hindsight:
tags: Optional list of tags to filter memories by
tags_match: How to match tags - "any" (OR, includes untagged), "all" (AND, includes untagged),
"any_strict" (OR, excludes untagged), "all_strict" (AND, excludes untagged). Default: "any"
tag_groups: Optional list of tag group filters for advanced boolean tag matching.
Each element is a dict representing a tag group node (TagGroupLeaf, TagGroupAnd,
TagGroupOr, or TagGroupNot). Example::
[{"tags": ["customer"], "match": "all"}, {"not": {"tags": ["internal"]}}]
Returns:
RecallResponse with results, optional entities, optional chunks, optional source_facts, and optional trace
@@ -725,6 +845,12 @@ class Hindsight:
else None,
)
tag_groups_objs = None
if tag_groups is not None:
from hindsight_client_api.models.recall_request_tag_groups_inner import RecallRequestTagGroupsInner
tag_groups_objs = [RecallRequestTagGroupsInner.from_dict(tg) for tg in tag_groups]
request_obj = recall_request.RecallRequest(
query=query,
types=types,
@@ -735,6 +861,7 @@ class Hindsight:
include=include_opts,
tags=tags,
tags_match=tags_match,
tag_groups=tag_groups_objs,
)
return await self._memory_api.recall_memories(bank_id, request_obj, _request_timeout=self._timeout)
@@ -749,9 +876,14 @@ class Hindsight:
response_schema: dict[str, Any] | None = None,
tags: list[str] | None = None,
tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any",
include_facts: bool = False,
tag_groups: list[dict[str, Any]] | None = None,
fact_types: list[str] | None = None,
exclude_mental_models: bool = False,
exclude_mental_model_ids: list[str] | None = None,
) -> ReflectResponse:
"""
Generate a contextual answer based on bank identity and memories (async).
Generate a contextual answer based on bank identity and memories (async — preferred over :meth:`reflect`).
Args:
bank_id: The memory bank ID
@@ -765,11 +897,25 @@ class Hindsight:
tags: Optional list of tags to filter memories by
tags_match: How to match tags - "any" (OR, includes untagged), "all" (AND, includes untagged),
"any_strict" (OR, excludes untagged), "all_strict" (AND, excludes untagged). Default: "any"
include_facts: If True, the response will include a 'based_on' field listing
the memories, mental models, and directives used to construct the answer.
tag_groups: Optional list of tag group filters for advanced boolean tag matching.
fact_types: Optional list of fact types to include (world, experience, observation).
exclude_mental_models: If True, exclude all mental models from reflection (default: False).
exclude_mental_model_ids: Optional list of specific mental model IDs to exclude.
Returns:
ReflectResponse with answer text, optionally facts used, and optionally
structured_output if response_schema was provided
"""
include = ReflectIncludeOptions(facts={}) if include_facts else None
tag_groups_objs = None
if tag_groups is not None:
from hindsight_client_api.models.recall_request_tag_groups_inner import RecallRequestTagGroupsInner
tag_groups_objs = [RecallRequestTagGroupsInner.from_dict(tg) for tg in tag_groups]
request_obj = reflect_request.ReflectRequest(
query=query,
budget=budget,
@@ -778,6 +924,11 @@ class Hindsight:
response_schema=response_schema,
tags=tags,
tags_match=tags_match,
include=include,
tag_groups=tag_groups_objs,
fact_types=fact_types,
exclude_mental_models=exclude_mental_models or None,
exclude_mental_model_ids=exclude_mental_model_ids,
)
return await self._memory_api.reflect(bank_id, request_obj, _request_timeout=self._timeout)
@@ -795,7 +946,7 @@ class Hindsight:
id: str | None = None,
):
"""
Create a mental model (runs reflect in background).
Create a mental model (sync wrapper — use ``await client.mental_models.create_mental_model(...)`` in async code).
Args:
bank_id: The memory bank ID
@@ -828,7 +979,7 @@ class Hindsight:
def list_mental_models(self, bank_id: str, tags: list[str] | None = None):
"""
List all mental models in a bank.
List all mental models in a bank (sync wrapper — use ``await client.mental_models.list_mental_models(...)`` in async code).
Args:
bank_id: The memory bank ID
@@ -841,7 +992,7 @@ class Hindsight:
def get_mental_model(self, bank_id: str, mental_model_id: str):
"""
Get a specific mental model.
Get a specific mental model (sync wrapper — use ``await client.mental_models.get_mental_model(...)`` in async code).
Args:
bank_id: The memory bank ID
@@ -854,7 +1005,7 @@ class Hindsight:
def refresh_mental_model(self, bank_id: str, mental_model_id: str):
"""
Refresh a mental model to update with current knowledge.
Refresh a mental model (sync wrapper — use ``await client.mental_models.refresh_mental_model(...)`` in async code).
Args:
bank_id: The memory bank ID
@@ -876,7 +1027,7 @@ class Hindsight:
trigger: dict[str, Any] | None = None,
):
"""
Update a mental model's metadata.
Update a mental model's metadata (sync wrapper — use ``await client.mental_models.update_mental_model(...)`` in async code).
Args:
bank_id: The memory bank ID
@@ -908,7 +1059,7 @@ class Hindsight:
def delete_mental_model(self, bank_id: str, mental_model_id: str):
"""
Delete a mental model.
Delete a mental model (sync wrapper — use ``await client.mental_models.delete_mental_model(...)`` in async code).
Args:
bank_id: The memory bank ID
@@ -918,7 +1069,7 @@ class Hindsight:
def get_mental_model_history(self, bank_id: str, mental_model_id: str):
"""
Get the content change history of a mental model.
Get the content change history of a mental model (sync wrapper — use ``await client.mental_models.get_mental_model_history(...)`` in async code).
Returns a list of history entries (most recent first), each with
``previous_content`` and ``changed_at`` fields.
@@ -941,7 +1092,7 @@ class Hindsight:
tags: list[str] | None = None,
):
"""
Create a directive (hard rule for reflect).
Create a directive (sync wrapper — use ``await client.directives.create_directive(...)`` in async code).
Args:
bank_id: The memory bank ID
@@ -968,7 +1119,7 @@ class Hindsight:
def list_directives(self, bank_id: str, tags: list[str] | None = None):
"""
List all directives in a bank.
List all directives in a bank (sync wrapper — use ``await client.directives.list_directives(...)`` in async code).
Args:
bank_id: The memory bank ID
@@ -981,7 +1132,7 @@ class Hindsight:
def get_directive(self, bank_id: str, directive_id: str):
"""
Get a specific directive.
Get a specific directive (sync wrapper — use ``await client.directives.get_directive(...)`` in async code).
Args:
bank_id: The memory bank ID
@@ -1003,7 +1154,7 @@ class Hindsight:
tags: list[str] | None = None,
):
"""
Update a directive.
Update a directive (sync wrapper — use ``await client.directives.update_directive(...)`` in async code).
Args:
bank_id: The memory bank ID
@@ -1031,7 +1182,7 @@ class Hindsight:
def delete_directive(self, bank_id: str, directive_id: str):
"""
Delete a directive.
Delete a directive (sync wrapper — use ``await client.directives.delete_directive(...)`` in async code).
Args:
bank_id: The memory bank ID
@@ -1041,7 +1192,7 @@ class Hindsight:
def get_bank_config(self, bank_id: str) -> dict[str, Any]:
"""
Get the resolved configuration for a bank, including any bank-level overrides.
Get the resolved configuration for a bank (sync wrapper — use ``await client.banks.get_bank_config(...)`` in async code).
Can be disabled on the server by setting ``HINDSIGHT_API_ENABLE_BANK_CONFIG_API=false``.
@@ -1079,7 +1230,7 @@ class Hindsight:
disposition_empathy: int | None = None,
) -> dict[str, Any]:
"""
Update configuration overrides for a bank.
Update configuration overrides for a bank (sync wrapper — use ``await client.banks.update_bank_config(...)`` in async code).
Can be disabled on the server by setting ``HINDSIGHT_API_ENABLE_BANK_CONFIG_API=false``.
@@ -1131,7 +1282,7 @@ class Hindsight:
def reset_bank_config(self, bank_id: str) -> dict[str, Any]:
"""
Reset all bank-level configuration overrides, reverting to server defaults.
Reset all bank-level config overrides (sync wrapper — use ``await client.banks.reset_bank_config(...)`` in async code).
Can be disabled on the server by setting ``HINDSIGHT_API_ENABLE_BANK_CONFIG_API=false``.
@@ -1155,7 +1306,7 @@ class Hindsight:
def delete_bank(self, bank_id: str):
"""
Delete a memory bank.
Delete a memory bank (sync wrapper — prefer :meth:`adelete_bank` in async code).
Args:
bank_id: The memory bank ID
@@ -1164,7 +1315,7 @@ class Hindsight:
async def adelete_bank(self, bank_id: str):
"""
Delete a memory bank (async).
Delete a memory bank (async — preferred over :meth:`delete_bank`).
Args:
bank_id: The memory bank ID