improve docs
This commit is contained in:
parent
f42476bf94
commit
94665b2111
16 changed files with 529 additions and 1183 deletions
|
|
@ -1760,7 +1760,7 @@ def _register_routes(app: FastAPI):
|
|||
|
||||
# Submit task to background queue
|
||||
await app.state.memory._task_backend.submit_task({
|
||||
'type': 'batch_put',
|
||||
'type': 'batch_retain',
|
||||
'operation_id': str(operation_id),
|
||||
'bank_id': bank_id,
|
||||
'contents': contents
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ import time
|
|||
import asyncio
|
||||
from typing import Optional, Any, Dict, List
|
||||
from openai import AsyncOpenAI, RateLimitError, APIError, APIStatusError, APIConnectionError, LengthFinishReasonError
|
||||
from google import genai
|
||||
from google.genai import types as genai_types
|
||||
from google.genai import errors as genai_errors
|
||||
import logging
|
||||
|
||||
# Seed applied to every Groq request for deterministic behavior.
|
||||
|
|
@ -33,9 +36,9 @@ class OutputTooLongError(Exception):
|
|||
|
||||
class LLMProvider:
|
||||
"""
|
||||
Unified LLM provider using OpenAI-compatible API.
|
||||
Unified LLM provider.
|
||||
|
||||
Supports OpenAI, Groq, and Ollama (any OpenAI-compatible endpoint).
|
||||
Supports OpenAI, Groq, Ollama (OpenAI-compatible), and Gemini.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
|
|
@ -50,7 +53,7 @@ class LLMProvider:
|
|||
Initialize LLM provider.
|
||||
|
||||
Args:
|
||||
provider: Provider name ("openai", "groq", "ollama").
|
||||
provider: Provider name ("openai", "groq", "ollama", "gemini").
|
||||
api_key: API key.
|
||||
base_url: Base URL for the API.
|
||||
model: Model name.
|
||||
|
|
@ -63,7 +66,7 @@ class LLMProvider:
|
|||
self.reasoning_effort = reasoning_effort
|
||||
|
||||
# Validate provider
|
||||
valid_providers = ["openai", "groq", "ollama"]
|
||||
valid_providers = ["openai", "groq", "ollama", "gemini"]
|
||||
if self.provider not in valid_providers:
|
||||
raise ValueError(
|
||||
f"Invalid LLM provider: {self.provider}. Must be one of: {', '.join(valid_providers)}"
|
||||
|
|
@ -80,11 +83,16 @@ class LLMProvider:
|
|||
if self.provider != "ollama" and not self.api_key:
|
||||
raise ValueError(f"API key not found for {self.provider}")
|
||||
|
||||
# Create OpenAI-compatible client for all providers
|
||||
if self.provider == "ollama":
|
||||
# Create client based on provider
|
||||
if self.provider == "gemini":
|
||||
self._gemini_client = genai.Client(api_key=self.api_key)
|
||||
self._client = None
|
||||
elif self.provider == "ollama":
|
||||
self._client = AsyncOpenAI(api_key="ollama", base_url=self.base_url, max_retries=0)
|
||||
self._gemini_client = None
|
||||
else:
|
||||
self._client = AsyncOpenAI(api_key=self.api_key, base_url=self.base_url, max_retries=0)
|
||||
self._gemini_client = None
|
||||
|
||||
logger.info(
|
||||
f"Initialized LLM: provider={self.provider}, model={self.model}, base_url={self.base_url}"
|
||||
|
|
@ -127,6 +135,13 @@ class LLMProvider:
|
|||
start_time = time.time()
|
||||
import json
|
||||
|
||||
# Handle Gemini provider separately
|
||||
if self.provider == "gemini":
|
||||
return await self._call_gemini(
|
||||
messages, response_format, max_retries, initial_backoff,
|
||||
max_backoff, skip_validation, start_time
|
||||
)
|
||||
|
||||
call_params = {
|
||||
"model": self.model,
|
||||
"messages": messages,
|
||||
|
|
@ -233,6 +248,150 @@ class LLMProvider:
|
|||
raise last_exception
|
||||
raise RuntimeError(f"LLM call failed after all retries with no exception captured")
|
||||
|
||||
async def _call_gemini(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
response_format: Optional[Any],
|
||||
max_retries: int,
|
||||
initial_backoff: float,
|
||||
max_backoff: float,
|
||||
skip_validation: bool,
|
||||
start_time: float,
|
||||
) -> Any:
|
||||
"""Handle Gemini-specific API calls."""
|
||||
import json
|
||||
|
||||
# Convert OpenAI-style messages to Gemini format
|
||||
system_instruction = None
|
||||
gemini_contents = []
|
||||
|
||||
for msg in messages:
|
||||
role = msg.get('role', 'user')
|
||||
content = msg.get('content', '')
|
||||
|
||||
if role == 'system':
|
||||
if system_instruction:
|
||||
system_instruction += "\n\n" + content
|
||||
else:
|
||||
system_instruction = content
|
||||
elif role == 'assistant':
|
||||
gemini_contents.append(genai_types.Content(
|
||||
role="model",
|
||||
parts=[genai_types.Part(text=content)]
|
||||
))
|
||||
else:
|
||||
gemini_contents.append(genai_types.Content(
|
||||
role="user",
|
||||
parts=[genai_types.Part(text=content)]
|
||||
))
|
||||
|
||||
# Add JSON schema instruction if response_format is provided
|
||||
if response_format is not None and hasattr(response_format, 'model_json_schema'):
|
||||
schema = response_format.model_json_schema()
|
||||
schema_msg = f"\n\nYou must respond with valid JSON matching this schema:\n{json.dumps(schema, indent=2)}"
|
||||
if system_instruction:
|
||||
system_instruction += schema_msg
|
||||
else:
|
||||
system_instruction = schema_msg
|
||||
|
||||
# Build generation config
|
||||
config_kwargs = {}
|
||||
if system_instruction:
|
||||
config_kwargs['system_instruction'] = system_instruction
|
||||
if response_format is not None:
|
||||
config_kwargs['response_mime_type'] = 'application/json'
|
||||
config_kwargs['response_schema'] = response_format
|
||||
|
||||
generation_config = genai_types.GenerateContentConfig(**config_kwargs) if config_kwargs else None
|
||||
|
||||
last_exception = None
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
response = await self._gemini_client.aio.models.generate_content(
|
||||
model=self.model,
|
||||
contents=gemini_contents,
|
||||
config=generation_config,
|
||||
)
|
||||
|
||||
content = response.text
|
||||
|
||||
# Handle empty response
|
||||
if content is None:
|
||||
block_reason = None
|
||||
if hasattr(response, 'candidates') and response.candidates:
|
||||
candidate = response.candidates[0]
|
||||
if hasattr(candidate, 'finish_reason'):
|
||||
block_reason = candidate.finish_reason
|
||||
|
||||
if attempt < max_retries:
|
||||
logger.warning(f"Gemini returned empty response (reason: {block_reason}), retrying...")
|
||||
backoff = min(initial_backoff * (2 ** attempt), max_backoff)
|
||||
await asyncio.sleep(backoff)
|
||||
continue
|
||||
else:
|
||||
raise RuntimeError(f"Gemini returned empty response after {max_retries + 1} attempts")
|
||||
|
||||
if response_format is not None:
|
||||
json_data = json.loads(content)
|
||||
if skip_validation:
|
||||
result = json_data
|
||||
else:
|
||||
result = response_format.model_validate(json_data)
|
||||
else:
|
||||
result = content
|
||||
|
||||
# Log slow calls
|
||||
duration = time.time() - start_time
|
||||
if duration > 10.0 and hasattr(response, 'usage_metadata') and response.usage_metadata:
|
||||
usage = response.usage_metadata
|
||||
logger.info(
|
||||
f"slow llm call: model={self.provider}/{self.model}, "
|
||||
f"input_tokens={usage.prompt_token_count}, output_tokens={usage.candidates_token_count}, "
|
||||
f"time={duration:.3f}s"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
last_exception = e
|
||||
if attempt < max_retries:
|
||||
logger.warning(f"Gemini returned invalid JSON, retrying...")
|
||||
backoff = min(initial_backoff * (2 ** attempt), max_backoff)
|
||||
await asyncio.sleep(backoff)
|
||||
continue
|
||||
else:
|
||||
logger.error(f"Gemini returned invalid JSON after {max_retries + 1} attempts")
|
||||
raise
|
||||
|
||||
except genai_errors.APIError as e:
|
||||
# Fast fail on 4xx client errors (except 429 rate limit)
|
||||
if e.code and 400 <= e.code < 500 and e.code != 429:
|
||||
logger.error(f"Gemini client error (HTTP {e.code}), not retrying: {str(e)}")
|
||||
raise
|
||||
|
||||
# Retry on 429 and 5xx
|
||||
if e.code in (429, 500, 502, 503, 504):
|
||||
last_exception = e
|
||||
if attempt < max_retries:
|
||||
backoff = min(initial_backoff * (2 ** attempt), max_backoff)
|
||||
jitter = backoff * 0.2 * (2 * (time.time() % 1) - 1)
|
||||
await asyncio.sleep(backoff + jitter)
|
||||
else:
|
||||
logger.error(f"Gemini API error after {max_retries + 1} attempts: {str(e)}")
|
||||
raise
|
||||
else:
|
||||
logger.error(f"Gemini API error: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error during Gemini call: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
|
||||
if last_exception:
|
||||
raise last_exception
|
||||
raise RuntimeError(f"Gemini call failed after all retries")
|
||||
|
||||
@classmethod
|
||||
def for_memory(cls) -> "LLMProvider":
|
||||
"""Create provider for memory operations from environment variables."""
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ class MemoryEngine:
|
|||
await self._handle_reinforce_opinion(task_dict)
|
||||
elif task_type == 'form_opinion':
|
||||
await self._handle_form_opinion(task_dict)
|
||||
elif task_type == 'batch_put':
|
||||
elif task_type == 'batch_retain':
|
||||
await self._handle_batch_retain(task_dict)
|
||||
elif task_type == 'regenerate_observations':
|
||||
await self._handle_regenerate_observations(task_dict)
|
||||
|
|
|
|||
|
|
@ -157,7 +157,11 @@ class Hindsight:
|
|||
types: Optional[List[str]] = None,
|
||||
max_tokens: int = 4096,
|
||||
budget: str = "mid",
|
||||
) -> List[RecallResult]:
|
||||
trace: bool = False,
|
||||
query_timestamp: Optional[str] = None,
|
||||
include_entities: bool = False,
|
||||
max_entity_tokens: int = 500,
|
||||
) -> RecallResponse:
|
||||
"""
|
||||
Recall memories using semantic similarity.
|
||||
|
||||
|
|
@ -167,20 +171,31 @@ class Hindsight:
|
|||
types: Optional list of fact types to filter (world, experience, opinion, observation)
|
||||
max_tokens: Maximum tokens in results (default: 4096)
|
||||
budget: Budget level for recall - "low", "mid", or "high" (default: "mid")
|
||||
trace: Enable trace output (default: False)
|
||||
query_timestamp: Optional ISO format date string (e.g., '2023-05-30T23:40:00')
|
||||
include_entities: Include entity observations in results (default: False)
|
||||
max_entity_tokens: Maximum tokens for entity observations (default: 500)
|
||||
|
||||
Returns:
|
||||
List of RecallResult objects
|
||||
RecallResponse with results, optional entities, and optional trace
|
||||
"""
|
||||
from hindsight_client_api.models import include_options, entity_include_options
|
||||
|
||||
include_opts = include_options.IncludeOptions(
|
||||
entities=entity_include_options.EntityIncludeOptions(max_tokens=max_entity_tokens) if include_entities else None
|
||||
)
|
||||
|
||||
request_obj = recall_request.RecallRequest(
|
||||
query=query,
|
||||
types=types,
|
||||
budget=budget,
|
||||
max_tokens=max_tokens,
|
||||
trace=False,
|
||||
trace=trace,
|
||||
query_timestamp=query_timestamp,
|
||||
include=include_opts,
|
||||
)
|
||||
|
||||
response = _run_async(self._api.recall_memories(bank_id, request_obj))
|
||||
return response.results if hasattr(response, 'results') else []
|
||||
return _run_async(self._api.recall_memories(bank_id, request_obj))
|
||||
|
||||
def reflect(
|
||||
self,
|
||||
|
|
@ -209,55 +224,6 @@ class Hindsight:
|
|||
|
||||
return _run_async(self._api.reflect(bank_id, request_obj))
|
||||
|
||||
# Full-featured methods (expose more options)
|
||||
|
||||
def recall_memories(
|
||||
self,
|
||||
bank_id: str,
|
||||
query: str,
|
||||
types: Optional[List[str]] = None,
|
||||
budget: str = "mid",
|
||||
max_tokens: int = 4096,
|
||||
trace: bool = False,
|
||||
query_timestamp: Optional[str] = None,
|
||||
include_entities: bool = True,
|
||||
max_entity_tokens: int = 500,
|
||||
) -> RecallResponse:
|
||||
"""
|
||||
Recall memories with all options (full-featured).
|
||||
|
||||
Args:
|
||||
bank_id: The memory bank ID
|
||||
query: Search query
|
||||
types: Optional list of fact types to filter (world, experience, opinion, observation)
|
||||
budget: Budget level - "low", "mid", or "high"
|
||||
max_tokens: Maximum tokens in results
|
||||
trace: Enable trace output
|
||||
query_timestamp: Optional ISO format date string (e.g., '2023-05-30T23:40:00')
|
||||
include_entities: Include entity observations in results (default: True)
|
||||
max_entity_tokens: Maximum tokens for entity observations (default: 500)
|
||||
|
||||
Returns:
|
||||
RecallResponse with results, optional entities, and optional trace
|
||||
"""
|
||||
from hindsight_client_api.models import include_options, entity_include_options
|
||||
|
||||
include_opts = include_options.IncludeOptions(
|
||||
entities=entity_include_options.EntityIncludeOptions(max_tokens=max_entity_tokens) if include_entities 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,
|
||||
)
|
||||
|
||||
return _run_async(self._api.recall_memories(bank_id, request_obj))
|
||||
|
||||
def list_memories(
|
||||
self,
|
||||
bank_id: str,
|
||||
|
|
|
|||
|
|
@ -92,32 +92,33 @@ class TestRecall:
|
|||
|
||||
def test_recall_basic(self, client, bank_id):
|
||||
"""Test basic memory search."""
|
||||
results = client.recall(
|
||||
response = client.recall(
|
||||
bank_id=bank_id,
|
||||
query="What does Alice like?",
|
||||
)
|
||||
|
||||
assert results is not None
|
||||
assert len(results) > 0
|
||||
assert response is not None
|
||||
assert response.results is not None
|
||||
assert len(response.results) > 0
|
||||
|
||||
# Check that at least one result contains relevant information
|
||||
result_texts = [r.text for r in results]
|
||||
result_texts = [r.text for r in response.results]
|
||||
assert any("Alice" in text or "Python" in text or "programming" in text for text in result_texts)
|
||||
|
||||
def test_recall_with_max_tokens(self, client, bank_id):
|
||||
"""Test search with token limit."""
|
||||
results = client.recall(
|
||||
response = client.recall(
|
||||
bank_id=bank_id,
|
||||
query="outdoor activities",
|
||||
max_tokens=1024,
|
||||
)
|
||||
|
||||
assert results is not None
|
||||
assert isinstance(results, list)
|
||||
assert response is not None
|
||||
assert response.results is not None
|
||||
|
||||
def test_recall_memories_full_featured(self, client, bank_id):
|
||||
"""Test recall_memories with all features."""
|
||||
response = client.recall_memories(
|
||||
def test_recall_full_featured(self, client, bank_id):
|
||||
"""Test recall with all features."""
|
||||
response = client.recall(
|
||||
bank_id=bank_id,
|
||||
query="What are people's hobbies?",
|
||||
types=["world"],
|
||||
|
|
|
|||
|
|
@ -160,15 +160,14 @@ hindsight retain my-bank "Project deadline: April 15 (extended)" --document-id p
|
|||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## List Documents
|
||||
## Get Document
|
||||
|
||||
View all documents in a memory bank:
|
||||
Retrieve a document's original text and metadata. This is useful for expanding document context after a recall operation returns memories with document references.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Using the low-level API
|
||||
from hindsight_client_api import ApiClient, Configuration
|
||||
from hindsight_client_api.api import DefaultApi
|
||||
|
||||
|
|
@ -176,19 +175,16 @@ config = Configuration(host="http://localhost:8888")
|
|||
api_client = ApiClient(config)
|
||||
api = DefaultApi(api_client)
|
||||
|
||||
# List all documents
|
||||
response = api.list_documents(bank_id="my-bank")
|
||||
|
||||
for doc in response.items:
|
||||
print(f"{doc.id}: {doc.memory_unit_count} memories")
|
||||
print(f" Created: {doc.created_at}")
|
||||
|
||||
# With pagination
|
||||
response = api.list_documents(
|
||||
# Get document to expand context from recall results
|
||||
doc = api.get_document(
|
||||
bank_id="my-bank",
|
||||
limit=50,
|
||||
offset=0
|
||||
document_id="meeting-2024-03-15"
|
||||
)
|
||||
|
||||
print(f"Document: {doc.id}")
|
||||
print(f"Original text: {doc.original_text}")
|
||||
print(f"Memory count: {doc.memory_unit_count}")
|
||||
print(f"Created: {doc.created_at}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
|
@ -199,129 +195,28 @@ import { sdk, createClient, createConfig } from '@vectorize-io/hindsight-client'
|
|||
|
||||
const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));
|
||||
|
||||
// List all documents
|
||||
const response = await sdk.listDocuments({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' }
|
||||
});
|
||||
|
||||
for (const doc of response.data.items) {
|
||||
console.log(`${doc.id}: ${doc.memory_unit_count} memories`);
|
||||
console.log(` Created: ${doc.created_at}`);
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# List documents
|
||||
hindsight documents list my-bank
|
||||
|
||||
# With limit
|
||||
hindsight documents list my-bank --limit 50
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Get Document Details
|
||||
|
||||
Retrieve a specific document with its content:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Get document
|
||||
doc = api.get_document(
|
||||
bank_id="my-bank",
|
||||
document_id="meeting-2024-03-15"
|
||||
)
|
||||
|
||||
print(f"Document: {doc.id}")
|
||||
print(f"Original text: {doc.original_text[:200]}...")
|
||||
print(f"Memories: {doc.memory_unit_count}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Get document
|
||||
const doc = await sdk.getDocument({
|
||||
// Get document to expand context from recall results
|
||||
const { data: doc } = await sdk.getDocument({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', document_id: 'meeting-2024-03-15' }
|
||||
});
|
||||
|
||||
console.log(`Document: ${doc.data.id}`);
|
||||
console.log(`Original text: ${doc.data.original_text.substring(0, 200)}...`);
|
||||
console.log(`Memories: ${doc.data.memory_unit_count}`);
|
||||
console.log(`Document: ${doc.id}`);
|
||||
console.log(`Original text: ${doc.original_text}`);
|
||||
console.log(`Memory count: ${doc.memory_unit_count}`);
|
||||
console.log(`Created: ${doc.created_at}`);
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# Get document
|
||||
hindsight documents get my-bank meeting-2024-03-15
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Delete Documents
|
||||
|
||||
Remove a document and all its memories:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Delete document (removes all associated memories)
|
||||
api.delete_document(
|
||||
bank_id="my-bank",
|
||||
document_id="old-meeting"
|
||||
)
|
||||
|
||||
# Bulk delete
|
||||
for doc_id in ["old-1", "old-2", "old-3"]:
|
||||
api.delete_document(bank_id="my-bank", document_id=doc_id)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Delete document
|
||||
await sdk.deleteDocument({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', document_id: 'old-meeting' }
|
||||
});
|
||||
|
||||
// Bulk delete
|
||||
for (const docId of ['old-1', 'old-2', 'old-3']) {
|
||||
await sdk.deleteDocument({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', document_id: docId }
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# Delete document
|
||||
hindsight documents delete my-bank old-meeting
|
||||
|
||||
# Confirm deletion
|
||||
hindsight documents delete my-bank old-meeting --confirm
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Document Response Format
|
||||
|
||||
```json
|
||||
|
|
@ -329,76 +224,13 @@ hindsight documents delete my-bank old-meeting --confirm
|
|||
"id": "meeting-2024-03-15",
|
||||
"bank_id": "my-bank",
|
||||
"original_text": "Alice presented the Q4 roadmap...",
|
||||
"content_hash": "abc123def456",
|
||||
"memory_unit_count": 12,
|
||||
"created_at": "2024-03-15T14:00:00Z",
|
||||
"retain_params": {
|
||||
"context": "team meeting",
|
||||
"event_date": "2024-03-15"
|
||||
}
|
||||
"updated_at": "2024-03-15T14:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Meeting Notes
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from datetime import date
|
||||
|
||||
# Store meeting notes with date-based IDs
|
||||
client.retain(
|
||||
bank_id="team-memory",
|
||||
content=meeting_transcript,
|
||||
document_id=f"meeting-{date.today()}"
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Documentation
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
# Store docs with version tracking
|
||||
docs_dir = Path("docs")
|
||||
version = "1.0"
|
||||
|
||||
for file in docs_dir.glob("*.md"):
|
||||
client.retain(
|
||||
bank_id="docs-memory",
|
||||
content=file.read_text(),
|
||||
document_id=f"docs-{file.stem}-v{version}"
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Conversation History
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Store chat history with session IDs
|
||||
client.retain(
|
||||
bank_id="chat-memory",
|
||||
content=conversation,
|
||||
document_id=f"session-{session_id}"
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Entities**](./entities) — Track people, places, and concepts
|
||||
|
|
|
|||
|
|
@ -4,13 +4,10 @@ sidebar_position: 7
|
|||
|
||||
# Entities
|
||||
|
||||
Entities are the people, organizations, places, and concepts that Hindsight automatically tracks across your memory bank.
|
||||
Entities are the people, organizations, places, and concepts that Hindsight automatically extracts and tracks across your memory bank.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) and understand [how retain works](./retain).
|
||||
:::info Automatic Feature
|
||||
You don't need to do anything to use entities—Hindsight extracts them automatically when you call `retain`. However, understanding how entities work is important because they power key features in [recall](./recall) and [reflect](./reflect).
|
||||
:::
|
||||
|
||||
## Why Entities Matter
|
||||
|
|
@ -21,243 +18,95 @@ Entities improve recall quality in two ways:
|
|||
|
||||
2. **Observations** — Hindsight synthesizes high-level summaries about each entity from multiple facts. Including entity observations in recall provides richer context.
|
||||
|
||||
:::tip Include Entities in Recall
|
||||
Use `include_entities=True` in your recall calls to get entity observations alongside fact results. See [Recall](./recall) for details.
|
||||
:::
|
||||
## What Gets Extracted?
|
||||
|
||||
## What Are Entities?
|
||||
When you retain information, the LLM extracts named entities from each fact:
|
||||
|
||||
When you retain information, Hindsight automatically identifies and tracks entities:
|
||||
- **People** — Names like "Alice", "Dr. Smith", "CEO John"
|
||||
- **Organizations** — Companies, teams, institutions
|
||||
- **Places** — Cities, countries, specific locations
|
||||
- **Products/Objects** — Software, tools, significant items
|
||||
- **Concepts** — Abstract themes like "career growth", "friendship"
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
client.retain(
|
||||
bank_id="my-bank",
|
||||
content="Alice works at Google in Mountain View. She specializes in TensorFlow."
|
||||
)
|
||||
```
|
||||
Content: "Alice works at Google in Mountain View. She specializes in TensorFlow."
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||
|
||||
await client.retain('my-bank', 'Alice works at Google in Mountain View. She specializes in TensorFlow.');
|
||||
Entities extracted:
|
||||
- Alice (person)
|
||||
- Google (organization)
|
||||
- Mountain View (location)
|
||||
- TensorFlow (product)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
**Entities extracted:**
|
||||
- **Alice** (person)
|
||||
- **Google** (organization)
|
||||
- **Mountain View** (location)
|
||||
- **TensorFlow** (product)
|
||||
|
||||
## Entity Resolution
|
||||
|
||||
Multiple mentions are unified into a single entity:
|
||||
When the same entity is mentioned multiple times (possibly with different names), Hindsight resolves them to a single canonical entity using a scoring algorithm:
|
||||
|
||||
- "Alice" + "Alice Chen" + "Alice C." → one person
|
||||
- "Bob" + "Robert Chen" → one person (nickname)
|
||||
- Context-aware: "Apple (company)" vs "apple (fruit)"
|
||||
### Resolution Factors
|
||||
|
||||
## List Entities
|
||||
1. **Name similarity (50%)** — How closely the text matches existing entity names. Handles variations like "Alice" vs "Alice Chen" or partial matches.
|
||||
|
||||
Get all entities tracked in a memory bank:
|
||||
2. **Co-occurrence (30%)** — Entities that frequently appear together are more likely to be the same. If "Alice" always appears with "Google" and "TensorFlow", a new mention of "Alice" near those entities scores higher for matching.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
3. **Temporal proximity (20%)** — Recent mentions are weighted more heavily. If an entity was seen in the last 7 days, new similar mentions are more likely to match.
|
||||
|
||||
```python
|
||||
# Using the low-level API
|
||||
from hindsight_client_api import ApiClient, Configuration
|
||||
from hindsight_client_api.api import DefaultApi
|
||||
### Resolution Threshold
|
||||
|
||||
config = Configuration(host="http://localhost:8888")
|
||||
api_client = ApiClient(config)
|
||||
api = DefaultApi(api_client)
|
||||
A match requires a combined score above **0.6** (60%). Below this threshold, Hindsight creates a new entity rather than risk merging distinct entities.
|
||||
|
||||
# List all entities
|
||||
response = api.list_entities(bank_id="my-bank")
|
||||
|
||||
for entity in response.items:
|
||||
print(f"{entity.canonical_name}: {entity.mention_count} mentions")
|
||||
|
||||
# List with pagination
|
||||
response = api.list_entities(
|
||||
bank_id="my-bank",
|
||||
limit=50,
|
||||
offset=0
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
import { sdk, createClient, createConfig } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));
|
||||
|
||||
// List all entities
|
||||
const response = await sdk.listEntities({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' }
|
||||
});
|
||||
|
||||
for (const entity of response.data.items) {
|
||||
console.log(`${entity.canonical_name}: ${entity.mention_count} mentions`);
|
||||
}
|
||||
|
||||
// List with pagination
|
||||
const paginated = await sdk.listEntities({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' },
|
||||
query: { limit: 50, offset: 0 }
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# List all entities
|
||||
hindsight entities list my-bank
|
||||
|
||||
# With limit
|
||||
hindsight entities list my-bank --limit 50
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Get Entity Details
|
||||
|
||||
Retrieve detailed information about a specific entity:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Get entity details with observations
|
||||
entity = api.get_entity(
|
||||
bank_id="my-bank",
|
||||
entity_id="entity-uuid"
|
||||
)
|
||||
|
||||
print(f"Entity: {entity.canonical_name}")
|
||||
print(f"First seen: {entity.first_seen}")
|
||||
print(f"Mentions: {entity.mention_count}")
|
||||
|
||||
# Observations (synthesized summaries)
|
||||
for obs in entity.observations:
|
||||
print(f" - {obs.text}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Get entity details
|
||||
const entity = await sdk.getEntity({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', entity_id: 'entity-uuid' }
|
||||
});
|
||||
|
||||
console.log(`Entity: ${entity.data.canonical_name}`);
|
||||
console.log(`First seen: ${entity.data.first_seen}`);
|
||||
console.log(`Mentions: ${entity.data.mention_count}`);
|
||||
|
||||
// Observations
|
||||
for (const obs of entity.data.observations) {
|
||||
console.log(` - ${obs.text}`);
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# Get entity details
|
||||
hindsight entities get my-bank entity-uuid
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
This means:
|
||||
- Exact name matches with recent co-occurring entities → strong match
|
||||
- Partial name matches without context → likely creates new entity
|
||||
- Same name in completely different contexts → may create separate entities
|
||||
|
||||
## Entity Observations
|
||||
|
||||
Observations are high-level summaries automatically synthesized from multiple facts:
|
||||
Observations are **derived state**—high-level summaries that Hindsight automatically synthesizes from the facts associated with an entity. They provide a condensed view of what the system knows about important entities.
|
||||
|
||||
**Facts about Alice:**
|
||||
**Example:**
|
||||
|
||||
Facts about Alice:
|
||||
- "Alice works at Google"
|
||||
- "Alice is a software engineer"
|
||||
- "Alice specializes in ML"
|
||||
- "Alice joined Google in 2020"
|
||||
- "Alice leads the search team"
|
||||
|
||||
**Observation created:**
|
||||
- "Alice is a software engineer at Google specializing in ML"
|
||||
Observation created:
|
||||
- "Alice is a software engineer at Google who joined in 2020, specializes in ML, and leads the search team"
|
||||
|
||||
Observations are generated in the background after retaining information.
|
||||
### How Observations Work
|
||||
|
||||
## Regenerate Observations
|
||||
Observations are **not generated for every entity**. When you retain new documents:
|
||||
|
||||
Force regeneration of entity observations:
|
||||
1. **Top entities selected** — Hindsight identifies the top 5 most-mentioned entities in the batch
|
||||
2. **Threshold check** — Only entities with at least 5 facts get observations
|
||||
3. **Regeneration** — Observations are regenerated using the entity's most recent 50 facts
|
||||
4. **Old observations replaced** — Previous observations are deleted and new ones created
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
This means:
|
||||
- Frequently mentioned entities get observations; rarely mentioned ones don't
|
||||
- Observations stay up-to-date as new information is retained
|
||||
- The system prioritizes entities that matter most to your memory bank
|
||||
|
||||
```python
|
||||
# Regenerate observations for an entity
|
||||
api.regenerate_entity_observations(
|
||||
bank_id="my-bank",
|
||||
entity_id="entity-uuid"
|
||||
)
|
||||
```
|
||||
### Observations vs Opinions
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
Observations are **objective summaries**—they synthesize facts without any bias or perspective. This is different from [opinions](./opinions), which are influenced by the memory bank's disposition.
|
||||
|
||||
```typescript
|
||||
// Regenerate observations
|
||||
await sdk.regenerateEntityObservations({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', entity_id: 'entity-uuid' }
|
||||
});
|
||||
```
|
||||
| | Observations | Opinions |
|
||||
|---|---|---|
|
||||
| **Purpose** | Summarize what's known about an entity | Express the bank's perspective on a topic |
|
||||
| **Disposition influence** | No | Yes |
|
||||
| **Scope** | Per-entity | Any topic |
|
||||
| **Generation** | Automatic (top entities) | On-demand via reflect |
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
### Using Observations
|
||||
|
||||
## Entity Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "entity-uuid",
|
||||
"canonical_name": "Alice Chen",
|
||||
"first_seen": "2024-01-15T10:30:00Z",
|
||||
"last_seen": "2024-03-20T14:22:00Z",
|
||||
"mention_count": 47,
|
||||
"observations": [
|
||||
{
|
||||
"text": "Alice is a software engineer at Google specializing in ML",
|
||||
"mentioned_at": "2024-03-20T15:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Observations are included in recall results when you set `include_entities=True`. They provide quick context about key entities without retrieving all underlying facts.
|
||||
|
||||
## Next Steps
|
||||
|
||||
- [**Memory Banks**](./memory-banks) — Configure bank disposition
|
||||
- [**Documents**](./documents) — Track document sources
|
||||
- [**Operations**](./operations) — Monitor background tasks
|
||||
- [**Recall**](./recall) — Use entities in memory retrieval
|
||||
- [**Reflect**](./reflect) — Get entity-aware responses
|
||||
|
|
|
|||
|
|
@ -2,17 +2,26 @@
|
|||
sidebar_position: 6
|
||||
---
|
||||
|
||||
# Memory Bank
|
||||
# Memory Banks
|
||||
|
||||
Configure memory bank disposition, background, and behavior.
|
||||
Memory banks have characteristics:
|
||||
- Banks are completely isolated from each other.
|
||||
- You don't need to pre-create it, Hindsight will create it for you with default settings.
|
||||
- Banks have a profile that influences how they form opinions from memories. (optional)
|
||||
Memory banks are isolated containers that store all memory-related data for a specific context or use case.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
## What is a Memory Bank?
|
||||
|
||||
A memory bank is a complete, isolated storage unit containing:
|
||||
|
||||
- **Memories** — Facts and information retained from conversations
|
||||
- **Documents** — Files and content indexed for retrieval
|
||||
- **Entities** — People, places, concepts extracted from memories
|
||||
- **Relationships** — Connections between entities in the knowledge graph
|
||||
|
||||
Banks are completely isolated from each other — memories stored in one bank are not visible to another.
|
||||
|
||||
You don't need to pre-create a bank. Hindsight will automatically create it with default settings when you first use it.
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) to install the client and start the server.
|
||||
:::
|
||||
|
|
@ -32,9 +41,9 @@ client.create_bank(
|
|||
name="Research Assistant",
|
||||
background="I am a research assistant specializing in machine learning",
|
||||
disposition={
|
||||
"skepticism": 4, # Questions claims, wants evidence
|
||||
"literalism": 3, # Balanced interpretation
|
||||
"empathy": 3 # Balanced emotional consideration
|
||||
"skepticism": 4,
|
||||
"literalism": 3,
|
||||
"empathy": 3
|
||||
}
|
||||
)
|
||||
```
|
||||
|
|
@ -75,51 +84,17 @@ hindsight bank disposition my-bank \
|
|||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Disposition Traits
|
||||
## Background and Disposition
|
||||
|
||||
Each trait is scored 1 to 5:
|
||||
Background and disposition are optional settings that influence how the bank forms opinions during [reflect](./reflect) operations.
|
||||
|
||||
| Trait | Low (1) | High (5) |
|
||||
|-------|---------|----------|
|
||||
| **Skepticism** | Trusting, accepts information at face value | Skeptical, questions and doubts claims |
|
||||
| **Literalism** | Flexible interpretation, reads between the lines | Literal interpretation, takes things exactly as stated |
|
||||
| **Empathy** | Detached, focuses on facts and logic | Empathetic, considers emotional context |
|
||||
:::info
|
||||
Background and disposition only affect the `reflect` operation (opinion formation). They do not impact `retain`, `recall`, or other memory operations.
|
||||
:::
|
||||
|
||||
### How Traits Affect Behavior
|
||||
### Background
|
||||
|
||||
**Skepticism** influences how the bank evaluates claims:
|
||||
|
||||
```python
|
||||
# High skepticism (5)
|
||||
"What's the source for this? Have these results been replicated?"
|
||||
|
||||
# Low skepticism (1)
|
||||
"That sounds reasonable, let's proceed with that assumption."
|
||||
```
|
||||
|
||||
**Literalism** affects interpretation:
|
||||
|
||||
```python
|
||||
# High literalism (5)
|
||||
"The requirement says 'users' - that means all users, no exceptions."
|
||||
|
||||
# Low literalism (1)
|
||||
"When they say 'users', they probably mean active users in this context."
|
||||
```
|
||||
|
||||
**Empathy** shapes how emotional context is considered:
|
||||
|
||||
```python
|
||||
# High empathy (5)
|
||||
"I understand this is frustrating. Let's find a solution that works for you."
|
||||
|
||||
# Low empathy (1)
|
||||
"Here are the facts: Option A has 20% better performance than Option B."
|
||||
```
|
||||
|
||||
## Background
|
||||
|
||||
The background is a first-person narrative providing bank context:
|
||||
The background is a first-person narrative providing context for opinion formation:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
|
@ -147,164 +122,12 @@ await client.createBank('financial-advisor', {
|
|||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Background influences:
|
||||
- How questions are interpreted
|
||||
- Perspective in responses
|
||||
- Opinion formation context
|
||||
### Disposition Traits
|
||||
|
||||
## Getting Bank Profile
|
||||
Disposition traits influence how opinions are formed during reflection. Each trait is scored 1 to 5:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Using the low-level API
|
||||
from hindsight_client_api import ApiClient, Configuration
|
||||
from hindsight_client_api.api import DefaultApi
|
||||
|
||||
config = Configuration(host="http://localhost:8888")
|
||||
api_client = ApiClient(config)
|
||||
api = DefaultApi(api_client)
|
||||
|
||||
profile = api.get_bank_profile("my-bank")
|
||||
|
||||
print(f"Name: {profile.name}")
|
||||
print(f"Background: {profile.background}")
|
||||
print(f"Disposition: {profile.disposition}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
const profile = await client.getBankProfile('my-bank');
|
||||
|
||||
console.log(`Name: ${profile.name}`);
|
||||
console.log(`Background: ${profile.background}`);
|
||||
console.log(`Disposition:`, profile.disposition);
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight bank profile my-bank
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Default Values
|
||||
|
||||
If not specified, banks use neutral defaults:
|
||||
|
||||
```python
|
||||
{
|
||||
"skepticism": 3,
|
||||
"literalism": 3,
|
||||
"empathy": 3,
|
||||
"background": ""
|
||||
}
|
||||
```
|
||||
|
||||
## Disposition Templates
|
||||
|
||||
Common disposition configurations:
|
||||
|
||||
| Use Case | Skepticism | Literalism | Empathy |
|
||||
|----------|------------|------------|---------|
|
||||
| **Customer Support** | 2 | 2 | 5 |
|
||||
| **Code Reviewer** | 4 | 5 | 2 |
|
||||
| **Legal Analyst** | 5 | 5 | 2 |
|
||||
| **Therapist/Coach** | 2 | 2 | 5 |
|
||||
| **Research Assistant** | 4 | 3 | 3 |
|
||||
| **Neutral (default)** | 3 | 3 | 3 |
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Customer support bank
|
||||
client.create_bank(
|
||||
bank_id="support",
|
||||
background="I am a friendly customer support agent",
|
||||
disposition={
|
||||
"skepticism": 2, # Trusting
|
||||
"literalism": 2, # Flexible interpretation
|
||||
"empathy": 5 # Very empathetic
|
||||
}
|
||||
)
|
||||
|
||||
# Code reviewer bank
|
||||
client.create_bank(
|
||||
bank_id="reviewer",
|
||||
background="I am a thorough code reviewer focused on quality",
|
||||
disposition={
|
||||
"skepticism": 4, # Questions assumptions
|
||||
"literalism": 5, # Exact interpretation
|
||||
"empathy": 2 # Direct, fact-focused
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Customer support bank
|
||||
await client.createBank('support', {
|
||||
background: 'I am a friendly customer support agent',
|
||||
disposition: {
|
||||
skepticism: 2,
|
||||
literalism: 2,
|
||||
empathy: 5
|
||||
}
|
||||
});
|
||||
|
||||
// Code reviewer bank
|
||||
await client.createBank('reviewer', {
|
||||
background: 'I am a thorough code reviewer focused on quality',
|
||||
disposition: {
|
||||
skepticism: 4,
|
||||
literalism: 5,
|
||||
empathy: 2
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Bank Isolation
|
||||
|
||||
Each bank has:
|
||||
- **Separate memories** — banks don't share memories
|
||||
- **Own disposition** — traits are per-bank
|
||||
- **Independent opinions** — formed from their own experiences
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Store to bank A
|
||||
client.retain(bank_id="bank-a", content="Python is great")
|
||||
|
||||
# Bank B doesn't see it
|
||||
results = client.recall(bank_id="bank-b", query="Python")
|
||||
# Returns empty
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Store to bank A
|
||||
await client.retain('bank-a', 'Python is great');
|
||||
|
||||
// Bank B doesn't see it
|
||||
const results = await client.recall('bank-b', 'Python');
|
||||
// Returns empty
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
| Trait | Low (1) | High (5) |
|
||||
|-------|---------|----------|
|
||||
| **Skepticism** | Trusting, accepts information at face value | Skeptical, questions and doubts claims |
|
||||
| **Literalism** | Flexible interpretation, reads between the lines | Literal interpretation, takes things exactly as stated |
|
||||
| **Empathy** | Detached, focuses on facts and logic | Empathetic, considers emotional context |
|
||||
|
|
|
|||
|
|
@ -4,290 +4,31 @@ sidebar_position: 9
|
|||
|
||||
# Operations
|
||||
|
||||
Monitor and manage long-running background tasks in Hindsight.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
Background tasks that Hindsight executes asynchronously.
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) and understand [how retain works](./retain).
|
||||
:::
|
||||
|
||||
## What Are Operations?
|
||||
## How Operations Work
|
||||
|
||||
When you call `retain_batch` with `async=True`, Hindsight processes the content in the background and returns immediately with an operation ID. Operations let you track and manage these async retain tasks.
|
||||
Hindsight processes several types of tasks in the background to maintain memory quality and consistency. These operations run automatically—you don't need to trigger them manually.
|
||||
|
||||
By default, async operations are executed in-process within the API service. This is managed automatically — you don't need to configure anything.
|
||||
By default, all background operations are executed in-process within the API service.
|
||||
|
||||
:::tip Scaling with Streaming
|
||||
For high-throughput workloads, you can extend the task backend to use a streaming platform like Kafka. This enables scale-out processing across multiple workers and handles backpressure on the API.
|
||||
:::note Kafka Integration
|
||||
Support for external streaming platforms like Kafka for scale-out processing is planned but **not available out of the box** in the current release.
|
||||
:::
|
||||
|
||||
## Async Batch Retain
|
||||
|
||||
For large content batches, use async mode to avoid timeouts:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
from hindsight_client import Hindsight
|
||||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[
|
||||
{"content": doc1_text},
|
||||
{"content": doc2_text},
|
||||
],
|
||||
retain_async=True
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
import { HindsightClient } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||
|
||||
await client.retainBatch('my-bank', [
|
||||
{ content: doc1Text },
|
||||
{ content: doc2Text },
|
||||
], { async: true });
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight retain my-bank --files docs/*.md --async
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## List Operations
|
||||
|
||||
View all operations for a memory bank:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Using the low-level API
|
||||
from hindsight_client_api import ApiClient, Configuration
|
||||
from hindsight_client_api.api import DefaultApi
|
||||
|
||||
config = Configuration(host="http://localhost:8888")
|
||||
api_client = ApiClient(config)
|
||||
api = DefaultApi(api_client)
|
||||
|
||||
# List all operations
|
||||
response = api.list_operations(bank_id="my-bank")
|
||||
|
||||
for op in response.items:
|
||||
print(f"{op.id}: {op.task_type} - {op.status}")
|
||||
print(f" Items: {op.items_count}")
|
||||
if op.error_message:
|
||||
print(f" Error: {op.error_message}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
import { sdk, createClient, createConfig } from '@vectorize-io/hindsight-client';
|
||||
|
||||
const apiClient = createClient(createConfig({ baseUrl: 'http://localhost:8888' }));
|
||||
|
||||
// List all operations
|
||||
const response = await sdk.listOperations({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' }
|
||||
});
|
||||
|
||||
for (const op of response.data.items) {
|
||||
console.log(`${op.id}: ${op.task_type} - ${op.status}`);
|
||||
console.log(` Items: ${op.items_count}`);
|
||||
if (op.error_message) {
|
||||
console.log(` Error: ${op.error_message}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# List all operations
|
||||
hindsight operations list my-bank
|
||||
|
||||
# Filter by status
|
||||
hindsight operations list my-bank --status running
|
||||
|
||||
# Watch all running operations
|
||||
hindsight operations watch my-bank --all
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Cancel Operations
|
||||
|
||||
Stop a running or pending operation:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Cancel operation
|
||||
api.cancel_operation(
|
||||
bank_id="my-bank",
|
||||
operation_id="op-abc123"
|
||||
)
|
||||
|
||||
# Cancel all pending operations
|
||||
response = api.list_operations(bank_id="my-bank")
|
||||
for op in response.items:
|
||||
if op.status == "pending":
|
||||
api.cancel_operation(bank_id="my-bank", operation_id=op.id)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Cancel operation
|
||||
await sdk.cancelOperation({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', operation_id: 'op-abc123' }
|
||||
});
|
||||
|
||||
// Cancel all pending
|
||||
const ops = await sdk.listOperations({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank' }
|
||||
});
|
||||
|
||||
for (const op of ops.data.items) {
|
||||
if (op.status === 'pending') {
|
||||
await sdk.cancelOperation({
|
||||
client: apiClient,
|
||||
path: { bank_id: 'my-bank', operation_id: op.id }
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
# Cancel operation
|
||||
hindsight operations cancel my-bank op-abc123
|
||||
|
||||
# Cancel all pending
|
||||
hindsight operations cancel my-bank --all-pending
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Operation States
|
||||
|
||||
| State | Description |
|
||||
|-------|-------------|
|
||||
| **pending** | Queued, waiting to start |
|
||||
| **running** | Currently processing |
|
||||
| **completed** | Successfully finished |
|
||||
| **failed** | Encountered an error |
|
||||
| **cancelled** | Stopped by user |
|
||||
|
||||
## Monitoring Strategies
|
||||
|
||||
### Polling
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
import time
|
||||
|
||||
def wait_for_operations(api, bank_id, poll_interval=5):
|
||||
"""Wait for all pending/running operations to complete."""
|
||||
while True:
|
||||
response = api.list_operations(bank_id=bank_id)
|
||||
|
||||
pending_or_running = [
|
||||
op for op in response.items
|
||||
if op.status in ['pending', 'running']
|
||||
]
|
||||
|
||||
if not pending_or_running:
|
||||
print("All operations completed!")
|
||||
break
|
||||
|
||||
for op in pending_or_running:
|
||||
print(f" {op.id}: {op.status} ({op.items_count} items)")
|
||||
|
||||
time.sleep(poll_interval)
|
||||
|
||||
# Use it
|
||||
wait_for_operations(api, "my-bank")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
async function waitForOperations(apiClient: any, bankId: string, pollInterval = 5000) {
|
||||
while (true) {
|
||||
const response = await sdk.listOperations({
|
||||
client: apiClient,
|
||||
path: { bank_id: bankId }
|
||||
});
|
||||
|
||||
const pendingOrRunning = response.data.items.filter(
|
||||
(op: any) => ['pending', 'running'].includes(op.status)
|
||||
);
|
||||
|
||||
if (pendingOrRunning.length === 0) {
|
||||
console.log('All operations completed!');
|
||||
break;
|
||||
}
|
||||
|
||||
for (const op of pendingOrRunning) {
|
||||
console.log(` ${op.id}: ${op.status} (${op.items_count} items)`);
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
||||
}
|
||||
}
|
||||
|
||||
// Use it
|
||||
await waitForOperations(apiClient, 'my-bank');
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Performance Tips
|
||||
|
||||
**Use async for large batches:**
|
||||
- Sync: < 100 items or < 100KB
|
||||
- Async: > 100 items or > 100KB
|
||||
|
||||
**Monitor progress:**
|
||||
- Check `items_count` field
|
||||
- Poll every 5-10 seconds
|
||||
|
||||
**Handle failures:**
|
||||
- Check `error_message` field for details
|
||||
- Retry with exponential backoff
|
||||
- Break large batches into smaller chunks
|
||||
## Operation Types
|
||||
|
||||
| Operation | Trigger | Description |
|
||||
|-----------|---------|-------------|
|
||||
| **batch_retain** | `retain_batch` with `async=True` | Processes large content batches in the background |
|
||||
| **form_opinion** | After each `reflect` call | Extracts and stores new opinions formed during reflection |
|
||||
| **reinforce_opinion** | After `retain` | Updates opinion confidence based on new supporting evidence |
|
||||
| **access_count_update** | After `recall` | Tracks which memories are accessed for relevance scoring |
|
||||
| **regenerate_observations** | Bank profile update | Regenerates entity observations when disposition changes |
|
||||
|
||||
## Next Steps
|
||||
|
||||
|
|
|
|||
|
|
@ -2,18 +2,22 @@
|
|||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Search Facts
|
||||
# Recall Memories
|
||||
|
||||
Retrieve memories using multi-strategy search.
|
||||
Retrieve memories using multi-strategy recall.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
:::info How Recall Works
|
||||
Learn about the four retrieval strategies (semantic, keyword, graph, temporal) and RRF fusion in the [Recall Architecture](/developer/retrieval) guide.
|
||||
:::
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) to install the client and start the server.
|
||||
:::
|
||||
|
||||
## Basic Search
|
||||
## Basic Recall
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
|
@ -23,7 +27,9 @@ from hindsight_client import Hindsight
|
|||
|
||||
client = Hindsight(base_url="http://localhost:8888")
|
||||
|
||||
client.recall(bank_id="my-bank", query="What does Alice do?")
|
||||
response = client.recall(bank_id="my-bank", query="What does Alice do?")
|
||||
for r in response.results:
|
||||
print(f"{r.text} (score: {r.weight:.2f})")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
|
@ -34,20 +40,23 @@ import { HindsightClient } from '@vectorize-io/hindsight-client';
|
|||
|
||||
const client = new HindsightClient({ baseUrl: 'http://localhost:8888' });
|
||||
|
||||
await client.recall('my-bank', 'What does Alice do?');
|
||||
const response = await client.recall('my-bank', 'What does Alice do?');
|
||||
for (const r of response.results) {
|
||||
console.log(`${r.text} (score: ${r.weight})`);
|
||||
}
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight memory search my-bank "What does Alice do?"
|
||||
hindsight recall my-bank "What does Alice do?"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Search Parameters
|
||||
## Recall Parameters
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
|
|
@ -55,43 +64,15 @@ hindsight memory search my-bank "What does Alice do?"
|
|||
| `types` | list | all | Filter: `world`, `experience`, `opinion` |
|
||||
| `budget` | string | "mid" | Budget level: "low", "mid", "high" |
|
||||
| `max_tokens` | int | 4096 | Token budget for results |
|
||||
| `trace` | bool | false | Enable trace output for debugging |
|
||||
| `include_entities` | bool | false | Include entity observations |
|
||||
| `max_entity_tokens` | int | 500 | Token budget for entity observations |
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
results = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
types=["world", "experience"],
|
||||
budget="high",
|
||||
max_tokens=8000
|
||||
)
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
const results = await client.recall('my-bank', 'What does Alice do?', {
|
||||
budget: 'high',
|
||||
maxTokens: 8000
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Full-Featured Search
|
||||
|
||||
For more control, use the full-featured recall method:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Full response with trace info
|
||||
response = client.recall_memories(
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
types=["world", "experience"],
|
||||
|
|
@ -103,22 +84,20 @@ response = client.recall_memories(
|
|||
)
|
||||
|
||||
# Access results
|
||||
for r in response["results"]:
|
||||
print(f"{r['text']} (score: {r['weight']:.2f})")
|
||||
for r in response.results:
|
||||
print(f"{r.text} (score: {r.weight:.2f})")
|
||||
|
||||
# Access entity observations (if include_entities=True)
|
||||
if "entities" in response:
|
||||
for entity in response["entities"]:
|
||||
print(f"Entity: {entity['name']}")
|
||||
if response.entities:
|
||||
for entity in response.entities:
|
||||
print(f"Entity: {entity.name}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Full response with trace info
|
||||
const response = await client.recallMemories('my-bank', {
|
||||
query: 'What does Alice do?',
|
||||
const response = await client.recall('my-bank', 'What does Alice do?', {
|
||||
types: ['world', 'experience'],
|
||||
budget: 'high',
|
||||
maxTokens: 8000,
|
||||
|
|
@ -134,44 +113,9 @@ for (const r of response.results) {
|
|||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Temporal Queries
|
||||
|
||||
Hindsight automatically detects time expressions and activates temporal search:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# These queries activate temporal-graph retrieval
|
||||
results = client.recall(bank_id="my-bank", query="What did Alice do last spring?")
|
||||
results = client.recall(bank_id="my-bank", query="What happened in June?")
|
||||
results = client.recall(bank_id="my-bank", query="Events from last year")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight memory search my-bank "What did Alice do last spring?"
|
||||
hindsight memory search my-bank "What happened between March and May?"
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Supported temporal expressions:
|
||||
|
||||
| Expression | Parsed As |
|
||||
|------------|-----------|
|
||||
| "last spring" | March 1 - May 31 (previous year) |
|
||||
| "in June" | June 1-30 (current/nearest year) |
|
||||
| "last year" | Jan 1 - Dec 31 (previous year) |
|
||||
| "last week" | 7 days ago - today |
|
||||
| "between March and May" | March 1 - May 31 |
|
||||
|
||||
## Filter by Fact Type
|
||||
|
||||
Search specific memory networks:
|
||||
Recall specific memory types:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
|
@ -210,20 +154,23 @@ facts = client.recall(
|
|||
<TabItem value="cli" label="CLI">
|
||||
|
||||
```bash
|
||||
hindsight memory search my-bank "Python" --fact-type opinion
|
||||
hindsight memory search my-bank "Alice" --fact-type world,experience
|
||||
hindsight recall my-bank "Python" --fact-type opinion
|
||||
hindsight recall my-bank "Alice" --fact-type world,experience
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info How Recall Works
|
||||
Learn about the four search strategies (semantic, keyword, graph, temporal) and RRF fusion in the [Recall Architecture](/developer/retrieval) guide.
|
||||
:::warning About Opinions
|
||||
Opinions are beliefs formed during [reflect](/developer/api/reflect) operations. Unlike world facts and experience, opinions are subjective interpretations and may not represent objective truth. Depending on your use case:
|
||||
- **Exclude opinions** (`types=["world", "experience"]`) when you need factual, verifiable information
|
||||
- **Include opinions** when you want the agent's perspective or formed beliefs
|
||||
- **Use opinions alone** (`types=["opinion"]`) only when specifically asking about the agent's views
|
||||
:::
|
||||
|
||||
## Token Budget Management
|
||||
|
||||
Hindsight is built for AI agents, not humans. Traditional search systems return "top-k" results, but agents don't think in terms of result counts—they think in tokens. An agent's context window is measured in tokens, and that's exactly how Hindsight measures results.
|
||||
Hindsight is built for AI agents, not humans. Traditional retrieval systems return "top-k" results, but agents don't think in terms of result counts—they think in tokens. An agent's context window is measured in tokens, and that's exactly how Hindsight measures results.
|
||||
|
||||
The `max_tokens` parameter lets you control how much of your agent's context budget to spend on memories:
|
||||
|
||||
|
|
@ -237,9 +184,9 @@ results = client.recall(bank_id="my-bank", query="Alice's email", max_tokens=500
|
|||
|
||||
This design means you never have to guess whether 10 results or 50 results will fit your context. Just specify the token budget and Hindsight returns as many relevant memories as will fit.
|
||||
|
||||
### Additional Context: Chunks and Entity Observations
|
||||
## Include Related Context
|
||||
|
||||
For the most relevant memories, you can optionally retrieve additional context—each with its own token budget:
|
||||
Beyond the core memory results, you can optionally retrieve additional context—each with its own token budget:
|
||||
|
||||
| Option | Parameter | Description |
|
||||
|--------|-----------|-------------|
|
||||
|
|
@ -247,19 +194,16 @@ For the most relevant memories, you can optionally retrieve additional context
|
|||
| **Entity Observations** | `include_entities`, `max_entity_tokens` | Related observations about entities mentioned in results |
|
||||
|
||||
```python
|
||||
response = client.recall_memories(
|
||||
response = client.recall(
|
||||
bank_id="my-bank",
|
||||
query="What does Alice do?",
|
||||
max_tokens=4096, # Budget for memories
|
||||
include_chunks=True,
|
||||
max_chunk_tokens=2000, # Budget for raw chunks
|
||||
include_entities=True,
|
||||
max_entity_tokens=1000 # Budget for entity observations
|
||||
)
|
||||
|
||||
# Access the additional context
|
||||
chunks = response.get("chunks", {})
|
||||
entities = response.get("entities", [])
|
||||
entities = response.entities or []
|
||||
```
|
||||
|
||||
This gives your agent richer context while maintaining precise control over total token consumption.
|
||||
|
|
@ -268,7 +212,7 @@ This gives your agent richer context while maintaining precise control over tota
|
|||
|
||||
The `budget` parameter controls graph traversal depth:
|
||||
|
||||
- **"low"**: Fast, shallow search — good for simple lookups
|
||||
- **"low"**: Fast, shallow retrieval — good for simple lookups
|
||||
- **"mid"**: Balanced — default for most queries
|
||||
- **"high"**: Deep exploration — finds indirect connections
|
||||
|
||||
|
|
|
|||
|
|
@ -6,9 +6,21 @@ sidebar_position: 3
|
|||
|
||||
Generate disposition-aware responses using retrieved memories.
|
||||
|
||||
When you call **reflect**, Hindsight performs a multi-step reasoning process:
|
||||
1. **Recalls** relevant memories from the bank based on your query
|
||||
2. **Applies** the bank's disposition traits to shape the reasoning style
|
||||
3. **Generates** a contextual answer grounded in the retrieved facts
|
||||
4. **Forms opinions** in the background based on the reasoning (available in subsequent calls)
|
||||
|
||||
The response includes the generated answer along with the facts that were used, providing full transparency into how the answer was derived.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
:::info How Reflect Works
|
||||
Learn about disposition-driven reasoning and opinion formation in the [Reflect Architecture](/developer/reflect) guide.
|
||||
:::
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) to install the client and start the server.
|
||||
:::
|
||||
|
|
@ -80,34 +92,50 @@ const response = await client.reflect('my-bank', 'What do you think about remote
|
|||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info How Reflect Works
|
||||
Learn about disposition-driven reasoning and opinion formation in the [Reflect Architecture](/developer/reflect) guide.
|
||||
:::
|
||||
## The Role of Context
|
||||
|
||||
## Opinion Formation
|
||||
The `context` parameter steers how the reflection is performed without impacting the memory recall. It provides situational information that helps shape the reasoning and response.
|
||||
|
||||
Reflect can form new opinions based on evidence:
|
||||
**How context is used:**
|
||||
- **Shapes reasoning**: Helps understand the situation when formulating an answer
|
||||
- **Disambiguates intent**: Clarifies what aspect of the query matters most
|
||||
- **Does not affect recall**: The same memories are retrieved regardless of context
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Context is passed to the LLM to help it understand the situation
|
||||
response = client.reflect(
|
||||
bank_id="my-bank",
|
||||
query="What do you think about Python vs JavaScript for data science?"
|
||||
query="What do you think about the proposal?",
|
||||
context="We're in a budget review meeting discussing Q4 spending"
|
||||
)
|
||||
```
|
||||
|
||||
# Response might include:
|
||||
# answer: "Based on what I know about data science workflows..."
|
||||
# new_opinions: [
|
||||
# {"text": "Python is better for data science", "id": "..."}
|
||||
# ]
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Context helps the LLM understand the current situation
|
||||
const response = await client.reflect('my-bank', 'What do you think about the proposal?', {
|
||||
context: "We're in a budget review meeting discussing Q4 spending"
|
||||
});
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
New opinions are automatically stored and influence future responses.
|
||||
## Opinion Formation
|
||||
|
||||
When reflect reasons about a question, it may form new **opinions** based on the evidence in the memory bank. These opinions are created in the background and become available in subsequent `reflect` and `recall` calls.
|
||||
|
||||
**Why opinions matter:**
|
||||
- **Consistent thinking**: Opinions ensure the memory bank maintains a coherent perspective over time
|
||||
- **Evolving viewpoints**: As more information is retained, opinions can be refined or updated
|
||||
- **Grounded reasoning**: Opinions are always derived from factual evidence in the memory bank
|
||||
|
||||
Opinions are stored as a special memory type and are automatically retrieved when relevant to future queries. This creates a natural evolution of the bank's perspective, similar to how humans form and refine their views based on accumulated experience.
|
||||
|
||||
## Disposition Influence
|
||||
|
||||
|
|
@ -165,7 +193,7 @@ const response = await client.reflect('cautious-advisor', 'Should I invest in cr
|
|||
|
||||
## Using Sources
|
||||
|
||||
The `facts_used` field shows which memories informed the response:
|
||||
The `based_on` field shows which memories informed the response:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
|
@ -173,10 +201,10 @@ The `facts_used` field shows which memories informed the response:
|
|||
```python
|
||||
response = client.reflect(bank_id="my-bank", query="Tell me about Alice")
|
||||
|
||||
print("Response:", response["answer"])
|
||||
print("Response:", response.text)
|
||||
print("\nBased on:")
|
||||
for fact in response.get("facts_used", []):
|
||||
print(f" - {fact['text']} (relevance: {fact['weight']:.2f})")
|
||||
for fact in response.based_on or []:
|
||||
print(f" - [{fact.type}] {fact.text}")
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
|
|
@ -185,10 +213,10 @@ for fact in response.get("facts_used", []):
|
|||
```typescript
|
||||
const response = await client.reflect('my-bank', 'Tell me about Alice');
|
||||
|
||||
console.log('Response:', response.answer);
|
||||
console.log('Response:', response.text);
|
||||
console.log('\nBased on:');
|
||||
for (const fact of response.facts_used || []) {
|
||||
console.log(` - ${fact.text} (relevance: ${fact.weight.toFixed(2)})`);
|
||||
for (const fact of response.based_on || []) {
|
||||
console.log(` - [${fact.type}] ${fact.text}`);
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,17 @@ sidebar_position: 2
|
|||
|
||||
# Ingest Data
|
||||
|
||||
Store memories, conversations, and documents into Hindsight.
|
||||
Store documents, conversations, and raw content into Hindsight to automatically extract and create memories.
|
||||
|
||||
When you **retain** content, Hindsight doesn't just store the raw text—it intelligently analyzes the content to extract meaningful facts, identify entities, and build a connected knowledge graph. This process transforms unstructured information into structured, queryable memories.
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
:::info How Retain Works
|
||||
Learn about fact extraction, entity resolution, and graph construction in the [Retain Architecture](/developer/retain) guide.
|
||||
:::
|
||||
|
||||
:::tip Prerequisites
|
||||
Make sure you've completed the [Quick Start](./quickstart) to install the client and start the server.
|
||||
:::
|
||||
|
|
@ -50,9 +56,18 @@ hindsight memory put my-bank "Alice works at Google as a software engineer"
|
|||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## The Importance of Context
|
||||
|
||||
The `context` parameter is crucial for guiding how Hindsight extracts memories from your content. Think of it as providing a lens through which the system interprets the information.
|
||||
|
||||
**Why context matters:**
|
||||
- **Steers memory extraction**: Context tells the memory bank what type of information to focus on and how to interpret ambiguous content
|
||||
- **Improves relevance**: Memories extracted with proper context are more accurately categorized and easier to retrieve
|
||||
- **Disambiguates meaning**: The same sentence can have different implications depending on context (e.g., "the project was terminated" means different things in a career vs. product context)
|
||||
|
||||
## Store with Context and Date
|
||||
|
||||
Add context and event dates for better retrieval:
|
||||
Always provide context and event dates for optimal memory extraction:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
|
@ -88,11 +103,11 @@ hindsight memory put my-bank "Alice got promoted" \
|
|||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The `timestamp` enables temporal queries like "What happened last spring?"
|
||||
The `timestamp` defaults to the current time if not specified. Providing explicit timestamps enables temporal queries like "What happened last spring?"
|
||||
|
||||
## Batch Ingestion
|
||||
|
||||
Store multiple memories in a single request:
|
||||
Store multiple items in a single request. **Batch ingestion is the recommended approach** as it significantly improves performance by reducing network overhead and allowing Hindsight to optimize the memory extraction process across related content.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
|
@ -144,51 +159,40 @@ hindsight memory put-files my-bank report.pdf --document-id "q4-report"
|
|||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info How Retain Works
|
||||
Learn about fact extraction, entity resolution, and graph construction in the [Retain Architecture](/developer/retain) guide.
|
||||
:::
|
||||
|
||||
## Async Ingestion
|
||||
|
||||
For large batches, use async ingestion:
|
||||
For large batches, use async ingestion to avoid blocking:
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="python" label="Python">
|
||||
|
||||
```python
|
||||
# Start async ingestion
|
||||
# Start async ingestion (returns immediately)
|
||||
result = client.retain_batch(
|
||||
bank_id="my-bank",
|
||||
items=[...large batch...],
|
||||
document_id="large-doc",
|
||||
async_=True
|
||||
retain_async=True
|
||||
)
|
||||
|
||||
# Result contains operation_id for tracking
|
||||
print(result["operation_id"])
|
||||
# Check if it was processed asynchronously
|
||||
print(result.var_async) # True
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="node" label="Node.js">
|
||||
|
||||
```typescript
|
||||
// Start async ingestion
|
||||
// Start async ingestion (returns immediately)
|
||||
const result = await client.retainBatch('my-bank', largeItems, {
|
||||
documentId: 'large-doc',
|
||||
async: true
|
||||
});
|
||||
|
||||
console.log(result.operation_id);
|
||||
console.log(result.async); // true
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Best Practices
|
||||
|
||||
| Do | Don't |
|
||||
|----|-------|
|
||||
| Include context for better retrieval | Store raw unstructured dumps |
|
||||
| Use document_id to group related content | Mix unrelated content in one batch |
|
||||
| Add timestamp for temporal queries | Omit dates if time matters |
|
||||
| Store conversations as they happen | Wait to batch everything |
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ Configure the LLM provider used for fact extraction, entity resolution, and reas
|
|||
|
||||
| Variable | Description | Default | Required |
|
||||
|----------|-------------|---------|----------|
|
||||
| `HINDSIGHT_API_LLM_PROVIDER` | LLM provider: `groq`, `openai`, `ollama` | `groq` | Yes |
|
||||
| `HINDSIGHT_API_LLM_PROVIDER` | LLM provider: `groq`, `openai`, `gemini`, `ollama` | `groq` | Yes |
|
||||
| `HINDSIGHT_API_LLM_API_KEY` | API key for LLM provider | - | Yes (except ollama) |
|
||||
| `HINDSIGHT_API_LLM_MODEL` | Model name | Provider-specific | No |
|
||||
| `HINDSIGHT_API_LLM_BASE_URL` | Custom LLM endpoint | Provider default | No |
|
||||
|
|
@ -47,6 +47,14 @@ export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
|
|||
export HINDSIGHT_API_LLM_MODEL=gpt-4o
|
||||
```
|
||||
|
||||
**Gemini**
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_LLM_PROVIDER=gemini
|
||||
export HINDSIGHT_API_LLM_API_KEY=xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash
|
||||
```
|
||||
|
||||
**Ollama (Local, No API Key)**
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -8,37 +8,50 @@ Hindsight includes a built-in [Model Context Protocol (MCP)](https://modelcontex
|
|||
|
||||
## Access
|
||||
|
||||
The MCP server is **enabled by default** and mounted at `/mcp` on the API server:
|
||||
The MCP server is **enabled by default** and mounted at `/mcp` on the API server. Each memory bank has its own MCP endpoint:
|
||||
|
||||
```
|
||||
http://localhost:8888/mcp
|
||||
http://localhost:8888/mcp/{bank_id}/
|
||||
```
|
||||
|
||||
To disable it, set the environment variable:
|
||||
For example, to connect to the memory bank `alice`:
|
||||
```
|
||||
http://localhost:8888/mcp/alice/
|
||||
```
|
||||
|
||||
To disable the MCP server, set the environment variable:
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_MCP_ENABLED=false
|
||||
```
|
||||
|
||||
## Per-Bank Endpoints
|
||||
|
||||
Unlike traditional MCP servers where tools require explicit identifiers, Hindsight uses **per-bank endpoints**. The `bank_id` is part of the URL path, so tools don't need to specify which bank to use—it's implicit from the connection.
|
||||
|
||||
This design:
|
||||
- **Simplifies tool usage** — no need to pass `bank_id` with every call
|
||||
- **Enforces isolation** — each MCP connection is scoped to a single bank
|
||||
- **Enables multi-tenant setups** — connect different users to different endpoints
|
||||
|
||||
---
|
||||
|
||||
## Available Tools
|
||||
|
||||
### hindsight_put
|
||||
### retain
|
||||
|
||||
Store information to a user's memory bank.
|
||||
Store information to long-term memory.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `bank_id` | string | Yes | Unique identifier for the user (e.g., `user_12345`, `alice@example.com`) |
|
||||
| `content` | string | Yes | The fact or memory to store |
|
||||
| `context` | string | Yes | Category for the memory (e.g., `personal_preferences`, `work_history`) |
|
||||
| `explanation` | string | No | Why this memory is being stored |
|
||||
| `context` | string | No | Category for the memory (default: `general`) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "hindsight_put",
|
||||
"name": "retain",
|
||||
"arguments": {
|
||||
"bank_id": "user_12345",
|
||||
"content": "User prefers Python over JavaScript for backend development",
|
||||
"context": "programming_preferences"
|
||||
}
|
||||
|
|
@ -53,23 +66,20 @@ Store information to a user's memory bank.
|
|||
|
||||
---
|
||||
|
||||
### hindsight_search
|
||||
### recall
|
||||
|
||||
Search a user's memory bank to provide personalized responses.
|
||||
Search memories to provide personalized responses.
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
|-----------|------|----------|-------------|
|
||||
| `bank_id` | string | Yes | Unique identifier for the user |
|
||||
| `query` | string | Yes | Natural language search query |
|
||||
| `max_tokens` | integer | No | Maximum tokens for results (default: 4096) |
|
||||
| `explanation` | string | No | Why this search is being performed |
|
||||
| `max_results` | integer | No | Maximum results to return (default: 10) |
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"name": "hindsight_search",
|
||||
"name": "recall",
|
||||
"arguments": {
|
||||
"bank_id": "user_12345",
|
||||
"query": "What are the user's programming language preferences?"
|
||||
}
|
||||
}
|
||||
|
|
@ -84,8 +94,7 @@ Search a user's memory bank to provide personalized responses.
|
|||
"text": "User prefers Python over JavaScript for backend development",
|
||||
"type": "world",
|
||||
"context": "programming_preferences",
|
||||
"event_date": null,
|
||||
"document_id": null
|
||||
"event_date": null
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -99,17 +108,22 @@ Search a user's memory bank to provide personalized responses.
|
|||
|
||||
---
|
||||
|
||||
## Per-User Isolation
|
||||
|
||||
Both tools require a `bank_id` that uniquely identifies the user. Memories are strictly isolated per bank — one user cannot access another user's memories.
|
||||
|
||||
**Best practices:**
|
||||
- Use consistent identifiers (user ID, email, session ID)
|
||||
- Don't share `bank_id` between different users
|
||||
- Only call these tools when you can identify the specific user
|
||||
|
||||
---
|
||||
|
||||
## Integration with AI Assistants
|
||||
|
||||
The MCP server can be used with any MCP-compatible AI assistant. For Claude Desktop integration using the CLI, see [MCP Server (CLI)](/sdks/mcp).
|
||||
|
||||
### Example: Claude Desktop Configuration
|
||||
|
||||
To connect Claude Desktop to a specific memory bank:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"hindsight-alice": {
|
||||
"url": "http://localhost:8888/mcp/alice/"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each user can have their own MCP server configuration pointing to their personal memory bank.
|
||||
|
|
|
|||
|
|
@ -8,45 +8,21 @@ curl http://localhost:8888/metrics
|
|||
|
||||
## Available Metrics
|
||||
|
||||
### Request Metrics
|
||||
### Operation Metrics
|
||||
|
||||
| Metric | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `hindsight_http_requests_total` | Counter | Total HTTP requests (labels: method, endpoint, status_code) |
|
||||
| `hindsight_http_request_duration_seconds` | Histogram | Request latency (labels: method, endpoint) |
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `hindsight.operation.duration` | Histogram | operation, bank_id, budget, max_tokens, success | Duration of operations in seconds |
|
||||
| `hindsight.operation.total` | Counter | operation, bank_id, budget, max_tokens, success | Total number of operations executed |
|
||||
|
||||
### Memory Operations
|
||||
The `operation` label values are: `retain`, `recall`, `reflect`.
|
||||
|
||||
| Metric | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `hindsight_retain_duration_seconds` | Histogram | Retain operation latency |
|
||||
| `hindsight_retain_items_total` | Counter | Total items retained |
|
||||
| `hindsight_recall_duration_seconds` | Histogram | Recall operation latency |
|
||||
| `hindsight_recall_results_count` | Histogram | Number of results per recall |
|
||||
| `hindsight_reflect_duration_seconds` | Histogram | Reflect operation latency |
|
||||
### Token Metrics
|
||||
|
||||
### LLM Metrics
|
||||
|
||||
| Metric | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `hindsight_llm_requests_total` | Counter | LLM API requests (labels: provider, model, status) |
|
||||
| `hindsight_llm_request_duration_seconds` | Histogram | LLM request latency |
|
||||
| `hindsight_llm_tokens_total` | Counter | Tokens consumed (labels: provider, token_type) |
|
||||
|
||||
### Database Metrics
|
||||
|
||||
| Metric | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `hindsight_db_connections_active` | Gauge | Active database connections |
|
||||
| `hindsight_db_connections_idle` | Gauge | Idle connections in pool |
|
||||
| `hindsight_db_query_duration_seconds` | Histogram | Query latency (labels: query_type) |
|
||||
|
||||
### Memory Bank Metrics
|
||||
|
||||
| Metric | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `hindsight_bank_memory_units_total` | Gauge | Total memories per bank |
|
||||
| `hindsight_bank_entities_total` | Gauge | Total entities per bank |
|
||||
| Metric | Type | Labels | Description |
|
||||
|--------|------|--------|-------------|
|
||||
| `hindsight.tokens.input` | Counter | operation, bank_id, budget, max_tokens | Input tokens consumed |
|
||||
| `hindsight.tokens.output` | Counter | operation, bank_id, budget, max_tokens | Output tokens generated |
|
||||
|
||||
## Prometheus Configuration
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ Hindsight uses several machine learning models for different tasks.
|
|||
| Model Type | Purpose | Default | Configurable |
|
||||
|------------|---------|---------|--------------|
|
||||
| **Embedding** | Vector representations for semantic search | `BAAI/bge-small-en-v1.5` | Yes |
|
||||
| **Cross-Encoder** | Reranking search results | `ms-marco-MiniLM-L-6-v2` | Yes |
|
||||
| **Cross-Encoder** | Reranking search results | `cross-encoder/ms-marco-MiniLM-L-6-v2` | Yes |
|
||||
| **LLM** | Fact extraction, reasoning, generation | Provider-specific | Yes |
|
||||
|
||||
All local models (embedding, cross-encoder) are automatically downloaded from HuggingFace on first run.
|
||||
|
|
@ -28,12 +28,20 @@ Converts text into dense vector representations for semantic similarity search.
|
|||
| `BAAI/bge-base-en-v1.5` | 768 | Higher accuracy, slower |
|
||||
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 384 | Multilingual (50+ languages) |
|
||||
|
||||
:::warning
|
||||
All embedding models must produce 384-dimensional vectors to match the database schema.
|
||||
:::
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_EMBEDDING_MODEL=BAAI/bge-base-en-v1.5
|
||||
export HINDSIGHT_API_EMBEDDING_DEVICE=cuda # or mps for Apple Silicon
|
||||
export HINDSIGHT_API_EMBEDDING_BATCH_SIZE=64
|
||||
# Local provider (default)
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
|
||||
export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
|
||||
|
||||
# TEI provider (remote)
|
||||
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=tei
|
||||
export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -48,16 +56,20 @@ Reranks initial search results to improve precision.
|
|||
|
||||
| Model | Use Case |
|
||||
|-------|----------|
|
||||
| `ms-marco-MiniLM-L-6-v2` | Default, fast |
|
||||
| `ms-marco-MiniLM-L-12-v2` | Higher accuracy |
|
||||
| `mmarco-mMiniLMv2-L12-H384-v1` | Multilingual |
|
||||
| `cross-encoder/ms-marco-MiniLM-L-6-v2` | Default, fast |
|
||||
| `cross-encoder/ms-marco-MiniLM-L-12-v2` | Higher accuracy |
|
||||
| `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` | Multilingual |
|
||||
|
||||
**Configuration:**
|
||||
|
||||
```bash
|
||||
export HINDSIGHT_API_RERANK_MODEL=cross-encoder/ms-marco-MiniLM-L-12-v2
|
||||
export HINDSIGHT_API_RERANK_TOP_K=50 # How many results to rerank
|
||||
export HINDSIGHT_API_RERANK_ENABLED=true # Set to false to disable
|
||||
# Local provider (default)
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=local
|
||||
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
|
||||
|
||||
# TEI provider (remote)
|
||||
export HINDSIGHT_API_RERANKER_PROVIDER=tei
|
||||
export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -66,14 +78,14 @@ export HINDSIGHT_API_RERANK_ENABLED=true # Set to false to disable
|
|||
|
||||
Used for fact extraction, entity resolution, opinion generation, and answer synthesis.
|
||||
|
||||
**Supported providers:** Groq, OpenAI, Ollama, Gemini
|
||||
**Supported providers:** Groq, OpenAI, Gemini, Ollama
|
||||
|
||||
| Provider | Recommended Model | Best For |
|
||||
|----------|------------------|----------|
|
||||
| **Groq** | `openai/gpt-oss-20b` | Fast inference, high throughput (recommended) |
|
||||
| **OpenAI** | `gpt-5-mini` | Good quality |
|
||||
| **Gemini** | `gemini-2.5-flash` | Good quality |
|
||||
| **Ollama** | `gpt-oss-20b` | Local deployment, privacy |
|
||||
| **Groq** | `openai/gpt-oss-20b` | Fast inference, high throughput (recommended) |
|
||||
| **OpenAI** | `gpt-4o` | Good quality |
|
||||
| **Gemini** | `gemini-2.0-flash` | Good quality, cost effective |
|
||||
| **Ollama** | `llama3.1` | Local deployment, privacy |
|
||||
|
||||
**Configuration:**
|
||||
|
||||
|
|
@ -86,28 +98,17 @@ export HINDSIGHT_API_LLM_MODEL=openai/gpt-oss-20b
|
|||
# OpenAI
|
||||
export HINDSIGHT_API_LLM_PROVIDER=openai
|
||||
export HINDSIGHT_API_LLM_API_KEY=sk-xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=gpt-5-mini
|
||||
export HINDSIGHT_API_LLM_MODEL=gpt-4o
|
||||
|
||||
# Gemini
|
||||
export HINDSIGHT_API_LLM_PROVIDER=gemini
|
||||
export HINDSIGHT_API_LLM_API_KEY=xxxxxxxxxxxx
|
||||
export HINDSIGHT_API_LLM_MODEL=gemini-2.5-flash
|
||||
export HINDSIGHT_API_LLM_MODEL=gemini-2.0-flash
|
||||
|
||||
# Ollama (local)
|
||||
export HINDSIGHT_API_LLM_PROVIDER=ollama
|
||||
export HINDSIGHT_API_LLM_BASE_URL=http://localhost:11434/v1
|
||||
export HINDSIGHT_API_LLM_MODEL=gpt-oss-20b
|
||||
export HINDSIGHT_API_LLM_MODEL=llama3.1
|
||||
```
|
||||
|
||||
**Note:** The LLM is the primary bottleneck for write operations. See [Performance](./performance) for optimization strategies.
|
||||
|
||||
---
|
||||
|
||||
## Model Comparison
|
||||
|
||||
| Provider | Model | Speed | Quality | Cost |
|
||||
|----------|-------|-------|---------|------|
|
||||
| Groq | gpt-oss-20b | Fast | Good | Free tier |
|
||||
| OpenAI | gpt-4o-mini | Medium | Good | $0.15 / $0.60 per 1M tokens |
|
||||
| OpenAI | gpt-4o | Slower | Best | $2.50 / $10.00 per 1M tokens |
|
||||
| Ollama | llama3.1 | Varies | Good | Free (local) |
|
||||
**Note:** The LLM is the primary bottleneck for retain operations. See [Performance](./performance) for optimization strategies.
|
||||
|
|
|
|||
Loading…
Reference in a new issue