Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96445a48c4 | ||
|
|
fc8476bde0 | ||
|
|
ddf348e0a1 |
@@ -171,6 +171,12 @@ class RecallRequest(BaseModel):
|
||||
description="Compound tag filter using boolean groups. Groups in the list are AND-ed. "
|
||||
"Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}.",
|
||||
)
|
||||
retrieval_weights: dict[str, float] | None = Field(
|
||||
default=None,
|
||||
description="Per-strategy weights for Reciprocal Rank Fusion. Keys: 'semantic', 'bm25', "
|
||||
"'graph', 'temporal'. Values are multipliers (1.0 = default, 2.0 = double influence, "
|
||||
"0.0 = disabled). Omitted keys default to the bank/server configuration.",
|
||||
)
|
||||
|
||||
@field_validator("query")
|
||||
@classmethod
|
||||
@@ -3215,6 +3221,7 @@ def _register_routes(app: FastAPI):
|
||||
tags=request.tags,
|
||||
tags_match=request.tags_match,
|
||||
tag_groups=request.tag_groups,
|
||||
retrieval_weights=request.retrieval_weights,
|
||||
)
|
||||
|
||||
# Convert core MemoryFact objects to API RecallResult objects (excluding internal metrics)
|
||||
|
||||
@@ -445,6 +445,10 @@ ENV_RECALL_BUDGET_ADAPTIVE_MID = "HINDSIGHT_API_RECALL_BUDGET_ADAPTIVE_MID"
|
||||
ENV_RECALL_BUDGET_ADAPTIVE_HIGH = "HINDSIGHT_API_RECALL_BUDGET_ADAPTIVE_HIGH"
|
||||
ENV_RECALL_BUDGET_MIN = "HINDSIGHT_API_RECALL_BUDGET_MIN"
|
||||
ENV_RECALL_BUDGET_MAX = "HINDSIGHT_API_RECALL_BUDGET_MAX"
|
||||
ENV_RECALL_WEIGHT_SEMANTIC = "HINDSIGHT_API_RECALL_WEIGHT_SEMANTIC"
|
||||
ENV_RECALL_WEIGHT_BM25 = "HINDSIGHT_API_RECALL_WEIGHT_BM25"
|
||||
ENV_RECALL_WEIGHT_GRAPH = "HINDSIGHT_API_RECALL_WEIGHT_GRAPH"
|
||||
ENV_RECALL_WEIGHT_TEMPORAL = "HINDSIGHT_API_RECALL_WEIGHT_TEMPORAL"
|
||||
|
||||
# Audit log settings
|
||||
ENV_AUDIT_LOG_ENABLED = "HINDSIGHT_API_AUDIT_LOG_ENABLED"
|
||||
@@ -677,6 +681,14 @@ DEFAULT_RECALL_BUDGET_ADAPTIVE_HIGH = 0.25
|
||||
DEFAULT_RECALL_BUDGET_MIN = 20 # Floor for the adaptive function
|
||||
DEFAULT_RECALL_BUDGET_MAX = 2000 # Ceiling for the adaptive function
|
||||
|
||||
# Recall retrieval weights (RRF fusion)
|
||||
# Per-strategy multipliers for Reciprocal Rank Fusion.
|
||||
# 1.0 = default (equal weight), 2.0 = double influence, 0.0 = disabled.
|
||||
DEFAULT_RECALL_WEIGHT_SEMANTIC = 1.0
|
||||
DEFAULT_RECALL_WEIGHT_BM25 = 1.0
|
||||
DEFAULT_RECALL_WEIGHT_GRAPH = 1.0
|
||||
DEFAULT_RECALL_WEIGHT_TEMPORAL = 1.0
|
||||
|
||||
# Disposition defaults (None = not set, fall back to bank DB value or 3)
|
||||
DEFAULT_DISPOSITION_SKEPTICISM = None
|
||||
DEFAULT_DISPOSITION_LITERALISM = None
|
||||
@@ -1122,6 +1134,12 @@ class HindsightConfig:
|
||||
recall_budget_min: int
|
||||
recall_budget_max: int
|
||||
|
||||
# Recall retrieval weights (hierarchical - can be overridden per bank or per request)
|
||||
recall_weight_semantic: float
|
||||
recall_weight_bm25: float
|
||||
recall_weight_graph: float
|
||||
recall_weight_temporal: float
|
||||
|
||||
# Disposition settings (hierarchical - can be overridden per bank; None = fall back to DB)
|
||||
disposition_skepticism: int | None
|
||||
disposition_literalism: int | None
|
||||
@@ -1261,6 +1279,11 @@ class HindsightConfig:
|
||||
"recall_budget_adaptive_high",
|
||||
"recall_budget_min",
|
||||
"recall_budget_max",
|
||||
# Recall retrieval weights
|
||||
"recall_weight_semantic",
|
||||
"recall_weight_bm25",
|
||||
"recall_weight_graph",
|
||||
"recall_weight_temporal",
|
||||
# Disposition settings
|
||||
"disposition_skepticism",
|
||||
"disposition_literalism",
|
||||
@@ -1841,6 +1864,11 @@ class HindsightConfig:
|
||||
),
|
||||
recall_budget_min=int(os.getenv(ENV_RECALL_BUDGET_MIN, str(DEFAULT_RECALL_BUDGET_MIN))),
|
||||
recall_budget_max=int(os.getenv(ENV_RECALL_BUDGET_MAX, str(DEFAULT_RECALL_BUDGET_MAX))),
|
||||
# Recall retrieval weights
|
||||
recall_weight_semantic=float(os.getenv(ENV_RECALL_WEIGHT_SEMANTIC, str(DEFAULT_RECALL_WEIGHT_SEMANTIC))),
|
||||
recall_weight_bm25=float(os.getenv(ENV_RECALL_WEIGHT_BM25, str(DEFAULT_RECALL_WEIGHT_BM25))),
|
||||
recall_weight_graph=float(os.getenv(ENV_RECALL_WEIGHT_GRAPH, str(DEFAULT_RECALL_WEIGHT_GRAPH))),
|
||||
recall_weight_temporal=float(os.getenv(ENV_RECALL_WEIGHT_TEMPORAL, str(DEFAULT_RECALL_WEIGHT_TEMPORAL))),
|
||||
# Disposition settings (None = fall back to DB value)
|
||||
disposition_skepticism=int(os.getenv(ENV_DISPOSITION_SKEPTICISM))
|
||||
if os.getenv(ENV_DISPOSITION_SKEPTICISM)
|
||||
|
||||
@@ -82,6 +82,7 @@ class MemoryEngineInterface(ABC):
|
||||
include_chunks: bool = False,
|
||||
max_chunk_tokens: int = 8192,
|
||||
request_context: "RequestContext",
|
||||
retrieval_weights: dict[str, float] | None = None,
|
||||
) -> "RecallResult":
|
||||
"""
|
||||
Recall memories relevant to a query.
|
||||
|
||||
@@ -2655,6 +2655,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
tag_groups: list[TagGroup] | None = None,
|
||||
created_after: datetime | None = None,
|
||||
created_before: datetime | None = None,
|
||||
retrieval_weights: dict[str, float] | None = None,
|
||||
_connection_budget: int | None = None,
|
||||
_quiet: bool = False,
|
||||
) -> RecallResultModel:
|
||||
@@ -2754,6 +2755,18 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
budget_config_dict = await self._config_resolver.get_bank_config(bank_id, request_context)
|
||||
thinking_budget = _resolve_thinking_budget(budget_config_dict, budget, max_tokens)
|
||||
|
||||
# Build effective retrieval weights: config defaults + per-request overrides
|
||||
effective_weights: dict[str, float] = {
|
||||
"semantic": float(budget_config_dict.get("recall_weight_semantic", 1.0)),
|
||||
"bm25": float(budget_config_dict.get("recall_weight_bm25", 1.0)),
|
||||
"graph": float(budget_config_dict.get("recall_weight_graph", 1.0)),
|
||||
"temporal": float(budget_config_dict.get("recall_weight_temporal", 1.0)),
|
||||
}
|
||||
if retrieval_weights:
|
||||
effective_weights.update(retrieval_weights)
|
||||
# Only pass weights if any differ from default (1.0)
|
||||
rrf_weights = effective_weights if any(w != 1.0 for w in effective_weights.values()) else None
|
||||
|
||||
# Log recall start with tags if present (skip if quiet mode for internal operations)
|
||||
if not _quiet:
|
||||
tags_info = f", tags={tags} ({tags_match})" if tags else ""
|
||||
@@ -2807,6 +2820,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
include_source_facts=include_source_facts,
|
||||
max_source_facts_tokens=max_source_facts_tokens,
|
||||
max_source_facts_tokens_per_observation=max_source_facts_tokens_per_observation,
|
||||
rrf_weights=rrf_weights,
|
||||
)
|
||||
break # Success - exit retry loop
|
||||
except Exception as e:
|
||||
@@ -2938,6 +2952,7 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
include_source_facts: bool = False,
|
||||
max_source_facts_tokens: int = 4096,
|
||||
max_source_facts_tokens_per_observation: int = -1,
|
||||
rrf_weights: dict[str, float] | None = None,
|
||||
) -> RecallResultModel:
|
||||
"""
|
||||
Search implementation with modular retrieval and reranking.
|
||||
@@ -3265,10 +3280,14 @@ class MemoryEngine(MemoryEngineInterface):
|
||||
# Merge 3 or 4 result lists depending on temporal constraint
|
||||
if temporal_results:
|
||||
merged_candidates = reciprocal_rank_fusion(
|
||||
[semantic_results, bm25_results, graph_results, temporal_results]
|
||||
[semantic_results, bm25_results, graph_results, temporal_results],
|
||||
weights=rrf_weights,
|
||||
)
|
||||
else:
|
||||
merged_candidates = reciprocal_rank_fusion([semantic_results, bm25_results, graph_results])
|
||||
merged_candidates = reciprocal_rank_fusion(
|
||||
[semantic_results, bm25_results, graph_results],
|
||||
weights=rrf_weights,
|
||||
)
|
||||
|
||||
step_duration = time.time() - step_start
|
||||
log_buffer.append(
|
||||
|
||||
@@ -7,15 +7,25 @@ from typing import Any
|
||||
from .types import MergedCandidate, RetrievalResult
|
||||
|
||||
|
||||
def reciprocal_rank_fusion(result_lists: list[list[RetrievalResult]], k: int = 60) -> list[MergedCandidate]:
|
||||
def reciprocal_rank_fusion(
|
||||
result_lists: list[list[RetrievalResult]],
|
||||
k: int = 60,
|
||||
weights: dict[str, float] | None = None,
|
||||
) -> list[MergedCandidate]:
|
||||
"""
|
||||
Merge multiple ranked result lists using Reciprocal Rank Fusion.
|
||||
|
||||
RRF formula: score(d) = sum_over_lists(1 / (k + rank(d)))
|
||||
RRF formula: score(d) = sum_over_lists(w_i / (k + rank(d)))
|
||||
|
||||
When weights are provided, each retrieval strategy's contribution is
|
||||
multiplied by its weight. A weight of 2.0 doubles that strategy's
|
||||
influence; 0.0 disables it entirely. Default weight is 1.0 (unweighted).
|
||||
|
||||
Args:
|
||||
result_lists: List of result lists, each containing RetrievalResult objects
|
||||
k: Constant for RRF formula (default: 60)
|
||||
weights: Optional mapping of strategy name to weight, e.g.
|
||||
{"semantic": 1.0, "bm25": 1.0, "graph": 2.0, "temporal": 1.0}
|
||||
|
||||
Returns:
|
||||
Merged list of MergedCandidate objects, sorted by RRF score
|
||||
@@ -25,8 +35,14 @@ def reciprocal_rank_fusion(result_lists: list[list[RetrievalResult]], k: int = 6
|
||||
bm25_results = [RetrievalResult(...), RetrievalResult(...), ...]
|
||||
graph_results = [RetrievalResult(...), RetrievalResult(...), ...]
|
||||
|
||||
# Unweighted (default)
|
||||
merged = reciprocal_rank_fusion([semantic_results, bm25_results, graph_results])
|
||||
# Returns: [MergedCandidate(...), MergedCandidate(...), ...]
|
||||
|
||||
# With graph retrieval weighted 2x
|
||||
merged = reciprocal_rank_fusion(
|
||||
[semantic_results, bm25_results, graph_results],
|
||||
weights={"graph": 2.0},
|
||||
)
|
||||
"""
|
||||
# Track scores from each list
|
||||
rrf_scores = {}
|
||||
@@ -37,6 +53,7 @@ def reciprocal_rank_fusion(result_lists: list[list[RetrievalResult]], k: int = 6
|
||||
|
||||
for source_idx, results in enumerate(result_lists):
|
||||
source_name = source_names[source_idx] if source_idx < len(source_names) else f"source_{source_idx}"
|
||||
weight = (weights or {}).get(source_name, 1.0)
|
||||
|
||||
for rank, retrieval in enumerate(results, start=1):
|
||||
# Type check to catch tuple issues
|
||||
@@ -56,12 +73,12 @@ def reciprocal_rank_fusion(result_lists: list[list[RetrievalResult]], k: int = 6
|
||||
if doc_id not in all_retrievals:
|
||||
all_retrievals[doc_id] = retrieval
|
||||
|
||||
# Calculate RRF score contribution
|
||||
# Calculate weighted RRF score contribution
|
||||
if doc_id not in rrf_scores:
|
||||
rrf_scores[doc_id] = 0.0
|
||||
source_ranks[doc_id] = {}
|
||||
|
||||
rrf_scores[doc_id] += 1.0 / (k + rank)
|
||||
rrf_scores[doc_id] += weight / (k + rank)
|
||||
source_ranks[doc_id][f"{source_name}_rank"] = rank
|
||||
|
||||
# Combine into final results with metadata
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
"""
|
||||
Tests for weighted Reciprocal Rank Fusion.
|
||||
|
||||
Validates that per-strategy weights correctly influence RRF merge ordering.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from hindsight_api.engine.search.fusion import reciprocal_rank_fusion
|
||||
from hindsight_api.engine.search.types import RetrievalResult
|
||||
|
||||
|
||||
def _make_result(id: str, **kwargs) -> RetrievalResult:
|
||||
"""Create a minimal RetrievalResult for testing."""
|
||||
return RetrievalResult(id=id, text=f"text-{id}", fact_type="world", **kwargs)
|
||||
|
||||
|
||||
class TestWeightedRRF:
|
||||
"""Tests for weighted Reciprocal Rank Fusion."""
|
||||
|
||||
def test_unweighted_default(self):
|
||||
"""Without weights, all strategies contribute equally (existing behavior)."""
|
||||
semantic = [_make_result("a"), _make_result("b")]
|
||||
bm25 = [_make_result("b"), _make_result("a")]
|
||||
graph = [_make_result("a"), _make_result("c")]
|
||||
|
||||
merged = reciprocal_rank_fusion([semantic, bm25, graph])
|
||||
|
||||
# "a" appears rank 1 in semantic + graph, rank 2 in bm25 → highest score
|
||||
assert merged[0].id == "a"
|
||||
# All candidates present
|
||||
assert {m.id for m in merged} == {"a", "b", "c"}
|
||||
|
||||
def test_weights_none_same_as_unweighted(self):
|
||||
"""Passing weights=None produces identical results to no weights."""
|
||||
semantic = [_make_result("a"), _make_result("b")]
|
||||
bm25 = [_make_result("b"), _make_result("a")]
|
||||
graph = [_make_result("c")]
|
||||
|
||||
merged_default = reciprocal_rank_fusion([semantic, bm25, graph])
|
||||
merged_none = reciprocal_rank_fusion([semantic, bm25, graph], weights=None)
|
||||
|
||||
assert [m.id for m in merged_default] == [m.id for m in merged_none]
|
||||
for d, n in zip(merged_default, merged_none):
|
||||
assert d.rrf_score == pytest.approx(n.rrf_score)
|
||||
|
||||
def test_all_weights_one_same_as_unweighted(self):
|
||||
"""Explicit weights of 1.0 produce identical results."""
|
||||
semantic = [_make_result("a"), _make_result("b")]
|
||||
bm25 = [_make_result("b"), _make_result("a")]
|
||||
graph = [_make_result("c")]
|
||||
|
||||
merged_default = reciprocal_rank_fusion([semantic, bm25, graph])
|
||||
merged_ones = reciprocal_rank_fusion(
|
||||
[semantic, bm25, graph],
|
||||
weights={"semantic": 1.0, "bm25": 1.0, "graph": 1.0},
|
||||
)
|
||||
|
||||
assert [m.id for m in merged_default] == [m.id for m in merged_ones]
|
||||
|
||||
def test_high_graph_weight_boosts_graph_results(self):
|
||||
"""A high graph weight should boost items that rank well in graph retrieval."""
|
||||
# "a" is rank 1 in semantic only
|
||||
# "b" is rank 1 in graph only
|
||||
semantic = [_make_result("a")]
|
||||
bm25 = []
|
||||
graph = [_make_result("b")]
|
||||
|
||||
# Without weights: "a" and "b" tie (both rank 1 in one list)
|
||||
merged_equal = reciprocal_rank_fusion([semantic, bm25, graph])
|
||||
scores_equal = {m.id: m.rrf_score for m in merged_equal}
|
||||
assert scores_equal["a"] == pytest.approx(scores_equal["b"])
|
||||
|
||||
# With graph weight 3.0: "b" should score higher
|
||||
merged_weighted = reciprocal_rank_fusion(
|
||||
[semantic, bm25, graph],
|
||||
weights={"graph": 3.0},
|
||||
)
|
||||
scores_weighted = {m.id: m.rrf_score for m in merged_weighted}
|
||||
assert scores_weighted["b"] > scores_weighted["a"]
|
||||
assert merged_weighted[0].id == "b"
|
||||
|
||||
def test_zero_weight_disables_strategy(self):
|
||||
"""A weight of 0.0 should completely disable a strategy's contribution."""
|
||||
# "a" is only in semantic, "b" is only in graph
|
||||
semantic = [_make_result("a")]
|
||||
bm25 = []
|
||||
graph = [_make_result("b")]
|
||||
|
||||
merged = reciprocal_rank_fusion(
|
||||
[semantic, bm25, graph],
|
||||
weights={"semantic": 0.0},
|
||||
)
|
||||
|
||||
scores = {m.id: m.rrf_score for m in merged}
|
||||
assert scores["a"] == 0.0 # semantic disabled
|
||||
assert scores["b"] > 0.0 # graph still contributes
|
||||
assert merged[0].id == "b"
|
||||
|
||||
def test_partial_weights_default_to_one(self):
|
||||
"""Omitted strategy keys default to weight 1.0."""
|
||||
semantic = [_make_result("a")]
|
||||
bm25 = [_make_result("b")]
|
||||
graph = [_make_result("c")]
|
||||
|
||||
# Only specify graph weight, others should be 1.0
|
||||
merged = reciprocal_rank_fusion(
|
||||
[semantic, bm25, graph],
|
||||
weights={"graph": 2.0},
|
||||
)
|
||||
|
||||
scores = {m.id: m.rrf_score for m in merged}
|
||||
# "a" (semantic, w=1.0) and "b" (bm25, w=1.0) should have equal scores
|
||||
assert scores["a"] == pytest.approx(scores["b"])
|
||||
# "c" (graph, w=2.0) should have double the score
|
||||
assert scores["c"] == pytest.approx(scores["a"] * 2.0)
|
||||
|
||||
def test_weights_with_temporal(self):
|
||||
"""Weights work correctly with 4 retrieval strategies including temporal."""
|
||||
semantic = [_make_result("a")]
|
||||
bm25 = [_make_result("b")]
|
||||
graph = [_make_result("c")]
|
||||
temporal = [_make_result("d")]
|
||||
|
||||
merged = reciprocal_rank_fusion(
|
||||
[semantic, bm25, graph, temporal],
|
||||
weights={"temporal": 5.0},
|
||||
)
|
||||
|
||||
scores = {m.id: m.rrf_score for m in merged}
|
||||
# "d" (temporal, w=5.0) should have highest score
|
||||
assert merged[0].id == "d"
|
||||
assert scores["d"] == pytest.approx(scores["a"] * 5.0)
|
||||
|
||||
def test_weight_changes_ranking_order(self):
|
||||
"""Demonstrate that weights can reverse the ranking of two items."""
|
||||
# Both "a" and "b" appear in semantic and graph, but in different positions
|
||||
# Semantic: a=1, b=2 → Graph: b=1, a=2
|
||||
semantic = [_make_result("a"), _make_result("b")]
|
||||
bm25 = []
|
||||
graph = [_make_result("b"), _make_result("a")]
|
||||
|
||||
# Unweighted: tied (both appear at rank 1 and rank 2, once each)
|
||||
merged_equal = reciprocal_rank_fusion([semantic, bm25, graph])
|
||||
scores_equal = {m.id: m.rrf_score for m in merged_equal}
|
||||
assert scores_equal["a"] == pytest.approx(scores_equal["b"])
|
||||
|
||||
# Weight graph 3x: "b" wins because it's rank 1 in the heavier strategy
|
||||
merged_graph_heavy = reciprocal_rank_fusion(
|
||||
[semantic, bm25, graph],
|
||||
weights={"graph": 3.0},
|
||||
)
|
||||
assert merged_graph_heavy[0].id == "b"
|
||||
|
||||
# Weight semantic 3x: "a" wins
|
||||
merged_semantic_heavy = reciprocal_rank_fusion(
|
||||
[semantic, bm25, graph],
|
||||
weights={"semantic": 3.0},
|
||||
)
|
||||
assert merged_semantic_heavy[0].id == "a"
|
||||
|
||||
def test_source_ranks_preserved_with_weights(self):
|
||||
"""Source ranks should be unaffected by weights — only scores change."""
|
||||
semantic = [_make_result("a"), _make_result("b")]
|
||||
bm25 = [_make_result("b")]
|
||||
graph = [_make_result("a")]
|
||||
|
||||
merged = reciprocal_rank_fusion(
|
||||
[semantic, bm25, graph],
|
||||
weights={"graph": 10.0},
|
||||
)
|
||||
|
||||
ranks = {m.id: m.source_ranks for m in merged}
|
||||
assert ranks["a"]["semantic_rank"] == 1
|
||||
assert ranks["a"]["graph_rank"] == 1
|
||||
assert ranks["b"]["semantic_rank"] == 2
|
||||
assert ranks["b"]["bm25_rank"] == 1
|
||||
|
||||
def test_rrf_rank_reflects_weighted_order(self):
|
||||
"""rrf_rank should reflect the weighted score ordering."""
|
||||
semantic = [_make_result("a")]
|
||||
bm25 = []
|
||||
graph = [_make_result("b")]
|
||||
|
||||
merged = reciprocal_rank_fusion(
|
||||
[semantic, bm25, graph],
|
||||
weights={"graph": 2.0},
|
||||
)
|
||||
|
||||
rank_map = {m.id: m.rrf_rank for m in merged}
|
||||
assert rank_map["b"] == 1 # graph-boosted item ranks first
|
||||
assert rank_map["a"] == 2
|
||||
@@ -5880,6 +5880,16 @@ components:
|
||||
$ref: '#/components/schemas/MentalModelTrigger_Input_tag_groups_inner'
|
||||
nullable: true
|
||||
type: array
|
||||
retrieval_weights:
|
||||
additionalProperties:
|
||||
type: number
|
||||
description: "Per-strategy weights for Reciprocal Rank Fusion. Keys: 'semantic',\
|
||||
\ 'bm25', 'graph', 'temporal'. Values are multipliers (1.0 = default, 2.0\
|
||||
\ = double influence, 0.0 = disabled). Omitted keys default to the bank/server\
|
||||
\ configuration."
|
||||
nullable: true
|
||||
title: Retrieval Weights
|
||||
type: object
|
||||
required:
|
||||
- query
|
||||
title: RecallRequest
|
||||
|
||||
@@ -33,6 +33,8 @@ type RecallRequest struct {
|
||||
// How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).
|
||||
TagsMatch *string `json:"tags_match,omitempty"`
|
||||
TagGroups []MentalModelTriggerInputTagGroupsInner `json:"tag_groups,omitempty"`
|
||||
// Per-strategy weights for Reciprocal Rank Fusion. Keys: 'semantic', 'bm25', 'graph', 'temporal'. Values are multipliers (1.0 = default, 2.0 = double influence, 0.0 = disabled). Omitted keys default to the bank/server configuration.
|
||||
RetrievalWeights map[string]float32 `json:"retrieval_weights,omitempty"`
|
||||
}
|
||||
|
||||
type _RecallRequest RecallRequest
|
||||
@@ -392,6 +394,39 @@ func (o *RecallRequest) SetTagGroups(v []MentalModelTriggerInputTagGroupsInner)
|
||||
o.TagGroups = v
|
||||
}
|
||||
|
||||
// GetRetrievalWeights returns the RetrievalWeights field value if set, zero value otherwise (both if not set or set to explicit null).
|
||||
func (o *RecallRequest) GetRetrievalWeights() map[string]float32 {
|
||||
if o == nil {
|
||||
var ret map[string]float32
|
||||
return ret
|
||||
}
|
||||
return o.RetrievalWeights
|
||||
}
|
||||
|
||||
// GetRetrievalWeightsOk returns a tuple with the RetrievalWeights field value if set, nil otherwise
|
||||
// and a boolean to check if the value has been set.
|
||||
// NOTE: If the value is an explicit nil, `nil, true` will be returned
|
||||
func (o *RecallRequest) GetRetrievalWeightsOk() (*map[string]float32, bool) {
|
||||
if o == nil || IsNil(o.RetrievalWeights) {
|
||||
return nil, false
|
||||
}
|
||||
return &o.RetrievalWeights, true
|
||||
}
|
||||
|
||||
// HasRetrievalWeights returns a boolean if a field has been set.
|
||||
func (o *RecallRequest) HasRetrievalWeights() bool {
|
||||
if o != nil && !IsNil(o.RetrievalWeights) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SetRetrievalWeights gets a reference to the given map[string]float32 and assigns it to the RetrievalWeights field.
|
||||
func (o *RecallRequest) SetRetrievalWeights(v map[string]float32) {
|
||||
o.RetrievalWeights = v
|
||||
}
|
||||
|
||||
func (o RecallRequest) MarshalJSON() ([]byte, error) {
|
||||
toSerialize,err := o.ToMap()
|
||||
if err != nil {
|
||||
@@ -430,6 +465,9 @@ func (o RecallRequest) ToMap() (map[string]interface{}, error) {
|
||||
if o.TagGroups != nil {
|
||||
toSerialize["tag_groups"] = o.TagGroups
|
||||
}
|
||||
if o.RetrievalWeights != nil {
|
||||
toSerialize["retrieval_weights"] = o.RetrievalWeights
|
||||
}
|
||||
return toSerialize, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ import pprint
|
||||
import re # noqa: F401
|
||||
import json
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictInt, StrictStr, field_validator
|
||||
from typing import Any, ClassVar, Dict, List, Optional
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, StrictStr, field_validator
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Union
|
||||
from hindsight_client_api.models.budget import Budget
|
||||
from hindsight_client_api.models.include_options import IncludeOptions
|
||||
from hindsight_client_api.models.mental_model_trigger_input_tag_groups_inner import MentalModelTriggerInputTagGroupsInner
|
||||
@@ -39,7 +39,8 @@ class RecallRequest(BaseModel):
|
||||
tags: Optional[List[StrictStr]] = None
|
||||
tags_match: Optional[StrictStr] = Field(default='any', description="How to match tags: 'any' (OR, includes untagged), 'all' (AND, includes untagged), 'any_strict' (OR, excludes untagged), 'all_strict' (AND, excludes untagged).")
|
||||
tag_groups: Optional[List[MentalModelTriggerInputTagGroupsInner]] = None
|
||||
__properties: ClassVar[List[str]] = ["query", "types", "budget", "max_tokens", "trace", "query_timestamp", "include", "tags", "tags_match", "tag_groups"]
|
||||
retrieval_weights: Optional[Dict[str, Union[StrictFloat, StrictInt]]] = Field(default=None, description="Per-strategy weights for Reciprocal Rank Fusion. Keys: 'semantic', 'bm25', 'graph', 'temporal'. Values are multipliers (1.0 = default, 2.0 = double influence, 0.0 = disabled). Omitted keys default to the bank/server configuration.")
|
||||
__properties: ClassVar[List[str]] = ["query", "types", "budget", "max_tokens", "trace", "query_timestamp", "include", "tags", "tags_match", "tag_groups", "retrieval_weights"]
|
||||
|
||||
@field_validator('tags_match')
|
||||
def tags_match_validate_enum(cls, value):
|
||||
@@ -120,6 +121,11 @@ class RecallRequest(BaseModel):
|
||||
if self.tag_groups is None and "tag_groups" in self.model_fields_set:
|
||||
_dict['tag_groups'] = None
|
||||
|
||||
# set to None if retrieval_weights (nullable) is None
|
||||
# and model_fields_set contains the field
|
||||
if self.retrieval_weights is None and "retrieval_weights" in self.model_fields_set:
|
||||
_dict['retrieval_weights'] = None
|
||||
|
||||
return _dict
|
||||
|
||||
@classmethod
|
||||
@@ -141,7 +147,8 @@ class RecallRequest(BaseModel):
|
||||
"include": IncludeOptions.from_dict(obj["include"]) if obj.get("include") is not None else None,
|
||||
"tags": obj.get("tags"),
|
||||
"tags_match": obj.get("tags_match") if obj.get("tags_match") is not None else 'any',
|
||||
"tag_groups": [MentalModelTriggerInputTagGroupsInner.from_dict(_item) for _item in obj["tag_groups"]] if obj.get("tag_groups") is not None else None
|
||||
"tag_groups": [MentalModelTriggerInputTagGroupsInner.from_dict(_item) for _item in obj["tag_groups"]] if obj.get("tag_groups") is not None else None,
|
||||
"retrieval_weights": obj.get("retrieval_weights")
|
||||
})
|
||||
return _obj
|
||||
|
||||
|
||||
@@ -68,11 +68,9 @@ export const createClient = (config: Config = {}): Client => {
|
||||
const request: Client["request"] = async (options) => {
|
||||
// @ts-expect-error
|
||||
const { opts, url } = await beforeRequest(options);
|
||||
// Exclude hey-api internal fields that conflict with Deno's RequestInit.client
|
||||
const { client: _client, ...optsForRequest } = opts as typeof opts & { client?: unknown };
|
||||
const requestInit: ReqInit = {
|
||||
redirect: "follow",
|
||||
...optsForRequest,
|
||||
...opts,
|
||||
body: getValidRequestBody(opts),
|
||||
};
|
||||
|
||||
|
||||
@@ -2340,6 +2340,14 @@ export type RecallRequest = {
|
||||
* Compound tag filter using boolean groups. Groups in the list are AND-ed. Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}.
|
||||
*/
|
||||
tag_groups?: Array<TagGroupLeaf | TagGroupAndInput | TagGroupOrInput | TagGroupNotInput> | null;
|
||||
/**
|
||||
* Retrieval Weights
|
||||
*
|
||||
* Per-strategy weights for Reciprocal Rank Fusion. Keys: 'semantic', 'bm25', 'graph', 'temporal'. Values are multipliers (1.0 = default, 2.0 = double influence, 0.0 = disabled). Omitted keys default to the bank/server configuration.
|
||||
*/
|
||||
retrieval_weights?: {
|
||||
[key: string]: number;
|
||||
} | null;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,6 +16,7 @@ export async function POST(request: NextRequest) {
|
||||
query_timestamp,
|
||||
tags,
|
||||
tags_match,
|
||||
retrieval_weights,
|
||||
} = body;
|
||||
|
||||
const response = await sdk.recallMemories({
|
||||
@@ -31,6 +32,7 @@ export async function POST(request: NextRequest) {
|
||||
query_timestamp,
|
||||
tags,
|
||||
tags_match,
|
||||
retrieval_weights,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -252,6 +252,12 @@ export class ControlPlaneClient {
|
||||
query_timestamp?: string;
|
||||
tags?: string[];
|
||||
tags_match?: "any" | "all" | "any_strict" | "all_strict";
|
||||
retrieval_weights?: {
|
||||
semantic?: number;
|
||||
bm25?: number;
|
||||
graph?: number;
|
||||
temporal?: number;
|
||||
};
|
||||
}) {
|
||||
return this.fetchApi("/api/recall", {
|
||||
method: "POST",
|
||||
|
||||
@@ -8993,6 +8993,21 @@
|
||||
],
|
||||
"title": "Tag Groups",
|
||||
"description": "Compound tag filter using boolean groups. Groups in the list are AND-ed. Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}."
|
||||
},
|
||||
"retrieval_weights": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Retrieval Weights",
|
||||
"description": "Per-strategy weights for Reciprocal Rank Fusion. Keys: 'semantic', 'bm25', 'graph', 'temporal'. Values are multipliers (1.0 = default, 2.0 = double influence, 0.0 = disabled). Omitted keys default to the bank/server configuration."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
||||
@@ -8993,6 +8993,21 @@
|
||||
],
|
||||
"title": "Tag Groups",
|
||||
"description": "Compound tag filter using boolean groups. Groups in the list are AND-ed. Each group is a leaf {tags, match} or compound {and: [...]}, {or: [...]}, {not: ...}."
|
||||
},
|
||||
"retrieval_weights": {
|
||||
"anyOf": [
|
||||
{
|
||||
"additionalProperties": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Retrieval Weights",
|
||||
"description": "Per-strategy weights for Reciprocal Rank Fusion. Keys: 'semantic', 'bm25', 'graph', 'temporal'. Values are multipliers (1.0 = default, 2.0 = double influence, 0.0 = disabled). Omitted keys default to the bank/server configuration."
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
||||
Reference in New Issue
Block a user