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 serde_json::{json, Value};
14use std::collections::HashMap;
15
16use crate::base::{
17 stream_chain_with_callbacks, substitute_template, BaseChain, ChainError, ChainResult,
18 ChainStream, StreamToken,
19};
20use crate::BoxedChatModel;
21
22pub struct LLMChain {
36 llm: BoxedChatModel,
38
39 prompt_template: String,
41
42 input_key: String,
44
45 output_key: String,
47
48 name: String,
50}
51
52impl LLMChain {
53 async fn stream_body(
56 &self,
57 inputs: HashMap<String, Value>,
58 config: Option<RunnableConfig>,
59 ) -> Result<ChainStream, ChainError> {
60 self.validate_inputs(&inputs)?;
61 if config.as_ref().is_some_and(|c| c.is_cancelled()) {
62 return Err(ChainError::StreamError("Operation cancelled".to_string()));
63 }
64 let prompt = self.render_prompt(&inputs)?;
65 let messages = vec![Message::human(&prompt)];
66 let llm_stream = self
67 .llm
68 .stream_chat(messages, config)
69 .await
70 .map_err(|e| ChainError::StreamError(format!("LLM stream failed: {}", e)))?;
71 let stream = llm_stream.map(move |result| match result {
72 Ok(chunk) => Ok(StreamToken {
73 token: chunk.text,
74 is_final: false,
75 }),
76 Err(e) => Err(ChainError::StreamError(format!(
77 "Stream token error: {}",
78 e
79 ))),
80 });
81 let final_stream = stream.chain(futures_util::stream::once(async move {
82 Ok(StreamToken {
83 token: String::new(),
84 is_final: true,
85 })
86 }));
87 Ok(Box::pin(final_stream))
88 }
89
90 pub fn new<L>(llm: L, prompt_template: impl Into<String>) -> Self
96 where
97 L: BaseChatModel + Send + Sync + 'static,
98 L::Error: Into<ProviderError>,
99 {
100 Self::from_wrapped(wrap_chat_model(llm), prompt_template)
101 }
102
103 pub(crate) fn from_wrapped(llm: BoxedChatModel, prompt_template: impl Into<String>) -> Self {
105 Self {
106 llm,
107 prompt_template: prompt_template.into(),
108 input_key: "question".to_string(),
109 output_key: "text".to_string(),
110 name: "llm_chain".to_string(),
111 }
112 }
113
114 pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
116 self.input_key = key.into();
117 self
118 }
119
120 pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
122 self.output_key = key.into();
123 self
124 }
125
126 pub fn with_name(mut self, name: impl Into<String>) -> Self {
128 self.name = name.into();
129 self
130 }
131
132 fn render_prompt(&self, inputs: &HashMap<String, Value>) -> Result<String, ChainError> {
140 let mut vars = HashMap::with_capacity(inputs.len());
141 for (key, value) in inputs {
142 let value_str = match value {
143 Value::String(s) => s.clone(),
144 _ => value.to_string(),
145 };
146 vars.insert(key.clone(), value_str);
147 }
148
149 let (prompt, missing) = substitute_template(&self.prompt_template, &vars);
150
151 if !missing.is_empty() {
152 return Err(ChainError::ExecutionError(format!(
153 "Prompt template has unreplaced variable(s): {}",
154 missing.join(", ")
155 )));
156 }
157
158 Ok(prompt)
159 }
160}
161
162#[async_trait]
163impl BaseChain for LLMChain {
164 fn input_keys(&self) -> Vec<&str> {
165 vec![&self.input_key]
166 }
167
168 fn output_keys(&self) -> Vec<&str> {
169 vec![&self.output_key]
170 }
171
172 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
173 self.validate_inputs(&inputs)?;
174
175 let prompt = self.render_prompt(&inputs)?;
176
177 let messages = vec![Message::human(&prompt)];
178 let result = self
179 .llm
180 .invoke(messages, None)
181 .await
182 .map_err(|e| ChainError::ExecutionError(format!("LLM call failed: {}", e)))?;
183
184 let mut output = HashMap::new();
185 output.insert(self.output_key.clone(), Value::String(result.content));
186
187 Ok(output)
188 }
189
190 async fn invoke_with_config(
195 &self,
196 inputs: HashMap<String, Value>,
197 config: Option<RunnableConfig>,
198 ) -> Result<ChainResult, ChainError> {
199 self.validate_inputs(&inputs)?;
200
201 let callbacks = config.as_ref().and_then(|c| c.callbacks.clone());
202
203 let mut run = RunTree::new(self.name(), RunType::Chain, json!({ "inputs": inputs }));
205
206 if let Some(ref cb) = callbacks {
208 cb.dispatch_chain_start(&run, &run.inputs).await;
209 }
210
211 let prompt = match self.render_prompt(&inputs) {
216 Ok(p) => p,
217 Err(e) => {
218 let msg = e.to_string();
219 run.end_with_error(msg.clone());
220 if let Some(ref cb) = callbacks {
221 cb.dispatch_chain_error(&run, &msg).await;
222 }
223 return Err(e);
224 }
225 };
226 let messages = vec![Message::human(&prompt)];
227
228 let mut llm_run = run.create_child(
232 format!("{}.llm", self.name()),
233 RunType::Llm,
234 json!({"messages_count": messages.len()}),
235 );
236 if let Some(ref cb) = callbacks {
237 cb.dispatch_llm_start(&llm_run, &messages).await;
238 }
239
240 let llm_config = config.clone();
242 let result = self.llm.invoke(messages, llm_config).await;
243
244 match result {
245 Ok(llm_result) => {
246 llm_run.end(json!({"response": &llm_result.content}));
248 if let Some(ref cb) = callbacks {
249 cb.dispatch_llm_end(&llm_run, &llm_result.content).await;
250 }
251
252 let mut output = HashMap::new();
253 output.insert(
254 self.output_key.clone(),
255 Value::String(llm_result.content.clone()),
256 );
257
258 run.end(json!({"output": &llm_result.content}));
259
260 if let Some(ref cb) = callbacks {
262 cb.dispatch_chain_end(&run, &json!({"output": llm_result.content}))
263 .await;
264 }
265
266 Ok(output)
267 }
268 Err(e) => {
269 let err_msg = e.to_string();
270
271 llm_run.end_with_error(err_msg.clone());
273 if let Some(ref cb) = callbacks {
274 cb.dispatch_llm_error(&llm_run, &err_msg).await;
275 }
276
277 run.end_with_error(err_msg.clone());
278
279 if let Some(ref cb) = callbacks {
281 cb.dispatch_chain_error(&run, &err_msg).await;
282 }
283
284 Err(ChainError::ExecutionError(format!(
285 "LLM call failed: {}",
286 err_msg
287 )))
288 }
289 }
290 }
291
292 async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
294 self.stream_body(inputs, None).await
295 }
296
297 async fn stream_with_config(
304 &self,
305 inputs: HashMap<String, Value>,
306 config: Option<RunnableConfig>,
307 ) -> Result<ChainStream, ChainError> {
308 let output_key = Some(self.output_key.clone());
309 stream_chain_with_callbacks(
310 self.name(),
311 inputs,
312 config.clone(),
313 output_key,
314 |inputs| async move { self.stream_body(inputs, config).await },
315 )
316 .await
317 }
318
319 fn name(&self) -> &str {
320 &self.name
321 }
322}
323
324pub struct LLMChainBuilder {
328 llm: BoxedChatModel,
329 prompt_template: String,
330 input_key: Option<String>,
331 output_key: Option<String>,
332 name: Option<String>,
333}
334
335impl LLMChainBuilder {
336 pub fn new<L>(llm: L, prompt_template: impl Into<String>) -> Self
338 where
339 L: BaseChatModel + Send + Sync + 'static,
340 L::Error: Into<ProviderError>,
341 {
342 Self {
343 llm: wrap_chat_model(llm),
344 prompt_template: prompt_template.into(),
345 input_key: None,
346 output_key: None,
347 name: None,
348 }
349 }
350
351 pub fn input_key(mut self, key: impl Into<String>) -> Self {
353 self.input_key = Some(key.into());
354 self
355 }
356
357 pub fn output_key(mut self, key: impl Into<String>) -> Self {
359 self.output_key = Some(key.into());
360 self
361 }
362
363 pub fn name(mut self, name: impl Into<String>) -> Self {
365 self.name = Some(name.into());
366 self
367 }
368
369 pub fn build(self) -> LLMChain {
371 let mut chain = LLMChain::from_wrapped(self.llm, self.prompt_template);
372
373 if let Some(key) = self.input_key {
374 chain = chain.with_input_key(key);
375 }
376
377 if let Some(key) = self.output_key {
378 chain = chain.with_output_key(key);
379 }
380
381 if let Some(name) = self.name {
382 chain = chain.with_name(name);
383 }
384
385 chain
386 }
387}