1use std::collections::HashMap;
5use std::sync::Arc;
6
7use async_trait::async_trait;
8use lc_core::runnables::RunnableConfig;
9use lc_core::BaseChatModel;
10use lc_providers::{wrap_chat_model, ProviderError};
11use lc_schema::Message;
12use serde_json::Value;
13
14use crate::base::{
15 run_chain_with_callbacks, stream_chain_with_callbacks, BaseChain, ChainError, ChainResult,
16 ChainStream,
17};
18use crate::BoxedChatModel;
19
20use super::destination::RouteDestination;
21use super::{route_tool, RouteDecision};
22
23pub struct LLMRouterChain {
27 llm: BoxedChatModel,
29
30 destinations: Vec<RouteDestination>,
32
33 default_chain: Option<Arc<dyn BaseChain>>,
35
36 input_key: String,
38
39 name: String,
41
42 verbose: bool,
44}
45
46impl LLMRouterChain {
47 pub fn new<L>(llm: L) -> Self
49 where
50 L: BaseChatModel + Send + Sync + 'static,
51 L::Error: Into<ProviderError>,
52 {
53 Self {
54 llm: wrap_chat_model(llm),
55 destinations: Vec::new(),
56 default_chain: None,
57 input_key: "input".to_string(),
58 name: "llm_router_chain".to_string(),
59 verbose: false,
60 }
61 }
62
63 pub fn add_route(
65 mut self,
66 name: impl Into<String>,
67 description: impl Into<String>,
68 chain: Arc<dyn BaseChain>,
69 ) -> Self {
70 self.destinations
71 .push(RouteDestination::new(name, description, chain));
72 self
73 }
74
75 pub fn add_route_with_keywords(
77 mut self,
78 name: impl Into<String>,
79 description: impl Into<String>,
80 chain: Arc<dyn BaseChain>,
81 keywords: Vec<&str>,
82 ) -> Self {
83 self.destinations
84 .push(RouteDestination::new(name, description, chain).with_keywords(keywords));
85 self
86 }
87
88 pub fn with_default(mut self, chain: Arc<dyn BaseChain>) -> Self {
90 self.default_chain = Some(chain);
91 self
92 }
93
94 pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
96 self.input_key = key.into();
97 self
98 }
99
100 pub fn with_name(mut self, name: impl Into<String>) -> Self {
102 self.name = name.into();
103 self
104 }
105
106 pub fn with_verbose(mut self, verbose: bool) -> Self {
108 self.verbose = verbose;
109 self
110 }
111
112 pub fn destinations(&self) -> &[RouteDestination] {
114 &self.destinations
115 }
116
117 pub fn default_chain(&self) -> Option<&Arc<dyn BaseChain>> {
119 self.default_chain.as_ref()
120 }
121
122 fn build_router_prompt(&self, input: &str) -> String {
124 let mut prompt =
125 String::from("Based on the user input, select the most appropriate handler.\n\n");
126 prompt.push_str("Available handlers:\n");
127
128 for (i, dest) in self.destinations.iter().enumerate() {
129 prompt.push_str(&format!(
130 "{}. {}: {}\n",
131 i + 1,
132 dest.name(),
133 dest.description()
134 ));
135 }
136
137 prompt.push_str("\nUser input: ");
138 prompt.push_str(input);
139 prompt.push_str(
142 "\n\nReturn only the chosen handler as JSON: {\"destination\": \"<handler name>\", \"reason\": \"<why this handler>\"}",
143 );
144
145 prompt
146 }
147
148 async fn route_with_llm(
157 &self,
158 input: &str,
159 config: Option<RunnableConfig>,
160 ) -> Result<RouteDecision, ChainError> {
161 let prompt = self.build_router_prompt(input);
162
163 let messages = vec![Message::human(&prompt)];
164
165 let map_err = |e| ChainError::Nested {
166 context: "LLM routing call failed".to_string(),
167 source: Box::new(e),
168 };
169
170 let result = match self.llm.bind_tools(vec![route_tool()]) {
171 Some(bound) => bound.chat(messages, config).await.map_err(map_err)?,
172 None => self.llm.chat(messages, config).await.map_err(map_err)?,
173 };
174
175 if let Some(decision) = result
176 .tool_calls
177 .as_ref()
178 .and_then(|calls| calls.first())
179 .and_then(|call| call.parse_arguments::<RouteDecision>().ok())
180 {
181 return Ok(decision);
182 }
183
184 Ok(RouteDecision::from_text(&result.content))
185 }
186
187 fn find_destination(&self, name: &str) -> Option<&RouteDestination> {
189 let name_lower = name.to_lowercase();
190 if let Some(dest) = self
192 .destinations
193 .iter()
194 .find(|d| d.name().eq_ignore_ascii_case(name))
195 {
196 return Some(dest);
197 }
198 self.destinations.iter().find(|d| {
200 let d_lower = d.name().to_lowercase();
201 name_lower.starts_with(&d_lower)
202 || name_lower.ends_with(&d_lower)
203 || name_lower
204 .split_whitespace()
205 .any(|word| word.eq_ignore_ascii_case(&d_lower))
206 })
207 }
208
209 async fn select_route(
211 &self,
212 input: &str,
213 config: Option<RunnableConfig>,
214 ) -> Result<&RouteDestination, ChainError> {
215 if self.destinations.is_empty() {
216 return Err(ChainError::ExecutionError(
217 "No route destinations configured".to_string(),
218 ));
219 }
220
221 if self.destinations.len() == 1 {
222 return Ok(&self.destinations[0]);
223 }
224
225 let llm_note: Option<String> = {
232 let llm_result = self.route_with_llm(input, config).await;
233 match llm_result {
234 Ok(decision) => {
235 if let Some(reason) = &decision.reason {
236 if self.verbose {
237 println!("LLM route reason: {}", reason);
238 }
239 }
240 if let Some(dest) = self.find_destination(&decision.destination) {
241 return Ok(dest);
242 }
243 Some(format!(
244 "LLM returned an unknown route destination {:?}",
245 decision.destination
246 ))
247 }
248 Err(e) => Some(format!("LLM routing call failed: {}", e)),
249 }
250 };
251
252 let mut best_match: Option<(&RouteDestination, usize)> = None;
254 for dest in &self.destinations {
255 for keyword in dest.keywords() {
256 if input.contains(keyword) {
257 let len = keyword.len();
258 if best_match.is_none() || len > best_match.unwrap().1 {
259 best_match = Some((dest, len));
260 }
261 }
262 }
263 }
264 if let Some((dest, _)) = best_match {
265 return Ok(dest);
266 }
267
268 Err(ChainError::ExecutionError(format!(
269 "No matching route destination found ({})",
270 llm_note.unwrap_or_else(|| "LLM and keyword matching both failed".to_string())
271 )))
272 }
273
274 async fn route_and_invoke(
277 &self,
278 inputs: HashMap<String, Value>,
279 config: Option<RunnableConfig>,
280 ) -> Result<ChainResult, ChainError> {
281 self.validate_inputs(&inputs)?;
282
283 let input = inputs
284 .get(&self.input_key)
285 .and_then(|v| v.as_str())
286 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
287
288 if self.verbose {
289 println!("\n=== LLMRouterChain execution ===");
290 println!("Input: {}", input);
291 println!("Route destination count: {}", self.destinations.len());
292 }
293
294 let route_result = self.select_route(input, config.clone()).await;
295
296 let chain = match route_result {
297 Ok(dest) => {
298 if self.verbose {
299 println!("Routed to: {} ({})", dest.name(), dest.description());
300 }
301 dest.chain()
302 }
303 Err(e) => {
304 if let Some(default) = &self.default_chain {
305 log::error!(
309 "routing failed, falling back to default chain (caller may receive an \
310 answer that does not match the input): {e}"
311 );
312 default
313 } else {
314 return Err(e);
315 }
316 }
317 };
318
319 let result = chain.invoke_with_config(inputs, config).await?;
320
321 if self.verbose {
322 println!("=== LLMRouterChain complete ===\n");
323 }
324
325 Ok(result)
326 }
327
328 async fn route_and_stream(
331 &self,
332 inputs: HashMap<String, Value>,
333 config: Option<RunnableConfig>,
334 ) -> Result<ChainStream, ChainError> {
335 self.validate_inputs(&inputs)?;
336
337 let input = inputs
338 .get(&self.input_key)
339 .and_then(|v| v.as_str())
340 .ok_or_else(|| ChainError::MissingInput(self.input_key.clone()))?;
341
342 let route_result = self.select_route(input, config.clone()).await;
343
344 let chain = match route_result {
345 Ok(dest) => dest.chain(),
346 Err(e) => {
347 if let Some(default) = &self.default_chain {
348 default
349 } else {
350 return Err(e);
351 }
352 }
353 };
354
355 chain.stream_with_config(inputs, config).await
356 }
357}
358
359#[async_trait]
360impl BaseChain for LLMRouterChain {
361 fn input_keys(&self) -> Vec<&str> {
362 vec![&self.input_key]
363 }
364
365 fn output_keys(&self) -> Vec<&str> {
366 let mut seen = std::collections::HashSet::new();
367 let mut result: Vec<&str> = Vec::new();
368
369 for dest in &self.destinations {
370 for key in dest.chain().output_keys() {
371 if seen.insert(key.to_string()) {
372 result.push(key);
373 }
374 }
375 }
376 if let Some(default) = &self.default_chain {
377 for key in default.output_keys() {
378 if seen.insert(key.to_string()) {
379 result.push(key);
380 }
381 }
382 }
383
384 if result.is_empty() {
385 vec!["output"]
386 } else {
387 result
388 }
389 }
390
391 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
392 self.route_and_invoke(inputs, None).await
393 }
394
395 async fn invoke_with_config(
401 &self,
402 inputs: HashMap<String, Value>,
403 config: Option<RunnableConfig>,
404 ) -> Result<ChainResult, ChainError> {
405 run_chain_with_callbacks(self.name(), inputs, config.clone(), |inputs| async move {
406 self.route_and_invoke(inputs, config).await
407 })
408 .await
409 }
410
411 async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
416 self.route_and_stream(inputs, None).await
417 }
418
419 async fn stream_with_config(
421 &self,
422 inputs: HashMap<String, Value>,
423 config: Option<RunnableConfig>,
424 ) -> Result<ChainStream, ChainError> {
425 let output_key = self.output_keys().first().map(|k| (*k).to_string());
426 stream_chain_with_callbacks(
427 self.name(),
428 inputs,
429 config.clone(),
430 output_key,
431 |inputs| async move { self.route_and_stream(inputs, config).await },
432 )
433 .await
434 }
435
436 fn name(&self) -> &str {
437 &self.name
438 }
439}