1use 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
20pub struct LLMChain<M: BaseChatModel> {
34 llm: M,
36
37 prompt_template: String,
39
40 input_key: String,
42
43 output_key: String,
45
46 name: String,
48}
49
50static 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 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 pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
72 self.input_key = key.into();
73 self
74 }
75
76 pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
78 self.output_key = key.into();
79 self
80 }
81
82 pub fn with_name(mut self, name: impl Into<String>) -> Self {
84 self.name = name.into();
85 self
86 }
87
88 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 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 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 let mut run = RunTree::new(
167 self.name(),
168 RunType::Chain,
169 json!({ "inputs": inputs }),
170 );
171
172 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 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 let llm_config = config.clone();
192 let result = self.llm.invoke(messages, llm_config).await;
193
194 match result {
195 Ok(llm_result) => {
196 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 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 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 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 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
282pub 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}