Milestone 8 — Retrieval API and Downstream Publishing
Overview
M8 exposes the governed knowledge in the Raw to Knowledge registry as a first-class public API. It adds semantic retrieval (Weaviate + registry fallback), two downstream export formats (JSON-LD and Markdown), a dashboard metrics endpoint backed by Prometheus gauges, and a /metrics scrape target for the Prometheus ecosystem.
Architecture
GET /v1/retrieval/search → RetrievalService.search()
├─ WeaviateAdapter.search_similar_answers() (primary)
└─ PostgresRegistryRepository.query() (fallback)
GET /v1/retrieval/answers/{id}/export → RetrievalService.export_answer()
├─ JSONLDExporter (format=jsonld)
└─ MarkdownAnswerExporter (format=markdown)
GET /v1/dashboard/metrics → DashboardService.get_metrics()
├─ registry counts by status/horn_type
├─ expiry window (30 days)
├─ work-item pending count
└─ Prometheus gauge sync
GET /metrics → generate_latest() — Prometheus scrape target
Domain Layer
src/raw_to_knowledge/domain/models/retrieval.py
| Model | Frozen | Purpose |
|---|---|---|
RetrievalResult |
✓ | Single search result with relevance_score and source label |
ExportFormat |
str Enum | jsonld | markdown |
AnswerExport |
✓ | Rendered export artifact with content and timestamp |
DashboardMetrics |
✓ | Registry health snapshot: counts, type distribution, F2 |
src/raw_to_knowledge/domain/services/answer_exporter.py
Two pure-Python (zero I/O) exporters:
JSONLDExporter.export(answer) -> str
- Produces a JSON-LD document using schema.org and raw_to_knowledge: vocabulary
- Includes all grammar fields, evidence links, review_due, version
MarkdownAnswerExporter.export(answer) -> str
- GitHub-flavoured Markdown with Provenance and optional Assertion Grammar sections
- Assertion Grammar section omitted when all grammar fields are None
Application Layer
src/raw_to_knowledge/application/retrieval/retrieval_service.py
class RetrievalService:
async def search(query, customer_scope=None, horn_type=None, limit=10) -> list[RetrievalResult]
async def export_answer(answer_id, fmt: ExportFormat) -> AnswerExport
Search algorithm:
1. Attempt vector_repo.search_similar_answers(query, limit=limit)
2. If fewer than 2 results returned, or vector repo raises → fall back to registry_repo.query(search_text=query, ...)
3. Deduplicate by answer_id, sort by relevance_score descending
4. Vector results labelled source="weaviate", fallback results labelled source="registry" with fixed score 0.70
src/raw_to_knowledge/application/dashboard/dashboard_service.py
class DashboardService:
async def get_metrics() -> DashboardMetrics
Queries:
- registry_repo.query(status="approved") → total_approved
- registry_repo.query(status="disputed") → total_disputed
- registry_repo.query(review_due_before=<+30d>) → expiring_soon
- Per-HornType count for all 9 Horn types → by_horn_type
- work_item_repo.list_work_items(state=NEW) + PENDING_VALIDATION → work_items_pending
- f2_score, precision, recall, last_evaluated → None (deferred to observability integration)
Infrastructure
src/raw_to_knowledge/infrastructure/metrics.py
Prometheus metric objects (module-level singletons):
| Metric | Type | Labels | Purpose |
|---|---|---|---|
raw_to_knowledge_retrieval_requests_total |
Counter | source |
Retrieval API call count |
raw_to_knowledge_retrieval_latency_seconds |
Histogram | — | End-to-end retrieval latency |
raw_to_knowledge_approved_answers_total |
Gauge | — | Updated by dashboard endpoint |
raw_to_knowledge_disputed_answers_total |
Gauge | — | Updated by dashboard endpoint |
raw_to_knowledge_answers_expiring_soon |
Gauge | — | Answers due within 30 days |
raw_to_knowledge_work_items_pending |
Gauge | — | NEW + PENDING_VALIDATION items |
raw_to_knowledge_f2_score |
Gauge | — | Latest evaluation F2 |
raw_to_knowledge_precision |
Gauge | — | Latest precision |
raw_to_knowledge_recall |
Gauge | — | Latest recall |
raw_to_knowledge_candidates_ingested_total |
Counter | flow_type |
Intake throughput |
raw_to_knowledge_validation_decisions_total |
Counter | decision |
Validation throughput |
raw_to_knowledge_classification_duration_seconds |
Histogram | — | Classification latency |
raw_to_knowledge_registry_query_duration_seconds |
Histogram | — | Registry read latency |
API Layer
Retrieval Router (GET /v1/retrieval/...)
GET /v1/retrieval/search
Query params:
q string required Search query text
customer_scope string optional Filter by customer scope
horn_type string optional Filter by Horn information type
limit int 1-50 Max results (default 10)
Response: list[RetrievalResultResponse]
answer_id, answer_text, horn_type, assertion_subtype,
subject, predicate, object_value, scope,
status, version, relevance_score, source
Instruments raw_to_knowledge_retrieval_latency_seconds (histogram) and raw_to_knowledge_retrieval_requests_total (counter, label = source of first result or "empty").
GET /v1/retrieval/answers/{answer_id}/export
Path: answer_id string Registry answer identifier
Query: format string "markdown" (default) | "jsonld"
Response:
Content-Type: text/markdown; charset=utf-8 (markdown)
application/ld+json (jsonld)
Content-Disposition: attachment; filename="answer-{id}.md|.jsonld"
Returns 404 (via ValueError from service) when answer not found. Returns 422 when an unknown format string is supplied.
Dashboard Router (GET /v1/dashboard/...)
GET /v1/dashboard/metrics
Response: DashboardMetricsResponse
total_approved int
total_disputed int
expiring_soon int
by_horn_type dict[str, int] (all 9 Horn types)
f2_score float | null
precision float | null
recall float | null
last_evaluated string | null (ISO 8601)
validation_throughput_7d int
work_items_pending int
Also syncs Prometheus gauges: raw_to_knowledge_approved_answers_total, raw_to_knowledge_disputed_answers_total, raw_to_knowledge_answers_expiring_soon, raw_to_knowledge_work_items_pending.
Prometheus Endpoint (GET /metrics)
Returns text/plain; version=0.0.4 Prometheus exposition format via prometheus_client.generate_latest(). Hidden from OpenAPI schema (include_in_schema=False).
Quality Gates
| Gate | Target | Status |
|---|---|---|
| Export test coverage — JSONLDExporter | 100% | ✓ (11 tests) |
| Export test coverage — MarkdownAnswerExporter | 100% | ✓ (13 tests) |
| Retrieval service — search paths | Both vector + fallback | ✓ (9 tests) |
| Retrieval service — export paths | Both formats | ✓ (5 tests) |
| Dashboard service | All metric fields | ✓ (11 tests) |
| Integration test | Skipped without WEAVIATE_URL | ✓ (conditional) |
| Syntax check — all M8 files | py_compile | ✓ |
Bug Fixes
GeneratedCandidate.answer_candidate_idwas typedstr(non-nullable) butAGWMPServiceset it toNonefor LLM-generated candidates. Fixed tostr | None = None.DashboardService.list_work_itemswas passing.value(string) instead ofWorkItemStateenum toPostgresWorkItemRepository.list_work_items. Fixed to pass enum directly.retrieval.pyexport route passed a raw stringfmttoRetrievalService.export_answerwhich expectsExportFormat. Fixed with explicitExportFormat(fmt)conversion and 422 on invalid format.
Version
0.8.0 — bumped in main.py, FastAPI constructor, and /health endpoint.
Dependencies Added
No new dependencies. prometheus-client was already a transitive dependency via FastAPI; pytest-asyncio was installed for the async test suite (M6 onward).