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(self.name(), RunType::Chain, json!({ "inputs": inputs }));
167
168        // on_chain_start
169        if let Some(ref cb) = callbacks {
170            cb.dispatch_chain_start(&run, &run.inputs).await;
171        }
172
173        let prompt = self.render_prompt(&inputs)?;
174        let messages = vec![Message::human(&prompt)];
175
176        // on_llm_start — single child run reused for both on_llm_end and
177        // on_llm_error, so the trace has exactly one LLM node per call
178        // (previously each callback created its own child, producing duplicate runs).
179        let mut llm_run = run.create_child(
180            format!("{}.llm", self.name()),
181            RunType::Llm,
182            json!({"messages_count": messages.len()}),
183        );
184        if let Some(ref cb) = callbacks {
185            cb.dispatch_llm_start(&llm_run, &messages).await;
186        }
187
188        // LLM call with config propagation
189        let llm_config = config.clone();
190        let result = self.llm.invoke(messages, llm_config).await;
191
192        match result {
193            Ok(llm_result) => {
194                // on_llm_end
195                llm_run.end(json!({"response": &llm_result.content}));
196                if let Some(ref cb) = callbacks {
197                    cb.dispatch_llm_end(&llm_run, &llm_result.content).await;
198                }
199
200                let mut output = HashMap::new();
201                output.insert(
202                    self.output_key.clone(),
203                    Value::String(llm_result.content.clone()),
204                );
205
206                run.end(json!({"output": &llm_result.content}));
207
208                // on_chain_end
209                if let Some(ref cb) = callbacks {
210                    cb.dispatch_chain_end(&run, &json!({"output": llm_result.content}))
211                        .await;
212                }
213
214                Ok(output)
215            }
216            Err(e) => {
217                let err_msg = e.to_string();
218
219                // on_llm_error
220                llm_run.end_with_error(err_msg.clone());
221                if let Some(ref cb) = callbacks {
222                    cb.dispatch_llm_error(&llm_run, &err_msg).await;
223                }
224
225                run.end_with_error(err_msg.clone());
226
227                // on_chain_error
228                if let Some(ref cb) = callbacks {
229                    cb.dispatch_chain_error(&run, &err_msg).await;
230                }
231
232                Err(ChainError::ExecutionError(format!(
233                    "LLM call failed: {}",
234                    err_msg
235                )))
236            }
237        }
238    }
239
240    /// Stream execution for LLMChain -- token by token output.
241    async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
242        self.validate_inputs(&inputs)?;
243
244        let prompt = self.render_prompt(&inputs)?;
245
246        let messages = vec![Message::human(&prompt)];
247        let llm_stream = self
248            .llm
249            .stream_chat(messages, None)
250            .await
251            .map_err(|e| ChainError::StreamError(format!("LLM stream failed: {}", e)))?;
252
253        let stream = llm_stream.map(move |result| match result {
254            Ok(token) => Ok(StreamToken {
255                token,
256                is_final: false,
257            }),
258            Err(e) => Err(ChainError::StreamError(format!(
259                "Stream token error: {}",
260                e
261            ))),
262        });
263
264        let final_stream = stream.chain(futures_util::stream::once(async move {
265            Ok(StreamToken {
266                token: String::new(),
267                is_final: true,
268            })
269        }));
270
271        Ok(Box::pin(final_stream))
272    }
273
274    fn name(&self) -> &str {
275        &self.name
276    }
277}
278
279/// LLMChain Builder.
280///
281/// Convenience builder for LLMChain.
282pub struct LLMChainBuilder<M: BaseChatModel> {
283    llm: M,
284    prompt_template: String,
285    input_key: Option<String>,
286    output_key: Option<String>,
287    name: Option<String>,
288}
289
290impl<M: BaseChatModel> LLMChainBuilder<M> {
291    pub fn new(llm: M, prompt_template: impl Into<String>) -> Self {
292        Self {
293            llm,
294            prompt_template: prompt_template.into(),
295            input_key: None,
296            output_key: None,
297            name: None,
298        }
299    }
300
301    pub fn input_key(mut self, key: impl Into<String>) -> Self {
302        self.input_key = Some(key.into());
303        self
304    }
305
306    pub fn output_key(mut self, key: impl Into<String>) -> Self {
307        self.output_key = Some(key.into());
308        self
309    }
310
311    pub fn name(mut self, name: impl Into<String>) -> Self {
312        self.name = Some(name.into());
313        self
314    }
315
316    pub fn build(self) -> LLMChain<M> {
317        let mut chain = LLMChain::new(self.llm, self.prompt_template);
318
319        if let Some(key) = self.input_key {
320            chain = chain.with_input_key(key);
321        }
322
323        if let Some(key) = self.output_key {
324            chain = chain.with_output_key(key);
325        }
326
327        if let Some(name) = self.name {
328            chain = chain.with_name(name);
329        }
330
331        chain
332    }
333}