1use scc_context::rank::{Reranker, ScoredEntity, SemanticScorer};
11use scc_indexer::embed::{cosine, rerank, EmbedConfig, EMBED_KINDS};
12use scc_store::Store;
13use std::collections::HashMap;
14
15pub struct EmbeddingScorer {
20 goal_vector: Vec<f32>,
21 vectors: HashMap<String, Vec<f32>>,
22}
23
24impl EmbeddingScorer {
26pub fn from_vectors(goal_vector: Vec<f32>, vectors: std::collections::HashMap<String, Vec<f32>>) -> Self {
28 EmbeddingScorer { goal_vector, vectors }
29 }
30pub fn new(goal: &str, cfg: &EmbedConfig, store: &Store) -> Result<EmbeddingScorer, String> {
32 let vectors = scc_indexer::embed::embed_texts(cfg, &[goal])?;
33 let goal_vector = vectors
34 .into_iter()
35 .next()
36 .ok_or_else(|| "embedding request returned no vector".to_string())?;
37 let mut map = HashMap::new();
38 for kind in EMBED_KINDS {
39 for e in store.entities_by_kind(kind).map_err(|e| e.to_string())? {
40 if let Ok(Some((v, _))) = store.get_embedding(&e.id) {
41 map.insert(e.id, v);
42 }
43 }
44 }
45 Ok(EmbeddingScorer {
46 goal_vector,
47 vectors: map,
48 })
49 }
50}
51
52impl SemanticScorer for EmbeddingScorer {
54fn score(&self, _goal: &str, entity: &scc_core::Entity) -> f64 {
56 match self.vectors.get(&entity.id) {
57 Some(v) => cosine(&self.goal_vector, v),
58 None => 0.0,
59 }
60 }
61}
62
63pub struct EngineReranker {
67 cfg: EmbedConfig,
68}
69
70impl EngineReranker {
72pub fn new(cfg: &EmbedConfig) -> EngineReranker {
74 EngineReranker { cfg: cfg.clone() }
75 }
76}
77
78impl Reranker for EngineReranker {
80fn rerank(&self, goal: &str, candidates: &mut Vec<ScoredEntity>) {
82 if candidates.is_empty() || self.cfg.rerank_model.is_none() {
83 return;
84 }
85 let docs: Vec<String> = candidates
86 .iter()
87 .take(30)
88 .map(|c| format!("{} {}", c.kind, c.name))
89 .collect();
90 if let Ok(scores) = rerank(&self.cfg, goal, &docs) {
91 for (c, s) in candidates.iter_mut().take(30).zip(scores.iter()) {
92 c.score = c.score * 0.2 + s * 5.0;
95 c.reason = format!("{} + rerank", c.reason);
96 }
97 candidates.sort_by(|a, b| {
98 b.score
99 .partial_cmp(&a.score)
100 .unwrap_or(std::cmp::Ordering::Equal)
101 });
102 }
103 }
105}
106
107pub fn remote_inference_allowed(config: &scc_indexer::Config) -> bool {
113 if !config.inference.enabled {
114 return false;
115 }
116 let cfg = EmbedConfig::from_config(&config.inference);
117 !cfg.is_remote() || config.security.allow_remote_models
118}
119
120pub fn rankers(
124 store: &Store,
125 config: &scc_indexer::Config,
126 goal: &str,
127) -> (Option<EmbeddingScorer>, Option<EngineReranker>) {
128 if !remote_inference_allowed(config) {
129 if config.inference.enabled {
130 eprintln!(
131 "scc: warning: remote inference blocked by security policy \
132 (security.allow_remote_models is false); using lexical ranking only"
133 );
134 }
135 return (None, None);
136 }
137 let cfg = EmbedConfig::from_config(&config.inference);
138 let scorer = EmbeddingScorer::new(goal, &cfg, store).ok();
139 let reranker = if cfg.rerank_model.is_some() {
140 Some(EngineReranker::new(&cfg))
141 } else {
142 None
143 };
144 (scorer, reranker)
145}