vectorless/retrieval/pilot/
config.rs1use serde::{Deserialize, Serialize};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct PilotConfig {
18 pub mode: PilotMode,
20 pub budget: BudgetConfig,
22 pub intervention: InterventionConfig,
24 pub guide_at_start: bool,
26 pub guide_at_backtrack: bool,
28 pub prompt_template_path: Option<String>,
30}
31
32impl Default for PilotConfig {
33 fn default() -> Self {
34 Self {
35 mode: PilotMode::Balanced,
36 budget: BudgetConfig::default(),
37 intervention: InterventionConfig::default(),
38 guide_at_start: true,
39 guide_at_backtrack: true,
40 prompt_template_path: None,
41 }
42 }
43}
44
45impl PilotConfig {
46 pub fn with_mode(mode: PilotMode) -> Self {
48 Self {
49 mode,
50 ..Default::default()
51 }
52 }
53
54 pub fn high_quality() -> Self {
56 Self {
57 mode: PilotMode::Aggressive,
58 budget: BudgetConfig {
59 max_tokens_per_query: 5000,
60 max_tokens_per_call: 1000,
61 max_calls_per_query: 10,
62 max_calls_per_level: 3,
63 hard_limit: false,
64 },
65 intervention: InterventionConfig {
66 fork_threshold: 2,
67 score_gap_threshold: 0.2,
68 low_score_threshold: 0.4,
69 max_interventions_per_level: 3,
70 },
71 guide_at_start: true,
72 guide_at_backtrack: true,
73 prompt_template_path: None,
74 }
75 }
76
77 pub fn low_cost() -> Self {
79 Self {
80 mode: PilotMode::Conservative,
81 budget: BudgetConfig {
82 max_tokens_per_query: 500,
83 max_tokens_per_call: 200,
84 max_calls_per_query: 2,
85 max_calls_per_level: 1,
86 hard_limit: true,
87 },
88 intervention: InterventionConfig {
89 fork_threshold: 5,
90 score_gap_threshold: 0.1,
91 low_score_threshold: 0.2,
92 max_interventions_per_level: 1,
93 },
94 guide_at_start: false,
95 guide_at_backtrack: true,
96 prompt_template_path: None,
97 }
98 }
99
100 pub fn algorithm_only() -> Self {
102 Self {
103 mode: PilotMode::AlgorithmOnly,
104 ..Default::default()
105 }
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
113pub enum PilotMode {
114 Aggressive,
116 #[default]
118 Balanced,
119 Conservative,
121 AlgorithmOnly,
123}
124
125impl PilotMode {
126 pub fn uses_llm(&self) -> bool {
128 !matches!(self, PilotMode::AlgorithmOnly)
129 }
130
131 pub fn fork_threshold_multiplier(&self) -> f32 {
133 match self {
134 PilotMode::Aggressive => 0.5, PilotMode::Balanced => 1.0,
136 PilotMode::Conservative => 2.0, PilotMode::AlgorithmOnly => f32::MAX,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct BudgetConfig {
147 pub max_tokens_per_query: usize,
149 pub max_tokens_per_call: usize,
151 pub max_calls_per_query: usize,
153 pub max_calls_per_level: usize,
155 pub hard_limit: bool,
157}
158
159impl Default for BudgetConfig {
160 fn default() -> Self {
161 Self {
162 max_tokens_per_query: 2000,
163 max_tokens_per_call: 500,
164 max_calls_per_query: 5,
165 max_calls_per_level: 2,
166 hard_limit: true,
167 }
168 }
169}
170
171impl BudgetConfig {
172 pub fn is_within_budget(&self, used: usize) -> bool {
174 used < self.max_tokens_per_query
175 }
176
177 pub fn remaining_tokens(&self, used: usize) -> usize {
179 self.max_tokens_per_query.saturating_sub(used)
180 }
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct InterventionConfig {
188 pub fork_threshold: usize,
190 pub score_gap_threshold: f32,
192 pub low_score_threshold: f32,
194 pub max_interventions_per_level: usize,
196}
197
198impl Default for InterventionConfig {
199 fn default() -> Self {
200 Self {
201 fork_threshold: 3,
202 score_gap_threshold: 0.15,
203 low_score_threshold: 0.3,
204 max_interventions_per_level: 2,
205 }
206 }
207}
208
209impl InterventionConfig {
210 pub fn should_intervene_at_fork(&self, candidate_count: usize) -> bool {
212 candidate_count > self.fork_threshold
213 }
214
215 pub fn scores_are_close(&self, scores: &[f32]) -> bool {
217 if scores.len() < 2 {
218 return false;
219 }
220 let max_score = scores.iter().cloned().fold(0.0, f32::max);
221 let min_score = scores.iter().cloned().fold(1.0, f32::min);
222 (max_score - min_score) < self.score_gap_threshold
223 }
224
225 pub fn is_low_confidence(&self, best_score: f32) -> bool {
227 best_score < self.low_score_threshold
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[test]
236 fn test_pilot_mode_uses_llm() {
237 assert!(PilotMode::Aggressive.uses_llm());
238 assert!(PilotMode::Balanced.uses_llm());
239 assert!(PilotMode::Conservative.uses_llm());
240 assert!(!PilotMode::AlgorithmOnly.uses_llm());
241 }
242
243 #[test]
244 fn test_budget_config() {
245 let config = BudgetConfig::default();
246 assert!(config.is_within_budget(1000));
247 assert!(!config.is_within_budget(3000));
248 assert_eq!(config.remaining_tokens(1500), 500);
249 }
250
251 #[test]
252 fn test_intervention_config() {
253 let config = InterventionConfig::default();
254
255 assert!(!config.should_intervene_at_fork(2));
257 assert!(config.should_intervene_at_fork(4));
258
259 assert!(config.scores_are_close(&[0.5, 0.55, 0.52]));
261 assert!(!config.scores_are_close(&[0.3, 0.8]));
262
263 assert!(config.is_low_confidence(0.2));
265 assert!(!config.is_low_confidence(0.5));
266 }
267
268 #[test]
269 fn test_pilot_config_presets() {
270 let high = PilotConfig::high_quality();
271 assert_eq!(high.mode, PilotMode::Aggressive);
272
273 let low = PilotConfig::low_cost();
274 assert_eq!(low.mode, PilotMode::Conservative);
275
276 let algo = PilotConfig::algorithm_only();
277 assert_eq!(algo.mode, PilotMode::AlgorithmOnly);
278 }
279}