Compare commits

..
4 Commits
Author SHA1 Message Date
Nicolò Boschi 34bb86e1fe support tags 2026-01-13 18:18:01 +01:00
Nicolò Boschi b18b91588c support tags 2026-01-13 18:10:48 +01:00
Nicolò Boschi 7afbd1ef6c feat: add memory tags 2026-01-13 15:42:54 +01:00
Nicolò Boschi 48b19f5543 feat: add memory tags 2026-01-13 15:30:13 +01:00
38 changed files with 117 additions and 1634 deletions
+2 -2
View File
@@ -5,7 +5,7 @@
[Documentation](https://hindsight.vectorize.io) • [Paper](https://arxiv.org/abs/2512.12818) • [Cookbook](https://hindsight.vectorize.io/cookbook) • [Hindsight Cloud](https://vectorize.io/hindsight/cloud)
[![CI](https://github.com/vectorize-io/hindsight/actions/workflows/release.yml/badge.svg)](https://github.com/vectorize-io/hindsight/actions/workflows/release.yml)
[![Slack Community](https://img.shields.io/badge/Slack-Join%20Community-4A154B?logo=slack)](https://join.slack.com/t/hindsight-space/shared_invite/zt-3nhbm4w29-LeSJ5Ixi6j8PdiYOCPlOgg)
[![Slack Community](https://img.shields.io/badge/Slack-Join%20Community-4A154B?logo=slack)](https://join.slack.com/t/hindsight-space/shared_invite/zt-3klo21kua-VUCC_zHP5rIcXFB1_5yw6A)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
![PyPI - Downloads](https://img.shields.io/pypi/dm/hindsight-api?label=PyPI)
![NPM Downloads](https://img.shields.io/npm/dm/%40vectorize-io%2Fhindsight-client?logoColor=orange&label=NPM&color=blue&link=https%3A%2F%2Fwww.npmjs.com%2Fpackage%2F%40vectorize-io%2Fhindsight-client)
@@ -242,7 +242,7 @@ client.reflect(bank_id="my-bank", query="What should I know about Alice?")
- [CLI](https://hindsight.vectorize.io/sdks/cli)
**Community:**
- [Slack](https://join.slack.com/t/hindsight-space/shared_invite/zt-3nhbm4w29-LeSJ5Ixi6j8PdiYOCPlOgg)
- [Slack](https://join.slack.com/t/hindsight-space/shared_invite/zt-3klo21kua-VUCC_zHP5rIcXFB1_5yw6A)
- [GitHub Issues](https://github.com/vectorize-io/hindsight/issues)
---
+2 -2
View File
@@ -2,8 +2,8 @@ apiVersion: v2
name: hindsight
description: Hindsight helm chart
type: application
version: 0.3.0
appVersion: "0.3.0"
version: 0.2.1
appVersion: "0.2.1"
keywords:
- ai
- memory
-20
View File
@@ -41,19 +41,10 @@ ENV_EMBEDDINGS_LOCAL_MODEL = "HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL"
ENV_EMBEDDINGS_TEI_URL = "HINDSIGHT_API_EMBEDDINGS_TEI_URL"
ENV_EMBEDDINGS_OPENAI_API_KEY = "HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY"
ENV_EMBEDDINGS_OPENAI_MODEL = "HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL"
ENV_EMBEDDINGS_OPENAI_BASE_URL = "HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL"
ENV_COHERE_API_KEY = "HINDSIGHT_API_COHERE_API_KEY"
ENV_EMBEDDINGS_COHERE_MODEL = "HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL"
ENV_EMBEDDINGS_COHERE_BASE_URL = "HINDSIGHT_API_EMBEDDINGS_COHERE_BASE_URL"
ENV_RERANKER_COHERE_MODEL = "HINDSIGHT_API_RERANKER_COHERE_MODEL"
ENV_RERANKER_COHERE_BASE_URL = "HINDSIGHT_API_RERANKER_COHERE_BASE_URL"
# LiteLLM gateway configuration (for embeddings and reranker via LiteLLM proxy)
ENV_LITELLM_API_BASE = "HINDSIGHT_API_LITELLM_API_BASE"
ENV_LITELLM_API_KEY = "HINDSIGHT_API_LITELLM_API_KEY"
ENV_EMBEDDINGS_LITELLM_MODEL = "HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL"
ENV_RERANKER_LITELLM_MODEL = "HINDSIGHT_API_RERANKER_LITELLM_MODEL"
ENV_RERANKER_PROVIDER = "HINDSIGHT_API_RERANKER_PROVIDER"
ENV_RERANKER_LOCAL_MODEL = "HINDSIGHT_API_RERANKER_LOCAL_MODEL"
@@ -130,11 +121,6 @@ DEFAULT_RERANKER_FLASHRANK_CACHE_DIR = None # Use default cache directory
DEFAULT_EMBEDDINGS_COHERE_MODEL = "embed-english-v3.0"
DEFAULT_RERANKER_COHERE_MODEL = "rerank-english-v3.0"
# LiteLLM defaults
DEFAULT_LITELLM_API_BASE = "http://localhost:4000"
DEFAULT_EMBEDDINGS_LITELLM_MODEL = "text-embedding-3-small"
DEFAULT_RERANKER_LITELLM_MODEL = "cohere/rerank-english-v3.0"
DEFAULT_HOST = "0.0.0.0"
DEFAULT_PORT = 8888
DEFAULT_LOG_LEVEL = "info"
@@ -238,8 +224,6 @@ class HindsightConfig:
embeddings_provider: str
embeddings_local_model: str
embeddings_tei_url: str | None
embeddings_openai_base_url: str | None
embeddings_cohere_base_url: str | None
# Reranker
reranker_provider: str
@@ -248,7 +232,6 @@ class HindsightConfig:
reranker_tei_batch_size: int
reranker_tei_max_concurrent: int
reranker_max_candidates: int
reranker_cohere_base_url: str | None
# Server
host: str
@@ -317,8 +300,6 @@ class HindsightConfig:
embeddings_provider=os.getenv(ENV_EMBEDDINGS_PROVIDER, DEFAULT_EMBEDDINGS_PROVIDER),
embeddings_local_model=os.getenv(ENV_EMBEDDINGS_LOCAL_MODEL, DEFAULT_EMBEDDINGS_LOCAL_MODEL),
embeddings_tei_url=os.getenv(ENV_EMBEDDINGS_TEI_URL),
embeddings_openai_base_url=os.getenv(ENV_EMBEDDINGS_OPENAI_BASE_URL) or None,
embeddings_cohere_base_url=os.getenv(ENV_EMBEDDINGS_COHERE_BASE_URL) or None,
# Reranker
reranker_provider=os.getenv(ENV_RERANKER_PROVIDER, DEFAULT_RERANKER_PROVIDER),
reranker_local_model=os.getenv(ENV_RERANKER_LOCAL_MODEL, DEFAULT_RERANKER_LOCAL_MODEL),
@@ -328,7 +309,6 @@ class HindsightConfig:
os.getenv(ENV_RERANKER_TEI_MAX_CONCURRENT, str(DEFAULT_RERANKER_TEI_MAX_CONCURRENT))
),
reranker_max_candidates=int(os.getenv(ENV_RERANKER_MAX_CANDIDATES, str(DEFAULT_RERANKER_MAX_CANDIDATES))),
reranker_cohere_base_url=os.getenv(ENV_RERANKER_COHERE_BASE_URL) or None,
# Server
host=os.getenv(ENV_HOST, DEFAULT_HOST),
port=int(os.getenv(ENV_PORT, DEFAULT_PORT)),
@@ -15,24 +15,18 @@ from concurrent.futures import ThreadPoolExecutor
import httpx
from ..config import (
DEFAULT_LITELLM_API_BASE,
DEFAULT_RERANKER_COHERE_MODEL,
DEFAULT_RERANKER_FLASHRANK_CACHE_DIR,
DEFAULT_RERANKER_FLASHRANK_MODEL,
DEFAULT_RERANKER_LITELLM_MODEL,
DEFAULT_RERANKER_LOCAL_MAX_CONCURRENT,
DEFAULT_RERANKER_LOCAL_MODEL,
DEFAULT_RERANKER_PROVIDER,
DEFAULT_RERANKER_TEI_BATCH_SIZE,
DEFAULT_RERANKER_TEI_MAX_CONCURRENT,
ENV_COHERE_API_KEY,
ENV_LITELLM_API_BASE,
ENV_LITELLM_API_KEY,
ENV_RERANKER_COHERE_BASE_URL,
ENV_RERANKER_COHERE_MODEL,
ENV_RERANKER_FLASHRANK_CACHE_DIR,
ENV_RERANKER_FLASHRANK_MODEL,
ENV_RERANKER_LITELLM_MODEL,
ENV_RERANKER_LOCAL_MAX_CONCURRENT,
ENV_RERANKER_LOCAL_MODEL,
ENV_RERANKER_PROVIDER,
@@ -398,7 +392,6 @@ class CohereCrossEncoder(CrossEncoderModel):
self,
api_key: str,
model: str = DEFAULT_RERANKER_COHERE_MODEL,
base_url: str | None = None,
timeout: float = 60.0,
):
"""
@@ -407,12 +400,10 @@ class CohereCrossEncoder(CrossEncoderModel):
Args:
api_key: Cohere API key
model: Cohere rerank model name (default: rerank-english-v3.0)
base_url: Custom base URL for Cohere-compatible API (e.g., Azure-hosted endpoint)
timeout: Request timeout in seconds (default: 60.0)
"""
self.api_key = api_key
self.model = model
self.base_url = base_url
self.timeout = timeout
self._client = None
@@ -430,14 +421,8 @@ class CohereCrossEncoder(CrossEncoderModel):
except ImportError:
raise ImportError("cohere is required for CohereCrossEncoder. Install it with: pip install cohere")
base_url_msg = f" at {self.base_url}" if self.base_url else ""
logger.info(f"Reranker: initializing Cohere provider with model {self.model}{base_url_msg}")
# Build client kwargs, only including base_url if set (for Azure or custom endpoints)
client_kwargs = {"api_key": self.api_key, "timeout": self.timeout}
if self.base_url:
client_kwargs["base_url"] = self.base_url
self._client = cohere.Client(**client_kwargs)
logger.info(f"Reranker: initializing Cohere provider with model {self.model}")
self._client = cohere.Client(api_key=self.api_key, timeout=self.timeout)
logger.info("Reranker: Cohere provider initialized")
async def predict(self, pairs: list[tuple[str, str]]) -> list[float]:
@@ -656,116 +641,6 @@ class FlashRankCrossEncoder(CrossEncoderModel):
return await loop.run_in_executor(FlashRankCrossEncoder._executor, self._predict_sync, pairs)
class LiteLLMCrossEncoder(CrossEncoderModel):
"""
LiteLLM cross-encoder implementation using LiteLLM proxy's /rerank endpoint.
LiteLLM provides a unified interface for multiple reranking providers via
the Cohere-compatible /rerank endpoint.
See: https://docs.litellm.ai/docs/rerank
Supported providers via LiteLLM:
- Cohere (rerank-english-v3.0, etc.) - prefix with cohere/
- Together AI - prefix with together_ai/
- Azure AI - prefix with azure_ai/
- Jina AI - prefix with jina_ai/
- AWS Bedrock - prefix with bedrock/
- Voyage AI - prefix with voyage/
"""
def __init__(
self,
api_base: str = DEFAULT_LITELLM_API_BASE,
api_key: str | None = None,
model: str = DEFAULT_RERANKER_LITELLM_MODEL,
timeout: float = 60.0,
):
"""
Initialize LiteLLM cross-encoder client.
Args:
api_base: Base URL of the LiteLLM proxy (default: http://localhost:4000)
api_key: API key for the LiteLLM proxy (optional, depends on proxy config)
model: Reranking model name (default: cohere/rerank-english-v3.0)
Use provider prefix (e.g., cohere/, together_ai/, voyage/)
timeout: Request timeout in seconds (default: 60.0)
"""
self.api_base = api_base.rstrip("/")
self.api_key = api_key
self.model = model
self.timeout = timeout
self._async_client: httpx.AsyncClient | None = None
@property
def provider_name(self) -> str:
return "litellm"
async def initialize(self) -> None:
"""Initialize the async HTTP client."""
if self._async_client is not None:
return
logger.info(f"Reranker: initializing LiteLLM provider at {self.api_base} with model {self.model}")
headers = {"Content-Type": "application/json"}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
self._async_client = httpx.AsyncClient(timeout=self.timeout, headers=headers)
logger.info("Reranker: LiteLLM provider initialized")
async def predict(self, pairs: list[tuple[str, str]]) -> list[float]:
"""
Score query-document pairs using the LiteLLM proxy's /rerank endpoint.
Args:
pairs: List of (query, document) tuples to score
Returns:
List of relevance scores
"""
if self._async_client is None:
raise RuntimeError("Reranker not initialized. Call initialize() first.")
if not pairs:
return []
# Group pairs by query (LiteLLM rerank expects one query with multiple documents)
query_groups: dict[str, list[tuple[int, str]]] = {}
for idx, (query, text) in enumerate(pairs):
if query not in query_groups:
query_groups[query] = []
query_groups[query].append((idx, text))
all_scores = [0.0] * len(pairs)
for query, indexed_texts in query_groups.items():
texts = [text for _, text in indexed_texts]
indices = [idx for idx, _ in indexed_texts]
# LiteLLM /rerank follows Cohere API format
response = await self._async_client.post(
f"{self.api_base}/rerank",
json={
"model": self.model,
"query": query,
"documents": texts,
"top_n": len(texts), # Return all scores
},
)
response.raise_for_status()
result = response.json()
# Map scores back to original positions
# Response format: {"results": [{"index": 0, "relevance_score": 0.9}, ...]}
for item in result.get("results", []):
original_idx = item["index"]
score = item.get("relevance_score", item.get("score", 0.0))
all_scores[indices[original_idx]] = score
return all_scores
def create_cross_encoder_from_env() -> CrossEncoderModel:
"""
Create a CrossEncoderModel instance based on environment variables.
@@ -796,20 +671,14 @@ def create_cross_encoder_from_env() -> CrossEncoderModel:
if not api_key:
raise ValueError(f"{ENV_COHERE_API_KEY} is required when {ENV_RERANKER_PROVIDER} is 'cohere'")
model = os.environ.get(ENV_RERANKER_COHERE_MODEL, DEFAULT_RERANKER_COHERE_MODEL)
base_url = os.environ.get(ENV_RERANKER_COHERE_BASE_URL) or None
return CohereCrossEncoder(api_key=api_key, model=model, base_url=base_url)
return CohereCrossEncoder(api_key=api_key, model=model)
elif provider == "flashrank":
model = os.environ.get(ENV_RERANKER_FLASHRANK_MODEL, DEFAULT_RERANKER_FLASHRANK_MODEL)
cache_dir = os.environ.get(ENV_RERANKER_FLASHRANK_CACHE_DIR, DEFAULT_RERANKER_FLASHRANK_CACHE_DIR)
return FlashRankCrossEncoder(model_name=model, cache_dir=cache_dir)
elif provider == "litellm":
api_base = os.environ.get(ENV_LITELLM_API_BASE, DEFAULT_LITELLM_API_BASE)
api_key = os.environ.get(ENV_LITELLM_API_KEY)
model = os.environ.get(ENV_RERANKER_LITELLM_MODEL, DEFAULT_RERANKER_LITELLM_MODEL)
return LiteLLMCrossEncoder(api_base=api_base, api_key=api_key, model=model)
elif provider == "rrf":
return RRFPassthroughCrossEncoder()
else:
raise ValueError(
f"Unknown reranker provider: {provider}. Supported: 'local', 'tei', 'cohere', 'flashrank', 'litellm', 'rrf'"
f"Unknown reranker provider: {provider}. Supported: 'local', 'tei', 'cohere', 'flashrank', 'rrf'"
)
@@ -17,23 +17,16 @@ import httpx
from ..config import (
DEFAULT_EMBEDDINGS_COHERE_MODEL,
DEFAULT_EMBEDDINGS_LITELLM_MODEL,
DEFAULT_EMBEDDINGS_LOCAL_MODEL,
DEFAULT_EMBEDDINGS_OPENAI_MODEL,
DEFAULT_EMBEDDINGS_PROVIDER,
DEFAULT_LITELLM_API_BASE,
ENV_COHERE_API_KEY,
ENV_EMBEDDINGS_COHERE_BASE_URL,
ENV_EMBEDDINGS_COHERE_MODEL,
ENV_EMBEDDINGS_LITELLM_MODEL,
ENV_EMBEDDINGS_LOCAL_MODEL,
ENV_EMBEDDINGS_OPENAI_API_KEY,
ENV_EMBEDDINGS_OPENAI_BASE_URL,
ENV_EMBEDDINGS_OPENAI_MODEL,
ENV_EMBEDDINGS_PROVIDER,
ENV_EMBEDDINGS_TEI_URL,
ENV_LITELLM_API_BASE,
ENV_LITELLM_API_KEY,
ENV_LLM_API_KEY,
)
@@ -329,7 +322,6 @@ class OpenAIEmbeddings(Embeddings):
self,
api_key: str,
model: str = DEFAULT_EMBEDDINGS_OPENAI_MODEL,
base_url: str | None = None,
batch_size: int = 100,
max_retries: int = 3,
):
@@ -339,13 +331,11 @@ class OpenAIEmbeddings(Embeddings):
Args:
api_key: OpenAI API key
model: OpenAI embedding model name (default: text-embedding-3-small)
base_url: Custom base URL for OpenAI-compatible API (e.g., Azure OpenAI endpoint)
batch_size: Maximum batch size for embedding requests (default: 100)
max_retries: Maximum number of retries for failed requests (default: 3)
"""
self.api_key = api_key
self.model = model
self.base_url = base_url
self.batch_size = batch_size
self.max_retries = max_retries
self._client = None
@@ -371,14 +361,8 @@ class OpenAIEmbeddings(Embeddings):
except ImportError:
raise ImportError("openai is required for OpenAIEmbeddings. Install it with: pip install openai")
base_url_msg = f" at {self.base_url}" if self.base_url else ""
logger.info(f"Embeddings: initializing OpenAI provider with model {self.model}{base_url_msg}")
# Build client kwargs, only including base_url if set (for Azure or custom endpoints)
client_kwargs = {"api_key": self.api_key, "max_retries": self.max_retries}
if self.base_url:
client_kwargs["base_url"] = self.base_url
self._client = OpenAI(**client_kwargs)
logger.info(f"Embeddings: initializing OpenAI provider with model {self.model}")
self._client = OpenAI(api_key=self.api_key, max_retries=self.max_retries)
# Try to get dimension from known models, otherwise do a test embedding
if self.model in self.MODEL_DIMENSIONS:
@@ -451,7 +435,6 @@ class CohereEmbeddings(Embeddings):
self,
api_key: str,
model: str = DEFAULT_EMBEDDINGS_COHERE_MODEL,
base_url: str | None = None,
batch_size: int = 96,
timeout: float = 60.0,
input_type: str = "search_document",
@@ -462,7 +445,6 @@ class CohereEmbeddings(Embeddings):
Args:
api_key: Cohere API key
model: Cohere embedding model name (default: embed-english-v3.0)
base_url: Custom base URL for Cohere-compatible API (e.g., Azure-hosted endpoint)
batch_size: Maximum batch size for embedding requests (default: 96, Cohere's limit)
timeout: Request timeout in seconds (default: 60.0)
input_type: Input type for embeddings (default: search_document).
@@ -470,7 +452,6 @@ class CohereEmbeddings(Embeddings):
"""
self.api_key = api_key
self.model = model
self.base_url = base_url
self.batch_size = batch_size
self.timeout = timeout
self.input_type = input_type
@@ -497,14 +478,8 @@ class CohereEmbeddings(Embeddings):
except ImportError:
raise ImportError("cohere is required for CohereEmbeddings. Install it with: pip install cohere")
base_url_msg = f" at {self.base_url}" if self.base_url else ""
logger.info(f"Embeddings: initializing Cohere provider with model {self.model}{base_url_msg}")
# Build client kwargs, only including base_url if set (for Azure or custom endpoints)
client_kwargs = {"api_key": self.api_key, "timeout": self.timeout}
if self.base_url:
client_kwargs["base_url"] = self.base_url
self._client = cohere.Client(**client_kwargs)
logger.info(f"Embeddings: initializing Cohere provider with model {self.model}")
self._client = cohere.Client(api_key=self.api_key, timeout=self.timeout)
# Try to get dimension from known models, otherwise do a test embedding
if self.model in self.MODEL_DIMENSIONS:
@@ -554,123 +529,6 @@ class CohereEmbeddings(Embeddings):
return all_embeddings
class LiteLLMEmbeddings(Embeddings):
"""
LiteLLM embeddings implementation using LiteLLM proxy's /embeddings endpoint.
LiteLLM provides a unified interface for multiple embedding providers.
The proxy exposes an OpenAI-compatible /embeddings endpoint.
See: https://docs.litellm.ai/docs/embedding/supported_embedding
Supported providers via LiteLLM:
- OpenAI (text-embedding-3-small, text-embedding-ada-002, etc.)
- Cohere (embed-english-v3.0, etc.) - prefix with cohere/
- Vertex AI (textembedding-gecko, etc.) - prefix with vertex_ai/
- HuggingFace, Mistral, Voyage AI, etc.
The embedding dimension is auto-detected from the model at initialization.
"""
def __init__(
self,
api_base: str = DEFAULT_LITELLM_API_BASE,
api_key: str | None = None,
model: str = DEFAULT_EMBEDDINGS_LITELLM_MODEL,
batch_size: int = 100,
timeout: float = 60.0,
):
"""
Initialize LiteLLM embeddings client.
Args:
api_base: Base URL of the LiteLLM proxy (default: http://localhost:4000)
api_key: API key for the LiteLLM proxy (optional, depends on proxy config)
model: Embedding model name (default: text-embedding-3-small)
Use provider prefix for non-OpenAI models (e.g., cohere/embed-english-v3.0)
batch_size: Maximum batch size for embedding requests (default: 100)
timeout: Request timeout in seconds (default: 60.0)
"""
self.api_base = api_base.rstrip("/")
self.api_key = api_key
self.model = model
self.batch_size = batch_size
self.timeout = timeout
self._client: httpx.Client | None = None
self._dimension: int | None = None
@property
def provider_name(self) -> str:
return "litellm"
@property
def dimension(self) -> int:
if self._dimension is None:
raise RuntimeError("Embeddings not initialized. Call initialize() first.")
return self._dimension
async def initialize(self) -> None:
"""Initialize the HTTP client and detect embedding dimension."""
if self._client is not None:
return
logger.info(f"Embeddings: initializing LiteLLM provider at {self.api_base} with model {self.model}")
headers = {"Content-Type": "application/json"}
if self.api_key:
headers["Authorization"] = f"Bearer {self.api_key}"
self._client = httpx.Client(timeout=self.timeout, headers=headers)
# Do a test embedding to detect dimension
try:
response = self._client.post(
f"{self.api_base}/embeddings",
json={"model": self.model, "input": ["test"]},
)
response.raise_for_status()
result = response.json()
if result.get("data") and len(result["data"]) > 0:
self._dimension = len(result["data"][0]["embedding"])
logger.info(f"Embeddings: LiteLLM provider initialized (model: {self.model}, dim: {self._dimension})")
except httpx.HTTPError as e:
raise RuntimeError(f"Failed to connect to LiteLLM proxy at {self.api_base}: {e}")
def encode(self, texts: list[str]) -> list[list[float]]:
"""
Generate embeddings using the LiteLLM proxy.
Args:
texts: List of text strings to encode
Returns:
List of embedding vectors
"""
if self._client is None:
raise RuntimeError("Embeddings not initialized. Call initialize() first.")
if not texts:
return []
all_embeddings = []
# Process in batches
for i in range(0, len(texts), self.batch_size):
batch = texts[i : i + self.batch_size]
response = self._client.post(
f"{self.api_base}/embeddings",
json={"model": self.model, "input": batch},
)
response.raise_for_status()
result = response.json()
# Sort by index to ensure correct order
batch_embeddings = sorted(result["data"], key=lambda x: x["index"])
all_embeddings.extend([e["embedding"] for e in batch_embeddings])
return all_embeddings
def create_embeddings_from_env() -> Embeddings:
"""
Create an Embeddings instance based on environment variables.
@@ -700,21 +558,12 @@ def create_embeddings_from_env() -> Embeddings:
f"when {ENV_EMBEDDINGS_PROVIDER} is 'openai'"
)
model = os.environ.get(ENV_EMBEDDINGS_OPENAI_MODEL, DEFAULT_EMBEDDINGS_OPENAI_MODEL)
base_url = os.environ.get(ENV_EMBEDDINGS_OPENAI_BASE_URL) or None
return OpenAIEmbeddings(api_key=api_key, model=model, base_url=base_url)
return OpenAIEmbeddings(api_key=api_key, model=model)
elif provider == "cohere":
api_key = os.environ.get(ENV_COHERE_API_KEY)
if not api_key:
raise ValueError(f"{ENV_COHERE_API_KEY} is required when {ENV_EMBEDDINGS_PROVIDER} is 'cohere'")
model = os.environ.get(ENV_EMBEDDINGS_COHERE_MODEL, DEFAULT_EMBEDDINGS_COHERE_MODEL)
base_url = os.environ.get(ENV_EMBEDDINGS_COHERE_BASE_URL) or None
return CohereEmbeddings(api_key=api_key, model=model, base_url=base_url)
elif provider == "litellm":
api_base = os.environ.get(ENV_LITELLM_API_BASE, DEFAULT_LITELLM_API_BASE)
api_key = os.environ.get(ENV_LITELLM_API_KEY)
model = os.environ.get(ENV_EMBEDDINGS_LITELLM_MODEL, DEFAULT_EMBEDDINGS_LITELLM_MODEL)
return LiteLLMEmbeddings(api_base=api_base, api_key=api_key, model=model)
return CohereEmbeddings(api_key=api_key, model=model)
else:
raise ValueError(
f"Unknown embeddings provider: {provider}. Supported: 'local', 'tei', 'openai', 'cohere', 'litellm'"
)
raise ValueError(f"Unknown embeddings provider: {provider}. Supported: 'local', 'tei', 'openai', 'cohere'")
@@ -3783,7 +3783,7 @@ Guidelines:
SELECT id, canonical_name, mention_count, first_seen, last_seen, metadata
FROM {fq_table("entities")}
WHERE bank_id = $1
ORDER BY mention_count DESC, last_seen DESC, id ASC
ORDER BY mention_count DESC, last_seen DESC
LIMIT $2 OFFSET $3
""",
bank_id,
-3
View File
@@ -187,15 +187,12 @@ def main():
embeddings_provider=config.embeddings_provider,
embeddings_local_model=config.embeddings_local_model,
embeddings_tei_url=config.embeddings_tei_url,
embeddings_openai_base_url=config.embeddings_openai_base_url,
embeddings_cohere_base_url=config.embeddings_cohere_base_url,
reranker_provider=config.reranker_provider,
reranker_local_model=config.reranker_local_model,
reranker_tei_url=config.reranker_tei_url,
reranker_tei_batch_size=config.reranker_tei_batch_size,
reranker_tei_max_concurrent=config.reranker_tei_max_concurrent,
reranker_max_candidates=config.reranker_max_candidates,
reranker_cohere_base_url=config.reranker_cohere_base_url,
host=args.host,
port=args.port,
log_level=args.log_level,
-15
View File
@@ -28,15 +28,6 @@ from opentelemetry.sdk.resources import Resource
if TYPE_CHECKING:
import asyncpg
def _get_tenant() -> str:
"""Get current tenant (schema) from context for metrics labeling."""
# Import here to avoid circular imports
from hindsight_api.engine.memory_engine import get_current_schema
return get_current_schema()
# Custom bucket boundaries for operation duration (in seconds)
# Fine granularity in 0-30s range where most operations complete
DURATION_BUCKETS = (0.1, 0.25, 0.5, 0.75, 1.0, 2.0, 3.0, 5.0, 7.5, 10.0, 15.0, 20.0, 30.0, 60.0, 120.0)
@@ -332,7 +323,6 @@ class MetricsCollector(MetricsCollectorBase):
"operation": operation,
"bank_id": bank_id,
"source": source,
"tenant": _get_tenant(),
}
if budget:
attributes["budget"] = budget
@@ -383,7 +373,6 @@ class MetricsCollector(MetricsCollectorBase):
"model": model,
"scope": scope,
"success": str(success).lower(),
"tenant": _get_tenant(),
}
# Record duration
@@ -436,14 +425,10 @@ class MetricsCollector(MetricsCollectorBase):
status_code = status_code_getter()
status_class = f"{status_code // 100}xx"
# Get tenant from context (may be set during request processing)
tenant = _get_tenant()
attributes = {
**base_attributes,
"status_code": str(status_code),
"status_class": status_class,
"tenant": tenant,
}
# Record duration and count
+1 -31
View File
@@ -7,7 +7,6 @@ This module provides the ASGI app for uvicorn import string usage:
For CLI usage, use the hindsight-api command instead.
"""
import logging
import os
import warnings
@@ -18,12 +17,6 @@ warnings.filterwarnings("ignore", message="websockets.server.WebSocketServerProt
from hindsight_api import MemoryEngine
from hindsight_api.api import create_app
from hindsight_api.config import get_config
from hindsight_api.extensions import (
DefaultExtensionContext,
OperationValidatorExtension,
TenantExtension,
load_extension,
)
# Disable tokenizers parallelism to avoid warnings
os.environ["TOKENIZERS_PARALLELISM"] = "false"
@@ -32,33 +25,10 @@ os.environ["TOKENIZERS_PARALLELISM"] = "false"
config = get_config()
config.configure_logging()
# Load operation validator extension if configured
operation_validator = load_extension("OPERATION_VALIDATOR", OperationValidatorExtension)
if operation_validator:
logging.info(f"Loaded operation validator: {operation_validator.__class__.__name__}")
# Load tenant extension if configured
tenant_extension = load_extension("TENANT", TenantExtension)
if tenant_extension:
logging.info(f"Loaded tenant extension: {tenant_extension.__class__.__name__}")
# Create app at module level (required for uvicorn import string)
# MemoryEngine reads configuration from environment variables automatically
# Note: run_migrations=True by default, but migrations are idempotent so safe with workers
_memory = MemoryEngine(
operation_validator=operation_validator,
tenant_extension=tenant_extension,
run_migrations=config.run_migrations_on_startup,
)
# Set extension context on tenant extension (needed for schema provisioning)
if tenant_extension:
extension_context = DefaultExtensionContext(
database_url=config.database_url,
memory_engine=_memory,
)
tenant_extension.set_context(extension_context)
logging.info("Extension context set on tenant extension")
_memory = MemoryEngine(run_migrations=config.run_migrations_on_startup)
# Create unified app with both HTTP and optionally MCP
app = create_app(
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "hindsight-api"
version = "0.3.0"
version = "0.2.1"
description = "Hindsight: Agent Memory That Works Like Human Memory"
readme = "README.md"
requires-python = ">=3.11"
-396
View File
@@ -1,396 +0,0 @@
"""
Tests for hindsight_api.main module (single-worker code path).
The main.py module is used when running with a single worker:
hindsight-api (or hindsight-api --workers 1)
When workers=1, main.py creates the app directly and passes it to uvicorn.
These tests ensure that extensions are properly loaded in this code path.
Compare with test_server_module.py which tests the multi-worker path (workers > 1).
"""
import sys
from unittest.mock import MagicMock, patch
class TestMainModuleExtensionLoading:
"""Tests that main.py correctly loads extensions when configured via environment."""
def test_main_loads_tenant_extension_when_configured(self, monkeypatch):
"""
Verify that main.py loads tenant extension from HINDSIGHT_API_TENANT_EXTENSION.
This ensures extension loading works in the single-worker code path.
"""
# Set up environment to configure a tenant extension
monkeypatch.setenv(
"HINDSIGHT_API_TENANT_EXTENSION",
"tests.test_main_module:MockTenantExtension",
)
# Ensure single worker mode
monkeypatch.setenv("HINDSIGHT_API_WORKERS", "1")
# Track what extensions were loaded via load_extension
loaded_extensions = {}
# Get the real load_extension function
from hindsight_api.extensions.loader import load_extension as real_load_extension
def tracking_load_extension(name, base_class):
"""Track calls to load_extension and delegate to original."""
result = real_load_extension(name, base_class)
loaded_extensions[name] = result
return result
with patch("hindsight_api.main.MemoryEngine") as mock_engine, \
patch("hindsight_api.main.create_app") as mock_create_app, \
patch("hindsight_api.main.get_config") as mock_get_config, \
patch("hindsight_api.main.load_extension", side_effect=tracking_load_extension), \
patch("hindsight_api.main.DefaultExtensionContext"), \
patch("hindsight_api.main.print_banner"), \
patch("uvicorn.run"): # Don't actually start uvicorn
mock_config = MagicMock()
mock_config.host = "0.0.0.0"
mock_config.port = 8888
mock_config.log_level = "info"
mock_config.mcp_enabled = False
mock_config.run_migrations_on_startup = False
mock_config.database_url = "postgresql://test:test@localhost/test"
mock_get_config.return_value = mock_config
mock_engine.return_value = MagicMock()
mock_create_app.return_value = MagicMock()
# Mock sys.argv to simulate CLI invocation
with patch.object(sys, 'argv', ['hindsight-api']):
from hindsight_api.main import main
main()
# Verify TENANT extension was loaded
assert "TENANT" in loaded_extensions, \
"main.py did not call load_extension('TENANT', ...) - extensions not loaded!"
assert loaded_extensions["TENANT"] is not None, \
"load_extension('TENANT', ...) returned None despite env var being set"
assert isinstance(loaded_extensions["TENANT"], MockTenantExtension), \
f"Expected MockTenantExtension, got {type(loaded_extensions['TENANT'])}"
def test_main_loads_operation_validator_when_configured(self, monkeypatch):
"""
Verify that main.py loads operation validator from HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION.
"""
monkeypatch.setenv(
"HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION",
"tests.test_main_module:MockOperationValidator",
)
monkeypatch.setenv("HINDSIGHT_API_WORKERS", "1")
loaded_extensions = {}
from hindsight_api.extensions.loader import load_extension as real_load_extension
def tracking_load_extension(name, base_class):
result = real_load_extension(name, base_class)
loaded_extensions[name] = result
return result
with patch("hindsight_api.main.MemoryEngine") as mock_engine, \
patch("hindsight_api.main.create_app") as mock_create_app, \
patch("hindsight_api.main.get_config") as mock_get_config, \
patch("hindsight_api.main.load_extension", side_effect=tracking_load_extension), \
patch("hindsight_api.main.DefaultExtensionContext"), \
patch("hindsight_api.main.print_banner"), \
patch("uvicorn.run"):
mock_config = MagicMock()
mock_config.host = "0.0.0.0"
mock_config.port = 8888
mock_config.log_level = "info"
mock_config.mcp_enabled = False
mock_config.run_migrations_on_startup = False
mock_config.database_url = "postgresql://test:test@localhost/test"
mock_get_config.return_value = mock_config
mock_engine.return_value = MagicMock()
mock_create_app.return_value = MagicMock()
with patch.object(sys, 'argv', ['hindsight-api']):
from hindsight_api.main import main
main()
assert "OPERATION_VALIDATOR" in loaded_extensions, \
"main.py did not call load_extension('OPERATION_VALIDATOR', ...)"
assert loaded_extensions["OPERATION_VALIDATOR"] is not None
assert isinstance(loaded_extensions["OPERATION_VALIDATOR"], MockOperationValidator)
def test_main_passes_extensions_to_memory_engine(self, monkeypatch):
"""
Verify that main.py passes loaded extensions to MemoryEngine constructor.
This is the critical test - even if extensions are loaded, they must be
passed to MemoryEngine for authentication to work.
"""
monkeypatch.setenv(
"HINDSIGHT_API_TENANT_EXTENSION",
"tests.test_main_module:MockTenantExtension",
)
monkeypatch.setenv("HINDSIGHT_API_WORKERS", "1")
memory_engine_calls = []
def capture_memory_engine(*args, **kwargs):
memory_engine_calls.append({"args": args, "kwargs": kwargs})
return MagicMock()
with patch("hindsight_api.main.MemoryEngine", side_effect=capture_memory_engine), \
patch("hindsight_api.main.create_app") as mock_create_app, \
patch("hindsight_api.main.get_config") as mock_get_config, \
patch("hindsight_api.main.DefaultExtensionContext"), \
patch("hindsight_api.main.print_banner"), \
patch("uvicorn.run"):
mock_config = MagicMock()
mock_config.host = "0.0.0.0"
mock_config.port = 8888
mock_config.log_level = "info"
mock_config.mcp_enabled = False
mock_config.run_migrations_on_startup = False
mock_config.database_url = "postgresql://test:test@localhost/test"
mock_get_config.return_value = mock_config
mock_create_app.return_value = MagicMock()
with patch.object(sys, 'argv', ['hindsight-api']):
from hindsight_api.main import main
main()
# Verify MemoryEngine was called
assert len(memory_engine_calls) == 1, "MemoryEngine should be called exactly once"
call_kwargs = memory_engine_calls[0]["kwargs"]
# THE CRITICAL ASSERTION: tenant_extension must be passed and not None
assert "tenant_extension" in call_kwargs, \
"MemoryEngine was not called with tenant_extension parameter!"
assert call_kwargs["tenant_extension"] is not None, \
"tenant_extension was None - main.py did not pass loaded extension to MemoryEngine!"
def test_main_sets_extension_context_on_tenant_extension(self, monkeypatch):
"""
Verify that main.py sets the extension context on tenant extension.
This is required for tenant extensions that need to provision schemas.
"""
monkeypatch.setenv(
"HINDSIGHT_API_TENANT_EXTENSION",
"tests.test_main_module:MockTenantExtension",
)
monkeypatch.setenv("HINDSIGHT_API_WORKERS", "1")
captured_tenant_ext = [None]
def capture_memory_engine(*args, **kwargs):
captured_tenant_ext[0] = kwargs.get("tenant_extension")
return MagicMock()
context_created = []
def capture_context(*args, **kwargs):
ctx = MagicMock()
context_created.append(ctx)
return ctx
with patch("hindsight_api.main.MemoryEngine", side_effect=capture_memory_engine), \
patch("hindsight_api.main.create_app") as mock_create_app, \
patch("hindsight_api.main.get_config") as mock_get_config, \
patch("hindsight_api.main.DefaultExtensionContext", side_effect=capture_context), \
patch("hindsight_api.main.print_banner"), \
patch("uvicorn.run"):
mock_config = MagicMock()
mock_config.host = "0.0.0.0"
mock_config.port = 8888
mock_config.log_level = "info"
mock_config.mcp_enabled = False
mock_config.run_migrations_on_startup = False
mock_config.database_url = "postgresql://test:test@localhost/test"
mock_get_config.return_value = mock_config
mock_create_app.return_value = MagicMock()
with patch.object(sys, 'argv', ['hindsight-api']):
from hindsight_api.main import main
main()
# Verify context was created and set
assert len(context_created) == 1, "DefaultExtensionContext should be created"
assert captured_tenant_ext[0] is not None, "Tenant extension should be captured"
assert captured_tenant_ext[0]._context_set, \
"set_context was not called on tenant extension"
def test_main_works_without_extensions(self, monkeypatch):
"""
Verify that main.py works correctly when no extensions are configured.
"""
# Ensure no extension env vars are set
monkeypatch.delenv("HINDSIGHT_API_TENANT_EXTENSION", raising=False)
monkeypatch.delenv("HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION", raising=False)
monkeypatch.setenv("HINDSIGHT_API_WORKERS", "1")
memory_engine_calls = []
def capture_memory_engine(*args, **kwargs):
memory_engine_calls.append({"args": args, "kwargs": kwargs})
return MagicMock()
with patch("hindsight_api.main.MemoryEngine", side_effect=capture_memory_engine), \
patch("hindsight_api.main.create_app") as mock_create_app, \
patch("hindsight_api.main.get_config") as mock_get_config, \
patch("hindsight_api.main.print_banner"), \
patch("uvicorn.run"):
mock_config = MagicMock()
mock_config.host = "0.0.0.0"
mock_config.port = 8888
mock_config.log_level = "info"
mock_config.mcp_enabled = False
mock_config.run_migrations_on_startup = False
mock_config.database_url = "postgresql://test:test@localhost/test"
mock_get_config.return_value = mock_config
mock_create_app.return_value = MagicMock()
with patch.object(sys, 'argv', ['hindsight-api']):
from hindsight_api.main import main
main()
# Should work without extensions
assert len(memory_engine_calls) == 1
call_kwargs = memory_engine_calls[0]["kwargs"]
# Extensions should be None when not configured
assert call_kwargs.get("tenant_extension") is None
assert call_kwargs.get("operation_validator") is None
def test_main_uses_app_object_for_single_worker(self, monkeypatch):
"""
Verify that main.py passes the app object (not import string) when workers=1.
This is important because it means single-worker mode uses the app created
in main.py (with extensions loaded), not server.py.
"""
monkeypatch.setenv("HINDSIGHT_API_WORKERS", "1")
monkeypatch.delenv("HINDSIGHT_API_TENANT_EXTENSION", raising=False)
uvicorn_calls = []
def capture_uvicorn_run(**kwargs):
uvicorn_calls.append(kwargs)
mock_app = MagicMock()
with patch("hindsight_api.main.MemoryEngine") as mock_engine, \
patch("hindsight_api.main.create_app", return_value=mock_app), \
patch("hindsight_api.main.get_config") as mock_get_config, \
patch("hindsight_api.main.print_banner"), \
patch("uvicorn.run", side_effect=capture_uvicorn_run):
mock_config = MagicMock()
mock_config.host = "0.0.0.0"
mock_config.port = 8888
mock_config.log_level = "info"
mock_config.mcp_enabled = False
mock_config.run_migrations_on_startup = False
mock_config.database_url = "postgresql://test:test@localhost/test"
mock_get_config.return_value = mock_config
mock_engine.return_value = MagicMock()
with patch.object(sys, 'argv', ['hindsight-api', '--workers', '1']):
from hindsight_api.main import main
main()
assert len(uvicorn_calls) == 1
# With workers=1, should pass app object, not import string
assert uvicorn_calls[0]["app"] is mock_app, \
"main.py should pass app object (not import string) when workers=1"
def test_main_uses_import_string_for_multiple_workers(self, monkeypatch):
"""
Verify that main.py uses import string when workers > 1.
This is important because multi-worker mode requires server.py to be imported
by each worker process.
"""
monkeypatch.setenv("HINDSIGHT_API_WORKERS", "2")
monkeypatch.delenv("HINDSIGHT_API_TENANT_EXTENSION", raising=False)
uvicorn_calls = []
def capture_uvicorn_run(**kwargs):
uvicorn_calls.append(kwargs)
with patch("hindsight_api.main.MemoryEngine") as mock_engine, \
patch("hindsight_api.main.create_app") as mock_create_app, \
patch("hindsight_api.main.get_config") as mock_get_config, \
patch("hindsight_api.main.print_banner"), \
patch("uvicorn.run", side_effect=capture_uvicorn_run):
mock_config = MagicMock()
mock_config.host = "0.0.0.0"
mock_config.port = 8888
mock_config.log_level = "info"
mock_config.mcp_enabled = False
mock_config.run_migrations_on_startup = False
mock_config.database_url = "postgresql://test:test@localhost/test"
mock_get_config.return_value = mock_config
mock_engine.return_value = MagicMock()
mock_create_app.return_value = MagicMock()
with patch.object(sys, 'argv', ['hindsight-api', '--workers', '2']):
from hindsight_api.main import main
main()
assert len(uvicorn_calls) == 1
# With workers > 1, should use import string
assert uvicorn_calls[0]["app"] == "hindsight_api.server:app", \
"main.py should use import string when workers > 1"
assert uvicorn_calls[0]["workers"] == 2
# Mock extensions for testing
from hindsight_api.extensions import (
TenantExtension,
TenantContext,
RequestContext,
OperationValidatorExtension,
ValidationResult,
RetainContext,
RecallContext,
ReflectContext,
)
class MockTenantExtension(TenantExtension):
"""Mock tenant extension for testing main.py extension loading."""
def __init__(self, config: dict):
super().__init__(config)
self._context_set = False
async def authenticate(self, request_context: RequestContext) -> TenantContext:
return TenantContext(schema_name="public")
def set_context(self, context) -> None:
self._context_set = True
class MockOperationValidator(OperationValidatorExtension):
"""Mock operation validator for testing main.py extension loading."""
def __init__(self, config: dict):
super().__init__(config)
async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
return ValidationResult.accept()
async def validate_recall(self, ctx: RecallContext) -> ValidationResult:
return ValidationResult.accept()
async def validate_reflect(self, ctx: ReflectContext) -> ValidationResult:
return ValidationResult.accept()
-290
View File
@@ -1,290 +0,0 @@
"""
Tests for hindsight_api.server module (multi-worker code path).
The server.py module is used when running with multiple workers:
uvicorn hindsight_api.server:app --workers 2
This module executes code at import time, creating the app at module level.
These tests ensure that extensions are properly loaded in this code path,
which was previously a regression that caused authentication bypass in production.
"""
import importlib
import sys
from unittest.mock import MagicMock, patch
def _clean_server_module():
"""Remove hindsight_api.server from sys.modules for fresh import."""
modules_to_remove = [k for k in sys.modules.keys() if k.startswith("hindsight_api.server")]
for mod in modules_to_remove:
del sys.modules[mod]
class TestServerModuleExtensionLoading:
"""Tests that server.py correctly loads extensions when configured via environment."""
def test_server_loads_tenant_extension_when_configured(self, monkeypatch):
"""
Verify that server.py loads tenant extension from HINDSIGHT_API_TENANT_EXTENSION.
This test catches the regression where server.py didn't call load_extension(),
causing authentication to be bypassed in multi-worker deployments.
"""
# Set up environment to configure a tenant extension
monkeypatch.setenv(
"HINDSIGHT_API_TENANT_EXTENSION",
"tests.test_server_module:MockTenantExtension",
)
_clean_server_module()
# Track what extensions were loaded via load_extension
loaded_extensions = {}
# Get the real load_extension function
from hindsight_api.extensions.loader import load_extension as real_load_extension
def tracking_load_extension(name, base_class):
"""Track calls to load_extension and delegate to original."""
result = real_load_extension(name, base_class)
loaded_extensions[name] = result
return result
# Patch at source level BEFORE importing server
# Note: We patch the entire hindsight_api module namespace
with patch("hindsight_api.MemoryEngine") as mock_engine, \
patch("hindsight_api.api.create_app") as mock_create_app, \
patch("hindsight_api.config.get_config") as mock_get_config, \
patch("hindsight_api.extensions.load_extension", side_effect=tracking_load_extension), \
patch("hindsight_api.extensions.DefaultExtensionContext"):
mock_config = MagicMock()
mock_config.mcp_enabled = False
mock_config.run_migrations_on_startup = False
mock_config.database_url = "postgresql://test:test@localhost/test"
mock_get_config.return_value = mock_config
mock_engine.return_value = MagicMock()
mock_create_app.return_value = MagicMock()
# Now import server - this triggers module-level code
import hindsight_api.server
# Verify TENANT extension was loaded
assert "TENANT" in loaded_extensions, \
"server.py did not call load_extension('TENANT', ...) - extensions not loaded!"
assert loaded_extensions["TENANT"] is not None, \
"load_extension('TENANT', ...) returned None despite env var being set"
assert isinstance(loaded_extensions["TENANT"], MockTenantExtension), \
f"Expected MockTenantExtension, got {type(loaded_extensions['TENANT'])}"
def test_server_loads_operation_validator_when_configured(self, monkeypatch):
"""
Verify that server.py loads operation validator from HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION.
"""
monkeypatch.setenv(
"HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION",
"tests.test_server_module:MockOperationValidator",
)
_clean_server_module()
loaded_extensions = {}
from hindsight_api.extensions.loader import load_extension as real_load_extension
def tracking_load_extension(name, base_class):
result = real_load_extension(name, base_class)
loaded_extensions[name] = result
return result
with patch("hindsight_api.MemoryEngine") as mock_engine, \
patch("hindsight_api.api.create_app") as mock_create_app, \
patch("hindsight_api.config.get_config") as mock_get_config, \
patch("hindsight_api.extensions.load_extension", side_effect=tracking_load_extension), \
patch("hindsight_api.extensions.DefaultExtensionContext"):
mock_config = MagicMock()
mock_config.mcp_enabled = False
mock_config.run_migrations_on_startup = False
mock_config.database_url = "postgresql://test:test@localhost/test"
mock_get_config.return_value = mock_config
mock_engine.return_value = MagicMock()
mock_create_app.return_value = MagicMock()
import hindsight_api.server
assert "OPERATION_VALIDATOR" in loaded_extensions, \
"server.py did not call load_extension('OPERATION_VALIDATOR', ...)"
assert loaded_extensions["OPERATION_VALIDATOR"] is not None
assert isinstance(loaded_extensions["OPERATION_VALIDATOR"], MockOperationValidator)
def test_server_passes_extensions_to_memory_engine(self, monkeypatch):
"""
Verify that server.py passes loaded extensions to MemoryEngine constructor.
This is the critical test - even if extensions are loaded, they must be
passed to MemoryEngine for authentication to work.
"""
monkeypatch.setenv(
"HINDSIGHT_API_TENANT_EXTENSION",
"tests.test_server_module:MockTenantExtension",
)
_clean_server_module()
memory_engine_calls = []
def capture_memory_engine(*args, **kwargs):
memory_engine_calls.append({"args": args, "kwargs": kwargs})
return MagicMock()
with patch("hindsight_api.MemoryEngine", side_effect=capture_memory_engine), \
patch("hindsight_api.api.create_app") as mock_create_app, \
patch("hindsight_api.config.get_config") as mock_get_config, \
patch("hindsight_api.extensions.DefaultExtensionContext"):
mock_config = MagicMock()
mock_config.mcp_enabled = False
mock_config.run_migrations_on_startup = False
mock_config.database_url = "postgresql://test:test@localhost/test"
mock_get_config.return_value = mock_config
mock_create_app.return_value = MagicMock()
import hindsight_api.server
# Verify MemoryEngine was called
assert len(memory_engine_calls) == 1, "MemoryEngine should be called exactly once"
call_kwargs = memory_engine_calls[0]["kwargs"]
# THE CRITICAL ASSERTION: tenant_extension must be passed and not None
assert "tenant_extension" in call_kwargs, \
"MemoryEngine was not called with tenant_extension parameter!"
assert call_kwargs["tenant_extension"] is not None, \
"tenant_extension was None - server.py did not pass loaded extension to MemoryEngine!"
def test_server_sets_extension_context_on_tenant_extension(self, monkeypatch):
"""
Verify that server.py sets the extension context on tenant extension.
This is required for tenant extensions that need to provision schemas.
"""
monkeypatch.setenv(
"HINDSIGHT_API_TENANT_EXTENSION",
"tests.test_server_module:MockTenantExtension",
)
_clean_server_module()
context_set_calls = []
captured_tenant_ext = [None]
def capture_memory_engine(*args, **kwargs):
captured_tenant_ext[0] = kwargs.get("tenant_extension")
return MagicMock()
def capture_context(*args, **kwargs):
ctx = MagicMock()
context_set_calls.append(ctx)
return ctx
with patch("hindsight_api.MemoryEngine", side_effect=capture_memory_engine), \
patch("hindsight_api.api.create_app") as mock_create_app, \
patch("hindsight_api.config.get_config") as mock_get_config, \
patch("hindsight_api.extensions.DefaultExtensionContext", side_effect=capture_context):
mock_config = MagicMock()
mock_config.mcp_enabled = False
mock_config.run_migrations_on_startup = False
mock_config.database_url = "postgresql://test:test@localhost/test"
mock_get_config.return_value = mock_config
mock_create_app.return_value = MagicMock()
import hindsight_api.server
# Verify context was created and set
assert len(context_set_calls) == 1, "DefaultExtensionContext should be created"
assert captured_tenant_ext[0] is not None, "Tenant extension should be captured"
assert captured_tenant_ext[0]._context_set, \
"set_context was not called on tenant extension"
def test_server_works_without_extensions(self, monkeypatch):
"""
Verify that server.py works correctly when no extensions are configured.
"""
# Ensure no extension env vars are set
monkeypatch.delenv("HINDSIGHT_API_TENANT_EXTENSION", raising=False)
monkeypatch.delenv("HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION", raising=False)
_clean_server_module()
memory_engine_calls = []
def capture_memory_engine(*args, **kwargs):
memory_engine_calls.append({"args": args, "kwargs": kwargs})
return MagicMock()
with patch("hindsight_api.MemoryEngine", side_effect=capture_memory_engine), \
patch("hindsight_api.api.create_app") as mock_create_app, \
patch("hindsight_api.config.get_config") as mock_get_config:
mock_config = MagicMock()
mock_config.mcp_enabled = False
mock_config.run_migrations_on_startup = False
mock_config.database_url = "postgresql://test:test@localhost/test"
mock_get_config.return_value = mock_config
mock_create_app.return_value = MagicMock()
import hindsight_api.server
# Should work without extensions
assert len(memory_engine_calls) == 1
call_kwargs = memory_engine_calls[0]["kwargs"]
# Extensions should be None when not configured
assert call_kwargs.get("tenant_extension") is None
assert call_kwargs.get("operation_validator") is None
# Mock extensions for testing
from hindsight_api.extensions import (
TenantExtension,
TenantContext,
RequestContext,
OperationValidatorExtension,
ValidationResult,
RetainContext,
RecallContext,
ReflectContext,
)
class MockTenantExtension(TenantExtension):
"""Mock tenant extension for testing server.py extension loading."""
def __init__(self, config: dict):
super().__init__(config)
self._context_set = False
async def authenticate(self, request_context: RequestContext) -> TenantContext:
return TenantContext(schema_name="public")
def set_context(self, context) -> None:
self._context_set = True
class MockOperationValidator(OperationValidatorExtension):
"""Mock operation validator for testing server.py extension loading."""
def __init__(self, config: dict):
super().__init__(config)
async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
return ValidationResult.accept()
async def validate_recall(self, ctx: RecallContext) -> ValidationResult:
return ValidationResult.accept()
async def validate_reflect(self, ctx: ReflectContext) -> ValidationResult:
return ValidationResult.accept()
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "hindsight-cli"
version = "0.3.0"
version = "0.2.1"
edition = "2021"
authors = ["Hindsight Team"]
description = "A beautiful CLI for Hindsight - semantic memory system"
@@ -115,7 +115,6 @@ class Hindsight:
document_id: Optional[str] = None,
metadata: Optional[Dict[str, str]] = None,
entities: Optional[List[Dict[str, str]]] = None,
tags: Optional[List[str]] = None,
) -> RetainResponse:
"""
Store a single memory (simplified interface).
@@ -128,14 +127,13 @@ class Hindsight:
document_id: Optional document ID for grouping
metadata: Optional user-defined metadata
entities: Optional list of entities [{"text": "...", "type": "..."}]
tags: Optional list of tags for this memory
Returns:
RetainResponse with success status
"""
return self.retain_batch(
bank_id=bank_id,
items=[{"content": content, "timestamp": timestamp, "context": context, "metadata": metadata, "entities": entities, "tags": tags}],
items=[{"content": content, "timestamp": timestamp, "context": context, "metadata": metadata, "entities": entities}],
document_id=document_id,
)
@@ -145,17 +143,15 @@ class Hindsight:
items: List[Dict[str, Any]],
document_id: Optional[str] = None,
retain_async: bool = False,
document_tags: Optional[List[str]] = None,
) -> RetainResponse:
"""
Store multiple memories in batch.
Args:
bank_id: The memory bank ID
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id', 'entities', 'tags'
items: List of memory items with 'content' and optional 'timestamp', 'context', 'metadata', 'document_id', 'entities'
document_id: Optional document ID for grouping memories (applied to items that don't have their own)
retain_async: If True, process asynchronously in background (default: False)
document_tags: Optional list of tags to apply to all memories in this batch
Returns:
RetainResponse with success status and item count
@@ -179,14 +175,12 @@ class Hindsight:
# Use item's document_id if provided, otherwise fall back to batch-level document_id
document_id=item.get("document_id") or document_id,
entities=entities,
tags=item.get("tags"),
)
)
request_obj = retain_request.RetainRequest(
items=memory_items,
async_=retain_async,
document_tags=document_tags,
)
return _run_async(self._memory_api.retain_memories(bank_id, request_obj))
@@ -204,8 +198,6 @@ class Hindsight:
max_entity_tokens: int = 500,
include_chunks: bool = False,
max_chunk_tokens: int = 8192,
tags: Optional[List[str]] = None,
tags_match: str = "any",
) -> RecallResponse:
"""
Recall memories using semantic similarity.
@@ -222,9 +214,6 @@ class Hindsight:
max_entity_tokens: Maximum tokens for entity observations (default: 500)
include_chunks: Include raw text chunks in results (default: False)
max_chunk_tokens: Maximum tokens for chunks (default: 8192)
tags: Optional list of tags to filter memories by
tags_match: How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged),
'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any'
Returns:
RecallResponse with results, optional entities, optional chunks, and optional trace
@@ -244,8 +233,6 @@ class Hindsight:
trace=trace,
query_timestamp=query_timestamp,
include=include_opts,
tags=tags,
tags_match=tags_match,
)
return _run_async(self._memory_api.recall_memories(bank_id, request_obj))
@@ -258,8 +245,6 @@ class Hindsight:
context: Optional[str] = None,
max_tokens: Optional[int] = None,
response_schema: Optional[Dict[str, Any]] = None,
tags: Optional[List[str]] = None,
tags_match: str = "any",
) -> ReflectResponse:
"""
Generate a contextual answer based on bank identity and memories.
@@ -273,9 +258,6 @@ class Hindsight:
response_schema: Optional JSON Schema for structured output. When provided,
the response will include a 'structured_output' field with the LLM
response parsed according to this schema.
tags: Optional list of tags to filter memories by
tags_match: How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged),
'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any'
Returns:
ReflectResponse with answer text, optionally facts used, and optionally
@@ -287,8 +269,6 @@ class Hindsight:
context=context,
max_tokens=max_tokens,
response_schema=response_schema,
tags=tags,
tags_match=tags_match,
)
return _run_async(self._memory_api.reflect(bank_id, request_obj))
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "hindsight-client"
version = "0.3.0"
version = "0.2.1"
description = "Python client for Hindsight - Semantic memory system with personality-driven thinking"
authors = [
{name = "Hindsight Team"}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@vectorize-io/hindsight-client",
"version": "0.3.0",
"version": "0.2.1",
"description": "TypeScript client for Hindsight - Semantic memory system with personality-driven thinking",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
+1 -22
View File
@@ -102,8 +102,6 @@ export class HindsightClient {
documentId?: string;
async?: boolean;
entities?: EntityInput[];
/** Optional list of tags for this memory */
tags?: string[];
}
): Promise<RetainResponse> {
const item: {
@@ -113,7 +111,6 @@ export class HindsightClient {
metadata?: Record<string, string>;
document_id?: string;
entities?: EntityInput[];
tags?: string[];
} = { content };
if (options?.timestamp) {
item.timestamp =
@@ -133,9 +130,6 @@ export class HindsightClient {
if (options?.entities) {
item.entities = options.entities;
}
if (options?.tags) {
item.tags = options.tags;
}
const response = await sdk.retainMemories({
client: this.client,
@@ -198,10 +192,6 @@ export class HindsightClient {
maxEntityTokens?: number;
includeChunks?: boolean;
maxChunkTokens?: number;
/** Optional list of tags to filter memories by */
tags?: string[];
/** How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any' */
tagsMatch?: 'any' | 'all' | 'any_strict' | 'all_strict';
}
): Promise<RecallResponse> {
const response = await sdk.recallMemories({
@@ -218,8 +208,6 @@ export class HindsightClient {
entities: options?.includeEntities ? { max_tokens: options?.maxEntityTokens ?? 500 } : undefined,
chunks: options?.includeChunks ? { max_tokens: options?.maxChunkTokens ?? 8192 } : undefined,
},
tags: options?.tags,
tags_match: options?.tagsMatch,
},
});
@@ -232,14 +220,7 @@ export class HindsightClient {
async reflect(
bankId: string,
query: string,
options?: {
context?: string;
budget?: Budget;
/** Optional list of tags to filter memories by */
tags?: string[];
/** How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged). Default: 'any' */
tagsMatch?: 'any' | 'all' | 'any_strict' | 'all_strict';
}
options?: { context?: string; budget?: Budget }
): Promise<ReflectResponse> {
const response = await sdk.reflect({
client: this.client,
@@ -248,8 +229,6 @@ export class HindsightClient {
query,
context: options?.context,
budget: options?.budget || 'low',
tags: options?.tags,
tags_match: options?.tagsMatch,
},
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@vectorize-io/hindsight-control-plane",
"version": "0.3.0",
"version": "0.2.1",
"description": "Control plane for Hindsight - Semantic memory system",
"bin": {
"hindsight-control-plane": "./bin/cli.js"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "hindsight-dev"
version = "0.3.0"
version = "0.2.1"
description = "Development utilities for Hindsight"
requires-python = ">=3.11"
dependencies = [
+2 -39
View File
@@ -8,48 +8,11 @@ This changelog highlights user-facing changes only. Internal maintenance, CI/CD,
For full release details, see [GitHub Releases](https://github.com/vectorize-io/hindsight/releases).
## [0.3.0](https://github.com/vectorize-io/hindsight/releases/tag/v0.3.0)
## [Unreleased]
**Features**
- Add memory tags so you can label and filter memories during recall/reflect. ([`20c8f8b`](https://github.com/vectorize-io/hindsight/commit/20c8f8b))
- Allow choosing different AI providers/models per operation. ([`e6709d5`](https://github.com/vectorize-io/hindsight/commit/e6709d5))
- Add Cohere support for embeddings and reranking. ([`4de0730`](https://github.com/vectorize-io/hindsight/commit/4de0730))
- Add configurable embedding dimensions and OpenAI embeddings support. ([`70de23e`](https://github.com/vectorize-io/hindsight/commit/70de23e))
- Support custom base URLs for OpenAI-style embeddings and Cohere endpoints. ([`fa53917`](https://github.com/vectorize-io/hindsight/commit/fa53917))
- Add LiteLLM gateway support for routing LLM/embedding requests. ([`d47c8a2`](https://github.com/vectorize-io/hindsight/commit/d47c8a2))
- Add multilingual content support to improve handling and retrieval across languages. ([`c65c6a9`](https://github.com/vectorize-io/hindsight/commit/c65c6a9))
- Add delete memory bank capability. ([`4b82d2d`](https://github.com/vectorize-io/hindsight/commit/4b82d2d))
- Add backup/restore tooling for memory banks. ([`67b273d`](https://github.com/vectorize-io/hindsight/commit/67b273d))
**Improvements**
- Add retention modes to control how memories are extracted and stored. ([`fb31a35`](https://github.com/vectorize-io/hindsight/commit/fb31a35))
- Add offline (optional) database migrations to support restricted/air-gapped deployments. ([`233bd2e`](https://github.com/vectorize-io/hindsight/commit/233bd2e))
- Add database connection configuration options for more flexible deployments. ([`33fac2c`](https://github.com/vectorize-io/hindsight/commit/33fac2c))
- Load .env automatically on startup to simplify configuration. ([`c06d9b4`](https://github.com/vectorize-io/hindsight/commit/c06d9b4))
- Expose an operation ID from retain requests so async/background processing can be tracked. ([`1dacd0e`](https://github.com/vectorize-io/hindsight/commit/1dacd0e))
- Add per-request LLM token usage metrics for monitoring and cost tracking. ([`29a542d`](https://github.com/vectorize-io/hindsight/commit/29a542d))
- Add LLM call latency metrics for performance monitoring. ([`5e1f13e`](https://github.com/vectorize-io/hindsight/commit/5e1f13e))
- Include tenant in metrics labels for better multi-tenant observability. ([`1ffc2a4`](https://github.com/vectorize-io/hindsight/commit/1ffc2a4))
- Add async processing option to MCP retain tool for background retention workflows. ([`37fc7fb`](https://github.com/vectorize-io/hindsight/commit/37fc7fb))
**Bug Fixes**
- Fix extension loading in multi-worker deployments so all workers load extensions correctly. ([`f5f3fca`](https://github.com/vectorize-io/hindsight/commit/f5f3fca))
- Improve recall performance by batching recall queries. ([`5991308`](https://github.com/vectorize-io/hindsight/commit/5991308))
- Improve retrieval quality and stability for large memory banks (graph/MPFP retrieval fixes). ([`6232e69`](https://github.com/vectorize-io/hindsight/commit/6232e69))
- Fix entities list being limited to 100 entities. ([`26bf571`](https://github.com/vectorize-io/hindsight/commit/26bf571))
- Fix UI only showing the first 1000 memories. ([`67c1a42`](https://github.com/vectorize-io/hindsight/commit/67c1a42))
- Fix duplicated causal relationships and improve token usage during processing. ([`49e233c`](https://github.com/vectorize-io/hindsight/commit/49e233c))
- Improve causal link detection accuracy. ([`2a00df0`](https://github.com/vectorize-io/hindsight/commit/2a00df0))
- Make retain max completion tokens configurable to prevent truncation issues. ([`7715a51`](https://github.com/vectorize-io/hindsight/commit/7715a51))
- Fix Python SDK not sending the Authorization header, preventing authenticated requests. ([`39e3f7c`](https://github.com/vectorize-io/hindsight/commit/39e3f7c))
- Fix stats endpoint missing tenant authentication in multi-tenant setups. ([`d6ff191`](https://github.com/vectorize-io/hindsight/commit/d6ff191))
- Fix embedding dimension handling for tenant schemas in multi-tenant databases. ([`6fe9314`](https://github.com/vectorize-io/hindsight/commit/6fe9314))
- Fix Groq free-tier compatibility so requests work correctly. ([`d899d18`](https://github.com/vectorize-io/hindsight/commit/d899d18))
- Fix security vulnerability (qs / CVE-2025-15284). ([`b3becb6`](https://github.com/vectorize-io/hindsight/commit/b3becb6))
- Restore MCP tools for listing and creating memory banks. ([`9fd5679`](https://github.com/vectorize-io/hindsight/commit/9fd5679))
- Add per-request token usage tracking to retain and reflect endpoints for cost monitoring and billing integration.
## [0.2.0](https://github.com/vectorize-io/hindsight/releases/tag/v0.2.0)
+1 -51
View File
@@ -43,13 +43,11 @@ Make sure you've completed the [Quick Start](./quickstart) to install the client
|-----------|------|---------|-------------|
| `query` | string | required | Natural language query |
| `types` | list | all | Filter: `world`, `experience`, `opinion` |
| `budget` | string | "mid" | Budget level: `low`, `mid`, `high` |
| `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 |
| `tags` | list | None | Filter memories by tags (see [Tag Filtering](#filter-by-tags)) |
| `tags_match` | string | "any" | How to match tags: `any`, `all`, `any_strict`, `all_strict` |
<Tabs>
<TabItem value="python" label="Python">
@@ -129,51 +127,3 @@ The `budget` parameter controls graph traversal depth:
<CodeSnippet code={recallMjs} section="recall-budget-levels" language="javascript" />
</TabItem>
</Tabs>
## Filter by Tags
Tags enable **visibility scoping**—filter memories based on tags assigned during [retain](./retain#tagging-memories). This is essential for multi-user agents where each user should only see their own memories.
### Basic Tag Filtering
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={recallPy} section="recall-with-tags" language="python" />
</TabItem>
</Tabs>
### Tag Match Modes
The `tags_match` parameter controls how tags are matched:
| Mode | Behavior | Untagged Memories |
|------|----------|-------------------|
| `any` | OR: memory has ANY of the specified tags | **Included** |
| `all` | AND: memory has ALL of the specified tags | **Included** |
| `any_strict` | OR: memory has ANY of the specified tags | **Excluded** |
| `all_strict` | AND: memory has ALL of the specified tags | **Excluded** |
**Strict modes** are useful when you want to ensure only tagged memories are returned:
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={recallPy} section="recall-tags-strict" language="python" />
</TabItem>
</Tabs>
**AND matching** requires all specified tags to be present:
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={recallPy} section="recall-tags-all" language="python" />
</TabItem>
</Tabs>
### Use Cases
| Scenario | Tags | Mode | Result |
|----------|------|------|--------|
| User A's memories only | `["user:alice"]` | `any_strict` | Only memories tagged `user:alice` |
| Support + feedback | `["support", "feedback"]` | `any` | Memories with either tag + untagged |
| Multi-user room | `["user:alice", "room:general"]` | `all_strict` | Only memories with both tags |
| Global + user-specific | `["user:alice"]` | `any` | Alice's memories + shared (untagged) |
+1 -24
View File
@@ -50,12 +50,10 @@ Make sure you've completed the [Quick Start](./quickstart) to install the client
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `query` | string | required | Question or prompt |
| `budget` | string | "low" | Budget level: `low`, `mid`, `high` |
| `budget` | string | "low" | Budget level: "low", "mid", "high" |
| `context` | string | None | Additional context for the query |
| `max_tokens` | int | 4096 | Maximum tokens for the response |
| `response_schema` | object | None | JSON Schema for [structured output](#structured-output) |
| `tags` | list | None | Filter memories by tags during reflection |
| `tags_match` | string | "any" | How to match tags: `any`, `all`, `any_strict`, `all_strict` |
### Response Fields
@@ -249,24 +247,3 @@ hindsight memory reflect hiring-team \
- Use `model_validate()` to parse the response back into your Pydantic model
- Keep schemas focused — extract only what you need
- Use `Optional` fields for data that may not always be available
## Filter by Tags
Like [recall](./recall#filter-by-tags), reflect supports tag filtering to scope which memories are considered during reasoning. This is essential for multi-user scenarios where reflection should only consider memories relevant to a specific user.
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={reflectPy} section="reflect-with-tags" language="python" />
</TabItem>
</Tabs>
The `tags_match` parameter works the same as in recall:
| Mode | Behavior |
|------|----------|
| `any` | OR matching, includes untagged memories |
| `all` | AND matching, includes untagged memories |
| `any_strict` | OR matching, excludes untagged memories |
| `all_strict` | AND matching, excludes untagged memories |
See [Retain API](./retain#tagging-memories) for how to tag memories and [Recall API](./recall#filter-by-tags) for more details on tag matching modes.
@@ -129,55 +129,3 @@ For large batches, use async ingestion to avoid blocking:
<CodeSnippet code={retainMjs} section="retain-async" language="javascript" />
</TabItem>
</Tabs>
## Tagging Memories
Tags enable **visibility scoping**—useful when one memory bank serves multiple users but each should only see relevant memories. For example, an agent that chats with multiple users can tag memories by user ID and filter during recall.
### Tag Individual Items
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={retainPy} section="retain-with-tags" language="python" />
</TabItem>
</Tabs>
### Apply Tags to All Items in a Batch
Use `document_tags` to apply the same tags to all items in a request:
<Tabs>
<TabItem value="python" label="Python">
<CodeSnippet code={retainPy} section="retain-with-document-tags" language="python" />
</TabItem>
</Tabs>
When both `document_tags` and item-level `tags` are provided, they are merged together.
### Tag Naming Conventions
Use consistent naming patterns for tags:
| Pattern | Example | Use Case |
|---------|---------|----------|
| `user:<id>` | `user:alice` | Multi-user agent filtering |
| `session:<id>` | `session:123` | Session-based scoping |
| `room:<id>` | `room:general` | Chat room isolation |
| `topic:<name>` | `topic:feedback` | Topic categorization |
### Listing Tags
Use the list tags API to discover existing tags, useful for UI autocomplete or wildcard expansion:
```python
# List all tags in a bank
tags = client.list_tags(bank_id="my-bank")
for tag in tags.items:
print(f"{tag.tag}: {tag.count} memories")
# Search with wildcards (* matches any characters)
user_tags = client.list_tags(bank_id="my-bank", q="user:*")
admin_tags = client.list_tags(bank_id="my-bank", q="*-admin")
```
See [Recall API](./recall#filter-by-tags) for filtering memories by tags during retrieval.
+2 -46
View File
@@ -139,18 +139,13 @@ export HINDSIGHT_API_REFLECT_LLM_MODEL=llama-3.3-70b-versatile
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `local`, `tei`, `openai`, `cohere`, or `litellm` | `local` |
| `HINDSIGHT_API_EMBEDDINGS_PROVIDER` | Provider: `local`, `tei`, `openai`, or `cohere` | `local` |
| `HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL` | Model for local provider | `BAAI/bge-small-en-v1.5` |
| `HINDSIGHT_API_EMBEDDINGS_TEI_URL` | TEI server URL | - |
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY` | OpenAI API key (falls back to `HINDSIGHT_API_LLM_API_KEY`) | - |
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL` | OpenAI embedding model | `text-embedding-3-small` |
| `HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL` | Custom base URL for OpenAI-compatible API (e.g., Azure OpenAI) | - |
| `HINDSIGHT_API_COHERE_API_KEY` | Cohere API key (shared for embeddings and reranker) | - |
| `HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL` | Cohere embedding model | `embed-english-v3.0` |
| `HINDSIGHT_API_EMBEDDINGS_COHERE_BASE_URL` | Custom base URL for Cohere-compatible API (e.g., Azure-hosted) | - |
| `HINDSIGHT_API_LITELLM_API_BASE` | LiteLLM proxy base URL (shared for embeddings and reranker) | `http://localhost:4000` |
| `HINDSIGHT_API_LITELLM_API_KEY` | LiteLLM proxy API key (optional, depends on proxy config) | - |
| `HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL` | LiteLLM embedding model (use provider prefix, e.g., `cohere/embed-english-v3.0`) | `text-embedding-3-small` |
```bash
# Local (default) - uses SentenceTransformers
@@ -162,12 +157,6 @@ export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=sk-xxxxxxxxxxxx # or reuses HINDSIGHT_API_LLM_API_KEY
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small # 1536 dimensions
# Azure OpenAI - embeddings via Azure endpoint
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=your-azure-api-key
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small
export HINDSIGHT_API_EMBEDDINGS_OPENAI_BASE_URL=https://your-resource.openai.azure.com/openai/deployments/your-deployment
# TEI - HuggingFace Text Embeddings Inference (recommended for production)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=tei
export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
@@ -176,18 +165,6 @@ export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
export HINDSIGHT_API_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0 # 1024 dimensions
# Azure-hosted Cohere - embeddings via custom endpoint
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
export HINDSIGHT_API_COHERE_API_KEY=your-azure-api-key
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0
export HINDSIGHT_API_EMBEDDINGS_COHERE_BASE_URL=https://your-azure-cohere-endpoint.com
# LiteLLM proxy - unified gateway for multiple providers
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
export HINDSIGHT_API_LITELLM_API_KEY=your-litellm-key # optional
export HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL=text-embedding-3-small # or cohere/embed-english-v3.0
```
#### Embedding Dimensions
@@ -210,15 +187,13 @@ Supported OpenAI embedding dimensions:
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_RERANKER_PROVIDER` | Provider: `local`, `tei`, `cohere`, `flashrank`, `litellm`, or `rrf` | `local` |
| `HINDSIGHT_API_RERANKER_PROVIDER` | Provider: `local`, `tei`, or `cohere` | `local` |
| `HINDSIGHT_API_RERANKER_LOCAL_MODEL` | Model for local provider | `cross-encoder/ms-marco-MiniLM-L-6-v2` |
| `HINDSIGHT_API_RERANKER_LOCAL_MAX_CONCURRENT` | Max concurrent local reranking (prevents CPU thrashing under load) | `4` |
| `HINDSIGHT_API_RERANKER_TEI_URL` | TEI server URL | - |
| `HINDSIGHT_API_RERANKER_TEI_BATCH_SIZE` | Batch size for TEI reranking | `128` |
| `HINDSIGHT_API_RERANKER_TEI_MAX_CONCURRENT` | Max concurrent TEI reranking requests | `8` |
| `HINDSIGHT_API_RERANKER_COHERE_MODEL` | Cohere rerank model | `rerank-english-v3.0` |
| `HINDSIGHT_API_RERANKER_COHERE_BASE_URL` | Custom base URL for Cohere-compatible API (e.g., Azure-hosted) | - |
| `HINDSIGHT_API_RERANKER_LITELLM_MODEL` | LiteLLM rerank model (use provider prefix, e.g., `cohere/rerank-english-v3.0`) | `cohere/rerank-english-v3.0` |
```bash
# Local (default) - uses SentenceTransformers CrossEncoder
@@ -233,27 +208,8 @@ export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
export HINDSIGHT_API_COHERE_API_KEY=your-api-key # shared with embeddings
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0
# Azure-hosted Cohere - reranking via custom endpoint
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
export HINDSIGHT_API_COHERE_API_KEY=your-azure-api-key
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0
export HINDSIGHT_API_RERANKER_COHERE_BASE_URL=https://your-azure-cohere-endpoint.com
# LiteLLM proxy - unified gateway for multiple reranking providers
export HINDSIGHT_API_RERANKER_PROVIDER=litellm
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
export HINDSIGHT_API_LITELLM_API_KEY=your-litellm-key # optional
export HINDSIGHT_API_RERANKER_LITELLM_MODEL=cohere/rerank-english-v3.0 # or voyage/rerank-2, together_ai/...
```
LiteLLM supports multiple reranking providers via the `/rerank` endpoint:
- Cohere (`cohere/rerank-english-v3.0`, `cohere/rerank-multilingual-v3.0`)
- Together AI (`together_ai/...`)
- Voyage AI (`voyage/rerank-2`)
- Jina AI (`jina_ai/...`)
- AWS Bedrock (`bedrock/...`)
### Authentication
By default, Hindsight runs without authentication. For production deployments, enable API key authentication using the built-in tenant extension:
+12 -102
View File
@@ -95,71 +95,29 @@ Converts text into dense vector representations for semantic similarity search.
**Default:** `BAAI/bge-small-en-v1.5` (384 dimensions, ~130MB)
### Supported Providers
**Alternatives:**
| Provider | Description | Best For |
|----------|-------------|----------|
| `local` | SentenceTransformers (default) | Development, low latency |
| `openai` | OpenAI embeddings API | Production, high quality |
| `cohere` | Cohere embeddings API | Production, multilingual |
| `tei` | HuggingFace Text Embeddings Inference | Production, self-hosted |
| `litellm` | LiteLLM proxy (unified gateway) | Multi-provider setups |
| Model | Use Case |
|-------|----------|
| `BAAI/bge-small-en-v1.5` | Default, fast, good quality |
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | Multilingual (50+ languages) |
### Local Models
| Model | Dimensions | Use Case |
|-------|------------|----------|
| `BAAI/bge-small-en-v1.5` | 384 | Default, fast, good quality |
| `sentence-transformers/paraphrase-multilingual-MiniLM-L12-v2` | 384 | Multilingual (50+ languages) |
### OpenAI Models
| Model | Dimensions | Use Case |
|-------|------------|----------|
| `text-embedding-3-small` | 1536 | Default OpenAI, cost-effective |
| `text-embedding-3-large` | 3072 | Higher quality, more expensive |
| `text-embedding-ada-002` | 1536 | Legacy model |
### Cohere Models
| Model | Dimensions | Use Case |
|-------|------------|----------|
| `embed-english-v3.0` | 1024 | English text |
| `embed-multilingual-v3.0` | 1024 | 100+ languages |
:::warning Embedding Dimensions
Hindsight automatically detects the embedding dimension at startup and adjusts the database schema. Once memories are stored, you cannot change dimensions without losing data.
:::warning
All embedding models must produce **384-dimensional vectors** to match the database schema.
:::
**Configuration Examples:**
**Configuration:**
```bash
# Local provider (default)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=local
export HINDSIGHT_API_EMBEDDINGS_LOCAL_MODEL=BAAI/bge-small-en-v1.5
# OpenAI
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=openai
export HINDSIGHT_API_EMBEDDINGS_OPENAI_API_KEY=sk-xxxxxxxxxxxx
export HINDSIGHT_API_EMBEDDINGS_OPENAI_MODEL=text-embedding-3-small
# Cohere
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=cohere
export HINDSIGHT_API_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_EMBEDDINGS_COHERE_MODEL=embed-english-v3.0
# TEI (self-hosted)
# TEI provider (remote)
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=tei
export HINDSIGHT_API_EMBEDDINGS_TEI_URL=http://localhost:8080
# LiteLLM proxy
export HINDSIGHT_API_EMBEDDINGS_PROVIDER=litellm
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
export HINDSIGHT_API_EMBEDDINGS_LITELLM_MODEL=text-embedding-3-small
```
See [Configuration](./configuration#embeddings) for all options including Azure OpenAI and custom endpoints.
---
## Cross-Encoder (Reranker)
@@ -168,18 +126,7 @@ Reranks initial search results to improve precision.
**Default:** `cross-encoder/ms-marco-MiniLM-L-6-v2` (~85MB)
### Supported Providers
| Provider | Description | Best For |
|----------|-------------|----------|
| `local` | SentenceTransformers CrossEncoder (default) | Development, low latency |
| `cohere` | Cohere rerank API | Production, high quality |
| `tei` | HuggingFace Text Embeddings Inference | Production, self-hosted |
| `flashrank` | FlashRank (lightweight, fast) | Resource-constrained environments |
| `litellm` | LiteLLM proxy (unified gateway) | Multi-provider setups |
| `rrf` | RRF-only (no neural reranking) | Testing, minimal resources |
### Local Models
**Alternatives:**
| Model | Use Case |
|-------|----------|
@@ -187,51 +134,14 @@ Reranks initial search results to improve precision.
| `cross-encoder/ms-marco-MiniLM-L-12-v2` | Higher accuracy |
| `cross-encoder/mmarco-mMiniLMv2-L12-H384-v1` | Multilingual |
### Cohere Models
| Model | Use Case |
|-------|----------|
| `rerank-english-v3.0` | English text |
| `rerank-multilingual-v3.0` | 100+ languages |
### LiteLLM Supported Providers
LiteLLM supports multiple reranking providers via the `/rerank` endpoint:
| Provider | Model Example |
|----------|---------------|
| Cohere | `cohere/rerank-english-v3.0` |
| Together AI | `together_ai/...` |
| Voyage AI | `voyage/rerank-2` |
| Jina AI | `jina_ai/...` |
| AWS Bedrock | `bedrock/...` |
**Configuration Examples:**
**Configuration:**
```bash
# Local provider (default)
export HINDSIGHT_API_RERANKER_PROVIDER=local
export HINDSIGHT_API_RERANKER_LOCAL_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
# Cohere
export HINDSIGHT_API_RERANKER_PROVIDER=cohere
export HINDSIGHT_API_COHERE_API_KEY=your-api-key
export HINDSIGHT_API_RERANKER_COHERE_MODEL=rerank-english-v3.0
# TEI (self-hosted)
# TEI provider (remote)
export HINDSIGHT_API_RERANKER_PROVIDER=tei
export HINDSIGHT_API_RERANKER_TEI_URL=http://localhost:8081
# FlashRank (lightweight)
export HINDSIGHT_API_RERANKER_PROVIDER=flashrank
# LiteLLM proxy
export HINDSIGHT_API_RERANKER_PROVIDER=litellm
export HINDSIGHT_API_LITELLM_API_BASE=http://localhost:4000
export HINDSIGHT_API_RERANKER_LITELLM_MODEL=cohere/rerank-english-v3.0
# RRF-only (no neural reranking)
export HINDSIGHT_API_RERANKER_PROVIDER=rrf
```
See [Configuration](./configuration#reranker) for all options including Azure-hosted endpoints and batch settings.
+1 -1
View File
@@ -183,4 +183,4 @@ Disposition creates **consistent character** across conversations while allowing
- [**Retain**](./retain) — How rich facts are stored
- [**Recall**](./retrieval) — How multi-strategy search works
- [**Reflect API**](./api/reflect) — Code examples, parameters, and tag filtering
- [API Reference: Reflect](./api/reflect) — Code examples and usage
+27 -6
View File
@@ -169,13 +169,34 @@ As facts accumulate about an entity, Hindsight synthesizes **observations** —
## Tagging Memories
Tags enable visibility scoping—useful when one memory bank serves multiple users but each should only see relevant memories.
You can tag memories for filtering during recall—useful when one memory bank serves multiple users but each user should only see relevant memories.
- **Item tags**: Tag individual memories with specific scopes
- **Document tags**: Apply tags to all items in a batch
- **Tag filtering**: Filter during recall/reflect by tags
```python
# Tag memories for specific users
client.retain(
bank_id="my-agent",
items=[
{
"content": "Alice prefers morning meetings",
"tags": ["user_alice"]
}
]
)
See [Retain API](./api/retain) for code examples and [Recall API](./api/recall) for filtering options.
# Apply tags to all items in a batch
client.retain(
bank_id="my-agent",
document_tags=["session_123", "user_alice"], # Applied to all items
items=[
{"content": "Alice discussed the project timeline"},
{"content": "Alice mentioned she needs help with Python"}
]
)
```
During recall, use `tags_match` to control matching:
- `"any"` (default): OR matching - returns memories where **any** tag overlaps
- `"all"`: AND matching - returns memories containing **all** specified tags
---
@@ -198,4 +219,4 @@ All stored in your isolated **memory bank**, ready for `recall()` and `reflect()
- [**Recall**](./retrieval) — How multi-strategy search retrieves relevant memories
- [**Reflect**](./reflect) — How disposition influences reasoning and opinion formation
- [**Retain API**](./api/retain) — Code examples and parameters
- [API Reference](./api/retain) — Code examples for retaining memories
+3 -4
View File
@@ -133,9 +133,9 @@ Hindsight is built for AI agents, not humans. Traditional search systems return
**Parameters you control:**
- `max_tokens`: How much memory content to return (default: 4096 tokens)
- `budget`: Search depth level (low, mid, high)
- `types`: Filter by world, experience, opinion, or all
- `tags`: Filter memories by visibility tags
- `tags_match`: How to match tags (see [Recall API](./api/recall) for all options)
- `fact_type`: Filter by world, experience, opinion, or all
- `tags`: Filter memories by tags
- `tags_match`: How to match tags - `"any"` for OR (default), `"all"` for AND
### Expanding Context: Chunks and Entity Observations
@@ -243,4 +243,3 @@ See [Configuration → Retrieval](./configuration#retrieval) for available algor
- [**Retain**](./retain) — How memories are stored with rich context
- [**Reflect**](./reflect) — How disposition influences reasoning
- [**Recall API**](./api/recall) — Code examples, parameters, and tag filtering
-33
View File
@@ -116,39 +116,6 @@ results = client.recall(bank_id="my-bank", query="How are Alice and Bob connecte
# [/docs:recall-budget-levels]
# [docs:recall-with-tags]
# Filter recall to only memories tagged for a specific user
response = client.recall(
bank_id="my-bank",
query="What feedback did the user give?",
tags=["user:alice"],
tags_match="any" # OR matching, includes untagged (default)
)
# [/docs:recall-with-tags]
# [docs:recall-tags-strict]
# Strict mode: only return memories that have matching tags (exclude untagged)
response = client.recall(
bank_id="my-bank",
query="What did the user say?",
tags=["user:alice"],
tags_match="any_strict" # OR matching, excludes untagged memories
)
# [/docs:recall-tags-strict]
# [docs:recall-tags-all]
# AND matching: require ALL specified tags to be present
response = client.recall(
bank_id="my-bank",
query="What bugs were reported?",
tags=["user:alice", "bug-report"],
tags_match="all_strict" # Memory must have BOTH tags
)
# [/docs:recall-tags-all]
# =============================================================================
# Cleanup (not shown in docs)
# =============================================================================
-11
View File
@@ -81,17 +81,6 @@ for fact in response.based_on or []:
# [/docs:reflect-sources]
# [docs:reflect-with-tags]
# Filter reflection to only consider memories for a specific user
response = client.reflect(
bank_id="my-bank",
query="What does this user think about our product?",
tags=["user:alice"],
tags_match="any_strict" # Only use memories tagged for this user
)
# [/docs:reflect-with-tags]
# =============================================================================
# Cleanup (not shown in docs)
# =============================================================================
-33
View File
@@ -67,39 +67,6 @@ print(result.var_async) # True
# [/docs:retain-async]
# [docs:retain-with-tags]
# Tag individual items for visibility scoping
client.retain_batch(
bank_id="my-bank",
items=[
{
"content": "User Alice said she loves the new dashboard",
"tags": ["user:alice", "feedback"]
},
{
"content": "User Bob reported a bug in the search feature",
"tags": ["user:bob", "bug-report"]
}
],
document_id="user_feedback_001"
)
# [/docs:retain-with-tags]
# [docs:retain-with-document-tags]
# Apply tags to all items in a batch
client.retain_batch(
bank_id="my-bank",
items=[
{"content": "Alice mentioned she prefers dark mode"},
{"content": "Bob asked about keyboard shortcuts"}
],
document_id="support_session_123",
document_tags=["session:123", "support"] # Applied to all items
)
# [/docs:retain-with-document-tags]
# =============================================================================
# Cleanup (not shown in docs)
# =============================================================================
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "hindsight-embed"
version = "0.3.0"
version = "0.2.1"
description = "Hindsight embedded CLI - local memory operations without a server"
readme = "README.md"
requires-python = ">=3.11"
@@ -1,6 +1,6 @@
[project]
name = "hindsight-litellm"
version = "0.3.0"
version = "0.2.1"
description = "Universal LLM memory integration via LiteLLM - works with 100+ providers"
readme = "README.md"
requires-python = ">=3.10"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "hindsight-all"
version = "0.3.0"
version = "0.2.1"
description = "Hindsight: Agent Memory That Works Like Human Memory - All-in-One Bundle"
readme = "README.md"
requires-python = ">=3.11"
@@ -54,7 +54,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(hindsight_http_requests_total{tenant=~\"$tenant\"})",
"expr": "sum(hindsight_http_requests_total)",
"refId": "A"
}
],
@@ -99,7 +99,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(rate(hindsight_http_requests_total{tenant=~\"$tenant\"}[1m]))",
"expr": "sum(rate(hindsight_http_requests_total[1m]))",
"refId": "A"
}
],
@@ -191,7 +191,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(rate(hindsight_http_requests_total{status_class=\"5xx\", tenant=~\"$tenant\"}[5m])) / sum(rate(hindsight_http_requests_total{tenant=~\"$tenant\"}[5m]))",
"expr": "sum(rate(hindsight_http_requests_total{status_class=\"5xx\"}[5m])) / sum(rate(hindsight_http_requests_total[5m]))",
"refId": "A"
}
],
@@ -236,7 +236,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "histogram_quantile(0.95, sum by (le) (rate(hindsight_http_duration_seconds_bucket{tenant=~\"$tenant\"}[5m])))",
"expr": "histogram_quantile(0.95, sum by (le) (rate(hindsight_http_duration_seconds_bucket[5m])))",
"refId": "A"
}
],
@@ -313,7 +313,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum by (endpoint) (rate(hindsight_http_requests_total{tenant=~\"$tenant\"}[1m]))",
"expr": "sum by (endpoint) (rate(hindsight_http_requests_total[1m]))",
"legendFormat": "{{endpoint}}",
"refId": "A"
}
@@ -404,17 +404,17 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "histogram_quantile(0.50, sum by (le) (rate(hindsight_http_duration_seconds_bucket{tenant=~\"$tenant\"}[5m])))",
"expr": "histogram_quantile(0.50, sum by (le) (rate(hindsight_http_duration_seconds_bucket[5m])))",
"legendFormat": "p50",
"refId": "A"
},
{
"expr": "histogram_quantile(0.95, sum by (le) (rate(hindsight_http_duration_seconds_bucket{tenant=~\"$tenant\"}[5m])))",
"expr": "histogram_quantile(0.95, sum by (le) (rate(hindsight_http_duration_seconds_bucket[5m])))",
"legendFormat": "p95",
"refId": "B"
},
{
"expr": "histogram_quantile(0.99, sum by (le) (rate(hindsight_http_duration_seconds_bucket{tenant=~\"$tenant\"}[5m])))",
"expr": "histogram_quantile(0.99, sum by (le) (rate(hindsight_http_duration_seconds_bucket[5m])))",
"legendFormat": "p99",
"refId": "C"
}
@@ -505,12 +505,12 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(rate(hindsight_http_requests_total{status_class=\"5xx\", tenant=~\"$tenant\"}[1m])) / sum(rate(hindsight_http_requests_total{tenant=~\"$tenant\"}[1m]))",
"expr": "sum(rate(hindsight_http_requests_total{status_class=\"5xx\"}[1m])) / sum(rate(hindsight_http_requests_total[1m]))",
"legendFormat": "5xx Error Rate",
"refId": "A"
},
{
"expr": "sum(rate(hindsight_http_requests_total{status_class=\"4xx\", tenant=~\"$tenant\"}[1m])) / sum(rate(hindsight_http_requests_total{tenant=~\"$tenant\"}[1m]))",
"expr": "sum(rate(hindsight_http_requests_total{status_class=\"4xx\"}[1m])) / sum(rate(hindsight_http_requests_total[1m]))",
"legendFormat": "4xx Error Rate",
"refId": "B"
}
@@ -1276,36 +1276,7 @@
"schemaVersion": 38,
"tags": ["hindsight", "api", "service"],
"templating": {
"list": [
{
"allValue": ".*",
"current": {
"selected": true,
"text": "All",
"value": "$__all"
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"definition": "label_values(hindsight_http_requests_total, tenant)",
"hide": 0,
"includeAll": true,
"label": "Tenant",
"multi": false,
"name": "tenant",
"options": [],
"query": {
"query": "label_values(hindsight_http_requests_total, tenant)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
}
]
"list": []
},
"time": {
"from": "now-30m",
@@ -46,7 +46,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(hindsight_llm_calls_total{tenant=~\"$tenant\"})",
"expr": "sum(hindsight_llm_calls_total)",
"refId": "A"
}
],
@@ -91,7 +91,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(hindsight_llm_tokens_input_tokens_total{tenant=~\"$tenant\"}) + sum(hindsight_llm_tokens_output_tokens_total{tenant=~\"$tenant\"})",
"expr": "sum(hindsight_llm_tokens_input_tokens_total) + sum(hindsight_llm_tokens_output_tokens_total)",
"refId": "A"
}
],
@@ -137,7 +137,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(hindsight_llm_tokens_input_tokens_total{tenant=~\"$tenant\"})",
"expr": "sum(hindsight_llm_tokens_input_tokens_total)",
"refId": "A"
}
],
@@ -183,7 +183,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(hindsight_llm_tokens_output_tokens_total{tenant=~\"$tenant\"})",
"expr": "sum(hindsight_llm_tokens_output_tokens_total)",
"refId": "A"
}
],
@@ -260,7 +260,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum by (scope) (rate(hindsight_llm_calls_total{tenant=~\"$tenant\"}[1m]))",
"expr": "sum by (scope) (rate(hindsight_llm_calls_total[1m]))",
"legendFormat": "{{scope}}",
"refId": "A"
}
@@ -347,12 +347,12 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(rate(hindsight_llm_tokens_input_tokens_total{tenant=~\"$tenant\"}[1m]))",
"expr": "sum(rate(hindsight_llm_tokens_input_tokens_total[1m]))",
"legendFormat": "Input",
"refId": "A"
},
{
"expr": "sum(rate(hindsight_llm_tokens_output_tokens_total{tenant=~\"$tenant\"}[1m]))",
"expr": "sum(rate(hindsight_llm_tokens_output_tokens_total[1m]))",
"legendFormat": "Output",
"refId": "B"
}
@@ -430,7 +430,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "histogram_quantile(0.95, sum by (scope, le) (rate(hindsight_llm_duration_seconds_bucket{tenant=~\"$tenant\"}[5m])))",
"expr": "histogram_quantile(0.95, sum by (scope, le) (rate(hindsight_llm_duration_seconds_bucket[5m])))",
"legendFormat": "{{scope}}",
"refId": "A"
}
@@ -508,12 +508,12 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum by (scope) (rate(hindsight_llm_tokens_input_tokens_total{tenant=~\"$tenant\"}[1m]))",
"expr": "sum by (scope) (rate(hindsight_llm_tokens_input_tokens_total[1m]))",
"legendFormat": "{{scope}} (input)",
"refId": "A"
},
{
"expr": "sum by (scope) (rate(hindsight_llm_tokens_output_tokens_total{tenant=~\"$tenant\"}[1m]))",
"expr": "sum by (scope) (rate(hindsight_llm_tokens_output_tokens_total[1m]))",
"legendFormat": "{{scope}} (output)",
"refId": "B"
}
@@ -526,36 +526,7 @@
"schemaVersion": 38,
"tags": ["hindsight", "llm"],
"templating": {
"list": [
{
"allValue": ".*",
"current": {
"selected": true,
"text": "All",
"value": "$__all"
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"definition": "label_values(hindsight_llm_calls_total, tenant)",
"hide": 0,
"includeAll": true,
"label": "Tenant",
"multi": false,
"name": "tenant",
"options": [],
"query": {
"query": "label_values(hindsight_llm_calls_total, tenant)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
}
]
"list": []
},
"time": {
"from": "now-30m",
@@ -46,7 +46,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(hindsight_operation_operations_total{tenant=~\"$tenant\"})",
"expr": "sum(hindsight_operation_operations_total)",
"refId": "A"
}
],
@@ -91,7 +91,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(rate(hindsight_operation_operations_total{tenant=~\"$tenant\"}[1m]))",
"expr": "sum(rate(hindsight_operation_operations_total[1m]))",
"refId": "A"
}
],
@@ -137,7 +137,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(rate(hindsight_operation_operations_total{operation=\"retain\", tenant=~\"$tenant\"}[1m]))",
"expr": "sum(rate(hindsight_operation_operations_total{operation=\"retain\"}[1m]))",
"refId": "A"
}
],
@@ -183,7 +183,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(rate(hindsight_operation_operations_total{operation=\"recall\", tenant=~\"$tenant\"}[1m]))",
"expr": "sum(rate(hindsight_operation_operations_total{operation=\"recall\"}[1m]))",
"refId": "A"
}
],
@@ -229,7 +229,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum(rate(hindsight_operation_operations_total{operation=\"reflect\", tenant=~\"$tenant\"}[1m]))",
"expr": "sum(rate(hindsight_operation_operations_total{operation=\"reflect\"}[1m]))",
"refId": "A"
}
],
@@ -319,7 +319,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum by (operation) (rate(hindsight_operation_operations_total{tenant=~\"$tenant\"}[1m]))",
"expr": "sum by (operation) (rate(hindsight_operation_operations_total[1m]))",
"legendFormat": "{{operation}}",
"refId": "A"
}
@@ -410,17 +410,17 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "histogram_quantile(0.50, sum by (le) (rate(hindsight_operation_duration_seconds_bucket{operation=\"recall\", tenant=~\"$tenant\"}[5m])))",
"expr": "histogram_quantile(0.50, sum by (le) (rate(hindsight_operation_duration_seconds_bucket{operation=\"recall\"}[5m])))",
"legendFormat": "p50",
"refId": "A"
},
{
"expr": "histogram_quantile(0.95, sum by (le) (rate(hindsight_operation_duration_seconds_bucket{operation=\"recall\", tenant=~\"$tenant\"}[5m])))",
"expr": "histogram_quantile(0.95, sum by (le) (rate(hindsight_operation_duration_seconds_bucket{operation=\"recall\"}[5m])))",
"legendFormat": "p95",
"refId": "B"
},
{
"expr": "histogram_quantile(0.99, sum by (le) (rate(hindsight_operation_duration_seconds_bucket{operation=\"recall\", tenant=~\"$tenant\"}[5m])))",
"expr": "histogram_quantile(0.99, sum by (le) (rate(hindsight_operation_duration_seconds_bucket{operation=\"recall\"}[5m])))",
"legendFormat": "p99",
"refId": "C"
}
@@ -498,7 +498,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "histogram_quantile(0.95, sum by (operation, le) (rate(hindsight_operation_duration_seconds_bucket{tenant=~\"$tenant\"}[5m])))",
"expr": "histogram_quantile(0.95, sum by (operation, le) (rate(hindsight_operation_duration_seconds_bucket[5m])))",
"legendFormat": "{{operation}}",
"refId": "A"
}
@@ -576,7 +576,7 @@
"pluginVersion": "10.0.0",
"targets": [
{
"expr": "sum by (bank_id) (rate(hindsight_operation_operations_total{tenant=~\"$tenant\"}[1m]))",
"expr": "sum by (bank_id) (rate(hindsight_operation_operations_total[1m]))",
"legendFormat": "{{bank_id}}",
"refId": "A"
}
@@ -589,36 +589,7 @@
"schemaVersion": 38,
"tags": ["hindsight"],
"templating": {
"list": [
{
"allValue": ".*",
"current": {
"selected": true,
"text": "All",
"value": "$__all"
},
"datasource": {
"type": "prometheus",
"uid": "prometheus"
},
"definition": "label_values(hindsight_operation_operations_total, tenant)",
"hide": 0,
"includeAll": true,
"label": "Tenant",
"multi": false,
"name": "tenant",
"options": [],
"query": {
"query": "label_values(hindsight_operation_operations_total, tenant)",
"refId": "PrometheusVariableQueryEditor-VariableQuery"
},
"refresh": 2,
"regex": "",
"skipUrlSync": false,
"sort": 1,
"type": "query"
}
]
"list": []
},
"time": {
"from": "now-30m",
Generated
+5 -5
View File
@@ -1257,7 +1257,7 @@ wheels = [
[[package]]
name = "hindsight-all"
version = "0.3.0"
version = "0.2.1"
source = { editable = "hindsight" }
dependencies = [
{ name = "hindsight-api" },
@@ -1281,7 +1281,7 @@ provides-extras = ["test"]
[[package]]
name = "hindsight-api"
version = "0.3.0"
version = "0.2.1"
source = { editable = "hindsight-api" }
dependencies = [
{ name = "alembic" },
@@ -1397,7 +1397,7 @@ dev = [
[[package]]
name = "hindsight-client"
version = "0.3.0"
version = "0.2.1"
source = { editable = "hindsight-clients/python" }
dependencies = [
{ name = "aiohttp" },
@@ -1431,7 +1431,7 @@ provides-extras = ["test"]
[[package]]
name = "hindsight-dev"
version = "0.3.0"
version = "0.2.1"
source = { editable = "hindsight-dev" }
dependencies = [
{ name = "hindsight-api" },
@@ -1466,7 +1466,7 @@ dev = [
[[package]]
name = "hindsight-embed"
version = "0.3.0"
version = "0.2.1"
source = { editable = "hindsight-embed" }
dependencies = [
{ name = "httpx" },