use crate::config::Config;
use crate::llm::{LlmClient, Message};
use crate::store::CodeBlock;
use anyhow::Result;
const REASONING_SYSTEM_PROMPT: &str = "You are an expert code-retrieval judge. Given a \
developer's search query and a numbered list of candidate code locations (file path, symbol \
names, and a code snippet), decide which candidates actually ANSWER the query. Reason about \
intent and behaviour, not just keyword overlap: a snippet that implements what the query asks \
for outranks one that merely mentions the words. Return the candidate numbers ordered from most \
to least relevant, INCLUDING ONLY those that are genuinely relevant.";
pub async fn reason_rank_code_blocks(
query: &str,
blocks: Vec<CodeBlock>,
config: &Config,
) -> Result<Vec<CodeBlock>> {
let rc = &config.search.reasoning;
if !rc.enabled || blocks.is_empty() {
return Ok(blocks);
}
let mut pool = blocks;
pool.truncate(rc.max_candidates.max(1));
let client = match LlmClient::with_model(config, &rc.model) {
Ok(c) => c,
Err(e) => {
tracing::warn!("reasoning: cannot build LLM client ({e}); keeping input order");
return Ok(pool);
}
};
let snippet_len = match rc.context_level.as_str() {
"signatures" => 200,
"full" => 4000,
_ => 600, };
let mut prompt = String::new();
prompt.push_str(&format!("Query: {}\n\nCandidates:\n", query));
for (i, b) in pool.iter().enumerate() {
let syms = if b.symbols.is_empty() {
"(none)".to_string()
} else {
b.symbols.join(", ")
};
let snippet = crate::indexer::contextual::strip_enriched_preamble(&b.content);
let snippet = crate::utils::truncate_at_char_boundary(snippet, snippet_len);
prompt.push_str(&format!(
"[{}] {} (lines {}-{})\nSymbols: {}\n<code>\n{}\n</code>\n\n",
i + 1,
b.path,
b.start_line,
b.end_line,
syms,
snippet
));
}
prompt.push_str(&format!(
"Return a JSON object {{\"ranked\": [numbers]}} listing candidate numbers (1-{}) that \
answer the query, most relevant first, omitting irrelevant ones. Output ONLY the JSON.",
pool.len()
));
let schema = serde_json::json!({
"type": "object",
"properties": { "ranked": { "type": "array", "items": { "type": "integer" } } },
"required": ["ranked"]
});
let messages = vec![
Message::system(REASONING_SYSTEM_PROMPT),
Message::user(&prompt),
];
let json = match client.chat_completion_json(messages, Some(schema)).await {
Ok(v) => v,
Err(e) => {
tracing::warn!("reasoning: LLM call failed ({e}); keeping input order");
return Ok(pool);
}
};
let ranked: Vec<usize> = json
.get("ranked")
.and_then(|v| v.as_array())
.or_else(|| json.as_array())
.map(|a| {
a.iter()
.filter_map(|x| x.as_u64())
.map(|n| n as usize)
.collect()
})
.unwrap_or_default();
if ranked.is_empty() {
tracing::warn!("reasoning: empty/invalid ranking; keeping input order");
return Ok(pool);
}
let n = pool.len();
const RRF_K: f32 = 60.0;
let rw = rc.reasoning_weight.max(0.0);
let mut score = vec![0.0f32; n];
for (i, s) in score.iter_mut().enumerate() {
*s += 1.0 / (RRF_K + i as f32); }
for (rrank, &num) in ranked.iter().enumerate() {
if num >= 1 && num <= n {
score[num - 1] += rw / (RRF_K + rrank as f32); }
}
let mut order: Vec<usize> = (0..n).collect();
order.sort_by(|&a, &b| {
score[b]
.partial_cmp(&score[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut out: Vec<CodeBlock> = order
.iter()
.enumerate()
.map(|(pos, &i)| {
let mut b = pool[i].clone();
b.distance = Some((pos as f32 + 1.0) / (n as f32 + 1.0));
b
})
.collect();
out.truncate(rc.final_top_k.max(1));
Ok(out)
}