use std::time::Duration;
use async_trait::async_trait;
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue};
use serde::{Deserialize, Serialize};
use super::Reranker;
use crate::{
error::{RagError, Result},
store::ScoredChunk,
};
#[derive(Debug, Clone)]
pub struct LlamaServerReranker {
endpoint_url: String,
model_id: String,
client: reqwest::Client,
api_key: Option<String>,
}
impl LlamaServerReranker {
pub fn new(base_or_endpoint: impl Into<String>, model_id: impl Into<String>) -> Self {
let raw = base_or_endpoint.into();
let endpoint_url = if raw.ends_with("/rerank") {
raw
} else {
format!("{}/v1/rerank", raw.trim_end_matches('/'))
};
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(60))
.build()
.unwrap_or_default();
Self {
endpoint_url,
model_id: model_id.into(),
client,
api_key: None,
}
}
pub fn with_api_key(mut self, api_key: impl Into<String>) -> Self {
self.api_key = Some(api_key.into());
self
}
pub fn with_client(mut self, client: reqwest::Client) -> Self {
self.client = client;
self
}
}
#[derive(Debug, Serialize)]
struct RerankRequest<'a> {
model: &'a str,
query: &'a str,
documents: &'a [String],
top_n: Option<usize>,
}
#[derive(Debug, Deserialize)]
struct RerankItem {
index: usize,
#[serde(alias = "score")]
relevance_score: f64,
}
#[derive(Debug, Deserialize)]
struct RerankResponse {
results: Vec<RerankItem>,
}
#[async_trait]
impl Reranker for LlamaServerReranker {
fn model_id(&self) -> &str {
&self.model_id
}
async fn rerank(&self, query: &str, candidates: &mut [ScoredChunk]) -> Result<()> {
if candidates.is_empty() {
return Ok(());
}
let documents: Vec<String> = candidates.iter().map(|c| c.chunk.content.clone()).collect();
let request_payload = RerankRequest {
model: &self.model_id,
query,
documents: &documents,
top_n: Some(candidates.len()),
};
let mut headers = HeaderMap::new();
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
if let Some(ref key) = self.api_key
&& let Ok(val) = HeaderValue::from_str(&format!("Bearer {}", key))
{
headers.insert(AUTHORIZATION, val);
}
let resp = self
.client
.post(&self.endpoint_url)
.headers(headers)
.json(&request_payload)
.send()
.await
.map_err(|err| RagError::RerankFailed(format!("HTTP request failed: {err}")))?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp
.text()
.await
.unwrap_or_else(|_| "<unreadable body>".to_string());
return Err(RagError::RerankFailed(format!(
"Reranker endpoint error {status}: {body}"
)));
}
let parsed: RerankResponse = resp.json().await.map_err(|err| {
RagError::RerankFailed(format!("Failed to parse rerank response: {err}"))
})?;
let mut scored_map: std::collections::HashMap<usize, f64> =
std::collections::HashMap::new();
for item in parsed.results {
scored_map.insert(item.index, item.relevance_score);
}
for (idx, candidate) in candidates.iter_mut().enumerate() {
if let Some(&new_score) = scored_map.get(&idx) {
candidate.score = new_score;
}
}
candidates.sort_by(|a, b| b.score.total_cmp(&a.score));
Ok(())
}
}