Wire the guarded conversational RAG answer layer end-to-end
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
"""Exports the domain's counters; the only module that names prometheus_client.
|
||||
|
||||
`rag/metrics.py` defines what is counted and why. This decides how it leaves
|
||||
the process, and is imported lazily so the service runs — and the suite passes
|
||||
— with no metrics stack installed.
|
||||
|
||||
Counter names carry a `duocthu_` prefix and a `_total` suffix because that is
|
||||
what Prometheus expects of a counter; the dashboard queries them by name.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from rag.metrics import (
|
||||
ABSTENTION,
|
||||
ANSWER_EXTRACTIVE,
|
||||
GENERATION_REJECTED,
|
||||
GENERATION_SERVED,
|
||||
RETRIEVAL_ROUTE,
|
||||
)
|
||||
|
||||
_LABELS: dict[str, tuple[str, ...]] = {
|
||||
ABSTENTION: ("reason",),
|
||||
GENERATION_REJECTED: ("reason",),
|
||||
RETRIEVAL_ROUTE: ("route",),
|
||||
GENERATION_SERVED: (),
|
||||
ANSWER_EXTRACTIVE: (),
|
||||
}
|
||||
|
||||
_HELP = {
|
||||
ABSTENTION: "Answers refused, by the reason retrieval gave.",
|
||||
GENERATION_REJECTED: (
|
||||
"Generations discarded before reaching the caller. `reason=\"ungrounded_number\"` "
|
||||
"counts answers that stated a figure absent from the cited source."
|
||||
),
|
||||
GENERATION_SERVED: "Generations that passed grounding verification and were served.",
|
||||
ANSWER_EXTRACTIVE: "Answers served as verbatim source text.",
|
||||
RETRIEVAL_ROUTE: "Retrievals by route: section filter, or similarity fallback.",
|
||||
}
|
||||
|
||||
|
||||
class PrometheusMetrics:
|
||||
"""Domain `Metrics` backed by a Prometheus registry."""
|
||||
|
||||
def __init__(self, registry: Any | None = None) -> None:
|
||||
from prometheus_client import CollectorRegistry, Counter
|
||||
|
||||
self._registry = registry or CollectorRegistry()
|
||||
self._counters = {
|
||||
name: Counter(name, _HELP[name], labels, registry=self._registry)
|
||||
for name, labels in _LABELS.items()
|
||||
}
|
||||
|
||||
@property
|
||||
def registry(self) -> Any:
|
||||
return self._registry
|
||||
|
||||
def increment(self, name: str, **labels: str) -> None:
|
||||
counter = self._counters.get(name)
|
||||
if counter is None:
|
||||
return
|
||||
# An unexpected label would raise at scrape time, far from its cause.
|
||||
# Metrics must not be able to break a clinical answer, so a mismatch
|
||||
# drops the sample rather than the request.
|
||||
expected = set(_LABELS[name])
|
||||
if set(labels) != expected:
|
||||
return
|
||||
(counter.labels(**labels) if labels else counter).inc()
|
||||
|
||||
def render(self) -> tuple[bytes, str]:
|
||||
from prometheus_client import CONTENT_TYPE_LATEST, generate_latest
|
||||
|
||||
return generate_latest(self._registry), CONTENT_TYPE_LATEST
|
||||
Reference in New Issue
Block a user