Make the first RAG slice executable without risking production data
Some checks failed
verify / verify (push) Has been cancelled

The Stage 1 foundation now proves provider contracts with mocks and validates PostgreSQL/pgvector ingestion, approval binding, retrieval, reranking, and idempotency using only synthetic data. Live Bailian validation remains gated on rotating the exposed key.

Constraint: The key shown in chat is compromised and cannot be used or committed

Rejected: Mark Stage 1 complete from mock and offline results | real three-model smoke is still required

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Do not enable real-data ingestion until Stage 3 cloud approval and outbound manifest controls are enforced end to end

Tested: make verify; 41 pytest tests; strict mypy; Ruff; Compose config; pinned image build; empty-volume migration; role denial; two idempotent 20-vector seeds; database restart persistence

Not-tested: Live Bailian calls require a newly rotated key; React product UI is not implemented
This commit is contained in:
2026-07-12 15:41:58 +08:00
parent ec1acb36b5
commit f4ba5d5342
61 changed files with 6886 additions and 20 deletions

View File

@@ -0,0 +1,154 @@
"""Deterministic local providers for tests and offline Docker verification only."""
from __future__ import annotations
import hashlib
import math
import re
import time
from collections.abc import Sequence
from app.ports.model_providers import (
EmbeddingResult,
ModelProviderError,
ProviderErrorKind,
ProviderUsage,
RankedItem,
RerankResult,
)
_TOKEN_PATTERN = re.compile(r"[\u3400-\u9fff]+|[a-zA-Z0-9_.%+-]+")
def invalid_input(operation: str, code: str) -> ModelProviderError:
return ModelProviderError(
operation=operation,
kind=ProviderErrorKind.INVALID_REQUEST,
provider_code=code,
)
def lexical_features(text: str) -> tuple[str, ...]:
"""Create stable character n-grams/words without pretending to be a tokenizer."""
features: list[str] = []
for token in _TOKEN_PATTERN.findall(text.lower()):
if "\u3400" <= token[0] <= "\u9fff":
features.extend(token)
features.extend(token[index : index + 2] for index in range(len(token) - 1))
else:
features.append(token)
return tuple(features)
class FakeEmbeddingProvider:
"""Feature-hashing embedding used to validate plumbing without cloud calls."""
def __init__(self, dimension: int = 1024) -> None:
if dimension < 1:
raise ValueError("dimension must be positive")
self.dimension = dimension
def _vector(self, text: str) -> tuple[float, ...]:
vector = [0.0] * self.dimension
for feature in lexical_features(text):
digest = hashlib.sha256(feature.encode("utf-8")).digest()
index = int.from_bytes(digest[:4], "big") % self.dimension
sign = 1.0 if digest[4] & 1 else -1.0
vector[index] += sign
norm = math.sqrt(sum(value * value for value in vector))
if norm == 0:
vector[0] = 1.0
norm = 1.0
return tuple(value / norm for value in vector)
async def _embed(self, texts: Sequence[str]) -> EmbeddingResult:
if isinstance(texts, (str, bytes)) or not isinstance(texts, Sequence):
raise invalid_input("fake.embedding", "invalid_input_collection")
if not texts:
raise invalid_input("fake.embedding", "empty_input")
if len(texts) > 10:
raise invalid_input("fake.embedding", "batch_size_exceeded")
token_counts = []
for text in texts:
if not isinstance(text, str) or not text:
raise invalid_input("fake.embedding", "invalid_input_text")
token_count = len(text.encode("utf-8"))
if token_count > 8_192:
raise invalid_input("fake.embedding", "text_token_limit_exceeded")
token_counts.append(token_count)
if sum(token_counts) > 33_000:
raise invalid_input("fake.embedding", "request_token_limit_exceeded")
started = time.perf_counter()
vectors = tuple(self._vector(text) for text in texts)
return EmbeddingResult(
vectors=vectors,
model="fake-feature-hash-v1",
request_id=None,
usage=ProviderUsage(input_tokens=sum(len(lexical_features(text)) for text in texts)),
elapsed_ms=(time.perf_counter() - started) * 1000,
)
async def embed_documents(self, texts: Sequence[str]) -> EmbeddingResult:
return await self._embed(texts)
async def embed_query(self, text: str) -> EmbeddingResult:
return await self._embed([text])
class FakeReranker:
"""Lexical-overlap reranker for deterministic offline flow tests."""
async def rerank(
self,
query: str,
documents: Sequence[str],
*,
top_n: int,
instruct: str | None = None,
) -> RerankResult:
del instruct
if not isinstance(query, str) or not query:
raise invalid_input("fake.rerank", "invalid_query")
if isinstance(documents, (str, bytes)) or not isinstance(documents, Sequence):
raise invalid_input("fake.rerank", "invalid_document_collection")
if not documents:
raise invalid_input("fake.rerank", "empty_documents")
if len(documents) > 500:
raise invalid_input("fake.rerank", "document_count_exceeded")
if isinstance(top_n, bool) or not isinstance(top_n, int) or top_n <= 0:
raise invalid_input("fake.rerank", "invalid_top_n")
query_tokens = len(query.encode("utf-8"))
if query_tokens > 4_000:
raise invalid_input("fake.rerank", "query_token_limit_exceeded")
document_tokens = []
for document in documents:
if not isinstance(document, str) or not document:
raise invalid_input("fake.rerank", "invalid_document")
count = len(document.encode("utf-8"))
if count > 4_000:
raise invalid_input("fake.rerank", "document_token_limit_exceeded")
document_tokens.append(count)
if query_tokens * len(documents) + sum(document_tokens) > 120_000:
raise invalid_input("fake.rerank", "request_token_limit_exceeded")
started = time.perf_counter()
query_features = set(lexical_features(query))
ranked: list[RankedItem] = []
for index, document in enumerate(documents):
document_features = set(lexical_features(document))
union = query_features | document_features
score = len(query_features & document_features) / len(union) if union else 0.0
ranked.append(RankedItem(index=index, relevance_score=score, document=document))
ranked.sort(key=lambda item: (-item.relevance_score, item.index))
return RerankResult(
items=tuple(ranked[:top_n]),
model="fake-lexical-rerank-v1",
request_id=None,
usage=ProviderUsage(
input_tokens=len(query_features)
+ sum(len(lexical_features(document)) for document in documents)
),
elapsed_ms=(time.perf_counter() - started) * 1000,
)