lc_chains/document_chains/
map_rerank.rs1use async_trait::async_trait;
5use futures_util::future::try_join_all;
6use lc_core::language_models::LLMResult;
7use lc_core::{BaseChatModel, Runnable};
8use lc_schema::Message;
9use lc_shared::document::Document;
10use regex::Regex;
11use serde_json::Value;
12use std::collections::HashMap;
13use std::sync::LazyLock;
14
15use crate::base::{BaseChain, ChainError, ChainResult};
16
17pub(crate) const DEFAULT_MAP_RERANK_PROMPT: &str = "Answer the question based on the following document, and provide a relevance score (0-100, higher is more relevant).
19
20Document content:
21{context}
22
23Question: {input}
24
25Please output in the following format:
26Relevance score: <score>
27Answer: <your answer>";
28
29pub struct MapRerankDocumentsChain<M: BaseChatModel> {
34 llm: M,
35 map_prompt_template: String,
36 document_variable_name: String,
37 input_key: String,
38 output_key: String,
39 name: String,
40 verbose: bool,
41 top_k: usize,
43}
44
45static SCORE_RE: LazyLock<Regex> =
47 LazyLock::new(|| Regex::new(r"(?i)(?:relevance\s*score|相关性评分)\s*[::]\s*(\d+)").unwrap());
48static SCORE_RE2: LazyLock<Regex> =
49 LazyLock::new(|| Regex::new(r"(?i)score\s*[::]\s*(\d+)").unwrap());
50
51fn truncate_str(s: &str, max_len: usize) -> &str {
53 if s.chars().count() <= max_len {
54 s
55 } else {
56 let end = s
57 .char_indices()
58 .nth(max_len)
59 .map(|(i, _)| i)
60 .unwrap_or(s.len());
61 &s[..end]
62 }
63}
64
65pub fn extract_score(text: &str) -> (u32, String) {
67 if let Some(caps) = SCORE_RE.captures(text) {
68 if let Ok(score) = caps[1].parse::<u32>() {
69 let cleaned = SCORE_RE.replace(text, "").trim().to_string();
70 let cleaned = cleaned
71 .trim_start_matches("Answer")
72 .trim_start_matches("答案")
73 .trim_start_matches(&[':', ':'][..])
74 .trim()
75 .to_string();
76 return (
77 std::cmp::min(score, 100),
78 if cleaned.is_empty() {
79 text.to_string()
80 } else {
81 cleaned
82 },
83 );
84 }
85 }
86
87 if let Some(caps) = SCORE_RE2.captures(text) {
88 if let Ok(score) = caps[1].parse::<u32>() {
89 let cleaned = SCORE_RE2.replace(text, "").trim().to_string();
90 return (
91 std::cmp::min(score, 100),
92 if cleaned.is_empty() {
93 text.to_string()
94 } else {
95 cleaned
96 },
97 );
98 }
99 }
100
101 (50, text.to_string())
102}
103
104impl<M: BaseChatModel> MapRerankDocumentsChain<M> {
105 pub fn new(llm: M) -> Self {
106 Self {
107 llm,
108 map_prompt_template: DEFAULT_MAP_RERANK_PROMPT.to_string(),
109 document_variable_name: "context".to_string(),
110 input_key: "input".to_string(),
111 output_key: "output".to_string(),
112 name: "map_rerank_documents".to_string(),
113 verbose: false,
114 top_k: 1,
115 }
116 }
117
118 pub fn with_map_prompt(mut self, template: impl Into<String>) -> Self {
119 self.map_prompt_template = template.into();
120 self
121 }
122
123 pub fn with_document_variable(mut self, name: impl Into<String>) -> Self {
124 self.document_variable_name = name.into();
125 self
126 }
127
128 pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
129 self.input_key = key.into();
130 self
131 }
132
133 pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
134 self.output_key = key.into();
135 self
136 }
137
138 pub fn with_name(mut self, name: impl Into<String>) -> Self {
139 self.name = name.into();
140 self
141 }
142
143 pub fn with_verbose(mut self, verbose: bool) -> Self {
144 self.verbose = verbose;
145 self
146 }
147
148 pub fn with_top_k(mut self, k: usize) -> Self {
150 self.top_k = k;
151 self
152 }
153
154 pub fn build_map_prompt(&self, context: &str, input: &str) -> String {
156 self.map_prompt_template
157 .replace(&format!("{{{}}}", self.document_variable_name), context)
158 .replace("{input}", input)
159 }
160
161 async fn map_document(
162 &self,
163 doc: &Document,
164 input: &str,
165 index: usize,
166 ) -> Result<(u32, String), ChainError>
167 where
168 <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
169 {
170 let prompt = self.build_map_prompt(&doc.content, input);
171 if self.verbose {
172 println!("\n--- Map document {} ---", index + 1);
173 }
174 let messages = vec![Message::human(&prompt)];
175 let response = self.llm.invoke(messages, None).await.map_err(|e| {
176 ChainError::ExecutionError(format!("Map call failed (document {}): {}", index + 1, e))
177 })?;
178 let (score, answer) = extract_score(&response.content);
179 if self.verbose {
180 println!(
181 "Document {} score: {}, answer: {}",
182 index + 1,
183 score,
184 truncate_str(&answer, 80)
185 );
186 }
187 Ok((score, answer))
188 }
189
190 pub async fn invoke_with_documents(
192 &self,
193 documents: Vec<Document>,
194 input: &str,
195 ) -> Result<Vec<(u32, String)>, ChainError>
196 where
197 <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
198 {
199 if documents.is_empty() {
200 return Err(ChainError::ExecutionError(
201 "Document list is empty".to_string(),
202 ));
203 }
204
205 if self.verbose {
206 println!("\n=== MapRerankDocumentsChain ===");
207 println!("Document count: {}, Input: {}", documents.len(), input);
208 println!("\n--- Map phase ---");
209 }
210
211 let mut map_futures = Vec::new();
212 for (i, doc) in documents.iter().enumerate() {
213 map_futures.push(self.map_document(doc, input, i));
214 }
215 let mut results: Vec<(u32, String)> = try_join_all(map_futures).await?;
216
217 results.sort_by(|a, b| b.0.cmp(&a.0));
218
219 if self.verbose {
220 println!("\n--- Rerank phase ---");
221 for (i, (score, answer)) in results.iter().enumerate() {
222 println!(
223 "Rank {}: score={}, answer={}",
224 i + 1,
225 score,
226 truncate_str(answer, 100)
227 );
228 }
229 }
230
231 let top_results: Vec<(u32, String)> = results.into_iter().take(self.top_k).collect();
232 if self.verbose {
233 println!("Selected {} best results", top_results.len());
234 println!("=== MapRerankDocumentsChain complete ===\n");
235 }
236 Ok(top_results)
237 }
238}
239
240#[async_trait]
241impl<M: BaseChatModel + Send + Sync + 'static> BaseChain for MapRerankDocumentsChain<M>
242where
243 <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
244{
245 fn input_keys(&self) -> Vec<&str> {
246 vec![&self.input_key, "documents"]
247 }
248 fn output_keys(&self) -> Vec<&str> {
249 vec![&self.output_key]
250 }
251
252 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
253 let input = inputs
254 .get(&self.input_key)
255 .and_then(|v| v.as_str())
256 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
257
258 let documents: Vec<Document> = inputs
259 .get("documents")
260 .and_then(|v| v.as_array())
261 .map(|arr| {
262 arr.iter()
263 .filter_map(|v| serde_json::from_value(v.clone()).ok())
264 .collect()
265 })
266 .ok_or_else(|| ChainError::MissingInput("documents".to_string()))?;
267
268 let results = self.invoke_with_documents(documents, input).await?;
269 let output_json: Vec<serde_json::Value> = results
270 .iter()
271 .map(|(score, answer)| serde_json::json!({"score": score, "answer": answer}))
272 .collect();
273
274 let mut result = HashMap::new();
275 result.insert(self.output_key.clone(), Value::Array(output_json));
276 Ok(result)
277 }
278
279 fn name(&self) -> &str {
280 &self.name
281 }
282}