fleet-memory/hindsight-clients/python/hindsight_client/__init__.py
Nicolò Boschi 37348c859e
feat: include occurred_end and mentioned_at in think-prompt fact serialization (#929)
Extend format_facts_for_prompt() to include occurred_end and mentioned_at
temporal fields (when non-null), matching the MemoryFact model. Also add
RecallResponse.to_prompt_string() to Python and TypeScript client SDKs so
users can serialize recall results (with chunks and entity summaries) into
LLM-ready prompt strings.

Closes #924
2026-04-08 10:33:14 +02:00

162 lines
5.4 KiB
Python

"""
Hindsight Client - Clean, pythonic wrapper for the Hindsight API.
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::
from hindsight_client import Hindsight
client = Hindsight(base_url="http://localhost:8888")
# Store a memory
client.retain(bank_id="alice", content="Alice loves AI")
# Search 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?")
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.disposition_traits import DispositionTraits
from hindsight_client_api.models.list_memory_units_response import ListMemoryUnitsResponse
from hindsight_client_api.models.recall_response import RecallResponse as _RecallResponse
from hindsight_client_api.models.recall_result import RecallResult as _RecallResult
from hindsight_client_api.models.reflect_fact import ReflectFact
from hindsight_client_api.models.reflect_response import ReflectResponse
# Re-export response types for convenient access
from hindsight_client_api.models.retain_response import RetainResponse
from .hindsight_client import Hindsight
# Add cleaner __repr__ and __iter__ for REPL usability
def _recall_result_repr(self):
text_preview = self.text[:80] + "..." if len(self.text) > 80 else self.text
return f"RecallResult(id='{self.id[:8]}...', type='{self.type}', text='{text_preview}')"
def _recall_response_repr(self):
count = len(self.results) if self.results else 0
extras = []
if self.trace:
extras.append("trace=True")
if self.entities:
extras.append(f"entities={len(self.entities)}")
if self.chunks:
extras.append(f"chunks={len(self.chunks)}")
extras_str = ", " + ", ".join(extras) if extras else ""
return f"RecallResponse({count} results{extras_str})"
def _recall_response_iter(self):
"""Iterate directly over results for convenience."""
return iter(self.results or [])
def _recall_response_len(self):
"""Return number of results."""
return len(self.results) if self.results else 0
def _recall_response_getitem(self, index):
"""Access results by index."""
return self.results[index]
def _recall_response_to_prompt_string(self) -> str:
"""Serialize the recall response to a string suitable for LLM prompts.
Builds a prompt containing:
- Facts: each result as a JSON object with ``text``, ``context``, and
temporal fields (``occurred_start``, ``occurred_end``, ``mentioned_at``).
If the result has a ``chunk_id`` matching a chunk in the response, the
chunk text is included as ``source_chunk``.
- Entities: entity summaries from observations, formatted as sections.
This mirrors the format used internally by Hindsight's reflect operation.
"""
import json
chunks_map = self.chunks or {}
sections: list[str] = []
# Facts
formatted_facts: list[dict] = []
for result in self.results or []:
fact_obj: dict = {"text": result.text}
if result.context:
fact_obj["context"] = result.context
for field in ("occurred_start", "occurred_end", "mentioned_at"):
value = getattr(result, field, None)
if value:
fact_obj[field] = value
if result.chunk_id and result.chunk_id in chunks_map:
fact_obj["source_chunk"] = chunks_map[result.chunk_id].text
formatted_facts.append(fact_obj)
sections.append("FACTS:\n" + json.dumps(formatted_facts, indent=2))
# Entities
if self.entities:
entity_parts: list[str] = []
for name, state in self.entities.items():
if state.observations:
obs_text = state.observations[0].text
entity_parts.append(f"## {name}\n{obs_text}")
if entity_parts:
sections.append("ENTITIES:\n" + "\n\n".join(entity_parts))
return "\n\n".join(sections)
_RecallResponse.to_prompt_string = _recall_response_to_prompt_string
_RecallResult.__repr__ = _recall_result_repr
_RecallResponse.__repr__ = _recall_response_repr
_RecallResponse.__iter__ = _recall_response_iter
_RecallResponse.__len__ = _recall_response_len
_RecallResponse.__getitem__ = _recall_response_getitem
# Re-export with patched repr
RecallResult = _RecallResult
RecallResponse = _RecallResponse
__all__ = [
"Hindsight",
# Response types
"RetainResponse",
"RecallResponse",
"RecallResult",
"ReflectResponse",
"ReflectFact",
"ListMemoryUnitsResponse",
"BankProfileResponse",
"DispositionTraits",
]