"""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)