Observability and evaluation
Observability is optional and provider-neutral. Without an observer, PyAIStack does not create events, write files, or add logging output.
Attach observers
from pyaistack import RAG
from pyaistack.observability import JsonlObserver, LoggingObserver
with JsonlObserver("rag_events.jsonl") as file_observer:
rag = RAG(observers=[LoggingObserver(), file_observer])
rag.add("AWS Lambda runs code without managing servers.")
print(rag.ask("Which service runs code?").text)
LoggingObserver sends one sanitized JSON record through Python logging.
JsonlObserver writes one sanitized event per line to the file path selected by
your application. InMemoryObserver retains events only in process memory for
tests or local inspection.
Event privacy
Events include a shared run_id, operation name, status, duration, model name,
and counts. They exclude prompts, generated answers, document text, credentials,
and metadata values by default.
Event format and operations
Every event has this JSON-compatible shape:
{
"schema_version": 1,
"timestamp": "2026-09-19T12:30:45.123Z",
"run_id": "one-id-for-the-rag-operation",
"operation": "retrieval",
"status": "success",
"duration_ms": 12.34,
"details": {"mode": "vector", "result_count": 3},
"error_type": null
}
schema_version identifies the event contract and timestamp records the
event creation time in UTC. status is success, failure, or skipped. Failed events include the
exception class name in error_type, not the exception message. Events emitted
while one ask() call is running share the same run_id.
| Operation | When it is emitted | Safe details |
|---|---|---|
embedding |
After document or query embedding. | Input count and embedding model name. |
indexing |
After documents and vectors are stored, or when no documents are supplied. | Document count, safe backend name when provided, and explicitly allowlisted metadata only. |
add |
After rag.add() completes or fails. |
Document count on success. |
retrieval |
After vector or hybrid search. | Retrieval mode, result count, metadata-filter key names, and safe backend name when provided. |
reranking |
After a configured reranker scores candidates. | Candidate and selected-result counts. |
search |
After a direct rag.search() call completes or fails. |
Result count and safe backend name when provided. |
context |
After bounded context construction during rag.ask(). |
Context character and excerpt counts. |
generation |
After the chat provider generates an answer. | Chat model name. |
answer |
After rag.ask() completes, fails, or is skipped for insufficient context. |
Source/citation counts or the skip reason. |
clear |
After rag.clear() removes the configured index or fails. |
Removed document count and safe backend name when provided. |
To intentionally include selected safe metadata fields during indexing, use an allowlist. Only scalar values or items in metadata lists are recorded, with bounded value counts and lengths.
from pyaistack import RAG, RAGConfig
from pyaistack.observability import LoggingObserver
rag = RAG(
config=RAGConfig(observability_metadata_keys=("category", "language")),
observers=[LoggingObserver()],
)
Do not allowlist source paths, user identifiers, secrets, or other sensitive application metadata.
Observer failures are isolated and never change an answer or replace the original provider/vector-store exception.
Retrieval evaluation reports
evaluate_retrieval_report() measures retrieval only. It reports Recall@k,
MRR, returned document IDs, first relevant rank, and elapsed time per labeled
query; it does not claim answer quality.
from pyaistack.evaluation import RetrievalCase, evaluate_retrieval_report
report = evaluate_retrieval_report(
rag,
[RetrievalCase("What does FIN-042 require?", frozenset({"document-id"}))],
top_k=3,
)
saved_report = report.to_dict()
Runnable example
Run the observability example from the repository root:
See ready examples for the complete example sequence.