Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
34bb86e1fe | ||
|
|
b18b91588c | ||
|
|
7afbd1ef6c | ||
|
|
48b19f5543 |
@@ -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)
|
||||
|
||||
[](https://github.com/vectorize-io/hindsight/actions/workflows/release.yml)
|
||||
[](https://join.slack.com/t/hindsight-space/shared_invite/zt-3nhbm4w29-LeSJ5Ixi6j8PdiYOCPlOgg)
|
||||
[](https://join.slack.com/t/hindsight-space/shared_invite/zt-3klo21kua-VUCC_zHP5rIcXFB1_5yw6A)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||

|
||||

|
||||
@@ -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,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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
@@ -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,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,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,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",
|
||||
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
# =============================================================================
|
||||
|
||||
@@ -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)
|
||||
# =============================================================================
|
||||
|
||||
@@ -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)
|
||||
# =============================================================================
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user