Compare commits

...
7 changed files with 43 additions and 13 deletions
+1
View File
@@ -26,6 +26,7 @@ HINDSIGHT_API_LOG_LEVEL=info
# Database (Optional - uses embedded pg0 by default)
# HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@host:5432/db
# HINDSIGHT_API_DATABASE_SCHEMA=public # PostgreSQL schema name (default: public)
# Embeddings Configuration (Optional - uses local by default)
# Provider: "local" (default) or "tei" (HuggingFace Text Embeddings Inference)
+5 -1
View File
@@ -20,6 +20,7 @@ logger = logging.getLogger(__name__)
# Environment variable names
ENV_DATABASE_URL = "HINDSIGHT_API_DATABASE_URL"
ENV_DATABASE_SCHEMA = "HINDSIGHT_API_DATABASE_SCHEMA"
ENV_LLM_PROVIDER = "HINDSIGHT_API_LLM_PROVIDER"
ENV_LLM_API_KEY = "HINDSIGHT_API_LLM_API_KEY"
ENV_LLM_MODEL = "HINDSIGHT_API_LLM_MODEL"
@@ -125,6 +126,7 @@ ENV_REFLECT_MAX_ITERATIONS = "HINDSIGHT_API_REFLECT_MAX_ITERATIONS"
# Default values
DEFAULT_DATABASE_URL = "pg0"
DEFAULT_DATABASE_SCHEMA = "public"
DEFAULT_LLM_PROVIDER = "openai"
DEFAULT_LLM_MODEL = "gpt-5-mini"
DEFAULT_LLM_MAX_CONCURRENT = 32
@@ -270,6 +272,7 @@ class HindsightConfig:
# Database
database_url: str
database_schema: str
# LLM (default, used as fallback for per-operation config)
llm_provider: str
@@ -367,6 +370,7 @@ class HindsightConfig:
return cls(
# Database
database_url=os.getenv(ENV_DATABASE_URL, DEFAULT_DATABASE_URL),
database_schema=os.getenv(ENV_DATABASE_SCHEMA, DEFAULT_DATABASE_SCHEMA),
# LLM
llm_provider=os.getenv(ENV_LLM_PROVIDER, DEFAULT_LLM_PROVIDER),
llm_api_key=os.getenv(ENV_LLM_API_KEY),
@@ -515,7 +519,7 @@ class HindsightConfig:
def log_config(self) -> None:
"""Log the current configuration (without sensitive values)."""
logger.info(f"Database: {self.database_url}")
logger.info(f"Database: {self.database_url} (schema: {self.database_schema})")
logger.info(f"LLM: provider={self.llm_provider}, model={self.llm_model}")
if self.retain_llm_provider or self.retain_llm_model:
retain_provider = self.retain_llm_provider or self.llm_provider
@@ -23,12 +23,17 @@ from ..metrics import get_metrics_collector
from .db_budget import budgeted_operation
# Context variable for current schema (async-safe, per-task isolation)
_current_schema: contextvars.ContextVar[str] = contextvars.ContextVar("current_schema", default="public")
# Note: default is None, actual default comes from config via get_current_schema()
_current_schema: contextvars.ContextVar[str | None] = contextvars.ContextVar("current_schema", default=None)
def get_current_schema() -> str:
"""Get the current schema from context (default: 'public')."""
return _current_schema.get()
"""Get the current schema from context (falls back to config default)."""
schema = _current_schema.get()
if schema is None:
# Fall back to configured default schema
return get_config().database_schema
return schema
def fq_table(table_name: str) -> str:
@@ -881,11 +886,12 @@ class MemoryEngine(MemoryEngineInterface):
if not self.db_url:
raise ValueError("Database URL is required for migrations")
logger.info("Running database migrations...")
run_migrations(self.db_url)
# Use configured database schema for migrations (defaults to "public")
run_migrations(self.db_url, schema=get_config().database_schema)
# Ensure embedding column dimension matches the model's dimension
# This is done after migrations and after embeddings.initialize()
ensure_embedding_dimension(self.db_url, self.embeddings.dimension)
ensure_embedding_dimension(self.db_url, self.embeddings.dimension, schema=get_config().database_schema)
logger.info(f"Connecting to PostgreSQL at {self.db_url}")
@@ -1,5 +1,6 @@
"""Built-in tenant extension implementations."""
from hindsight_api.config import get_config
from hindsight_api.extensions.tenant import AuthenticationError, Tenant, TenantContext, TenantExtension
from hindsight_api.models import RequestContext
@@ -10,11 +11,13 @@ class ApiKeyTenantExtension(TenantExtension):
This is a simple implementation that:
1. Validates the API key matches HINDSIGHT_API_TENANT_API_KEY
2. Returns 'public' as the schema for all authenticated requests
2. Returns the configured schema (HINDSIGHT_API_DATABASE_SCHEMA, default 'public')
for all authenticated requests
Configuration:
HINDSIGHT_API_TENANT_EXTENSION=hindsight_api.extensions.builtin.tenant:ApiKeyTenantExtension
HINDSIGHT_API_TENANT_API_KEY=your-secret-key
HINDSIGHT_API_DATABASE_SCHEMA=your-schema (optional, defaults to 'public')
For multi-tenant setups with separate schemas per tenant, implement a custom
TenantExtension that looks up the schema based on the API key or token claims.
@@ -27,11 +30,11 @@ class ApiKeyTenantExtension(TenantExtension):
raise ValueError("HINDSIGHT_API_TENANT_API_KEY is required when using ApiKeyTenantExtension")
async def authenticate(self, context: RequestContext) -> TenantContext:
"""Validate API key and return public schema context."""
"""Validate API key and return configured schema context."""
if context.api_key != self.expected_api_key:
raise AuthenticationError("Invalid API key")
return TenantContext(schema_name="public")
return TenantContext(schema_name=get_config().database_schema)
async def list_tenants(self) -> list[Tenant]:
"""Return public schema for single-tenant setup."""
return [Tenant(schema="public")]
"""Return configured schema for single-tenant setup."""
return [Tenant(schema=get_config().database_schema)]
+1
View File
@@ -170,6 +170,7 @@ def main():
if args.log_level != config.log_level:
config = HindsightConfig(
database_url=config.database_url,
database_schema=config.database_schema,
llm_provider=config.llm_provider,
llm_api_key=config.llm_api_key,
llm_model=config.llm_model,
@@ -20,10 +20,24 @@ The API service handles all memory operations (retain, recall, reflect).
| Variable | Description | Default |
|----------|-------------|---------|
| `HINDSIGHT_API_DATABASE_URL` | PostgreSQL connection string | `pg0` (embedded) |
| `HINDSIGHT_API_DATABASE_SCHEMA` | PostgreSQL schema name for tables | `public` |
| `HINDSIGHT_API_RUN_MIGRATIONS_ON_STARTUP` | Run database migrations on API startup | `true` |
If not provided, the server uses embedded `pg0` — convenient for development but not recommended for production.
The `DATABASE_SCHEMA` setting allows you to use a custom PostgreSQL schema instead of the default `public` schema. This is useful for:
- Multi-database setups where you want Hindsight tables in a dedicated schema
- Hosting platforms (e.g., Supabase) where `public` schema is reserved or shared
- Organizational preferences for schema naming conventions
```bash
# Example: Using a custom schema
export HINDSIGHT_API_DATABASE_URL=postgresql://user:pass@host:5432/dbname
export HINDSIGHT_API_DATABASE_SCHEMA=hindsight
```
Migrations will automatically create the schema if it doesn't exist and create all tables in the configured schema.
### Database Connection Pool
| Variable | Description | Default |
@@ -439,6 +453,7 @@ export HINDSIGHT_CP_DATAPLANE_API_URL=http://api.example.com:8888
```bash
# API Service
HINDSIGHT_API_DATABASE_URL=postgresql://hindsight:hindsight_dev@localhost:5432/hindsight
# HINDSIGHT_API_DATABASE_SCHEMA=public # optional, defaults to 'public'
HINDSIGHT_API_LLM_PROVIDER=groq
HINDSIGHT_API_LLM_API_KEY=gsk_xxxxxxxxxxxx
+2 -2
View File
@@ -13,7 +13,7 @@
},
"hindsight-clients/typescript": {
"name": "@vectorize-io/hindsight-client",
"version": "0.3.0",
"version": "0.4.0",
"license": "MIT",
"devDependencies": {
"@hey-api/openapi-ts": "0.88.0",
@@ -131,7 +131,7 @@
},
"hindsight-control-plane": {
"name": "@vectorize-io/hindsight-control-plane",
"version": "0.3.0",
"version": "0.4.0",
"license": "ISC",
"dependencies": {
"@radix-ui/react-alert-dialog": "^1.1.15",