Skip to main content

lc_chains/
llm_chain.rs

1// lc-chains/src/llm_chain.rs
2//! LLM Chain
3//!
4//! The most basic Chain, combining a Prompt and an LLM.
5
6use async_trait::async_trait;
7use futures_util::StreamExt;
8use lc_callbacks::{RunTree, RunType};
9use lc_core::language_models::LLMResult;
10use lc_core::runnables::RunnableConfig;
11use lc_core::{BaseChatModel, Runnable};
12use lc_schema::Message;
13use regex::Regex;
14use serde_json::{json, Value};
15use std::collections::HashMap;
16use std::sync::LazyLock;
17
18use crate::base::{BaseChain, ChainError, ChainResult, ChainStream, StreamToken};
19
20/// LLM Chain
21///
22/// Combines a Prompt template and an LLM. The most basic Chain.
23///
24/// # Examples
25/// ```ignore
26/// use lc_chains::LLMChain;
27///
28/// let chain = LLMChain::new(llm, "{question}");
29///
30/// let inputs = HashMap::from([("question".to_string(), "What is Rust?".into())]);
31/// let result = chain.invoke(inputs).await?;
32/// ```
33pub struct LLMChain<M: BaseChatModel> {
34    /// LLM client.
35    llm: M,
36
37    /// Prompt template.
38    prompt_template: String,
39
40    /// Input key name.
41    input_key: String,
42
43    /// Output key name.
44    output_key: String,
45
46    /// Chain name.
47    name: String,
48}
49
50/// Pre-compiled regex for detecting unreplaced template variables.
51static TEMPLATE_VAR_RE: LazyLock<Regex> =
52    LazyLock::new(|| Regex::new(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}").unwrap());
53
54impl<M: BaseChatModel> LLMChain<M> {
55    /// Create a new LLMChain.
56    ///
57    /// # Arguments
58    /// * `llm` - LLM client (any type implementing BaseChatModel)
59    /// * `prompt_template` - Prompt template string with {variable} placeholders
60    pub fn new(llm: M, prompt_template: impl Into<String>) -> Self {
61        Self {
62            llm,
63            prompt_template: prompt_template.into(),
64            input_key: "question".to_string(),
65            output_key: "text".to_string(),
66            name: "llm_chain".to_string(),
67        }
68    }
69
70    /// Set input key name.
71    pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
72        self.input_key = key.into();
73        self
74    }
75
76    /// Set output key name.
77    pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
78        self.output_key = key.into();
79        self
80    }
81
82    /// Set chain name.
83    pub fn with_name(mut self, name: impl Into<String>) -> Self {
84        self.name = name.into();
85        self
86    }
87
88    /// Render the Prompt template.
89    ///
90    /// Validates that all {variable} placeholders in the template
91    /// have been replaced. Returns an error if any unreplaced placeholders remain.
92    fn render_prompt(&self, inputs: &HashMap<String, Value>) -> Result<String, ChainError> {
93        let mut prompt = self.prompt_template.clone();
94
95        for (key, value) in inputs {
96            let placeholder = format!("{{{}}}", key);
97            let value_str = match value {
98                Value::String(s) => s.clone(),
99                _ => value.to_string(),
100            };
101            prompt = prompt.replace(&placeholder, &value_str);
102        }
103
104        // Check for unreplaced {variable} placeholders
105        let unreplaced: Vec<&str> = TEMPLATE_VAR_RE
106            .captures_iter(&prompt)
107            .filter_map(|c| c.get(1).map(|m| m.as_str()))
108            .collect();
109
110        if !unreplaced.is_empty() {
111            return Err(ChainError::ExecutionError(format!(
112                "Prompt template has unreplaced variable(s): {}",
113                unreplaced.join(", ")
114            )));
115        }
116
117        Ok(prompt)
118    }
119}
120
121#[async_trait]
122impl<M: BaseChatModel + Send + Sync + 'static> BaseChain for LLMChain<M>
123where
124    <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
125{
126    fn input_keys(&self) -> Vec<&str> {
127        vec![&self.input_key]
128    }
129
130    fn output_keys(&self) -> Vec<&str> {
131        vec![&self.output_key]
132    }
133
134    async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
135        self.validate_inputs(&inputs)?;
136
137        let prompt = self.render_prompt(&inputs)?;
138
139        let messages = vec![Message::human(&prompt)];
140        let result = self
141            .llm
142            .invoke(messages, None)
143            .await
144            .map_err(|e| ChainError::ExecutionError(format!("LLM call failed: {}", e)))?;
145
146        let mut output = HashMap::new();
147        output.insert(self.output_key.clone(), Value::String(result.content));
148
149        Ok(output)
150    }
151
152    /// Execute the Chain with callback propagation.
153    ///
154    /// Fires `on_chain_start` → `on_llm_start` → LLM call → `on_llm_end` → `on_chain_end`.
155    /// On error, fires `on_llm_error` / `on_chain_error` instead.
156    async fn invoke_with_config(
157        &self,
158        inputs: HashMap<String, Value>,
159        config: Option<RunnableConfig>,
160    ) -> Result<ChainResult, ChainError> {
161        self.validate_inputs(&inputs)?;
162
163        let callbacks = config.as_ref().and_then(|c| c.callbacks.clone());
164
165        // Create root RunTree for this chain invocation
166        let mut run = RunTree::new(
167            self.name(),
168            RunType::Chain,
169            json!({ "inputs": inputs }),
170        );
171
172        // on_chain_start
173        if let Some(ref cb) = callbacks {
174            cb.dispatch_chain_start(&run, &run.inputs).await;
175        }
176
177        let prompt = self.render_prompt(&inputs)?;
178        let messages = vec![Message::human(&prompt)];
179
180        // on_llm_start
181        if let Some(ref cb) = callbacks {
182            let llm_run = run.create_child(
183                format!("{}.llm", self.name()),
184                RunType::Llm,
185                json!({"messages_count": messages.len()}),
186            );
187            cb.dispatch_llm_start(&llm_run, &messages).await;
188        }
189
190        // LLM call with config propagation
191        let llm_config = config.clone();
192        let result = self.llm.invoke(messages, llm_config).await;
193
194        match result {
195            Ok(llm_result) => {
196                // on_llm_end
197                if let Some(ref cb) = callbacks {
198                    let llm_run = run.create_child(
199                        format!("{}.llm", self.name()),
200                        RunType::Llm,
201                        json!({"response": llm_result.content}),
202                    );
203                    cb.dispatch_llm_end(&llm_run, &llm_result.content).await;
204                }
205
206                let mut output = HashMap::new();
207                output.insert(self.output_key.clone(), Value::String(llm_result.content.clone()));
208
209                run.end(json!({"output": &llm_result.content}));
210
211                // on_chain_end
212                if let Some(ref cb) = callbacks {
213                    cb.dispatch_chain_end(&run, &json!({"output": llm_result.content})).await;
214                }
215
216                Ok(output)
217            }
218            Err(e) => {
219                let err_msg = e.to_string();
220
221                // on_llm_error
222                if let Some(ref cb) = callbacks {
223                    let llm_run = run.create_child(
224                        format!("{}.llm", self.name()),
225                        RunType::Llm,
226                        json!({"error": &err_msg}),
227                    );
228                    cb.dispatch_llm_error(&llm_run, &err_msg).await;
229                }
230
231                run.end_with_error(err_msg.clone());
232
233                // on_chain_error
234                if let Some(ref cb) = callbacks {
235                    cb.dispatch_chain_error(&run, &err_msg).await;
236                }
237
238                Err(ChainError::ExecutionError(format!("LLM call failed: {}", err_msg)))
239            }
240        }
241    }
242
243    /// Stream execution for LLMChain -- token by token output.
244    async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
245        self.validate_inputs(&inputs)?;
246
247        let prompt = self.render_prompt(&inputs)?;
248
249        let messages = vec![Message::human(&prompt)];
250        let llm_stream = self
251            .llm
252            .stream_chat(messages, None)
253            .await
254            .map_err(|e| ChainError::StreamError(format!("LLM stream failed: {}", e)))?;
255
256        let stream = llm_stream.map(move |result| match result {
257            Ok(token) => Ok(StreamToken {
258                token,
259                is_final: false,
260            }),
261            Err(e) => Err(ChainError::StreamError(format!(
262                "Stream token error: {}",
263                e
264            ))),
265        });
266
267        let final_stream = stream.chain(futures_util::stream::once(async move {
268            Ok(StreamToken {
269                token: String::new(),
270                is_final: true,
271            })
272        }));
273
274        Ok(Box::pin(final_stream))
275    }
276
277    fn name(&self) -> &str {
278        &self.name
279    }
280}
281
282/// LLMChain Builder.
283///
284/// Convenience builder for LLMChain.
285pub struct LLMChainBuilder<M: BaseChatModel> {
286    llm: M,
287    prompt_template: String,
288    input_key: Option<String>,
289    output_key: Option<String>,
290    name: Option<String>,
291}
292
293impl<M: BaseChatModel> LLMChainBuilder<M> {
294    pub fn new(llm: M, prompt_template: impl Into<String>) -> Self {
295        Self {
296            llm,
297            prompt_template: prompt_template.into(),
298            input_key: None,
299            output_key: None,
300            name: None,
301        }
302    }
303
304    pub fn input_key(mut self, key: impl Into<String>) -> Self {
305        self.input_key = Some(key.into());
306        self
307    }
308
309    pub fn output_key(mut self, key: impl Into<String>) -> Self {
310        self.output_key = Some(key.into());
311        self
312    }
313
314    pub fn name(mut self, name: impl Into<String>) -> Self {
315        self.name = Some(name.into());
316        self
317    }
318
319    pub fn build(self) -> LLMChain<M> {
320        let mut chain = LLMChain::new(self.llm, self.prompt_template);
321
322        if let Some(key) = self.input_key {
323            chain = chain.with_input_key(key);
324        }
325
326        if let Some(key) = self.output_key {
327            chain = chain.with_output_key(key);
328        }
329
330        if let Some(name) = self.name {
331            chain = chain.with_name(name);
332        }
333
334        chain
335    }
336}