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_core::language_models::LLMResult;
9use lc_core::{BaseChatModel, Runnable};
10use lc_schema::Message;
11use regex::Regex;
12use serde_json::Value;
13use std::collections::HashMap;
14use std::sync::LazyLock;
15
16use crate::base::{BaseChain, ChainError, ChainResult, ChainStream, StreamToken};
17
18/// LLM Chain
19///
20/// Combines a Prompt template and an LLM. The most basic Chain.
21///
22/// # Examples
23/// ```ignore
24/// use lc_chains::LLMChain;
25///
26/// let chain = LLMChain::new(llm, "{question}");
27///
28/// let inputs = HashMap::from([("question".to_string(), "What is Rust?".into())]);
29/// let result = chain.invoke(inputs).await?;
30/// ```
31pub struct LLMChain<M: BaseChatModel> {
32    /// LLM client.
33    llm: M,
34
35    /// Prompt template.
36    prompt_template: String,
37
38    /// Input key name.
39    input_key: String,
40
41    /// Output key name.
42    output_key: String,
43
44    /// Chain name.
45    name: String,
46}
47
48/// Pre-compiled regex for detecting unreplaced template variables.
49static TEMPLATE_VAR_RE: LazyLock<Regex> =
50    LazyLock::new(|| Regex::new(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}").unwrap());
51
52impl<M: BaseChatModel> LLMChain<M> {
53    /// Create a new LLMChain.
54    ///
55    /// # Arguments
56    /// * `llm` - LLM client (any type implementing BaseChatModel)
57    /// * `prompt_template` - Prompt template string with {variable} placeholders
58    pub fn new(llm: M, prompt_template: impl Into<String>) -> Self {
59        Self {
60            llm,
61            prompt_template: prompt_template.into(),
62            input_key: "question".to_string(),
63            output_key: "text".to_string(),
64            name: "llm_chain".to_string(),
65        }
66    }
67
68    /// Set input key name.
69    pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
70        self.input_key = key.into();
71        self
72    }
73
74    /// Set output key name.
75    pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
76        self.output_key = key.into();
77        self
78    }
79
80    /// Set chain name.
81    pub fn with_name(mut self, name: impl Into<String>) -> Self {
82        self.name = name.into();
83        self
84    }
85
86    /// Render the Prompt template.
87    ///
88    /// Validates that all {variable} placeholders in the template
89    /// have been replaced. Returns an error if any unreplaced placeholders remain.
90    fn render_prompt(&self, inputs: &HashMap<String, Value>) -> Result<String, ChainError> {
91        let mut prompt = self.prompt_template.clone();
92
93        for (key, value) in inputs {
94            let placeholder = format!("{{{}}}", key);
95            let value_str = match value {
96                Value::String(s) => s.clone(),
97                _ => value.to_string(),
98            };
99            prompt = prompt.replace(&placeholder, &value_str);
100        }
101
102        // Check for unreplaced {variable} placeholders
103        let unreplaced: Vec<&str> = TEMPLATE_VAR_RE
104            .captures_iter(&prompt)
105            .filter_map(|c| c.get(1).map(|m| m.as_str()))
106            .collect();
107
108        if !unreplaced.is_empty() {
109            return Err(ChainError::ExecutionError(format!(
110                "Prompt template has unreplaced variable(s): {}",
111                unreplaced.join(", ")
112            )));
113        }
114
115        Ok(prompt)
116    }
117}
118
119#[async_trait]
120impl<M: BaseChatModel + Send + Sync + 'static> BaseChain for LLMChain<M>
121where
122    <M as Runnable<Vec<Message>, LLMResult>>::Error: std::fmt::Display,
123{
124    fn input_keys(&self) -> Vec<&str> {
125        vec![&self.input_key]
126    }
127
128    fn output_keys(&self) -> Vec<&str> {
129        vec![&self.output_key]
130    }
131
132    async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
133        self.validate_inputs(&inputs)?;
134
135        let prompt = self.render_prompt(&inputs)?;
136
137        let messages = vec![Message::human(&prompt)];
138        let result = self
139            .llm
140            .invoke(messages, None)
141            .await
142            .map_err(|e| ChainError::ExecutionError(format!("LLM call failed: {}", e)))?;
143
144        let mut output = HashMap::new();
145        output.insert(self.output_key.clone(), Value::String(result.content));
146
147        Ok(output)
148    }
149
150    /// Stream execution for LLMChain -- token by token output.
151    async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
152        self.validate_inputs(&inputs)?;
153
154        let prompt = self.render_prompt(&inputs)?;
155
156        let messages = vec![Message::human(&prompt)];
157        let llm_stream = self
158            .llm
159            .stream_chat(messages, None)
160            .await
161            .map_err(|e| ChainError::StreamError(format!("LLM stream failed: {}", e)))?;
162
163        let stream = llm_stream.map(move |result| match result {
164            Ok(token) => Ok(StreamToken {
165                token,
166                is_final: false,
167            }),
168            Err(e) => Err(ChainError::StreamError(format!(
169                "Stream token error: {}",
170                e
171            ))),
172        });
173
174        let final_stream = stream.chain(futures_util::stream::once(async move {
175            Ok(StreamToken {
176                token: String::new(),
177                is_final: true,
178            })
179        }));
180
181        Ok(Box::pin(final_stream))
182    }
183
184    fn name(&self) -> &str {
185        &self.name
186    }
187}
188
189/// LLMChain Builder.
190///
191/// Convenience builder for LLMChain.
192pub struct LLMChainBuilder<M: BaseChatModel> {
193    llm: M,
194    prompt_template: String,
195    input_key: Option<String>,
196    output_key: Option<String>,
197    name: Option<String>,
198}
199
200impl<M: BaseChatModel> LLMChainBuilder<M> {
201    pub fn new(llm: M, prompt_template: impl Into<String>) -> Self {
202        Self {
203            llm,
204            prompt_template: prompt_template.into(),
205            input_key: None,
206            output_key: None,
207            name: None,
208        }
209    }
210
211    pub fn input_key(mut self, key: impl Into<String>) -> Self {
212        self.input_key = Some(key.into());
213        self
214    }
215
216    pub fn output_key(mut self, key: impl Into<String>) -> Self {
217        self.output_key = Some(key.into());
218        self
219    }
220
221    pub fn name(mut self, name: impl Into<String>) -> Self {
222        self.name = Some(name.into());
223        self
224    }
225
226    pub fn build(self) -> LLMChain<M> {
227        let mut chain = LLMChain::new(self.llm, self.prompt_template);
228
229        if let Some(key) = self.input_key {
230            chain = chain.with_input_key(key);
231        }
232
233        if let Some(key) = self.output_key {
234            chain = chain.with_output_key(key);
235        }
236
237        if let Some(name) = self.name {
238            chain = chain.with_name(name);
239        }
240
241        chain
242    }
243}