Skip to main content

systemprompt_models/brain/declarations/
importance.rs

1//! `importance.yaml` and `retrieval.yaml`: the scoring formula's weights
2//! and the retrieval tiers' fusion weights, quotas and the fill policy.
3//!
4//! The fill knobs: `fill_order` (`rank`: fused rank
5//! first, importance second; `importance`: the previous order);
6//! `linked_share` of `max_facts` reserved for one-hop link hits when a
7//! subject is named (0 reserves nothing); `min_fused_score` below which a
8//! fact is withheld as `low_score` (0 withholds nothing); `context_share`
9//! of `max_facts` that facts found only by the context tier may take when
10//! a subject is named (1 caps nothing, 0 admits them only when no subject
11//! is named). The planner knobs: `search_retry_quorum` runs the full-text
12//! tier again at that quorum when the first pass returns nothing (0 runs
13//! no second pass); `search_quorum_over` counts the quorum over synonym
14//! groups or over the query words they absorbed. The subject-tier knobs:
15//! `alias_min_sim` is the least alias similarity that names a subject
16//! (the trigram operator's own threshold, 0.6, is its floor) and
17//! `max_subjects` the most subjects one message may name. The extension
18//! knob: `extension_max_candidates` is the most facts the
19//! extension retrievers may introduce beyond what the core tiers found.
20//!
21//! The usage knobs close the citation loop on the fill:
22//! `usage_weight` adds `usage_weight × (cited_rate − ignored_rate)` to a
23//! fact's fused score before `min_fused_score` and the fill order (0 adds
24//! nothing). The rates are the `brain_usage` job's Laplace shares, which
25//! sum to one, so the term is centred: a fact never injected sits at
26//! 0.5/0.5 and a fact the job has not yet rated at 0/0, and both add
27//! exactly zero. `usage_min_recalls` is the fewest injections before the
28//! job rates a fact at all; under it the fact keeps the prior, so the
29//! term stays zero and a new fact is not punished (0 rates every fact).
30//! The in-context importance term is weighted separately by
31//! [`UsageWeights`] in `importance.yaml`.
32//!
33//! Copyright (c) systemprompt.io — Business Source License 1.1.
34//! See <https://systemprompt.io> for licensing details.
35
36use std::collections::BTreeMap;
37
38use serde::{Deserialize, Serialize};
39use systemprompt_identifiers::{FactKindId, WorkflowId};
40
41use crate::brain::source::SourceClass;
42
43/// The importance formula: a stored base and an in-context multiplier.
44#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
45#[serde(deny_unknown_fields, default)]
46pub struct ImportanceConfig {
47    pub base: BaseImportance,
48    pub in_context: InContextImportance,
49}
50
51/// Importance stored on write.
52#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
53#[serde(deny_unknown_fields, default)]
54pub struct BaseImportance {
55    pub kind_weight: BTreeMap<FactKindId, f64>,
56    pub source_weight: BTreeMap<SourceClass, f64>,
57    pub model_hint: Option<ModelHint>,
58    pub confirmed_by_owner: f64,
59}
60
61/// A field whose value the extractor scores, and that score's weight.
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
63#[serde(deny_unknown_fields)]
64pub struct ModelHint {
65    pub field: String,
66    pub weight: f64,
67}
68
69/// Importance computed per request.
70#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
71#[serde(deny_unknown_fields, default)]
72pub struct InContextImportance {
73    pub workflow_weight: BTreeMap<WorkflowId, BTreeMap<FactKindId, f64>>,
74    pub subject_match: f64,
75    pub recency: Recency,
76    pub usage: UsageWeights,
77}
78
79/// Exponential recency decay.
80#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
81#[serde(deny_unknown_fields, default)]
82pub struct Recency {
83    pub half_life_days: u32,
84}
85
86impl Default for Recency {
87    fn default() -> Self {
88        Self { half_life_days: 90 }
89    }
90}
91
92/// How past citation and neglect move a fact's score. The rates are the
93/// `brain_usage` job's Laplace-smoothed shares, so the defaults add at most
94/// `cited_rate_weight` and subtract at most `ignored_penalty`.
95#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
96#[serde(deny_unknown_fields, default)]
97pub struct UsageWeights {
98    pub cited_rate_weight: f64,
99    pub ignored_penalty: f64,
100}
101
102impl Default for UsageWeights {
103    fn default() -> Self {
104        Self {
105            cited_rate_weight: 0.2,
106            ignored_penalty: 0.1,
107        }
108    }
109}
110
111/// `retrieval.yaml`.
112#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113#[serde(deny_unknown_fields, default)]
114pub struct RetrievalDoc {
115    pub tiers: TierWeights,
116    pub rrf_k: u32,
117    pub max_facts: u32,
118    pub quotas: BTreeMap<FactKindId, u32>,
119    pub links_weight: f64,
120    pub search_max_df: f64,
121    pub search_quorum: f64,
122    pub search_retry_quorum: f64,
123    pub search_quorum_over: QuorumUnit,
124    pub alias_min_sim: f64,
125    pub max_subjects: u32,
126    pub fill_order: FillOrder,
127    pub linked_share: f64,
128    pub min_fused_score: f64,
129    pub context_share: f64,
130    pub usage_weight: f64,
131    pub usage_min_recalls: u32,
132    pub extension_max_candidates: u32,
133}
134
135impl Default for RetrievalDoc {
136    fn default() -> Self {
137        Self {
138            tiers: TierWeights::default(),
139            rrf_k: 60,
140            max_facts: 12,
141            quotas: BTreeMap::new(),
142            links_weight: 0.6,
143            search_max_df: 0.2,
144            search_quorum: 0.5,
145            search_retry_quorum: 0.0,
146            search_quorum_over: QuorumUnit::default(),
147            alias_min_sim: 0.6,
148            max_subjects: 4,
149            fill_order: FillOrder::default(),
150            linked_share: 0.0,
151            min_fused_score: 0.0,
152            context_share: 1.0,
153            usage_weight: 0.0,
154            usage_min_recalls: 0,
155            extension_max_candidates: 24,
156        }
157    }
158}
159
160/// What the full-text planner counts its quorum over.
161#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
162#[serde(rename_all = "snake_case")]
163pub enum QuorumUnit {
164    #[default]
165    Groups,
166    Words,
167}
168
169/// Which key orders the budget fill.
170#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
171#[serde(rename_all = "snake_case")]
172pub enum FillOrder {
173    #[default]
174    Rank,
175    Importance,
176}
177
178/// Reciprocal-rank-fusion weight of each retrieval tier.
179#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
180#[serde(deny_unknown_fields, default)]
181pub struct TierWeights {
182    pub pinned: f64,
183    pub subject: f64,
184    pub linked: f64,
185    pub context: f64,
186    pub full_text: f64,
187    pub extension: f64,
188}
189
190impl Default for TierWeights {
191    fn default() -> Self {
192        Self {
193            pinned: 1.0,
194            subject: 1.0,
195            linked: 0.6,
196            context: 0.8,
197            full_text: 0.6,
198            extension: 0.0,
199        }
200    }
201}