Files
wehub-resource-sync 555e282cc4
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:45 +08:00

140 lines
4.3 KiB
Python

"""
Scoring utilities for hybrid retrieval.
Provides:
- **BM25 normalization**: Sigmoid normalization of raw BM25 scores to [0, 1].
- **BM25 parameter selection**: Query-length-adaptive sigmoid parameters.
- **Additive scoring**: Combined scoring with semantic + BM25 + entity boost.
"""
from __future__ import annotations
import math
from typing import Any, Dict, List, Optional
def get_bm25_params(query: str, *, lemmatized: Optional[str] = None) -> tuple:
"""Get BM25 sigmoid parameters based on query length.
Longer queries tend to have higher raw BM25 scores, so we adjust
the sigmoid midpoint and steepness accordingly.
Returns:
(midpoint, steepness) for sigmoid normalization.
"""
if lemmatized is None:
from mem0.utils.lemmatization import lemmatize_for_bm25
lemmatized = lemmatize_for_bm25(query)
num_terms = len(lemmatized.split()) if lemmatized else 1
if num_terms <= 3:
return 5.0, 0.7
elif num_terms <= 6:
return 7.0, 0.6
elif num_terms <= 9:
return 9.0, 0.5
elif num_terms <= 15:
return 10.0, 0.5
else:
return 12.0, 0.5
def normalize_bm25(raw_score: float, midpoint: float, steepness: float) -> float:
"""Normalize BM25 score to [0, 1] using logistic sigmoid.
Args:
raw_score: Raw BM25 score (unbounded, typically 0-20+).
midpoint: Score at which sigmoid outputs 0.5.
steepness: Controls how quickly sigmoid transitions.
Returns:
Normalized score in range [0, 1].
"""
return 1.0 / (1.0 + math.exp(-steepness * (raw_score - midpoint)))
ENTITY_BOOST_WEIGHT = 0.5
def score_and_rank(
semantic_results: List[Dict[str, Any]],
bm25_scores: Dict[str, float],
entity_boosts: Dict[str, float],
threshold: float,
top_k: int,
explain: bool = False,
) -> List[Dict[str, Any]]:
"""Score candidates additively and return top-k results.
For each candidate:
semantic_score is taken from the result's score field.
combined = (semantic + bm25 + entity_boost) / max_possible
Threshold gates the semantic score BEFORE combining -- candidates
below the threshold are excluded even if BM25/entity would boost them.
The divisor adapts based on which signals are active:
- Semantic only: max_possible = 1.0
- Semantic + BM25: max_possible = 2.0
- Semantic + BM25 + entity: max_possible = 2.5
- Semantic + entity (no BM25): max_possible = 1.5
Args:
semantic_results: Candidate memories from vector search.
bm25_scores: Normalized keyword scores keyed by memory ID.
entity_boosts: Entity-link boosts keyed by memory ID.
threshold: Minimum semantic score required before hybrid scoring.
top_k: Maximum number of results to return.
explain: Include score_details in each result when true.
Returns:
List of scored result dicts sorted by combined score descending.
"""
has_bm25 = bool(bm25_scores)
has_entity = bool(entity_boosts)
max_possible = 1.0
if has_bm25:
max_possible += 1.0
if has_entity:
max_possible += ENTITY_BOOST_WEIGHT
scored: List[Dict[str, Any]] = []
for result in semantic_results:
mem_id = result.get("id")
if mem_id is None:
continue
semantic_score = result.get("score") or 0.0
if semantic_score < threshold:
continue
mem_id_str = str(mem_id)
bm25_score = bm25_scores.get(mem_id_str, 0.0)
entity_boost = entity_boosts.get(mem_id_str, 0.0)
raw_combined = semantic_score + bm25_score + entity_boost
combined = min(raw_combined / max_possible, 1.0)
scored_result = {
"id": mem_id_str,
"score": combined,
"payload": result.get("payload"),
}
if explain:
scored_result["score_details"] = {
"semantic_score": semantic_score,
"bm25_score": bm25_score,
"entity_boost": entity_boost,
"raw_score": raw_combined,
"max_possible_score": max_possible,
"final_score": combined,
"threshold": threshold,
}
scored.append(scored_result)
scored.sort(key=lambda x: x["score"], reverse=True)
return scored[:top_k]