1use std::collections::BTreeMap;
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11use systemprompt_identifiers::{BrainFactId, BrainRecallId, UserId, WorkflowId};
12
13use super::declarations::QuorumUnit;
14use super::fact::BrainFact;
15use super::scope::ScopeLevel;
16use crate::auth::RequestActor;
17use crate::origin::ClientKind;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum RetrievalTier {
23 Pinned,
24 Subject,
25 Linked,
26 Context,
27 FullText,
28 Extension,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
33#[serde(rename_all = "snake_case")]
34pub enum WithheldReason {
35 Scope,
36 Source,
37 AgentCapped,
38 Stale,
39 ReviewDue,
40 Disputed,
41 Quota,
42 MaxFacts,
43 Budget,
44 LowScore,
45 ContextCap,
46 Unavailable,
47}
48
49impl WithheldReason {
50 #[must_use]
51 pub const fn as_str(self) -> &'static str {
52 match self {
53 Self::Scope => "scope",
54 Self::Source => "source",
55 Self::AgentCapped => "agent_capped",
56 Self::Stale => "stale",
57 Self::ReviewDue => "review_due",
58 Self::Disputed => "disputed",
59 Self::Quota => "quota",
60 Self::MaxFacts => "max_facts",
61 Self::Budget => "budget",
62 Self::LowScore => "low_score",
63 Self::ContextCap => "context_cap",
64 Self::Unavailable => "unavailable",
65 }
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
79pub struct RecallCtx {
80 pub user_id: UserId,
81 pub actor: RequestActor,
82 pub client: ClientKind,
83 pub project: Option<String>,
84 pub workflow: Option<WorkflowId>,
85 pub scopes: Vec<ScopeLevel>,
86 pub now: DateTime<Utc>,
87 pub as_of: DateTime<Utc>,
88 #[serde(default)]
89 pub held_keys: Vec<String>,
90}
91
92impl RecallCtx {
93 #[must_use]
94 pub fn new(user_id: UserId, actor: RequestActor, now: DateTime<Utc>) -> Self {
95 Self {
96 user_id,
97 actor,
98 client: ClientKind::Other,
99 project: None,
100 workflow: None,
101 scopes: ScopeLevel::ALL.to_vec(),
102 now,
103 as_of: now,
104 held_keys: Vec::new(),
105 }
106 }
107
108 #[must_use]
109 pub fn with_as_of(mut self, as_of: DateTime<Utc>) -> Self {
110 self.as_of = as_of.min(self.now);
111 self
112 }
113
114 #[must_use]
115 pub fn with_held_keys(mut self, keys: Vec<String>) -> Self {
116 self.held_keys = keys;
117 self
118 }
119
120 #[must_use]
121 pub const fn with_client(mut self, client: ClientKind) -> Self {
122 self.client = client;
123 self
124 }
125
126 #[must_use]
127 pub fn with_project(mut self, project: impl Into<String>) -> Self {
128 self.project = Some(project.into());
129 self
130 }
131
132 #[must_use]
133 pub fn with_workflow(mut self, workflow: WorkflowId) -> Self {
134 self.workflow = Some(workflow);
135 self
136 }
137
138 #[must_use]
139 pub fn with_scopes(mut self, scopes: Vec<ScopeLevel>) -> Self {
140 self.scopes = scopes;
141 self
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
151pub struct RecallQuery {
152 pub subject_hints: String,
153 pub limit: u32,
154 #[serde(default = "RecallQuery::default_links_weight")]
155 pub links_weight: f64,
156 #[serde(default)]
157 pub synonyms: Vec<Vec<String>>,
158 #[serde(default = "RecallQuery::default_search_max_df")]
159 pub search_max_df: f64,
160 #[serde(default = "RecallQuery::default_search_quorum")]
161 pub search_quorum: f64,
162 #[serde(default)]
163 pub search_retry_quorum: f64,
164 #[serde(default)]
165 pub search_quorum_over: QuorumUnit,
166 #[serde(default = "RecallQuery::default_alias_min_sim")]
167 pub alias_min_sim: f64,
168 #[serde(default = "RecallQuery::default_max_subjects")]
169 pub max_subjects: u32,
170}
171
172impl RecallQuery {
173 #[must_use]
174 pub const fn new(subject_hints: String, limit: u32) -> Self {
175 Self {
176 subject_hints,
177 limit,
178 links_weight: Self::default_links_weight(),
179 synonyms: Vec::new(),
180 search_max_df: Self::default_search_max_df(),
181 search_quorum: Self::default_search_quorum(),
182 search_retry_quorum: 0.0,
183 search_quorum_over: QuorumUnit::Groups,
184 alias_min_sim: Self::default_alias_min_sim(),
185 max_subjects: Self::default_max_subjects(),
186 }
187 }
188
189 #[must_use]
190 pub const fn default_alias_min_sim() -> f64 {
191 0.6
192 }
193
194 #[must_use]
195 pub const fn default_max_subjects() -> u32 {
196 4
197 }
198
199 const fn default_links_weight() -> f64 {
200 0.6
201 }
202
203 const fn default_search_max_df() -> f64 {
204 0.2
205 }
206
207 const fn default_search_quorum() -> f64 {
208 0.5
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
214pub struct TierRank {
215 pub tier: RetrievalTier,
216 pub rank: u32,
217}
218
219#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
221pub struct ScoredFact {
222 pub fact: BrainFact,
223 pub score: f64,
224 pub ranks: Vec<TierRank>,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229pub struct WithheldFact {
230 pub id: BrainFactId,
231 pub reason: WithheldReason,
232}
233
234#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
236pub struct InjectionPlan {
237 pub facts: Vec<ScoredFact>,
238 pub withheld: Vec<WithheldFact>,
239 pub tokens: u32,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
244pub struct FactRef {
245 pub id: BrainFactId,
246 pub revision: i32,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
255pub struct StoredBlock {
256 pub rendered: String,
257 pub as_of: DateTime<Utc>,
258 pub facts: Vec<FactRef>,
259 #[serde(default)]
260 pub late: bool,
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
266pub struct RecallAudit {
267 pub recall_id: BrainRecallId,
268 pub injected: Vec<FactRef>,
269 pub withheld: BTreeMap<WithheldReason, u32>,
270 pub tokens: u32,
271 pub as_of: DateTime<Utc>,
272}
273
274impl RecallAudit {
275 #[must_use]
276 pub fn withheld_total(&self) -> u32 {
277 self.withheld.values().sum()
278 }
279}