diff --git a/hindsight-api-slim/hindsight_api/engine/retain/orchestrator.py b/hindsight-api-slim/hindsight_api/engine/retain/orchestrator.py index 54a8d735a..74f683b5c 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 _run_db_work() -> None: 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 _run_db_work() -> None: # 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 _run_db_work() -> None: 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 _run_delta_db_work() -> None: 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 _run_delta_db_work() -> None: # 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 _run_delta_db_work() -> None: 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 116508a62..cdbb8f3d4 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 @@ # 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 c88768a98..42ddc8944 100644 --- a/hindsight-clients/python/hindsight_client/hindsight_client.py +++ b/hindsight-clients/python/hindsight_client/hindsight_client.py @@ -12,7 +12,18 @@ 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 - from hindsight_client import Hindsight + 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.** - # Without authentication - client = Hindsight(base_url="http://localhost:8888") + Example — async (preferred):: + + from hindsight_client import Hindsight - # With API key authentication client = Hindsight(base_url="http://localhost:8888", api_key="your-api-key") - # Store a memory - client.retain(bank_id="alice", content="Alice loves AI") + # 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?") - # Recall memories + Example — sync (scripts / REPLs only):: + + # Outside an async context — sync wrappers are available + client.retain(bank_id="alice", content="Alice loves AI") 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 @@ def __init__(self, base_url: str, api_key: str | None = None, timeout: float = 3 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 @@ def retain( 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 @@ def retain_batch( 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 @@ def retain_batch( 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 @@ def retain_files( 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 @@ def recall( 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 @@ def recall( 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, - ) - - 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.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, + ) ) - 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 @@ def reflect( 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 @@ def reflect( "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 @@ def list_memories( 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 @@ def create_bank( 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 @@ def create_bank( 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 @@ def create_bank( retain_chunk_size=retain_chunk_size, enable_observations=enable_observations, observations_mission=observations_mission, + background=background, ) ) @@ -456,6 +556,7 @@ async def _acreate_bank( 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 @@ async def _acreate_bank( 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 @@ async def acreate_bank( 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 @@ async def acreate_bank( 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 @@ async def acreate_bank( 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 @@ async def aretain_batch( 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 @@ async def aretain_batch( 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 @@ async def aretain_batch( 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 @@ async def aretain_batch( 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 @@ async def aretain( 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 @@ async def arecall( 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 @@ async def arecall( 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 @@ async def arecall( 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 @@ async def arecall( 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 @@ async def areflect( 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 @@ async def areflect( 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 @@ async def areflect( 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 @@ def create_mental_model( 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 @@ def create_mental_model( 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 @@ def list_mental_models(self, bank_id: str, tags: list[str] | None = None): 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 @@ def get_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: bank_id: The memory bank ID @@ -876,7 +1027,7 @@ def update_mental_model( 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 @@ def update_mental_model( 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 @@ def delete_mental_model(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 ``previous_content`` and ``changed_at`` fields. @@ -941,7 +1092,7 @@ def create_directive( 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 @@ def create_directive( 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 @@ def list_directives(self, bank_id: str, tags: list[str] | None = None): 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 @@ def update_directive( 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 @@ def update_directive( 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 @@ def delete_directive(self, bank_id: str, directive_id: str): 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 @@ def update_bank_config( 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 @@ async def _aupdate_bank_config(self, bank_id: str, updates: dict[str, Any]) -> d 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 @@ async def _areset_bank_config(self, bank_id: str) -> dict[str, Any]: 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 @@ def delete_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: bank_id: The memory bank ID