docs(python-client): async-first pydoc + low-level API access + missing params (#711)

* 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)

* style(retain): format long function call arguments one-per-line
This commit is contained in:
Nicolò Boschi 2026-03-26 16:09:58 +01:00 committed by GitHub
parent 200bab233e
commit a0cea84d82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 444 additions and 158 deletions

View file

@ -127,9 +127,7 @@ async def _insert_facts_and_links(
if unit_ids: if unit_ids:
# Process entities # Process entities
step_start = time.time() step_start = time.time()
user_entities_per_content = { user_entities_per_content = {idx: content.entities for idx, content in enumerate(contents) if content.entities}
idx: content.entities for idx, content in enumerate(contents) if content.entities
}
entity_links = await entity_processing.process_entities_batch( entity_links = await entity_processing.process_entities_batch(
entity_resolver, entity_resolver,
conn, conn,
@ -217,9 +215,7 @@ async def _extract_and_embed(
embeddings = await embedding_processing.generate_embeddings_batch(embeddings_model, augmented_texts) 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") log_buffer.append(f" Generate embeddings: {len(embeddings)} embeddings in {time.time() - step_start:.3f}s")
processed_facts = [ processed_facts = [ProcessedFact.from_extracted_fact(ef, emb) for ef, emb in zip(extracted_facts, embeddings)]
ProcessedFact.from_extracted_fact(ef, emb) for ef, emb in zip(extracted_facts, embeddings)
]
return extracted_facts, processed_facts, chunks, usage return extracted_facts, processed_facts, chunks, usage
@ -268,23 +264,56 @@ async def retain_batch(
# --- Delta retain: check if we can skip unchanged chunks --- # --- Delta retain: check if we can skip unchanged chunks ---
if is_first_batch: if is_first_batch:
delta_result = await _try_delta_retain( delta_result = await _try_delta_retain(
pool, embeddings_model, llm_config, entity_resolver, format_date_fn, pool,
bank_id, contents_dicts, contents, config, document_id, fact_type_override, embeddings_model,
document_tags, agent_name, log_buffer, start_time, operation_id, schema, outbox_callback, 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: if delta_result is not None:
return delta_result return delta_result
# --- Full retain path --- # --- Full retain path ---
extracted_facts, processed_facts, chunks, usage = await _extract_and_embed( extracted_facts, processed_facts, chunks, usage = await _extract_and_embed(
contents, llm_config, agent_name, config, embeddings_model, format_date_fn, contents,
fact_type_override, log_buffer, pool, operation_id, schema, llm_config,
agent_name,
config,
embeddings_model,
format_date_fn,
fact_type_override,
log_buffer,
pool,
operation_id,
schema,
) )
if not extracted_facts: if not extracted_facts:
await _handle_zero_facts_documents( await _handle_zero_facts_documents(
pool, bank_id, contents_dicts, contents, config, document_id, pool,
is_first_batch, document_tags, chunks, log_buffer, start_time, bank_id,
contents_dicts,
contents,
config,
document_id,
is_first_batch,
document_tags,
chunks,
log_buffer,
start_time,
) )
return [[] for _ in contents], usage return [[] for _ in contents], usage
@ -336,8 +365,13 @@ async def retain_batch(
contents_dicts, document_tags, doc_contents=doc_contents contents_dicts, document_tags, doc_contents=doc_contents
) )
await fact_storage.handle_document_tracking( await fact_storage.handle_document_tracking(
conn, bank_id, actual_doc_id, combined_content, conn,
is_first_batch, retain_params, merged_tags, bank_id,
actual_doc_id,
combined_content,
is_first_batch,
retain_params,
merged_tags,
) )
document_ids_added.append(actual_doc_id) document_ids_added.append(actual_doc_id)
@ -382,8 +416,15 @@ async def retain_batch(
# Insert facts and create all links (shared pipeline) # Insert facts and create all links (shared pipeline)
result_unit_ids = await _insert_facts_and_links( result_unit_ids = await _insert_facts_and_links(
conn, entity_resolver, bank_id, contents, extracted_facts, conn,
processed_facts, config, log_buffer, outbox_callback, entity_resolver,
bank_id,
contents,
extracted_facts,
processed_facts,
config,
log_buffer,
outbox_callback,
) )
await entity_resolver.flush_pending_stats() await entity_resolver.flush_pending_stats()
@ -406,9 +447,24 @@ async def retain_batch(
async def _try_delta_retain( async def _try_delta_retain(
pool, embeddings_model, llm_config, entity_resolver, format_date_fn, pool,
bank_id, contents_dicts, contents, config, document_id, fact_type_override, embeddings_model,
document_tags, agent_name, log_buffer, start_time, operation_id, schema, outbox_callback, 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 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 # Nothing changed — just update document metadata/tags
log_buffer.append("[delta] No chunk changes detected — updating document metadata only") log_buffer.append("[delta] No chunk changes detected — updating document metadata only")
return await _delta_metadata_only( return await _delta_metadata_only(
pool, bank_id, contents_dicts, contents, effective_doc_id, pool,
document_tags, log_buffer, start_time, outbox_callback, 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 # Build content items for only the changed/new chunks
@ -481,14 +544,30 @@ async def _try_delta_retain(
if not delta_contents: if not delta_contents:
return await _delta_metadata_only( return await _delta_metadata_only(
pool, bank_id, contents_dicts, contents, effective_doc_id, pool,
document_tags, log_buffer, start_time, outbox_callback, bank_id,
contents_dicts,
contents,
effective_doc_id,
document_tags,
log_buffer,
start_time,
outbox_callback,
) )
# Extract facts and generate embeddings (shared pipeline) # Extract facts and generate embeddings (shared pipeline)
extracted_facts, processed_facts, new_chunk_metadata, usage = await _extract_and_embed( extracted_facts, processed_facts, new_chunk_metadata, usage = await _extract_and_embed(
delta_contents, llm_config, agent_name, config, embeddings_model, format_date_fn, delta_contents,
fact_type_override, log_buffer, pool, operation_id, schema, llm_config,
agent_name,
config,
embeddings_model,
format_date_fn,
fact_type_override,
log_buffer,
pool,
operation_id,
schema,
) )
# Database transaction # Database transaction
@ -510,7 +589,12 @@ async def _try_delta_retain(
combined_content = "\n".join([c.get("content", "") for c in contents_dicts]) combined_content = "\n".join([c.get("content", "") for c in contents_dicts])
retain_params, merged_tags = _build_retain_params(contents_dicts, document_tags) retain_params, merged_tags = _build_retain_params(contents_dicts, document_tags)
await fact_storage.upsert_document_metadata( 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") 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) # Insert facts and create all links (shared pipeline)
result_unit_ids = await _insert_facts_and_links( result_unit_ids = await _insert_facts_and_links(
conn, entity_resolver, bank_id, contents, extracted_facts, conn,
processed_facts, config, log_buffer, outbox_callback, entity_resolver,
bank_id,
contents,
extracted_facts,
processed_facts,
config,
log_buffer,
outbox_callback,
) )
await entity_resolver.flush_pending_stats() await entity_resolver.flush_pending_stats()
@ -591,8 +682,15 @@ async def _try_delta_retain(
async def _delta_metadata_only( async def _delta_metadata_only(
pool, bank_id, contents_dicts, contents, document_id, document_tags, pool,
log_buffer, start_time, outbox_callback, 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.""" """Handle the case where no chunks changed — just update document metadata and tags."""
async with acquire_with_retry(pool) as conn: 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]) combined_content = "\n".join([c.get("content", "") for c in contents_dicts])
retain_params, merged_tags = _build_retain_params(contents_dicts, document_tags) retain_params, merged_tags = _build_retain_params(contents_dicts, document_tags)
await fact_storage.upsert_document_metadata( 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) await fact_storage.update_memory_units_tags(conn, bank_id, document_id, merged_tags)
if outbox_callback: if outbox_callback:
@ -645,8 +748,17 @@ def _build_contents(contents_dicts: list[RetainContentDict], document_tags: list
async def _handle_zero_facts_documents( async def _handle_zero_facts_documents(
pool, bank_id, contents_dicts, contents, config, document_id, pool,
is_first_batch, document_tags, chunks, log_buffer, start_time, 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.""" """Handle document tracking when zero facts were extracted."""
docs_tracked = 0 docs_tracked = 0
@ -677,8 +789,13 @@ async def _handle_zero_facts_documents(
contents_dicts, document_tags, doc_contents=doc_contents contents_dicts, document_tags, doc_contents=doc_contents
) )
await fact_storage.handle_document_tracking( await fact_storage.handle_document_tracking(
conn, bank_id, actual_doc_id, combined_content, conn,
is_first_batch, retain_params, merged_tags, bank_id,
actual_doc_id,
combined_content,
is_first_batch,
retain_params,
merged_tags,
) )
docs_tracked += 1 docs_tracked += 1

View file

@ -1,18 +1,24 @@
""" """
Hindsight Client - Clean, pythonic wrapper for the Hindsight API. Hindsight Client - Clean, pythonic wrapper for the Hindsight API.
This package provides a high-level interface for common Hindsight operations. This package provides a high-level ``Hindsight`` class with simplified methods
For advanced use cases, use the auto-generated API client directly. 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 from hindsight_client import Hindsight
client = Hindsight(base_url="http://localhost:8888") client = Hindsight(base_url="http://localhost:8888")
# Store a memory # Store a memory
result = client.retain(bank_id="alice", content="Alice loves AI") client.retain(bank_id="alice", content="Alice loves AI")
print(result.success)
# Search memories # Search memories
response = client.recall(bank_id="alice", query="What does Alice like?") response = client.recall(bank_id="alice", query="What does Alice like?")
@ -22,7 +28,19 @@ Example:
# Generate contextual answer # Generate contextual answer
answer = client.reflect(bank_id="alice", query="What are my interests?") answer = client.reflect(bank_id="alice", query="What are my interests?")
print(answer.text) 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 from hindsight_client_api.models.bank_profile_response import BankProfileResponse

View file

@ -12,7 +12,18 @@ from pathlib import Path
from typing import Any, Literal from typing import Any, Literal
import hindsight_client_api 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 ( from hindsight_client_api.models import (
memory_item, memory_item,
recall_request, recall_request,
@ -44,27 +55,68 @@ class Hindsight:
""" """
High-level, easy-to-use Hindsight API client. High-level, easy-to-use Hindsight API client.
Example: This class provides simplified methods for the most common operations:
```python 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 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") 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") client.retain(bank_id="alice", content="Alice loves AI")
# Recall memories
response = client.recall(bank_id="alice", query="What does Alice like?") response = client.recall(bank_id="alice", query="What does Alice like?")
for r in response.results:
print(r.text)
# Generate contextual answer Example low-level API for advanced operations::
answer = client.reflect(bank_id="alice", query="What are my interests?")
``` # 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): 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._mental_models_api = mental_models_api.MentalModelsApi(self._api_client)
self._directives_api = directives_api.DirectivesApi(self._api_client) self._directives_api = directives_api.DirectivesApi(self._api_client)
self._files_api = files_api.FilesApi(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): def __enter__(self):
"""Context manager entry.""" """Context manager entry."""
@ -128,7 +240,7 @@ class Hindsight:
tags: list[str] | None = None, tags: list[str] | None = None,
) -> RetainResponse: ) -> RetainResponse:
""" """
Store a single memory (simplified interface). Store a single memory (sync wrapper prefer :meth:`aretain` in async code).
Args: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -167,11 +279,13 @@ class Hindsight:
retain_async: bool = False, retain_async: bool = False,
) -> RetainResponse: ) -> RetainResponse:
""" """
Store multiple memories in batch. Store multiple memories in batch (sync wrapper prefer :meth:`aretain_batch` in async code).
Args: Args:
bank_id: The memory bank ID 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_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) 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) retain_async: If True, process asynchronously in background (default: False)
@ -179,37 +293,16 @@ class Hindsight:
Returns: Returns:
RetainResponse with success status and item count RetainResponse with success status and item count
""" """
from hindsight_client_api.models.entity_input import EntityInput return _run_async(
from hindsight_client_api.models.timestamp import Timestamp self.aretain_batch(
bank_id=bank_id,
memory_items = [] items=items,
for item in items: document_id=document_id,
entities = None document_tags=document_tags,
if item.get("entities"): retain_async=retain_async,
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"),
)
) )
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( def retain_files(
self, self,
bank_id: str, bank_id: str,
@ -218,7 +311,7 @@ class Hindsight:
files_metadata: list[dict[str, Any]] | None = None, files_metadata: list[dict[str, Any]] | None = None,
) -> FileRetainResponse: ) -> 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 Files are automatically converted to text (PDF, DOCX, images via OCR, audio via
transcription, and more) and ingested as memories. Processing is always asynchronous transcription, and more) and ingested as memories. Processing is always asynchronous
@ -262,9 +355,10 @@ class Hindsight:
max_source_facts_tokens: int = 4096, max_source_facts_tokens: int = 4096,
tags: list[str] | None = None, tags: list[str] | None = None,
tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any", tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any",
tag_groups: list[dict[str, Any]] | None = None,
) -> RecallResponse: ) -> RecallResponse:
""" """
Recall memories using semantic similarity. Recall memories using semantic similarity (sync wrapper prefer :meth:`arecall` in async code).
Args: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -283,41 +377,32 @@ class Hindsight:
tags: Optional list of tags to filter memories by tags: Optional list of tags to filter memories by
tags_match: How to match tags - "any" (OR, includes untagged), "all" (AND, includes untagged), 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" "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: Returns:
RecallResponse with results, optional entities, optional chunks, optional source_facts, and optional trace RecallResponse with results, optional entities, optional chunks, optional source_facts, and optional trace
""" """
from hindsight_client_api.models import ( return _run_async(
chunk_include_options, self.arecall(
entity_include_options, bank_id=bank_id,
include_options, query=query,
source_facts_include_options, 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( def reflect(
self, self,
bank_id: str, bank_id: str,
@ -329,9 +414,13 @@ class Hindsight:
tags: list[str] | None = None, tags: list[str] | None = None,
tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any", tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any",
include_facts: bool = False, 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: ) -> 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: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -347,25 +436,33 @@ class Hindsight:
"any_strict" (OR, excludes untagged), "all_strict" (AND, excludes untagged). Default: "any" "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 include_facts: If True, the response will include a 'based_on' field listing
the memories, mental models, and directives used to construct the answer. 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: Returns:
ReflectResponse with answer text, optionally facts used, and optionally ReflectResponse with answer text, optionally facts used, and optionally
structured_output if response_schema was provided structured_output if response_schema was provided
""" """
include = ReflectIncludeOptions(facts={}) if include_facts else None return _run_async(
request_obj = reflect_request.ReflectRequest( self.areflect(
query=query, bank_id=bank_id,
budget=budget, query=query,
context=context, budget=budget,
max_tokens=max_tokens, context=context,
response_schema=response_schema, max_tokens=max_tokens,
tags=tags, response_schema=response_schema,
tags_match=tags_match, tags=tags,
include=include, 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( def list_memories(
self, self,
bank_id: str, bank_id: str,
@ -374,7 +471,7 @@ class Hindsight:
limit: int = 100, limit: int = 100,
offset: int = 0, offset: int = 0,
) -> ListMemoryUnitsResponse: ) -> 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( return _run_async(
self._memory_api.list_memories( self._memory_api.list_memories(
bank_id=bank_id, bank_id=bank_id,
@ -402,8 +499,9 @@ class Hindsight:
enable_observations: bool | None = None, enable_observations: bool | None = None,
observations_mission: str | None = None, observations_mission: str | None = None,
reflect_mission: str | None = None, reflect_mission: str | None = None,
background: str | None = None,
) -> BankProfileResponse: ) -> BankProfileResponse:
"""Create or update a memory bank. """Create or update a memory bank (sync wrapper — prefer :meth:`acreate_bank` in async code).
Args: Args:
bank_id: Unique identifier for the bank bank_id: Unique identifier for the bank
@ -420,6 +518,7 @@ class Hindsight:
enable_observations: Toggle automatic observation consolidation after retain(). enable_observations: Toggle automatic observation consolidation after retain().
observations_mission: Controls what gets synthesised into observations. Replaces built-in rules. observations_mission: Controls what gets synthesised into observations. Replaces built-in rules.
reflect_mission: Mission/context for Reflect operations. reflect_mission: Mission/context for Reflect operations.
background: Optional background context for the bank.
""" """
return _run_async( return _run_async(
self._acreate_bank( self._acreate_bank(
@ -437,6 +536,7 @@ class Hindsight:
retain_chunk_size=retain_chunk_size, retain_chunk_size=retain_chunk_size,
enable_observations=enable_observations, enable_observations=enable_observations,
observations_mission=observations_mission, observations_mission=observations_mission,
background=background,
) )
) )
@ -456,6 +556,7 @@ class Hindsight:
retain_chunk_size: int | None = None, retain_chunk_size: int | None = None,
enable_observations: bool | None = None, enable_observations: bool | None = None,
observations_mission: str | None = None, observations_mission: str | None = None,
background: str | None = None,
) -> BankProfileResponse: ) -> BankProfileResponse:
import aiohttp import aiohttp
@ -466,6 +567,8 @@ class Hindsight:
body["mission"] = mission body["mission"] = mission
if reflect_mission is not None: if reflect_mission is not None:
body["reflect_mission"] = reflect_mission body["reflect_mission"] = reflect_mission
if background is not None:
body["background"] = background
# Individual disposition fields take priority over legacy disposition dict # Individual disposition fields take priority over legacy disposition dict
if disposition_skepticism is not None: if disposition_skepticism is not None:
body["disposition_skepticism"] = disposition_skepticism body["disposition_skepticism"] = disposition_skepticism
@ -528,8 +631,9 @@ class Hindsight:
enable_observations: bool | None = None, enable_observations: bool | None = None,
observations_mission: str | None = None, observations_mission: str | None = None,
reflect_mission: str | None = None, reflect_mission: str | None = None,
background: str | None = None,
) -> BankProfileResponse: ) -> BankProfileResponse:
"""Create or update a memory bank (async). """Create or update a memory bank (async — preferred over :meth:`create_bank`).
Args: Args:
bank_id: Unique identifier for the bank bank_id: Unique identifier for the bank
@ -546,6 +650,7 @@ class Hindsight:
enable_observations: Toggle automatic observation consolidation after retain(). enable_observations: Toggle automatic observation consolidation after retain().
observations_mission: Controls what gets synthesised into observations. Replaces built-in rules. observations_mission: Controls what gets synthesised into observations. Replaces built-in rules.
reflect_mission: Mission/context for Reflect operations. reflect_mission: Mission/context for Reflect operations.
background: Optional background context for the bank.
""" """
return await self._acreate_bank( return await self._acreate_bank(
bank_id, bank_id,
@ -562,6 +667,7 @@ class Hindsight:
retain_chunk_size=retain_chunk_size, retain_chunk_size=retain_chunk_size,
enable_observations=enable_observations, enable_observations=enable_observations,
observations_mission=observations_mission, observations_mission=observations_mission,
background=background,
) )
async def aset_mission(self, bank_id: str, mission: str) -> dict[str, Any]: async def aset_mission(self, bank_id: str, mission: str) -> dict[str, Any]:
@ -581,11 +687,13 @@ class Hindsight:
retain_async: bool = False, retain_async: bool = False,
) -> RetainResponse: ) -> RetainResponse:
""" """
Store multiple memories in batch (async). Store multiple memories in batch (async preferred over :meth:`retain_batch`).
Args: Args:
bank_id: The memory bank ID 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_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) 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) retain_async: If True, process asynchronously in background (default: False)
@ -594,6 +702,7 @@ class Hindsight:
RetainResponse with success status and item count RetainResponse with success status and item count
""" """
from hindsight_client_api.models.entity_input import EntityInput 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 from hindsight_client_api.models.timestamp import Timestamp
memory_items = [] memory_items = []
@ -603,6 +712,9 @@ class Hindsight:
entities = [EntityInput(text=e["text"], type=e.get("type")) for e in item["entities"]] entities = [EntityInput(text=e["text"], type=e.get("type")) for e in item["entities"]]
raw_ts = item.get("timestamp") raw_ts = item.get("timestamp")
timestamp_val = Timestamp(actual_instance=raw_ts) if raw_ts is not None else None 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_items.append(
memory_item.MemoryItem( memory_item.MemoryItem(
content=item["content"], content=item["content"],
@ -613,6 +725,8 @@ class Hindsight:
document_id=item.get("document_id") or document_id, document_id=item.get("document_id") or document_id,
entities=entities, entities=entities,
tags=item.get("tags"), tags=item.get("tags"),
observation_scopes=obs_scopes,
strategy=item.get("strategy"),
) )
) )
@ -636,7 +750,7 @@ class Hindsight:
tags: list[str] | None = None, tags: list[str] | None = None,
) -> RetainResponse: ) -> RetainResponse:
""" """
Store a single memory (async). Store a single memory (async preferred over :meth:`retain`).
Args: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -683,9 +797,10 @@ class Hindsight:
max_source_facts_tokens: int = 4096, max_source_facts_tokens: int = 4096,
tags: list[str] | None = None, tags: list[str] | None = None,
tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any", tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any",
tag_groups: list[dict[str, Any]] | None = None,
) -> RecallResponse: ) -> RecallResponse:
""" """
Recall memories using semantic similarity (async). Recall memories using semantic similarity (async preferred over :meth:`recall`).
Args: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -704,6 +819,11 @@ class Hindsight:
tags: Optional list of tags to filter memories by tags: Optional list of tags to filter memories by
tags_match: How to match tags - "any" (OR, includes untagged), "all" (AND, includes untagged), 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" "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: Returns:
RecallResponse with results, optional entities, optional chunks, optional source_facts, and optional trace RecallResponse with results, optional entities, optional chunks, optional source_facts, and optional trace
@ -725,6 +845,12 @@ class Hindsight:
else None, 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( request_obj = recall_request.RecallRequest(
query=query, query=query,
types=types, types=types,
@ -735,6 +861,7 @@ class Hindsight:
include=include_opts, include=include_opts,
tags=tags, tags=tags,
tags_match=tags_match, tags_match=tags_match,
tag_groups=tag_groups_objs,
) )
return await self._memory_api.recall_memories(bank_id, request_obj, _request_timeout=self._timeout) 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, response_schema: dict[str, Any] | None = None,
tags: list[str] | None = None, tags: list[str] | None = None,
tags_match: Literal["any", "all", "any_strict", "all_strict"] = "any", 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: ) -> 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: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -765,11 +897,25 @@ class Hindsight:
tags: Optional list of tags to filter memories by tags: Optional list of tags to filter memories by
tags_match: How to match tags - "any" (OR, includes untagged), "all" (AND, includes untagged), 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" "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: Returns:
ReflectResponse with answer text, optionally facts used, and optionally ReflectResponse with answer text, optionally facts used, and optionally
structured_output if response_schema was provided 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( request_obj = reflect_request.ReflectRequest(
query=query, query=query,
budget=budget, budget=budget,
@ -778,6 +924,11 @@ class Hindsight:
response_schema=response_schema, response_schema=response_schema,
tags=tags, tags=tags,
tags_match=tags_match, 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) return await self._memory_api.reflect(bank_id, request_obj, _request_timeout=self._timeout)
@ -795,7 +946,7 @@ class Hindsight:
id: str | None = None, 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: Args:
bank_id: The memory bank ID 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): 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: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -841,7 +992,7 @@ class Hindsight:
def get_mental_model(self, bank_id: str, mental_model_id: str): 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: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -854,7 +1005,7 @@ class Hindsight:
def refresh_mental_model(self, bank_id: str, mental_model_id: str): 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: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -876,7 +1027,7 @@ class Hindsight:
trigger: dict[str, Any] | None = None, 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: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -908,7 +1059,7 @@ class Hindsight:
def delete_mental_model(self, bank_id: str, mental_model_id: str): 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: Args:
bank_id: The memory bank ID 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): 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 Returns a list of history entries (most recent first), each with
``previous_content`` and ``changed_at`` fields. ``previous_content`` and ``changed_at`` fields.
@ -941,7 +1092,7 @@ class Hindsight:
tags: list[str] | None = None, 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: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -968,7 +1119,7 @@ class Hindsight:
def list_directives(self, bank_id: str, tags: list[str] | None = None): 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: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -981,7 +1132,7 @@ class Hindsight:
def get_directive(self, bank_id: str, directive_id: str): 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: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -1003,7 +1154,7 @@ class Hindsight:
tags: list[str] | None = None, tags: list[str] | None = None,
): ):
""" """
Update a directive. Update a directive (sync wrapper use ``await client.directives.update_directive(...)`` in async code).
Args: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -1031,7 +1182,7 @@ class Hindsight:
def delete_directive(self, bank_id: str, directive_id: str): 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: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -1041,7 +1192,7 @@ class Hindsight:
def get_bank_config(self, bank_id: str) -> dict[str, Any]: 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``. 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, disposition_empathy: int | None = None,
) -> dict[str, Any]: ) -> 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``. 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]: 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``. 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): 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: Args:
bank_id: The memory bank ID bank_id: The memory bank ID
@ -1164,7 +1315,7 @@ class Hindsight:
async def adelete_bank(self, bank_id: str): async def adelete_bank(self, bank_id: str):
""" """
Delete a memory bank (async). Delete a memory bank (async preferred over :meth:`delete_bank`).
Args: Args:
bank_id: The memory bank ID bank_id: The memory bank ID