1use 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
18pub struct LLMChain<M: BaseChatModel> {
32 llm: M,
34
35 prompt_template: String,
37
38 input_key: String,
40
41 output_key: String,
43
44 name: String,
46}
47
48static 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 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 pub fn with_input_key(mut self, key: impl Into<String>) -> Self {
70 self.input_key = key.into();
71 self
72 }
73
74 pub fn with_output_key(mut self, key: impl Into<String>) -> Self {
76 self.output_key = key.into();
77 self
78 }
79
80 pub fn with_name(mut self, name: impl Into<String>) -> Self {
82 self.name = name.into();
83 self
84 }
85
86 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 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 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
189pub 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}