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!("Stream token error: {}", e))),
77 });
78 let final_stream = stream.chain(futures_util::stream::once(async move {
79 Ok(StreamToken {
80 token: String::new(),
81 is_final: true,
82 })
83 }));
84 Ok(Box::pin(final_stream))
85 }
86
87 pub fn new<L>(llm: L, prompt_template: impl Into<String>) -> Self
93 where
94 L: BaseChatModel + Send + Sync + 'static,
95 L::Error: Into<ProviderError>,
96 {
97 Self::from_wrapped(wrap_chat_model(llm), prompt_template)
98 }
99
100 pub(crate) fn from_wrapped(llm: BoxedChatModel, prompt_template: impl Into<String>) -> Self {
102 Self {
103 llm,
104 prompt_template: prompt_template.into(),
105 input_key: "question".to_string(),
106 output_key: "text".to_string(),
107 name: "llm_chain".to_string(),
108 }
109 }
110
111 pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
113 self.input_key = key.into();
114 self
115 }
116
117 pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
119 self.output_key = key.into();
120 self
121 }
122
123 pub fn with_name(mut self, name: impl Into<String>) -> Self {
125 self.name = name.into();
126 self
127 }
128
129 fn render_prompt(&self, inputs: &HashMap<String, Value>) -> Result<String, ChainError> {
137 let mut vars = HashMap::with_capacity(inputs.len());
138 for (key, value) in inputs {
139 let value_str = match value {
140 Value::String(s) => s.clone(),
141 _ => value.to_string(),
142 };
143 vars.insert(key.clone(), value_str);
144 }
145
146 let (prompt, missing) = substitute_template(&self.prompt_template, &vars);
147
148 if !missing.is_empty() {
149 return Err(ChainError::ExecutionError(format!(
150 "Prompt template has unreplaced variable(s): {}",
151 missing.join(", ")
152 )));
153 }
154
155 Ok(prompt)
156 }
157}
158
159#[async_trait]
160impl BaseChain for LLMChain {
161 fn input_keys(&self) -> Vec<&str> {
162 vec![&self.input_key]
163 }
164
165 fn output_keys(&self) -> Vec<&str> {
166 vec![&self.output_key]
167 }
168
169 async fn invoke(&self, inputs: HashMap<String, Value>) -> Result<ChainResult, ChainError> {
170 self.validate_inputs(&inputs)?;
171
172 let prompt = self.render_prompt(&inputs)?;
173
174 let messages = vec![Message::human(&prompt)];
175 let result = self
176 .llm
177 .invoke(messages, None)
178 .await
179 .map_err(|e| ChainError::ExecutionError(format!("LLM call failed: {}", e)))?;
180
181 let mut output = HashMap::new();
182 output.insert(self.output_key.clone(), Value::String(result.content));
183
184 Ok(output)
185 }
186
187 async fn invoke_with_config(
192 &self,
193 inputs: HashMap<String, Value>,
194 config: Option<RunnableConfig>,
195 ) -> Result<ChainResult, ChainError> {
196 self.validate_inputs(&inputs)?;
197
198 let callbacks = config.as_ref().and_then(|c| c.callbacks.clone());
199
200 let mut run = RunTree::new(self.name(), RunType::Chain, json!({ "inputs": inputs }));
202
203 if let Some(ref cb) = callbacks {
205 cb.dispatch_chain_start(&run, &run.inputs).await;
206 }
207
208 let prompt = match self.render_prompt(&inputs) {
213 Ok(p) => p,
214 Err(e) => {
215 let msg = e.to_string();
216 run.end_with_error(msg.clone());
217 if let Some(ref cb) = callbacks {
218 cb.dispatch_chain_error(&run, &msg).await;
219 }
220 return Err(e);
221 }
222 };
223 let messages = vec![Message::human(&prompt)];
224
225 let mut llm_run = run.create_child(
229 format!("{}.llm", self.name()),
230 RunType::Llm,
231 json!({"messages_count": messages.len()}),
232 );
233 if let Some(ref cb) = callbacks {
234 cb.dispatch_llm_start(&llm_run, &messages).await;
235 }
236
237 let llm_config = config.clone();
239 let result = self.llm.invoke(messages, llm_config).await;
240
241 match result {
242 Ok(llm_result) => {
243 llm_run.end(json!({"response": &llm_result.content}));
245 if let Some(ref cb) = callbacks {
246 cb.dispatch_llm_end(&llm_run, &llm_result.content).await;
247 }
248
249 let mut output = HashMap::new();
250 output.insert(
251 self.output_key.clone(),
252 Value::String(llm_result.content.clone()),
253 );
254
255 run.end(json!({"output": &llm_result.content}));
256
257 if let Some(ref cb) = callbacks {
259 cb.dispatch_chain_end(&run, &json!({"output": llm_result.content}))
260 .await;
261 }
262
263 Ok(output)
264 }
265 Err(e) => {
266 let err_msg = e.to_string();
267
268 llm_run.end_with_error(err_msg.clone());
270 if let Some(ref cb) = callbacks {
271 cb.dispatch_llm_error(&llm_run, &err_msg).await;
272 }
273
274 run.end_with_error(err_msg.clone());
275
276 if let Some(ref cb) = callbacks {
278 cb.dispatch_chain_error(&run, &err_msg).await;
279 }
280
281 Err(ChainError::ExecutionError(format!(
282 "LLM call failed: {}",
283 err_msg
284 )))
285 }
286 }
287 }
288
289 async fn stream(&self, inputs: HashMap<String, Value>) -> Result<ChainStream, ChainError> {
291 self.stream_body(inputs, None).await
292 }
293
294 async fn stream_with_config(
301 &self,
302 inputs: HashMap<String, Value>,
303 config: Option<RunnableConfig>,
304 ) -> Result<ChainStream, ChainError> {
305 let output_key = Some(self.output_key.clone());
306 stream_chain_with_callbacks(
307 self.name(),
308 inputs,
309 config.clone(),
310 output_key,
311 |inputs| async move { self.stream_body(inputs, config).await },
312 )
313 .await
314 }
315
316 fn name(&self) -> &str {
317 &self.name
318 }
319}
320
321pub struct LLMChainBuilder {
325 llm: BoxedChatModel,
326 prompt_template: String,
327 input_key: Option<String>,
328 output_key: Option<String>,
329 name: Option<String>,
330}
331
332impl LLMChainBuilder {
333 pub fn new<L>(llm: L, prompt_template: impl Into<String>) -> Self
335 where
336 L: BaseChatModel + Send + Sync + 'static,
337 L::Error: Into<ProviderError>,
338 {
339 Self {
340 llm: wrap_chat_model(llm),
341 prompt_template: prompt_template.into(),
342 input_key: None,
343 output_key: None,
344 name: None,
345 }
346 }
347
348 pub fn input_key(mut self, key: impl Into<String>) -> Self {
350 self.input_key = Some(key.into());
351 self
352 }
353
354 pub fn output_key(mut self, key: impl Into<String>) -> Self {
356 self.output_key = Some(key.into());
357 self
358 }
359
360 pub fn name(mut self, name: impl Into<String>) -> Self {
362 self.name = Some(name.into());
363 self
364 }
365
366 pub fn build(self) -> LLMChain {
368 let mut chain = LLMChain::from_wrapped(self.llm, self.prompt_template);
369
370 if let Some(key) = self.input_key {
371 chain = chain.with_input_key(key);
372 }
373
374 if let Some(key) = self.output_key {
375 chain = chain.with_output_key(key);
376 }
377
378 if let Some(name) = self.name {
379 chain = chain.with_name(name);
380 }
381
382 chain
383 }
384}