diff --git a/hindsight-api-slim/hindsight_api/engine/retain/orchestrator.py b/hindsight-api-slim/hindsight_api/engine/retain/orchestrator.py index 54a8d735..74f683b5 100644 --- a/hindsight-api-slim/hindsight_api/engine/retain/orchestrator.py +++ b/hindsight-api-slim/hindsight_api/engine/retain/orchestrator.py @@ -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 diff --git a/hindsight-clients/python/hindsight_client/__init__.py b/hindsight-clients/python/hindsight_client/__init__.py index 116508a6..cdbb8f3d 100644 --- a/hindsight-clients/python/hindsight_client/__init__.py +++ b/hindsight-clients/python/hindsight_client/__init__.py @@ -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 diff --git a/hindsight-clients/python/hindsight_client/hindsight_client.py b/hindsight-clients/python/hindsight_client/hindsight_client.py index c88768a9..42ddc894 100644 --- a/hindsight-clients/python/hindsight_client/hindsight_client.py +++ b/hindsight-clients/python/hindsight_client/hindsight_client.py @@ -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