Advanced
Advanced Features
Production-grade patterns for task durability, resilience, observability, and GDPR compliance.
Background Workers
By default, belief tracking runs as asyncio background tasks in the same event loop as your application. For production deployments that require task durability (surviving process restarts, guaranteed delivery, retry queues), you can offload tracking to Celery or RQ distributed workers.
Dispatcher Comparison
| Dispatcher | Durability | Setup | Best For |
|---|---|---|---|
asyncio | None Lost on crash | Zero — built-in | Default. Single-process apps, development, low-stakes tracking. |
sync | Yes Inline (blocking) | Zero — built-in | Debugging, unit testing, synchronous applications. |
celery | Yes Persisted in broker | Redis/RabbitMQ + workers | Production. Task retry, monitoring (Flower), multi-worker. |
rq | Yes Persisted in Redis | Redis + workers | Simpler alternative to Celery. Fewer features, easier setup. |
Celery Dispatcher
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
celery_app | Celery | (required) | A configured Celery application instance. Must have a broker configured (Redis or RabbitMQ). The dispatcher registers a Celery task that executes the tracking pipeline. |
from celery import Celery
from beliefstate import BeliefTracker, TrackerConfig
from beliefstate.dispatcher import CeleryDispatcher
celery_app = Celery("tasks", broker="redis://localhost:6379/0")
tracker = BeliefTracker(
config=TrackerConfig(),
adapter=app_adapter,
dispatcher=CeleryDispatcher(celery_app=celery_app)
)
RQ (Redis Queue) Dispatcher
Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
queue | rq.Queue | (required) | A configured RQ Queue instance. Must be connected to a Redis server. The dispatcher enqueues tracking tasks onto this queue for worker processing. |
from redis import Redis
from rq import Queue
from beliefstate.dispatcher import RQDispatcher
queue = Queue("belief-tasks", connection=Redis())
tracker = BeliefTracker(
config=TrackerConfig(),
adapter=app_adapter,
dispatcher=RQDispatcher(queue=queue)
)
Worker Setup
In your worker process, register the global tracker so tasks can execute:
from beliefstate.dispatcher import register_global_tracker
from my_app import tracker
# Register BEFORE processing any tasks
register_global_tracker(tracker)
Context Propagation: ContextVar (session_context) does NOT propagate across process boundaries. The dispatchers explicitly serialize session_id into task payloads to ensure it reaches worker processes correctly.
Resilience
BeliefState wraps internal adapter calls with production-grade resilience patterns to handle API failures gracefully. These patterns ensure that transient provider outages don't silently drop beliefs or cascade failures into your application.
Retry with Exponential Backoff
All adapter calls (generate, embeddings) automatically retry transient failures with jittered exponential backoff. Configure via TrackerConfig (global) or per-adapter RetryConfig (adapter-level). Transient errors include rate limits (HTTP 429), server errors (5xx), timeouts, and connection errors. Permanent errors (invalid API key, malformed request) are raised immediately.
Circuit Breaker
When an LLM API goes down, the circuit breaker pattern prevents cascading failures by fast-failing requests instead of queuing retries indefinitely:
config = TrackerConfig(
enable_circuit_breaker=True,
circuit_breaker_failure_threshold=5, # Trip after 5 consecutive failures
circuit_breaker_recovery_timeout=30.0, # Wait 30s before trying recovery
)
# Catch circuit breaker open state
from beliefstate import CircuitBreaker, CircuitBreakerOpenException
try:
result = await tracker.track(...)
except CircuitBreakerOpenException:
print("Circuit breaker is OPEN — failing fast")
Circuit Breaker State Machine:
- CLOSED (Normal): All requests flow through. If consecutive failures exceed
circuit_breaker_failure_threshold(default 5), the circuit trips to OPEN. - OPEN (Tripped): All requests immediately raise
CircuitBreakerOpenExceptionwithout hitting the LLM provider, saving rate limits and latency. - HALF-OPEN (Recovery probe): After
circuit_breaker_recovery_timeout(default 30s) elapses, the breaker allows a single probe request. If it succeeds → CLOSED. If it fails → back to OPEN for another cooldown cycle.
Health Checks
Use health checks to verify store and adapter connectivity at application startup or in readiness probes (Kubernetes /healthz endpoint):
# Check full tracker health (store + adapter)
health = await tracker.health_check()
print(health) # {"store": True, "adapter": True}
# Check individual adapter
is_healthy = await adapter.health_check()
if not is_healthy:
logger.error("Provider is not responding — use fallback")
Observability
BeliefState includes built-in OpenTelemetry instrumentation for distributed tracing and metric collection, plus structured JSON logging for cloud log aggregators.
OpenTelemetry Setup
setup_otel() Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
enabled | bool | True | Master switch for OpenTelemetry instrumentation. Set to False to completely disable tracing and metrics collection (zero overhead). |
service_name | str | "beliefstate" | Service name reported in traces and metrics. Set this to your application's name to identify traces in your observability dashboard (e.g., "my-chatbot-api"). |
otel_exporter_otlp_endpoint | str | "http://localhost:4317" | OTLP gRPC endpoint for exporting traces and metrics. Point this to your collector (Jaeger, Grafana Tempo, Datadog Agent, etc.). |
from beliefstate.observability import setup_otel
# Initialize OpenTelemetry tracing and metrics
setup_otel(
enabled=True,
service_name="my-app",
otel_exporter_otlp_endpoint="http://localhost:4317"
)
Distributed Tracing Spans
BeliefState creates hierarchical spans that trace the full lifecycle of each belief tracking task:
| Span Name | Description | Attributes |
|---|---|---|
beliefstate.extract_beliefs | Measures fact extraction prompt latency and token usage. | session_id, turn, beliefs_count, latency_ms |
beliefstate.detect_contradiction | Tracks time taken by semantic filters and NLI logic check. | session_id, candidates_checked, contradictions_found |
beliefstate.resolve_beliefs | Tracks store write times and resolution conflicts. | session_id, beliefs_added, beliefs_updated |
Exported Metrics
| Metric Name | Type | Description |
|---|---|---|
beliefstate.beliefs_extracted | Counter | Total number of facts parsed from conversation text. |
beliefstate.contradictions_detected | Counter | Total number of logical contradictions detected. |
beliefstate.beliefs_in_store | Gauge | Current number of active beliefs in the store. |
beliefstate.*_latency_ms | Histogram | Latency percentiles (p50, p90, p99) for database operations and provider calls. |
Structured Logging
All components emit structured JSON log events via the TrackerEvent dataclass. These events are designed for cloud log aggregators like Datadog, CloudWatch, and Loki.
TrackerEvent Fields
| Field | Type | Description |
|---|---|---|
session_id | str | The session ID this event relates to. |
operation | str | The pipeline stage: "extract_beliefs", "detect", "resolve", "store_write", etc. |
turn | int | The conversation turn number that triggered this event. |
latency_ms | float | Execution time in milliseconds for this operation. |
detail | str | Human-readable description of what happened (e.g., "Extracted 3 beliefs", "Found 1 contradiction"). |
extra | Optional[dict] | Arbitrary key-value metadata attached to the event (e.g., {"belief_count": 3, "contradiction_id": "..."}). |
{"session_id": "user_123", "operation": "extract_beliefs", "turn": 5, "detail": "Extracted 3 beliefs", "latency_ms": 142.5}
{"session_id": "user_123", "operation": "detect", "turn": 5, "detail": "Found 1 contradiction"}
{"session_id": "user_123", "operation": "resolve", "turn": 5, "detail": "Overwrote 1 belief", "latency_ms": 8.2}
from beliefstate.logging_utils import TrackerEvent, log_event
# Emit a custom tracker event
event = TrackerEvent(
session_id="user_123",
operation="custom_check",
turn=5,
latency_ms=42.0,
detail="Ran custom validation"
)
log_event(event)
GDPR & Privacy
BeliefState supports full session deletion with auditable receipts for GDPR compliance (right to erasure, Article 17). The clear_session() method ensures complete data removal including draining any in-flight background tasks.
# Delete all beliefs for a user (GDPR right to erasure)
receipt = await tracker.clear_session("user_123")
print(receipt)
# DeletionReceipt(
# session_id="user_123",
# beliefs_deleted=42,
# in_flight_tasks_drained=2,
# deleted_at=datetime(...)
# )
DeletionReceipt Fields
| Field | Type | Description |
|---|---|---|
session_id | str | The session ID whose data was deleted. |
beliefs_deleted | int | Number of belief records permanently removed from the store. |
in_flight_tasks_drained | int | Number of background tracking tasks that were cancelled or waited for completion before deletion. Ensures no new beliefs are written after the delete. |
deleted_at | datetime | UTC timestamp of when the deletion was executed. Serves as an auditable record for compliance. |
Concurrency & Deletion Race Conditions: If a user requests account deletion while background belief extraction tasks are still running in the asyncio event loop, those tasks could complete and write new facts after the database is cleared.
To prevent this, clear_session() calls the dispatcher's drain_session(session_id) method first, which cancels or waits for all active tracking tasks for that session ID before executing the SQL or Redis delete commands.
Deletion Workflow
- Drain in-flight tasks: The dispatcher cancels or awaits all active tracking tasks for the session.
- Delete from store: All beliefs for the session are permanently removed from the store (SQLite DELETE, Redis DEL).
- Clean up locks: Session-specific async locks, turn counters, and provider tracking entries are removed from memory.
- Generate receipt: A
DeletionReceiptis returned with the count of deleted records and timestamp.
Best Practices
- Store receipts: Log or persist
DeletionReceiptobjects as audit evidence for GDPR compliance reviews. - Redis TTL: For Redis stores, combine
clear_session()withset_session_ttl()to auto-expire sessions that aren't explicitly deleted. - Celery/RQ workers: The drain mechanism works across distributed workers — it signals the task queue to cancel pending tasks for the session.
Graceful Shutdown
BeliefTracker tracks all in-flight background tasks and provides a shutdown() method to drain them before process exit. This ensures no beliefs are lost during deployment restarts.
# Drain all pending background tasks before exit
await tracker.shutdown(grace_seconds=10.0)
# Or use as a context manager
async with tracker:
await tracker.track(...)
Shutdown Workflow
- Stop accepting new tasks: The tracker stops dispatching new background tasks.
- Drain in-flight tasks: Waits up to
grace_secondsfor all pending tasks to complete. - Cancel remaining: Any tasks still running after the grace period are cancelled.
- Close store: The store connection is closed (SQLite file handle, PostgreSQL pool, etc.).
Production deployments: Always call await tracker.shutdown() in your application's shutdown handler (e.g., FastAPI's shutdown event, or atexit for sync apps). Skipping this may result in lost beliefs from in-flight background tasks.
Concurrency Model
BeliefState uses a multi-layered concurrency model to coordinate background writes safely.
Per-Session Async Locks
Each session gets a dedicated asyncio.Lock that serializes background belief writes within that session. This prevents two concurrent extraction tasks from racing on the same belief triple.
# Internal: per-session lock ensures ordered writes
# Task A and Task B for the same session_id will execute sequentially
# Task C for a different session_id runs concurrently
Turn-Based Optimistic Concurrency
Every upsert() call includes the current turn number. The store compares it against the stored belief's turn and only overwrites if the incoming turn is newer or equal. This is a lightweight guard against stale writes from background tasks that complete out of order.
Task Dispatchers
Background tasks are dispatched via a pluggable dispatcher. The default AsyncioDispatcher wraps each task in an asyncio.Task and tracks it for shutdown draining. The tracker's _dispatch() method adds each task to a tracked set and removes it on completion.
Dispatcher choice matters: For single-process apps, asyncio is fine. For multi-worker production deployments, use celery or rq — each worker process maintains its own belief store, so choose Redis or PostgreSQL for shared state.
Hedging & Deduplication
BeliefState applies two layers of filtering before storing new beliefs: hedging-based confidence calibration and exact duplicate detection.
Hedging Pattern Calibration
After extraction, calibrate_confidence() scans the source quote for hedging language and caps the confidence score:
| Pattern | Confidence Ceiling | Effect |
|---|---|---|
"not sure", "unsure", "maybe" | 0.50 | is_hypothetical = True |
"might", "may", "could", "perhaps" | 0.60 | is_hypothetical = True |
"think", "believe", "probably" | 0.70 | Confidence capped |
"want to", "planning to" | 0.75 | Confidence capped |
Hypothetical beliefs (is_hypothetical=True) are excluded from prompt injection to avoid polluting the LLM context with unconfirmed claims.
Exact Duplicate Check
Before running the expensive embedding similarity pipeline, the detector performs an O(1) exact duplicate check via store.get_by_key(). If a belief with the same subject, predicate, and session already exists and the normalized values match, the new belief is silently skipped — no embedding, no NLI, no LLM call.
# Step 0: O(1) exact check — zero LLM/embedding cost
existing = await store.get_by_key(subject, predicate, session_id)
if existing and normalize_value(existing.value) == normalize_value(new_belief.value):
return # skip — exact duplicate
Deduplication Pipeline
The full detect_with_deduplication() pipeline runs these steps for each new belief:
- Exact duplicate check — O(1) key lookup, NFKC value comparison.
- Embedding dimension guard — Skips vector comparison if old and new embeddings have different dimensions (model upgrade scenario).
- Cosine similarity gate — Fast approximate check against stored embeddings.
- NLI judgment — Full natural language inference on cosine-similar candidates to classify as entailment, contradiction, or neutral.