Some checks failed
verify / verify (push) Has been cancelled
The API and ingestion tools now use a fixed internal model gateway while governed profiles, embedding cache assignments, traceable citations, and stable API errors establish the boundaries required by later workflows. Constraint: The current Alibaba Cloud workspace rejects all three live model calls with authentication failures Rejected: Give the API or seed tools the Bailian key and direct egress | combines database access, cloud credentials, and public network access Rejected: Mix offline and Bailian vectors in one demo namespace | makes profile activation and retrieval ambiguous Confidence: high Scope-risk: moderate Reversibility: clean Directive: Keep Bailian credentials and egress exclusive to model-gateway and create a new immutable profile hash for any embedding identity change Tested: make verify; 121 backend tests; 14 frontend tests; fresh and populated Alembic upgrade-downgrade-upgrade; two idempotent offline seeds; Docker health and HTTP retrieval; isolated provider smoke Not-tested: Successful live Bailian responses because the supplied workspace credential currently fails authentication
866 lines
31 KiB
Python
866 lines
31 KiB
Python
"""Idempotently embed, store, retrieve, and rerank the public synthetic corpus."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import sys
|
|
import uuid
|
|
from collections.abc import Sequence
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Any, cast
|
|
from urllib.parse import urlsplit
|
|
|
|
import psycopg
|
|
from pgvector import Vector
|
|
from pgvector.psycopg import register_vector
|
|
from psycopg.rows import dict_row
|
|
from psycopg.types.json import Jsonb
|
|
|
|
from app.adapters.fake import FakeEmbeddingProvider, FakeReranker, lexical_features
|
|
from app.adapters.model_gateway import ModelGatewayAdapter
|
|
from app.core.config import Settings
|
|
from app.core.demo_identity import (
|
|
ACCESS_SCOPE_ID,
|
|
IDENTITY_NAMESPACE,
|
|
KNOWLEDGE_BASE_ID,
|
|
offline_embedding_profile_hash,
|
|
)
|
|
from app.core.secrets import SecretFileError
|
|
from app.ports.model_providers import EmbeddingProvider, ModelProviderError, Reranker
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
|
DEFAULT_SAMPLE_ROOT = PROJECT_ROOT / "data" / "samples" / "public"
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DemoDocument:
|
|
source_id: str
|
|
title: str
|
|
content: str
|
|
region: str
|
|
mineral: str
|
|
page_no: int
|
|
cloud_policy_id: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DemoQuery:
|
|
qid: str
|
|
query: str
|
|
expected_doc_ids: tuple[str, ...]
|
|
answerable: bool
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class DemoNamespace:
|
|
mode: str
|
|
knowledge_base_id: uuid.UUID
|
|
access_scope_id: uuid.UUID
|
|
scope_name: str
|
|
knowledge_base_name: str
|
|
storage_prefix: str
|
|
|
|
|
|
OFFLINE_NAMESPACE = DemoNamespace(
|
|
mode="fake",
|
|
knowledge_base_id=KNOWLEDGE_BASE_ID,
|
|
access_scope_id=ACCESS_SCOPE_ID,
|
|
scope_name="synthetic-demo",
|
|
knowledge_base_name="虚构地质 PoC 知识库(离线)",
|
|
storage_prefix="synthetic/offline",
|
|
)
|
|
BAILIAN_NAMESPACE = DemoNamespace(
|
|
mode="bailian",
|
|
knowledge_base_id=uuid.uuid5(IDENTITY_NAMESPACE, "synthetic-bailian-knowledge-base"),
|
|
access_scope_id=uuid.uuid5(IDENTITY_NAMESPACE, "synthetic-bailian-public-scope"),
|
|
scope_name="synthetic-bailian-validation",
|
|
knowledge_base_name="虚构地质 PoC 知识库(百炼验证)",
|
|
storage_prefix="synthetic/bailian",
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class EmbeddedVector:
|
|
vector: tuple[float, ...]
|
|
request_id: str | None
|
|
usage: dict[str, int | None]
|
|
elapsed_ms: int
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PreparedChunk:
|
|
source_id: str
|
|
document_id: uuid.UUID
|
|
version_id: uuid.UUID
|
|
chunk_id: uuid.UUID
|
|
raw_sha256: str
|
|
cloud_text: str
|
|
cloud_text_sha256: str
|
|
embedding_prefix: str
|
|
embedding_text: str
|
|
embedding_text_sha256: str
|
|
outbound_manifest_sha256: str
|
|
embedding_profile_hash: str
|
|
vector: tuple[float, ...]
|
|
embedding_model: str
|
|
provider_request_id: str | None
|
|
embedding_usage: dict[str, int | None]
|
|
embedding_elapsed_ms: int
|
|
title: str
|
|
region: str
|
|
mineral: str
|
|
page_no: int
|
|
cloud_policy_id: str
|
|
|
|
|
|
class SeedContractError(ValueError):
|
|
def __init__(self, code: str) -> None:
|
|
self.code = code
|
|
super().__init__(code)
|
|
|
|
|
|
def sha256_text(value: str) -> str:
|
|
return hashlib.sha256(value.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def namespace_for_mode(mode: str) -> DemoNamespace:
|
|
if mode == "fake":
|
|
return OFFLINE_NAMESPACE
|
|
if mode == "bailian":
|
|
return BAILIAN_NAMESPACE
|
|
raise SeedContractError("invalid_provider_mode")
|
|
|
|
|
|
def load_jsonl(path: Path) -> list[dict[str, Any]]:
|
|
if not path.is_file():
|
|
raise SeedContractError("fixture_missing")
|
|
records: list[dict[str, Any]] = []
|
|
for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
|
try:
|
|
value = json.loads(line)
|
|
except json.JSONDecodeError as exc:
|
|
raise SeedContractError(f"invalid_jsonl_line_{line_number}") from exc
|
|
if not isinstance(value, dict):
|
|
raise SeedContractError(f"jsonl_object_required_line_{line_number}")
|
|
records.append(value)
|
|
return records
|
|
|
|
|
|
def load_documents(path: Path) -> list[DemoDocument]:
|
|
documents = []
|
|
for value in load_jsonl(path):
|
|
if value.get("source_type") != "synthetic":
|
|
raise SeedContractError("non_synthetic_document")
|
|
if value.get("review_state") != "LOCAL_PARSED_PENDING_CLOUD_REVIEW":
|
|
raise SeedContractError("invalid_initial_review_state")
|
|
documents.append(
|
|
DemoDocument(
|
|
source_id=str(value["doc_id"]),
|
|
title=str(value["title"]),
|
|
content=str(value["content"]),
|
|
region=str(value["region"]),
|
|
mineral=str(value["mineral"]),
|
|
page_no=int(value["page_no"]),
|
|
cloud_policy_id=str(value["cloud_policy_id"]),
|
|
)
|
|
)
|
|
if len(documents) != 20 or len({item.source_id for item in documents}) != 20:
|
|
raise SeedContractError("expected_twenty_unique_documents")
|
|
return documents
|
|
|
|
|
|
def load_queries(path: Path) -> list[DemoQuery]:
|
|
queries = [
|
|
DemoQuery(
|
|
qid=str(value["qid"]),
|
|
query=str(value["query"]),
|
|
expected_doc_ids=tuple(str(item) for item in value["expected_doc_ids"]),
|
|
answerable=bool(value["answerable"]),
|
|
)
|
|
for value in load_jsonl(path)
|
|
]
|
|
if not queries:
|
|
raise SeedContractError("query_set_empty")
|
|
return queries
|
|
|
|
|
|
def embedding_profile_hash(settings: Settings, mode: str) -> str:
|
|
if mode == "fake":
|
|
return offline_embedding_profile_hash(settings.embedding_dimension)
|
|
if mode != "bailian":
|
|
raise SeedContractError("invalid_provider_mode")
|
|
|
|
endpoint_identity = sha256_text(urlsplit(settings.bailian_openai_base_url).hostname or "")
|
|
model = settings.embedding_model
|
|
api_mode = "openai-compatible"
|
|
profile = {
|
|
"api_mode": api_mode,
|
|
"dimension": settings.embedding_dimension,
|
|
"endpoint_identity_hash": endpoint_identity,
|
|
"model": model,
|
|
"normalization": "provider-default",
|
|
"profile_version": 1,
|
|
}
|
|
return sha256_text(
|
|
json.dumps(profile, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
)
|
|
|
|
|
|
async def embed_in_batches(
|
|
provider: EmbeddingProvider,
|
|
texts: Sequence[str],
|
|
) -> tuple[tuple[EmbeddedVector, ...], str]:
|
|
vectors: list[EmbeddedVector] = []
|
|
resolved_model: str | None = None
|
|
for offset in range(0, len(texts), 10):
|
|
result = await provider.embed_documents(texts[offset : offset + 10])
|
|
if resolved_model is not None and result.model != resolved_model:
|
|
raise SeedContractError("embedding_model_changed_between_batches")
|
|
resolved_model = result.model
|
|
if len(result.vectors) != len(texts[offset : offset + 10]):
|
|
raise SeedContractError("embedding_batch_count_mismatch")
|
|
usage = {
|
|
"input_tokens": result.usage.input_tokens,
|
|
"output_tokens": result.usage.output_tokens,
|
|
"total_tokens": result.usage.total_tokens,
|
|
}
|
|
vectors.extend(
|
|
EmbeddedVector(
|
|
vector=vector,
|
|
request_id=result.request_id,
|
|
usage=usage,
|
|
elapsed_ms=max(0, round(result.elapsed_ms)),
|
|
)
|
|
for vector in result.vectors
|
|
)
|
|
if len(vectors) != len(texts) or resolved_model is None:
|
|
raise SeedContractError("embedding_result_count_mismatch")
|
|
return tuple(vectors), resolved_model
|
|
|
|
|
|
def prepare_chunks(
|
|
documents: Sequence[DemoDocument],
|
|
vectors: Sequence[EmbeddedVector],
|
|
*,
|
|
profile_hash: str,
|
|
embedding_model: str,
|
|
namespace: DemoNamespace = OFFLINE_NAMESPACE,
|
|
) -> list[PreparedChunk]:
|
|
prepared = []
|
|
for document, vector in zip(documents, vectors, strict=True):
|
|
raw_payload = json.dumps(
|
|
{
|
|
"content": document.content,
|
|
"mineral": document.mineral,
|
|
"page_no": document.page_no,
|
|
"region": document.region,
|
|
"title": document.title,
|
|
},
|
|
ensure_ascii=False,
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
)
|
|
raw_hash = sha256_text(raw_payload)
|
|
document_identity = (
|
|
f"document:{document.source_id}"
|
|
if namespace.mode == "fake"
|
|
else f"document:{namespace.mode}:{document.source_id}"
|
|
)
|
|
document_id = uuid.uuid5(IDENTITY_NAMESPACE, document_identity)
|
|
version_id = uuid.uuid5(
|
|
IDENTITY_NAMESPACE,
|
|
f"version:{document.source_id}:{raw_hash}:{profile_hash}",
|
|
)
|
|
chunk_id = uuid.uuid5(IDENTITY_NAMESPACE, f"chunk:{version_id}:0")
|
|
prefix = (
|
|
f"标题:{document.title}\n地区:{document.region}\n矿种:{document.mineral}\n正文:"
|
|
)
|
|
cloud_hash = sha256_text(document.content)
|
|
embedding_text = prefix + document.content
|
|
embedding_hash = sha256_text(embedding_text)
|
|
manifest_payload = json.dumps(
|
|
[
|
|
{
|
|
"cloud_text_sha256": cloud_hash,
|
|
"embedding_text_sha256": embedding_hash,
|
|
"ordinal": 0,
|
|
}
|
|
],
|
|
sort_keys=True,
|
|
separators=(",", ":"),
|
|
)
|
|
prepared.append(
|
|
PreparedChunk(
|
|
source_id=document.source_id,
|
|
document_id=document_id,
|
|
version_id=version_id,
|
|
chunk_id=chunk_id,
|
|
raw_sha256=raw_hash,
|
|
cloud_text=document.content,
|
|
cloud_text_sha256=cloud_hash,
|
|
embedding_prefix=prefix,
|
|
embedding_text=embedding_text,
|
|
embedding_text_sha256=embedding_hash,
|
|
outbound_manifest_sha256=sha256_text(manifest_payload),
|
|
embedding_profile_hash=profile_hash,
|
|
vector=vector.vector,
|
|
embedding_model=embedding_model,
|
|
provider_request_id=vector.request_id,
|
|
embedding_usage=vector.usage,
|
|
embedding_elapsed_ms=vector.elapsed_ms,
|
|
title=document.title,
|
|
region=document.region,
|
|
mineral=document.mineral,
|
|
page_no=document.page_no,
|
|
cloud_policy_id=document.cloud_policy_id,
|
|
)
|
|
)
|
|
return prepared
|
|
|
|
|
|
def database_dsn(settings: Settings) -> str:
|
|
return (
|
|
settings.database_url().set(drivername="postgresql").render_as_string(hide_password=False)
|
|
)
|
|
|
|
|
|
def write_chunks(
|
|
settings: Settings,
|
|
chunks: Sequence[PreparedChunk],
|
|
*,
|
|
namespace: DemoNamespace,
|
|
) -> dict[str, int]:
|
|
if not chunks:
|
|
raise SeedContractError("chunks_empty")
|
|
profile_hashes = {item.embedding_profile_hash for item in chunks}
|
|
resolved_models = {item.embedding_model for item in chunks}
|
|
if len(profile_hashes) != 1 or len(resolved_models) != 1:
|
|
raise SeedContractError("mixed_embedding_profiles")
|
|
profile_hash = next(iter(profile_hashes))
|
|
resolved_model = next(iter(resolved_models))
|
|
if namespace.mode == "fake":
|
|
provider = "local-synthetic"
|
|
api_mode = "deterministic-offline"
|
|
endpoint_identity_hash = sha256_text("local-fake")
|
|
else:
|
|
provider = "aliyun-bailian"
|
|
api_mode = "model-gateway/openai-compatible"
|
|
endpoint_identity_hash = sha256_text(
|
|
urlsplit(settings.bailian_openai_base_url).hostname or ""
|
|
)
|
|
|
|
with psycopg.connect(database_dsn(settings), row_factory=dict_row) as connection:
|
|
register_vector(connection)
|
|
connection.execute("SELECT pg_advisory_xact_lock(724202607120001)")
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO rag.model_profiles (
|
|
profile_hash, alias, kind, provider, model, api_mode, dimension,
|
|
endpoint_identity_hash, config_snapshot, synthetic, enabled
|
|
) VALUES (
|
|
%s, %s, 'embedding', %s, %s, %s, 1024, %s, %s, %s, true
|
|
)
|
|
ON CONFLICT (profile_hash) DO NOTHING
|
|
""",
|
|
(
|
|
profile_hash,
|
|
f"{namespace.mode}-embedding-{profile_hash[:12]}",
|
|
provider,
|
|
resolved_model,
|
|
api_mode,
|
|
endpoint_identity_hash,
|
|
Jsonb(
|
|
{
|
|
"dimension": settings.embedding_dimension,
|
|
"requested_model": settings.embedding_model,
|
|
"source": "synthetic-seed-v1",
|
|
}
|
|
),
|
|
namespace.mode == "fake",
|
|
),
|
|
)
|
|
registered_profile = connection.execute(
|
|
"""
|
|
SELECT kind, provider, model, api_mode, dimension, endpoint_identity_hash
|
|
FROM rag.model_profiles
|
|
WHERE profile_hash = %s
|
|
""",
|
|
(profile_hash,),
|
|
).fetchone()
|
|
if registered_profile is None or (
|
|
registered_profile["kind"] != "embedding"
|
|
or registered_profile["provider"] != provider
|
|
or registered_profile["model"] != resolved_model
|
|
or registered_profile["api_mode"] != api_mode
|
|
or registered_profile["dimension"] != settings.embedding_dimension
|
|
or registered_profile["endpoint_identity_hash"] != endpoint_identity_hash
|
|
):
|
|
raise SeedContractError("embedding_profile_collision")
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO rag.knowledge_bases (
|
|
id, name, description, active_embedding_profile_hash
|
|
)
|
|
VALUES (%s, %s, %s, %s)
|
|
ON CONFLICT (id) DO UPDATE
|
|
SET name = EXCLUDED.name,
|
|
description = EXCLUDED.description,
|
|
active_embedding_profile_hash = EXCLUDED.active_embedding_profile_hash,
|
|
updated_at = now()
|
|
""",
|
|
(
|
|
namespace.knowledge_base_id,
|
|
namespace.knowledge_base_name,
|
|
"仅含公开的合成验证文本",
|
|
profile_hash,
|
|
),
|
|
)
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO rag.access_scopes (id, knowledge_base_id, name)
|
|
VALUES (%s, %s, %s)
|
|
ON CONFLICT (id) DO NOTHING
|
|
""",
|
|
(
|
|
namespace.access_scope_id,
|
|
namespace.knowledge_base_id,
|
|
namespace.scope_name,
|
|
),
|
|
)
|
|
|
|
for item in chunks:
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO rag.documents (
|
|
id, knowledge_base_id, access_scope_id, raw_sha256,
|
|
filename, storage_key, mime_type, status
|
|
) VALUES (%s, %s, %s, %s, %s, %s, 'application/json',
|
|
'LOCAL_PARSED_PENDING_CLOUD_REVIEW')
|
|
ON CONFLICT (id) DO UPDATE
|
|
SET filename = EXCLUDED.filename,
|
|
storage_key = EXCLUDED.storage_key,
|
|
mime_type = EXCLUDED.mime_type,
|
|
updated_at = now()
|
|
""",
|
|
(
|
|
item.document_id,
|
|
namespace.knowledge_base_id,
|
|
namespace.access_scope_id,
|
|
item.raw_sha256,
|
|
f"{item.source_id}.json",
|
|
f"{namespace.storage_prefix}/{item.source_id}",
|
|
),
|
|
)
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO rag.document_versions (
|
|
id, document_id, parser_profile_hash, ocr_profile_hash,
|
|
normalization_profile_hash, chunk_profile_hash, cloud_policy_id,
|
|
outbound_manifest_sha256, review_state, embedding_profile_hash,
|
|
status, expected_chunk_count
|
|
) VALUES (
|
|
%s, %s, %s, NULL, %s, %s, %s, %s,
|
|
'LOCAL_PARSED_PENDING_CLOUD_REVIEW', %s, 'PROCESSING', 1
|
|
)
|
|
ON CONFLICT (id) DO NOTHING
|
|
""",
|
|
(
|
|
item.version_id,
|
|
item.document_id,
|
|
sha256_text("synthetic-jsonl-parser-v1"),
|
|
sha256_text("identity-normalization-v1"),
|
|
sha256_text("one-record-one-chunk-v1"),
|
|
item.cloud_policy_id,
|
|
item.outbound_manifest_sha256,
|
|
item.embedding_profile_hash,
|
|
),
|
|
)
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO rag.outbound_manifest_items (
|
|
document_version_id, ordinal, outbound_manifest_sha256,
|
|
cloud_text_sha256, embedding_text_sha256
|
|
)
|
|
SELECT %s, 0, %s, %s, %s
|
|
WHERE NOT EXISTS (
|
|
SELECT 1
|
|
FROM rag.outbound_manifest_items
|
|
WHERE document_version_id = %s AND ordinal = 0
|
|
)
|
|
ON CONFLICT (document_version_id, ordinal) DO NOTHING
|
|
""",
|
|
(
|
|
item.version_id,
|
|
item.outbound_manifest_sha256,
|
|
item.cloud_text_sha256,
|
|
item.embedding_text_sha256,
|
|
item.version_id,
|
|
),
|
|
)
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO rag.chunks (
|
|
id, knowledge_base_id, document_id, document_version_id,
|
|
access_scope_id, ordinal, display_text, cloud_text,
|
|
cloud_text_sha256, embedding_prefix, embedding_text,
|
|
embedding_text_sha256, embedded_text_sha256,
|
|
embedding_profile_hash, outbound_manifest_sha256, token_count,
|
|
page_start, page_end, section_path, metadata, embedding_model,
|
|
embedding_dimension, embedding, approval_status, index_status,
|
|
searchable
|
|
) VALUES (
|
|
%s, %s, %s, %s, %s, 0, %s, %s, %s, %s, %s, %s, %s,
|
|
%s, %s, %s, %s, %s, %s, %s, %s, 1024, %s,
|
|
'LOCAL_PARSED_PENDING_CLOUD_REVIEW', 'READY', false
|
|
)
|
|
ON CONFLICT (id) DO UPDATE SET
|
|
display_text = EXCLUDED.display_text,
|
|
cloud_text = EXCLUDED.cloud_text,
|
|
cloud_text_sha256 = EXCLUDED.cloud_text_sha256,
|
|
embedding_prefix = EXCLUDED.embedding_prefix,
|
|
embedding_text = EXCLUDED.embedding_text,
|
|
embedding_text_sha256 = EXCLUDED.embedding_text_sha256,
|
|
embedded_text_sha256 = EXCLUDED.embedded_text_sha256,
|
|
embedding_profile_hash = EXCLUDED.embedding_profile_hash,
|
|
outbound_manifest_sha256 = EXCLUDED.outbound_manifest_sha256,
|
|
token_count = EXCLUDED.token_count,
|
|
page_start = EXCLUDED.page_start,
|
|
page_end = EXCLUDED.page_end,
|
|
section_path = EXCLUDED.section_path,
|
|
metadata = EXCLUDED.metadata,
|
|
embedding_model = EXCLUDED.embedding_model,
|
|
updated_at = now()
|
|
""",
|
|
(
|
|
item.chunk_id,
|
|
namespace.knowledge_base_id,
|
|
item.document_id,
|
|
item.version_id,
|
|
namespace.access_scope_id,
|
|
item.cloud_text,
|
|
item.cloud_text,
|
|
item.cloud_text_sha256,
|
|
item.embedding_prefix,
|
|
item.embedding_text,
|
|
item.embedding_text_sha256,
|
|
item.embedding_text_sha256,
|
|
item.embedding_profile_hash,
|
|
item.outbound_manifest_sha256,
|
|
max(1, len(lexical_features(item.embedding_text))),
|
|
item.page_no,
|
|
item.page_no,
|
|
Jsonb([item.title]),
|
|
Jsonb(
|
|
{
|
|
"mineral": item.mineral,
|
|
"region": item.region,
|
|
"source_doc_id": item.source_id,
|
|
"source_type": "synthetic",
|
|
}
|
|
),
|
|
item.embedding_model,
|
|
Vector(list(item.vector)),
|
|
),
|
|
)
|
|
connection.execute(
|
|
"""
|
|
UPDATE rag.document_versions
|
|
SET review_state = 'CLOUD_APPROVED',
|
|
cloud_approved_at = COALESCE(cloud_approved_at, now()),
|
|
cloud_approved_by = 'seed-demo:synthetic-policy',
|
|
status = 'READY',
|
|
completed_at = COALESCE(completed_at, now())
|
|
WHERE id = %s
|
|
""",
|
|
(item.version_id,),
|
|
)
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO rag.embedding_cache (
|
|
profile_hash, embedding_text_sha256, embedding, resolved_model,
|
|
provider_request_id, usage, elapsed_ms
|
|
) VALUES (%s, %s, %s, %s, %s, %s, %s)
|
|
ON CONFLICT (profile_hash, embedding_text_sha256) DO NOTHING
|
|
""",
|
|
(
|
|
item.embedding_profile_hash,
|
|
item.embedding_text_sha256,
|
|
Vector(list(item.vector)),
|
|
item.embedding_model,
|
|
item.provider_request_id,
|
|
Jsonb(item.embedding_usage),
|
|
item.embedding_elapsed_ms,
|
|
),
|
|
)
|
|
connection.execute(
|
|
"""
|
|
INSERT INTO rag.chunk_embedding_assignments (
|
|
chunk_id, profile_hash, embedding_text_sha256,
|
|
cache_profile_hash, cache_embedding_text_sha256,
|
|
status, completed_at
|
|
) VALUES (%s, %s, %s, %s, %s, 'READY', now())
|
|
ON CONFLICT (chunk_id, profile_hash) DO NOTHING
|
|
""",
|
|
(
|
|
item.chunk_id,
|
|
item.embedding_profile_hash,
|
|
item.embedding_text_sha256,
|
|
item.embedding_profile_hash,
|
|
item.embedding_text_sha256,
|
|
),
|
|
)
|
|
connection.execute(
|
|
"""
|
|
UPDATE rag.chunks
|
|
SET approval_status = 'CLOUD_APPROVED',
|
|
index_status = 'READY',
|
|
updated_at = now()
|
|
WHERE id = %s
|
|
""",
|
|
(item.chunk_id,),
|
|
)
|
|
connection.execute(
|
|
"""
|
|
UPDATE rag.documents
|
|
SET active_version_id = %s,
|
|
raw_sha256 = %s,
|
|
status = 'READY',
|
|
updated_at = now()
|
|
WHERE id = %s
|
|
""",
|
|
(item.version_id, item.raw_sha256, item.document_id),
|
|
)
|
|
connection.execute(
|
|
"UPDATE rag.chunks SET searchable = true, updated_at = now() WHERE id = %s",
|
|
(item.chunk_id,),
|
|
)
|
|
|
|
counts = connection.execute(
|
|
"""
|
|
SELECT
|
|
count(*)::integer AS chunks,
|
|
count(*) FILTER (WHERE embedding IS NOT NULL)::integer AS vectors,
|
|
count(*) FILTER (WHERE searchable)::integer AS searchable
|
|
FROM rag.chunks
|
|
WHERE knowledge_base_id = %s
|
|
AND embedding_profile_hash = %s
|
|
""",
|
|
(namespace.knowledge_base_id, profile_hash),
|
|
).fetchone()
|
|
if counts is None:
|
|
raise SeedContractError("database_count_missing")
|
|
return {key: int(counts[key]) for key in ("chunks", "vectors", "searchable")}
|
|
|
|
|
|
def retrieve(
|
|
settings: Settings,
|
|
query_vector: tuple[float, ...],
|
|
*,
|
|
namespace: DemoNamespace,
|
|
profile_hash: str,
|
|
) -> list[dict[str, Any]]:
|
|
with psycopg.connect(database_dsn(settings), row_factory=dict_row) as connection:
|
|
register_vector(connection)
|
|
connection.execute("SET LOCAL hnsw.iterative_scan = strict_order")
|
|
connection.execute("SET LOCAL hnsw.ef_search = 100")
|
|
rows = connection.execute(
|
|
"""
|
|
SELECT chunk.id, chunk.metadata, chunk.embedding_text,
|
|
1 - (chunk.embedding <=> %s) AS vector_score
|
|
FROM rag.chunks AS chunk
|
|
JOIN rag.knowledge_bases AS knowledge_base
|
|
ON knowledge_base.id = chunk.knowledge_base_id
|
|
AND knowledge_base.active_embedding_profile_hash = %s
|
|
WHERE chunk.searchable
|
|
AND chunk.knowledge_base_id = %s
|
|
AND chunk.access_scope_id = %s
|
|
AND chunk.embedding_profile_hash = %s
|
|
ORDER BY chunk.embedding <=> %s
|
|
LIMIT %s
|
|
""",
|
|
(
|
|
Vector(list(query_vector)),
|
|
profile_hash,
|
|
namespace.knowledge_base_id,
|
|
namespace.access_scope_id,
|
|
profile_hash,
|
|
Vector(list(query_vector)),
|
|
settings.vector_top_k,
|
|
),
|
|
).fetchall()
|
|
return [dict(row) for row in rows]
|
|
|
|
|
|
async def evaluate_queries(
|
|
settings: Settings,
|
|
queries: Sequence[DemoQuery],
|
|
embedder: EmbeddingProvider,
|
|
reranker: Reranker,
|
|
*,
|
|
namespace: DemoNamespace,
|
|
profile_hash: str,
|
|
) -> dict[str, float | int]:
|
|
hits = 0
|
|
answerable = 0
|
|
for query in queries:
|
|
query_result = await embedder.embed_query(query.query)
|
|
candidates = retrieve(
|
|
settings,
|
|
query_result.vectors[0],
|
|
namespace=namespace,
|
|
profile_hash=profile_hash,
|
|
)
|
|
if not candidates:
|
|
continue
|
|
reranked = await reranker.rerank(
|
|
query.query,
|
|
[cast(str, item["embedding_text"]) for item in candidates],
|
|
top_n=min(settings.rerank_top_n, len(candidates)),
|
|
)
|
|
result_doc_ids = [
|
|
cast(dict[str, Any], candidates[item.index]["metadata"])["source_doc_id"]
|
|
for item in reranked.items[:3]
|
|
]
|
|
if query.answerable:
|
|
answerable += 1
|
|
if set(query.expected_doc_ids) & set(result_doc_ids):
|
|
hits += 1
|
|
return {
|
|
"answerable_queries": answerable,
|
|
"hit_at_3": hits,
|
|
"hit_rate_at_3": round(hits / answerable, 4) if answerable else 0.0,
|
|
}
|
|
|
|
|
|
def output_summary(payload: dict[str, Any]) -> None:
|
|
sys.stdout.write(json.dumps(payload, ensure_ascii=False, sort_keys=True) + "\n")
|
|
|
|
|
|
def safe_failure_site(error: BaseException) -> str:
|
|
traceback = error.__traceback__
|
|
selected: str | None = None
|
|
while traceback is not None:
|
|
filename = Path(traceback.tb_frame.f_code.co_filename).name
|
|
if filename == "seed_demo.py":
|
|
selected = f"{filename}:{traceback.tb_frame.f_code.co_name}:{traceback.tb_lineno}"
|
|
traceback = traceback.tb_next
|
|
return selected or "external_dependency"
|
|
|
|
|
|
async def async_main() -> int:
|
|
mode = os.getenv("DEMO_PROVIDER_MODE", "fake").strip().lower()
|
|
if mode not in {"fake", "bailian"}:
|
|
output_summary({"status": "failed", "error_kind": "invalid_provider_mode"})
|
|
return 2
|
|
|
|
settings = Settings()
|
|
namespace = namespace_for_mode(mode)
|
|
documents_path = Path(
|
|
os.getenv("DEMO_DOCUMENTS_PATH", str(DEFAULT_SAMPLE_ROOT / "demo_documents.jsonl"))
|
|
)
|
|
queries_path = Path(
|
|
os.getenv("DEMO_QUERIES_PATH", str(DEFAULT_SAMPLE_ROOT / "demo_queries.jsonl"))
|
|
)
|
|
cloud_gateway: ModelGatewayAdapter | None = None
|
|
try:
|
|
documents = load_documents(documents_path)
|
|
queries = load_queries(queries_path)
|
|
profile_hash = embedding_profile_hash(settings, mode)
|
|
embedder: EmbeddingProvider
|
|
reranker: Reranker
|
|
if mode == "bailian":
|
|
cloud_gateway = ModelGatewayAdapter.from_settings(settings)
|
|
embedder = cloud_gateway
|
|
reranker = cloud_gateway
|
|
else:
|
|
embedder = FakeEmbeddingProvider(settings.embedding_dimension)
|
|
reranker = FakeReranker()
|
|
|
|
texts = [
|
|
f"标题:{item.title}\n地区:{item.region}\n矿种:{item.mineral}\n正文:{item.content}"
|
|
for item in documents
|
|
]
|
|
vectors, resolved_model = await embed_in_batches(embedder, texts)
|
|
prepared = prepare_chunks(
|
|
documents,
|
|
vectors,
|
|
profile_hash=profile_hash,
|
|
embedding_model=resolved_model,
|
|
namespace=namespace,
|
|
)
|
|
counts = write_chunks(settings, prepared, namespace=namespace)
|
|
metrics = await evaluate_queries(
|
|
settings,
|
|
queries,
|
|
embedder,
|
|
reranker,
|
|
namespace=namespace,
|
|
profile_hash=profile_hash,
|
|
)
|
|
output_summary(
|
|
{
|
|
"counts": counts,
|
|
"embedding_model": resolved_model,
|
|
"metrics": metrics,
|
|
"provider_mode": mode,
|
|
"status": "ok",
|
|
}
|
|
)
|
|
return 0
|
|
except ModelProviderError as exc:
|
|
output_summary(
|
|
{
|
|
"status": "failed",
|
|
"error_kind": f"model_provider_{exc.kind.value}",
|
|
"status_code": exc.status_code,
|
|
}
|
|
)
|
|
return 1
|
|
except psycopg.Error as exc:
|
|
constraint_name = exc.diag.constraint_name
|
|
output_summary(
|
|
{
|
|
"status": "failed",
|
|
"error_kind": "database_error",
|
|
"sqlstate": exc.sqlstate,
|
|
"failure_site": safe_failure_site(exc),
|
|
"constraint": constraint_name
|
|
if constraint_name and constraint_name.replace("_", "").isalnum()
|
|
else None,
|
|
}
|
|
)
|
|
return 1
|
|
except SecretFileError:
|
|
output_summary({"status": "failed", "error_kind": "secret_configuration"})
|
|
return 1
|
|
except OSError:
|
|
output_summary({"status": "failed", "error_kind": "fixture_io_error"})
|
|
return 1
|
|
except SeedContractError as exc:
|
|
output_summary({"status": "failed", "error_kind": "seed_contract_error", "code": exc.code})
|
|
return 1
|
|
except ValueError as exc:
|
|
output_summary(
|
|
{
|
|
"status": "failed",
|
|
"error_kind": "fixture_or_contract_error",
|
|
"failure_site": safe_failure_site(exc),
|
|
}
|
|
)
|
|
return 1
|
|
finally:
|
|
if cloud_gateway is not None:
|
|
await cloud_gateway.aclose()
|
|
|
|
|
|
def main() -> None:
|
|
raise SystemExit(asyncio.run(async_main()))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|