* Load operation validator extension in main entry point Enable the operation validator extension to be loaded from environment configuration and passed to MemoryEngine, allowing pre/post operation hooks for usage metering, rate limiting, and audit logging. * Fix reflect background task authentication and add internal flag - Pass API key to background opinion storage task for proper auth - Add internal flag to RequestContext for tracking internal operations - Background opinion storage now authenticates correctly with tenant * Add api_key_id to RequestContext for usage tracking - Add api_key_id field to RequestContext to track which API key was used - Enables per-API-key usage analytics in the metering system * Fix HTTP error handling for authentication and validation errors - Add status_code parameter to ValidationResult and OperationValidationError - Convert OperationValidationError to HTTPException with proper status codes - Fix authentication errors to return 401 instead of raising internal errors - Re-raise HTTPException in exception handlers to prevent swallowing errors * Fix AuthenticationError handling in memory engine - Raise AuthenticationError from memory_engine._authenticate_tenant instead of HTTPException so unit tests pass - Add AuthenticationError handling in HTTP layer to convert to 401 responses - Fixes failing TestMemoryEngineTenantAuth tests * Add global exception handler for AuthenticationError Returns proper 401 status code for all authentication failures across all endpoints, not just the ones with explicit handlers. * Simplify exception handling: use global AuthenticationError handler - Remove redundant individual exception handlers - Add 'except AuthenticationError: raise' before generic Exception handlers to let global handler process auth errors uniformly * Refactor background tasks to use tenant_id instead of api_key This makes the core more generic - it passes tenant_id (which is extension-agnostic) rather than api_key (which is cloud-specific). - Add tenant_id field to RequestContext - Pass tenant_id instead of api_key to background tasks - Extensions can check internal=True with tenant_id to bypass normal auth * Fix exception propagation: include HTTPException in re-raise After cleanup of redundant exception handlers, 404 errors were returning 500 because HTTPException was caught by the generic except Exception handler. Fixed by combining AuthenticationError and HTTPException in the re-raise pattern.
327 lines
10 KiB
Python
327 lines
10 KiB
Python
"""Operation Validator Extension for validating retain/recall/reflect operations."""
|
|
|
|
from abc import ABC, abstractmethod
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from hindsight_api.extensions.base import Extension
|
|
|
|
if TYPE_CHECKING:
|
|
from hindsight_api.engine.memory_engine import Budget
|
|
from hindsight_api.engine.response_models import RecallResult as RecallResultModel
|
|
from hindsight_api.engine.response_models import ReflectResult
|
|
from hindsight_api.models import RequestContext
|
|
|
|
|
|
class OperationValidationError(Exception):
|
|
"""Raised when an operation fails validation."""
|
|
|
|
def __init__(self, reason: str, status_code: int = 403):
|
|
self.reason = reason
|
|
self.status_code = status_code
|
|
super().__init__(f"Operation validation failed: {reason}")
|
|
|
|
|
|
@dataclass
|
|
class ValidationResult:
|
|
"""Result of an operation validation."""
|
|
|
|
allowed: bool
|
|
reason: str | None = None
|
|
status_code: int = 403 # Default to Forbidden
|
|
|
|
@classmethod
|
|
def accept(cls) -> "ValidationResult":
|
|
"""Create an accepted validation result."""
|
|
return cls(allowed=True)
|
|
|
|
@classmethod
|
|
def reject(cls, reason: str, status_code: int = 403) -> "ValidationResult":
|
|
"""Create a rejected validation result with a reason and HTTP status code."""
|
|
return cls(allowed=False, reason=reason, status_code=status_code)
|
|
|
|
|
|
# =============================================================================
|
|
# Pre-operation Contexts (all user-provided parameters)
|
|
# =============================================================================
|
|
|
|
|
|
@dataclass
|
|
class RetainContext:
|
|
"""Context for a retain operation validation (pre-operation).
|
|
|
|
Contains ALL user-provided parameters for the retain operation.
|
|
"""
|
|
|
|
bank_id: str
|
|
contents: list[dict] # List of {content, context, event_date, document_id}
|
|
request_context: "RequestContext"
|
|
document_id: str | None = None
|
|
fact_type_override: str | None = None
|
|
confidence_score: float | None = None
|
|
|
|
|
|
@dataclass
|
|
class RecallContext:
|
|
"""Context for a recall operation validation (pre-operation).
|
|
|
|
Contains ALL user-provided parameters for the recall operation.
|
|
"""
|
|
|
|
bank_id: str
|
|
query: str
|
|
request_context: "RequestContext"
|
|
budget: "Budget | None" = None
|
|
max_tokens: int = 4096
|
|
enable_trace: bool = False
|
|
fact_types: list[str] = field(default_factory=list)
|
|
question_date: datetime | None = None
|
|
include_entities: bool = False
|
|
max_entity_tokens: int = 500
|
|
include_chunks: bool = False
|
|
max_chunk_tokens: int = 8192
|
|
|
|
|
|
@dataclass
|
|
class ReflectContext:
|
|
"""Context for a reflect operation validation (pre-operation).
|
|
|
|
Contains ALL user-provided parameters for the reflect operation.
|
|
"""
|
|
|
|
bank_id: str
|
|
query: str
|
|
request_context: "RequestContext"
|
|
budget: "Budget | None" = None
|
|
context: str | None = None
|
|
|
|
|
|
# =============================================================================
|
|
# Post-operation Contexts (includes results)
|
|
# =============================================================================
|
|
|
|
|
|
@dataclass
|
|
class RetainResult:
|
|
"""Result context for post-retain hook.
|
|
|
|
Contains the operation parameters and the result.
|
|
"""
|
|
|
|
bank_id: str
|
|
contents: list[dict]
|
|
request_context: "RequestContext"
|
|
document_id: str | None
|
|
fact_type_override: str | None
|
|
confidence_score: float | None
|
|
# Result
|
|
unit_ids: list[list[str]] # List of unit IDs per content item
|
|
success: bool = True
|
|
error: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class RecallResult:
|
|
"""Result context for post-recall hook.
|
|
|
|
Contains the operation parameters and the result.
|
|
"""
|
|
|
|
bank_id: str
|
|
query: str
|
|
request_context: "RequestContext"
|
|
budget: "Budget | None"
|
|
max_tokens: int
|
|
enable_trace: bool
|
|
fact_types: list[str]
|
|
question_date: datetime | None
|
|
include_entities: bool
|
|
max_entity_tokens: int
|
|
include_chunks: bool
|
|
max_chunk_tokens: int
|
|
# Result
|
|
result: "RecallResultModel | None" = None
|
|
success: bool = True
|
|
error: str | None = None
|
|
|
|
|
|
@dataclass
|
|
class ReflectResultContext:
|
|
"""Result context for post-reflect hook.
|
|
|
|
Contains the operation parameters and the result.
|
|
"""
|
|
|
|
bank_id: str
|
|
query: str
|
|
request_context: "RequestContext"
|
|
budget: "Budget | None"
|
|
context: str | None
|
|
# Result
|
|
result: "ReflectResult | None" = None
|
|
success: bool = True
|
|
error: str | None = None
|
|
|
|
|
|
class OperationValidatorExtension(Extension, ABC):
|
|
"""
|
|
Validates and hooks into retain/recall/reflect operations.
|
|
|
|
This extension allows implementing custom logic such as:
|
|
- Rate limiting (pre-operation)
|
|
- Quota enforcement (pre-operation)
|
|
- Permission checks (pre-operation)
|
|
- Content filtering (pre-operation)
|
|
- Usage tracking (post-operation)
|
|
- Audit logging (post-operation)
|
|
- Metrics collection (post-operation)
|
|
|
|
Enable via environment variable:
|
|
HINDSIGHT_API_OPERATION_VALIDATOR_EXTENSION=mypackage.validators:MyValidator
|
|
|
|
Configuration is passed from prefixed environment variables:
|
|
HINDSIGHT_API_OPERATION_VALIDATOR_MAX_REQUESTS=100
|
|
-> config = {"max_requests": "100"}
|
|
|
|
Hook execution order:
|
|
1. validate_retain/validate_recall/validate_reflect (pre-operation)
|
|
2. [operation executes]
|
|
3. on_retain_complete/on_recall_complete/on_reflect_complete (post-operation)
|
|
"""
|
|
|
|
# =========================================================================
|
|
# Pre-operation validation hooks (abstract - must be implemented)
|
|
# =========================================================================
|
|
|
|
@abstractmethod
|
|
async def validate_retain(self, ctx: RetainContext) -> ValidationResult:
|
|
"""
|
|
Validate a retain operation before execution.
|
|
|
|
Called before the retain operation is processed. Return ValidationResult.reject()
|
|
to prevent the operation from executing.
|
|
|
|
Args:
|
|
ctx: Context containing all user-provided parameters:
|
|
- bank_id: Bank identifier
|
|
- contents: List of content dicts
|
|
- request_context: Request context with auth info
|
|
- document_id: Optional document ID
|
|
- fact_type_override: Optional fact type override
|
|
- confidence_score: Optional confidence score
|
|
|
|
Returns:
|
|
ValidationResult indicating whether the operation is allowed.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def validate_recall(self, ctx: RecallContext) -> ValidationResult:
|
|
"""
|
|
Validate a recall operation before execution.
|
|
|
|
Called before the recall operation is processed. Return ValidationResult.reject()
|
|
to prevent the operation from executing.
|
|
|
|
Args:
|
|
ctx: Context containing all user-provided parameters:
|
|
- bank_id: Bank identifier
|
|
- query: Search query
|
|
- request_context: Request context with auth info
|
|
- budget: Budget level
|
|
- max_tokens: Maximum tokens to return
|
|
- enable_trace: Whether to include trace info
|
|
- fact_types: List of fact types to search
|
|
- question_date: Optional date context for query
|
|
- include_entities: Whether to include entity data
|
|
- max_entity_tokens: Max tokens for entities
|
|
- include_chunks: Whether to include chunks
|
|
- max_chunk_tokens: Max tokens for chunks
|
|
|
|
Returns:
|
|
ValidationResult indicating whether the operation is allowed.
|
|
"""
|
|
...
|
|
|
|
@abstractmethod
|
|
async def validate_reflect(self, ctx: ReflectContext) -> ValidationResult:
|
|
"""
|
|
Validate a reflect operation before execution.
|
|
|
|
Called before the reflect operation is processed. Return ValidationResult.reject()
|
|
to prevent the operation from executing.
|
|
|
|
Args:
|
|
ctx: Context containing all user-provided parameters:
|
|
- bank_id: Bank identifier
|
|
- query: Question to answer
|
|
- request_context: Request context with auth info
|
|
- budget: Budget level
|
|
- context: Optional additional context
|
|
|
|
Returns:
|
|
ValidationResult indicating whether the operation is allowed.
|
|
"""
|
|
...
|
|
|
|
# =========================================================================
|
|
# Post-operation hooks (optional - override to implement)
|
|
# =========================================================================
|
|
|
|
async def on_retain_complete(self, result: RetainResult) -> None:
|
|
"""
|
|
Called after a retain operation completes (success or failure).
|
|
|
|
Override this method to implement post-operation logic such as:
|
|
- Usage tracking
|
|
- Audit logging
|
|
- Metrics collection
|
|
- Notifications
|
|
|
|
Args:
|
|
result: Result context containing:
|
|
- All original operation parameters
|
|
- unit_ids: List of created unit IDs (if success)
|
|
- success: Whether the operation succeeded
|
|
- error: Error message (if failed)
|
|
"""
|
|
pass
|
|
|
|
async def on_recall_complete(self, result: RecallResult) -> None:
|
|
"""
|
|
Called after a recall operation completes (success or failure).
|
|
|
|
Override this method to implement post-operation logic such as:
|
|
- Usage tracking
|
|
- Audit logging
|
|
- Metrics collection
|
|
- Query analytics
|
|
|
|
Args:
|
|
result: Result context containing:
|
|
- All original operation parameters
|
|
- result: RecallResultModel (if success)
|
|
- success: Whether the operation succeeded
|
|
- error: Error message (if failed)
|
|
"""
|
|
pass
|
|
|
|
async def on_reflect_complete(self, result: ReflectResultContext) -> None:
|
|
"""
|
|
Called after a reflect operation completes (success or failure).
|
|
|
|
Override this method to implement post-operation logic such as:
|
|
- Usage tracking
|
|
- Audit logging
|
|
- Metrics collection
|
|
- Response analytics
|
|
|
|
Args:
|
|
result: Result context containing:
|
|
- All original operation parameters
|
|
- result: ReflectResult (if success)
|
|
- success: Whether the operation succeeded
|
|
- error: Error message (if failed)
|
|
"""
|
|
pass
|