1use async_trait::async_trait;
7use futures_util::StreamExt;
8use lc_callbacks::{RunTree, RunType};
9use lc_core::runnables::RunnableConfig;
10use lc_core::BaseChatModel;
11use lc_providers::{wrap_chat_model, ProviderError};
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};
19use crate::BoxedChatModel;
20
21pub struct LLMChain {
35 llm: BoxedChatModel,
37
38 prompt_template: String,
40
41 input_key: String,
43
44 output_key: String,
46
47 name: String,
49}
50
51static TEMPLATE_VAR_RE: LazyLock<Regex> =
53 LazyLock::new(|| Regex::new(r"\{([a-zA-Z_][a-zA-Z0-9_]*)\}").unwrap());
54
55impl LLMChain {
56 pub fn new<L>(llm: L, prompt_template: impl Into<String>) -> Self
62 where
63 L: BaseChatModel + Send + Sync + 'static,
64 L::Error: Into<ProviderError>,
65 {
66 Self::from_wrapped(wrap_chat_model(llm), prompt_template)
67 }
68
69 pub(crate) fn from_wrapped(llm: BoxedChatModel, prompt_template: impl Into<String>) -> Self {
71 Self {
72 llm,
73 prompt_template: prompt_template.into(),
74 input_key: "question".to_string(),
75 output_key: "text".to_string(),
76 name: "llm_chain".to_string(),
77 }
78 }
79
80 pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
82 self.input_key = key.into();
83 self
84 }
85
86 pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
88 self.output_key = key.into();
89 self
90 }
91
92 pub fn with_name(mut self, name: impl Into<String>) -> Self {
94 self.name = name.into();
95 self
96 }
97
98 fn render_prompt(&self, inputs: &HashMap<String, Value>) -> Result<String, ChainError> {
103 let mut prompt = self.prompt_template.clone();
104
105 for (key, value) in inputs {
106 let placeholder = format!("{{{}}}", key);
107 let value_str = match value {
108 Value::String(s) => s.clone(),
109 _ => value.to_string(),
110 };
111 prompt = prompt.replace(&placeholder, &value_str);
112 }
113
114 let unreplaced: Vec<&str> = TEMPLATE_VAR_RE
116 .captures_iter(&prompt)
117 .filter_map(|c| c.get(1).map(|m| m.as_str()))
118 .collect();
119
120 if !unreplaced.is_empty() {
121 return Err(ChainError::ExecutionError(format!(
122 "Prompt template has unreplaced variable(s): {}",
123 unreplaced.join(", ")
124 )));
125 }
126
127 Ok(prompt)
128 }
129}
130
131#[async_trait]
132impl BaseChain for LLMChain {
133 fn input_keys(&self) -> Vec<&str> {
134 vec![&self.input_key]
135 }
136
137 fn output_keys(&self) -> Vec<&str> {
138 vec![&self.output_key]
139 }
140
141 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
142 self.validate_inputs(&inputs)?;
143
144 let prompt = self.render_prompt(&inputs)?;
145
146 let messages = vec![Message::human(&prompt)];
147 let result = self
148 .llm
149 .invoke(messages, None)
150 .await
151 .map_err(|e| ChainError::ExecutionError(format!("LLM call failed: {}", e)))?;
152
153 let mut output = HashMap::new();
154 output.insert(self.output_key.clone(), Value::String(result.content));
155
156 Ok(output)
157 }
158
159 async fn invoke_with_config(
164 &self,
165 inputs: HashMap<String, Value>,
166 config: Option<RunnableConfig>,
167 ) -> Result<ChainResult, ChainError> {
168 self.validate_inputs(&inputs)?;
169
170 let callbacks = config.as_ref().and_then(|c| c.callbacks.clone());
171
172 let mut run = RunTree::new(self.name(), RunType::Chain, json!({ "inputs": inputs }));
174
175 if let Some(ref cb) = callbacks {
177 cb.dispatch_chain_start(&run, &run.inputs).await;
178 }
179
180 let prompt = self.render_prompt(&inputs)?;
181 let messages = vec![Message::human(&prompt)];
182
183 let mut llm_run = run.create_child(
187 format!("{}.llm", self.name()),
188 RunType::Llm,
189 json!({"messages_count": messages.len()}),
190 );
191 if let Some(ref cb) = callbacks {
192 cb.dispatch_llm_start(&llm_run, &messages).await;
193 }
194
195 let llm_config = config.clone();
197 let result = self.llm.invoke(messages, llm_config).await;
198
199 match result {
200 Ok(llm_result) => {
201 llm_run.end(json!({"response": &llm_result.content}));
203 if let Some(ref cb) = callbacks {
204 cb.dispatch_llm_end(&llm_run, &llm_result.content).await;
205 }
206
207 let mut output = HashMap::new();
208 output.insert(
209 self.output_key.clone(),
210 Value::String(llm_result.content.clone()),
211 );
212
213 run.end(json!({"output": &llm_result.content}));
214
215 if let Some(ref cb) = callbacks {
217 cb.dispatch_chain_end(&run, &json!({"output": llm_result.content}))
218 .await;
219 }
220
221 Ok(output)
222 }
223 Err(e) => {
224 let err_msg = e.to_string();
225
226 llm_run.end_with_error(err_msg.clone());
228 if let Some(ref cb) = callbacks {
229 cb.dispatch_llm_error(&llm_run, &err_msg).await;
230 }
231
232 run.end_with_error(err_msg.clone());
233
234 if let Some(ref cb) = callbacks {
236 cb.dispatch_chain_error(&run, &err_msg).await;
237 }
238
239 Err(ChainError::ExecutionError(format!(
240 "LLM call failed: {}",
241 err_msg
242 )))
243 }
244 }
245 }
246
247 async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
249 self.validate_inputs(&inputs)?;
250
251 let prompt = self.render_prompt(&inputs)?;
252
253 let messages = vec![Message::human(&prompt)];
254 let llm_stream = self
255 .llm
256 .stream_chat(messages, None)
257 .await
258 .map_err(|e| ChainError::StreamError(format!("LLM stream failed: {}", e)))?;
259
260 let stream = llm_stream.map(move |result| match result {
261 Ok(chunk) => Ok(StreamToken {
262 token: chunk.text,
263 is_final: false,
264 }),
265 Err(e) => Err(ChainError::StreamError(format!(
266 "Stream token error: {}",
267 e
268 ))),
269 });
270
271 let final_stream = stream.chain(futures_util::stream::once(async move {
272 Ok(StreamToken {
273 token: String::new(),
274 is_final: true,
275 })
276 }));
277
278 Ok(Box::pin(final_stream))
279 }
280
281 fn name(&self) -> &str {
282 &self.name
283 }
284}
285
286pub struct LLMChainBuilder {
290 llm: BoxedChatModel,
291 prompt_template: String,
292 input_key: Option<String>,
293 output_key: Option<String>,
294 name: Option<String>,
295}
296
297impl LLMChainBuilder {
298 pub fn new<L>(llm: L, prompt_template: impl Into<String>) -> Self
300 where
301 L: BaseChatModel + Send + Sync + 'static,
302 L::Error: Into<ProviderError>,
303 {
304 Self {
305 llm: wrap_chat_model(llm),
306 prompt_template: prompt_template.into(),
307 input_key: None,
308 output_key: None,
309 name: None,
310 }
311 }
312
313 pub fn input_key(mut self, key: impl Into<String>) -> Self {
315 self.input_key = Some(key.into());
316 self
317 }
318
319 pub fn output_key(mut self, key: impl Into<String>) -> Self {
321 self.output_key = Some(key.into());
322 self
323 }
324
325 pub fn name(mut self, name: impl Into<String>) -> Self {
327 self.name = Some(name.into());
328 self
329 }
330
331 pub fn build(self) -> LLMChain {
333 let mut chain = LLMChain::from_wrapped(self.llm, self.prompt_template);
334
335 if let Some(key) = self.input_key {
336 chain = chain.with_input_key(key);
337 }
338
339 if let Some(key) = self.output_key {
340 chain = chain.with_output_key(key);
341 }
342
343 if let Some(name) = self.name {
344 chain = chain.with_name(name);
345 }
346
347 chain
348 }
349}