langchain_rust/llm/cache/
mod.rs1use std::{
4 collections::HashMap,
5 pin::Pin,
6 sync::{Arc, Mutex},
7 time::Duration,
8};
9
10use async_trait::async_trait;
11use futures::{Stream, StreamExt};
12use serde_json::{json, Value};
13use tokio::time::Instant;
14
15use crate::{
16 language_models::{
17 box_llm_events, llm::LLM, options::CallOptions, result_events, GenerateResult, LLMError,
18 LLMEvent, LLMEventStream,
19 },
20 schemas::{Message, StreamData},
21};
22
23#[async_trait]
29pub trait LLMCache: Send + Sync {
30 async fn get(&self, key: &str) -> Option<GenerateResult>;
32 async fn put(&self, key: &str, value: GenerateResult);
34}
35
36struct Entry {
37 value: GenerateResult,
38 inserted: Instant,
39 last_used: u64,
40}
41
42#[derive(Default)]
43struct InMemoryState {
44 entries: HashMap<String, Entry>,
45 clock: u64,
46}
47
48#[derive(Default)]
54pub struct InMemoryLLMCache {
55 state: Mutex<InMemoryState>,
56 capacity: Option<usize>,
57 ttl: Option<Duration>,
58}
59
60impl InMemoryLLMCache {
61 pub fn new() -> Self {
62 Self::default()
63 }
64
65 pub fn with_capacity(mut self, capacity: usize) -> Self {
67 self.capacity = Some(capacity);
68 self
69 }
70
71 pub fn with_ttl(mut self, ttl: Duration) -> Self {
73 self.ttl = Some(ttl);
74 self
75 }
76
77 pub fn len(&self) -> usize {
80 self.state.lock().unwrap().entries.len()
81 }
82
83 pub fn is_empty(&self) -> bool {
84 self.len() == 0
85 }
86
87 pub fn clear(&self) {
88 self.state.lock().unwrap().entries.clear();
89 }
90
91 fn expired(&self, entry: &Entry, now: Instant) -> bool {
92 self.ttl
93 .is_some_and(|ttl| now.duration_since(entry.inserted) >= ttl)
94 }
95}
96
97#[async_trait]
98impl LLMCache for InMemoryLLMCache {
99 async fn get(&self, key: &str) -> Option<GenerateResult> {
100 let now = Instant::now();
101 let mut state = self.state.lock().unwrap();
102 state.clock += 1;
103 let clock = state.clock;
104 let expired = self.expired(state.entries.get(key)?, now);
105 if expired {
106 state.entries.remove(key);
107 return None;
108 }
109 let entry = state.entries.get_mut(key)?;
110 entry.last_used = clock;
111 Some(entry.value.clone())
112 }
113
114 async fn put(&self, key: &str, value: GenerateResult) {
115 let now = Instant::now();
116 let mut state = self.state.lock().unwrap();
117 state.clock += 1;
118 let clock = state.clock;
119 if self.capacity == Some(0) {
120 return;
121 }
122 if let Some(capacity) = self.capacity {
123 if !state.entries.contains_key(key) {
124 state.entries.retain(|_, e| !self.expired(e, now));
126 while state.entries.len() >= capacity {
127 let oldest = state
128 .entries
129 .iter()
130 .min_by_key(|(_, e)| e.last_used)
131 .map(|(k, _)| k.clone());
132 match oldest {
133 Some(k) => state.entries.remove(&k),
134 None => break,
135 };
136 }
137 }
138 }
139 state.entries.insert(
140 key.to_string(),
141 Entry {
142 value,
143 inserted: now,
144 last_used: clock,
145 },
146 );
147 }
148}
149
150pub struct CachedLLM {
174 llm: Box<dyn LLM>,
175 cache: Arc<dyn LLMCache>,
176 namespace: String,
177 options: CallOptions,
178}
179
180impl Clone for CachedLLM {
181 fn clone(&self) -> Self {
182 Self {
183 llm: self.llm.clone_box(),
184 cache: self.cache.clone(),
185 namespace: self.namespace.clone(),
186 options: self.options.clone(),
187 }
188 }
189}
190
191impl CachedLLM {
192 pub fn new<L: LLM + 'static>(llm: L, cache: Arc<dyn LLMCache>) -> Self {
193 Self {
194 llm: Box::new(llm),
195 cache,
196 namespace: std::any::type_name::<L>().to_string(),
197 options: CallOptions::new(),
198 }
199 }
200
201 pub fn with_namespace<S: Into<String>>(mut self, namespace: S) -> Self {
203 self.namespace = namespace.into();
204 self
205 }
206
207 pub fn cache_key(&self, messages: &[Message]) -> Result<String, LLMError> {
209 let key = json!({
210 "namespace": self.namespace,
211 "messages": messages,
212 "options": options_fingerprint(&self.options),
213 });
214 Ok(serde_json::to_string(&key)?)
215 }
216
217 async fn lookup(&self, key: &str) -> Option<GenerateResult> {
218 let mut hit = self.cache.get(key).await?;
219 hit.tokens = None;
220 Some(hit)
221 }
222}
223
224fn options_fingerprint(o: &CallOptions) -> Value {
227 json!({
228 "candidate_count": o.candidate_count,
229 "max_tokens": o.max_tokens,
230 "temperature": o.temperature,
231 "stop_words": o.stop_words,
232 "top_k": o.top_k,
233 "top_p": o.top_p,
234 "seed": o.seed,
235 "min_length": o.min_length,
236 "max_length": o.max_length,
237 "n": o.n,
238 "repetition_penalty": o.repetition_penalty,
239 "frequency_penalty": o.frequency_penalty,
240 "presence_penalty": o.presence_penalty,
241 "functions": o.functions.as_ref().map(|fs| {
242 fs.iter()
243 .map(|f| json!([f.name, f.description, f.parameters]))
244 .collect::<Vec<_>>()
245 }),
246 "function_call_behavior": o.function_call_behavior.as_ref().map(|b| format!("{b:?}")),
247 "response_format": o.response_format.as_ref().map(|r| format!("{r:?}")),
248 })
249}
250
251#[async_trait]
252impl LLM for CachedLLM {
253 async fn generate(&self, messages: &[Message]) -> Result<GenerateResult, LLMError> {
254 let key = self.cache_key(messages)?;
255 if let Some(hit) = self.lookup(&key).await {
256 if let Some(func) = &self.options.streaming_func {
257 let mut func = func.lock().await;
258 func(hit.generation.clone())
259 .await
260 .map_err(|_| LLMError::OtherError("streaming_func failed".into()))?;
261 }
262 return Ok(hit);
263 }
264 let result = self.llm.generate(messages).await?;
265 self.cache.put(&key, result.clone()).await;
266 Ok(result)
267 }
268
269 async fn stream(
270 &self,
271 messages: &[Message],
272 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamData, LLMError>> + Send>>, LLMError> {
273 let key = self.cache_key(messages)?;
274 if let Some(hit) = self.lookup(&key).await {
275 let chunk = StreamData::new(json!(hit.generation), None, hit.generation);
276 return Ok(Box::pin(futures::stream::iter([Ok(chunk)])));
277 }
278 self.llm.stream(messages).await
279 }
280
281 fn generate_stream<'a>(&'a self, messages: &'a [Message]) -> LLMEventStream<'a> {
282 box_llm_events(async_stream::try_stream! {
283 let key = self.cache_key(messages)?;
284 if let Some(hit) = self.lookup(&key).await {
285 for event in result_events(hit) {
286 yield event;
287 }
288 return;
289 }
290 let mut events = self.llm.generate_stream(messages);
291 while let Some(event) = events.next().await {
292 let event = event?;
293 if let LLMEvent::Done(result) = &event {
294 self.cache.put(&key, result.clone()).await;
295 }
296 yield event;
297 }
298 })
299 }
300
301 fn add_options(&mut self, options: CallOptions) {
302 self.options.merge_options(options.clone());
303 self.llm.add_options(options);
304 }
305
306 fn messages_to_string(&self, messages: &[Message]) -> String {
307 self.llm.messages_to_string(messages)
308 }
309}
310
311#[cfg(test)]
312mod tests;