| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- from __future__ import annotations
- from collections.abc import Mapping, Sequence
- from typing import Any, Protocol
- from sqlalchemy import text
- from app.core.knowledge.access import KnowledgeAccessContext
- from app.core.knowledge.retrieval.contracts import KnowledgeEvidence
- class QueryEmbeddingProvider(Protocol):
- def embed(self, texts: list[str]) -> list[list[float]]: ...
- def _vector(value: Sequence[float]) -> str:
- return "[" + ",".join(str(float(item)) for item in value) + "]"
- def _as_tuple(value: Any) -> tuple:
- if value is None:
- return ()
- if isinstance(value, (list, tuple)):
- return tuple(value)
- return tuple(value)
- def _evidence(row: Mapping[str, Any], retriever: str) -> KnowledgeEvidence:
- updated_at = row["source_updated_at"]
- return KnowledgeEvidence(
- chunk_id=str(row["chunk_id"]),
- content=row["content"],
- score=float(row["score"]),
- retriever=retriever,
- object_uid=str(row["object_uid"]),
- object_type=row["object_type"],
- object_version=int(row["object_version"]),
- business_domain_uid=(
- str(row["business_domain_uid"])
- if row["business_domain_uid"] is not None
- else None
- ),
- point_keys=tuple(str(value) for value in _as_tuple(row["point_keys"])),
- point_revisions=tuple(
- int(value) for value in _as_tuple(row["point_revisions"])
- ),
- generation=int(row["generation"]),
- source_updated_at=updated_at.isoformat() if updated_at else None,
- freshness_status=row["freshness_status"],
- section_path=row["section_path"],
- )
- _SELECT = """
- SELECT c.id AS chunk_id, c.content, c.section_path,
- d.object_uid, d.object_type, d.object_version,
- d.business_domain_uid, d.active_generation AS generation,
- d.source_updated_at, c.point_keys,
- COALESCE(points.point_revisions, ARRAY[]::bigint[]) AS point_revisions,
- CASE WHEN pending.source_revision > d.object_version
- THEN 'updating' ELSE 'fresh' END AS freshness_status,
- {score} AS score
- FROM public.governance_chunks c
- JOIN public.governance_documents d ON d.id = c.document_id
- LEFT JOIN LATERAL (
- SELECT array_agg(p.point_revision ORDER BY keys.ordinality) AS point_revisions
- FROM jsonb_array_elements_text(c.point_keys) WITH ORDINALITY AS keys(point_key, ordinality)
- JOIN public.knowledge_points p
- ON p.point_key = keys.point_key AND p.status = 'active'
- ) points ON TRUE
- LEFT JOIN LATERAL (
- SELECT MAX(cs.source_revision) AS source_revision
- FROM public.knowledge_change_sets cs
- WHERE cs.source_uid = d.object_uid
- AND cs.status NOT IN ('canonical_active','complete','failed','rolled_back')
- ) pending ON TRUE
- {embedding_join}
- WHERE d.status = 'active'
- AND d.active_generation IS NOT NULL
- AND (:global_access OR d.business_domain_uid = ANY(CAST(:domains AS uuid[])))
- {predicate}
- """
- class SqlLexicalRetriever:
- def __init__(self, session) -> None:
- self._session = session
- def retrieve(
- self, query: str, context: KnowledgeAccessContext, limit: int
- ) -> tuple[KnowledgeEvidence, ...]:
- score = """
- GREATEST(
- CASE WHEN lower(c.lexical_text) = lower(:query) THEN 1.0 ELSE 0.0 END,
- CASE WHEN lower(c.lexical_text) LIKE lower(:query) || '%' THEN 0.9 ELSE 0.0 END,
- similarity(COALESCE(c.lexical_text, ''), :query)
- )
- """
- statement = text(
- _SELECT.format(
- score=score,
- embedding_join="",
- predicate="""
- AND (
- lower(c.lexical_text) = lower(:query)
- OR lower(c.lexical_text) LIKE lower(:query) || '%'
- OR c.lexical_text % :query
- OR c.search_vector @@ plainto_tsquery('simple', :query)
- )
- ORDER BY score DESC, c.id
- LIMIT :limit
- """,
- )
- )
- rows = self._session.execute(
- statement,
- {
- "query": query.strip(),
- "global_access": context.global_access,
- "domains": list(context.business_domain_uids),
- "limit": limit,
- },
- ).mappings()
- return tuple(_evidence(row, "lexical") for row in rows)
- class SqlVectorRetriever:
- def __init__(self, session, embedder: QueryEmbeddingProvider) -> None:
- self._session = session
- self._embedder = embedder
- def retrieve(
- self, query: str, context: KnowledgeAccessContext, limit: int
- ) -> tuple[KnowledgeEvidence, ...]:
- vectors = self._embedder.embed([query])
- if len(vectors) != 1:
- raise RuntimeError("query embedder returned an unexpected vector count")
- statement = text(
- _SELECT.format(
- score="1 - (e.embedding <=> CAST(:query_embedding AS vector))",
- embedding_join="""
- JOIN public.knowledge_chunk_embeddings e ON e.chunk_id = c.id
- JOIN public.knowledge_embedding_profiles profile
- ON profile.id = e.profile_id AND profile.status = 'active'
- """,
- predicate="""
- ORDER BY e.embedding <=> CAST(:query_embedding AS vector), c.id
- LIMIT :limit
- """,
- )
- )
- rows = self._session.execute(
- statement,
- {
- "query_embedding": _vector(vectors[0]),
- "global_access": context.global_access,
- "domains": list(context.business_domain_uids),
- "limit": limit,
- },
- ).mappings()
- return tuple(_evidence(row, "vector") for row in rows)
- class UnavailableVectorRetriever:
- def __init__(self, reason: str = "query embedding is not configured") -> None:
- self._reason = reason
- def retrieve(self, _query, _context, _limit):
- raise RuntimeError(self._reason)
|