- A: gate store con cross-encoder — guardrail.decide async, conferma/scarta quasi-duplicati (CROSS_DUP_CONFIRMED/WEAK/LOW_COSINE), suggerimento supersedes in WARN, degrada a cosine-only se il reranker è giù - B: verifica supersede — cross-score (nuovo,vecchio) sotto soglia → supersede_warning non bloccante + audit - C: score composito in search — rerank + importance (nuovo campo payload) + recency decay (180gg) + authority, pesi SCORE_W_* da env - E: multi-query — SearchIn.queries (max 3), pool unito con dedup, rerank unico; endpoint POST /v1/score come primitiva cross-encoder (F-lite) - extension search.ts: param queries + rerank_score/composite in output - D: scripts/consolidate.py — dedup periodico a coppie via cross-encoder con report ntfy e --apply via gateway - test: 69 pass (+11 strategie); guardrail_version similarity-v2
97 lines
3.7 KiB
Python
97 lines
3.7 KiB
Python
"""Operazioni Qdrant condivise dalle route."""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from typing import Any, Optional
|
|
|
|
from qdrant_client.http import models as qm
|
|
|
|
from config import SPARSE_VECTOR_NAME
|
|
from models import SearchIn
|
|
|
|
|
|
def search_filter(body: SearchIn) -> qm.Filter | None:
|
|
must: list[Any] = []
|
|
for key in ("kind", "project_id", "scope", "parent_id", "level", "topic"):
|
|
value = getattr(body, key)
|
|
if value:
|
|
must.append(qm.FieldCondition(key=key, match=qm.MatchValue(value=value)))
|
|
if not body.include_superseded:
|
|
must.append(qm.IsEmptyCondition(is_empty=qm.PayloadField(key="superseded_by")))
|
|
must_not: list[Any] = []
|
|
if not body.include_private:
|
|
# default: esclude i record riservati (private=true) dalle ricerche standard
|
|
must_not.append(qm.FieldCondition(key="private", match=qm.MatchValue(value=True)))
|
|
if must or must_not:
|
|
return qm.Filter(must=must or None, must_not=must_not or None)
|
|
return None
|
|
|
|
|
|
def search(qdrant: Any, collection: str, body: SearchIn, vector: list[float], sparse: Any, limit: Optional[int] = None) -> list[Any]:
|
|
"""Ricerca ibrida o densa. Con reranking attivo limit > top_k per dare candidati extra allo stadio di rerank."""
|
|
eff_limit = limit if limit is not None else body.top_k
|
|
qfilter = search_filter(body)
|
|
if body.hybrid and sparse is not None:
|
|
return qdrant.query_points(
|
|
collection_name=collection,
|
|
prefetch=[
|
|
qm.Prefetch(query=vector, using="", limit=max(body.top_k * 4, eff_limit), score_threshold=body.min_score),
|
|
qm.Prefetch(query=sparse, using=SPARSE_VECTOR_NAME, limit=max(body.top_k * 4, eff_limit)),
|
|
],
|
|
query=qm.FusionQuery(fusion=qm.Fusion.RRF),
|
|
query_filter=qfilter,
|
|
limit=eff_limit,
|
|
with_payload=True,
|
|
).points
|
|
return qdrant.query_points(
|
|
collection_name=collection,
|
|
query=vector,
|
|
query_filter=qfilter,
|
|
limit=eff_limit,
|
|
score_threshold=body.min_score,
|
|
with_payload=True,
|
|
).points
|
|
|
|
|
|
def format_results(hits: list[Any]) -> list[dict]:
|
|
return [
|
|
{
|
|
"memory_id": h.id,
|
|
"score": round(h.score, 4),
|
|
"text": h.payload.get("text"),
|
|
"kind": h.payload.get("kind"),
|
|
"agent_id": h.payload.get("agent_id"),
|
|
"scope": h.payload.get("scope"),
|
|
"project_id": h.payload.get("project_id"),
|
|
"confidence": h.payload.get("confidence"),
|
|
"importance": h.payload.get("importance", 0.5),
|
|
"created_at": h.payload.get("created_at"),
|
|
"source": h.payload.get("source"),
|
|
"supersedes_id": h.payload.get("supersedes_id"),
|
|
"superseded_by": h.payload.get("superseded_by"),
|
|
"supersede_reason": h.payload.get("supersede_reason"),
|
|
"parent_id": h.payload.get("parent_id"),
|
|
"level": h.payload.get("level"),
|
|
"topic": h.payload.get("topic"),
|
|
"private": h.payload.get("private", False),
|
|
"links": h.payload.get("links"),
|
|
}
|
|
for h in hits
|
|
]
|
|
|
|
|
|
def reparent_active_children(qdrant: Any, collection: str, old_id: str, new_id: str) -> int:
|
|
children, _ = qdrant.scroll(
|
|
collection_name=collection,
|
|
scroll_filter=qm.Filter(must=[
|
|
qm.FieldCondition(key="parent_id", match=qm.MatchValue(value=old_id)),
|
|
qm.IsEmptyCondition(is_empty=qm.PayloadField(key="superseded_by")),
|
|
]),
|
|
limit=1000,
|
|
with_payload=False,
|
|
)
|
|
if not children:
|
|
return 0
|
|
qdrant.set_payload(collection_name=collection, payload={"parent_id": new_id}, points=[p.id for p in children])
|
|
return len(children)
|