Compare commits

...
Author SHA1 Message Date
Derek Bouius 96445a48c4 fix: regenerate OpenAPI spec and client SDKs for retrieval_weights
Add retrieval_weights field to generated Go, Python, and TypeScript
clients to match the new recall API parameter.
2026-05-14 14:45:29 -04:00
Derek Bouius fc8476bde0 fix: resolve NameError in weighted RRF by moving weight resolution to recall_async
The weight resolution logic referenced budget_config_dict which only
exists in recall_async scope, not in _search_with_retries. Move the
resolution to recall_async and pass the computed rrf_weights through.
2026-05-14 11:47:38 -04:00
Derek Bouius ddf348e0a1 feat: add weighted Reciprocal Rank Fusion for recall
Add per-strategy weights to RRF fusion, allowing tuning of how much
each retrieval method (semantic, BM25, graph, temporal) influences
the merged ranking.

Weights follow the hierarchical config model: server defaults via env
vars (HINDSIGHT_API_RECALL_WEIGHT_*), overridable per-bank via config
API, and per-request via the retrieval_weights field on recall.

Default weights are all 1.0, preserving existing behavior exactly.
2026-05-14 11:21:02 -04:00
15 changed files with 377 additions and 14 deletions
@@ -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
+10
View File
@@ -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,
},
});
+6
View File
@@ -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",
+15
View File
@@ -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",