1use std::io::Write;
7use std::path::Path;
8use std::pin::Pin;
9use std::sync::{Arc, Mutex};
10
11use async_trait::async_trait;
12use futures_util::{Stream, StreamExt};
13use lc_core::language_models::{BaseChatModel, BaseLanguageModel, LLMResult};
14use lc_core::runnables::{Runnable, RunnableConfig};
15use lc_core::tools::ToolDefinition;
16use lc_providers::ProviderError;
17use lc_schema::Message;
18use serde::{Deserialize, Serialize};
19
20use crate::error::TestkitError;
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct RecordedExchange {
25 pub messages: Vec<Message>,
27 pub response: LLMResult,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub tools: Option<Vec<ToolDefinition>>,
35}
36
37pub struct Recorder {
39 file: Mutex<std::fs::File>,
40}
41
42impl Recorder {
43 pub fn new(path: impl AsRef<Path>) -> std::io::Result<Self> {
45 let file = std::fs::OpenOptions::new()
46 .create(true)
47 .append(true)
48 .open(path)?;
49 Ok(Self {
50 file: Mutex::new(file),
51 })
52 }
53
54 pub fn record(&self, exchange: &RecordedExchange) {
56 let line = match serde_json::to_string(exchange) {
57 Ok(line) => line,
58 Err(e) => {
59 log::warn!("lc-testkit: failed to serialize recording: {e}");
60 return;
61 }
62 };
63 let Ok(mut file) = self.file.lock() else {
64 log::warn!("lc-testkit: recording lock poisoned");
65 return;
66 };
67 if let Err(e) = writeln!(file, "{line}") {
68 log::warn!("lc-testkit: failed to append recording: {e}");
69 }
70 }
71}
72
73fn to_testkit<E: Into<ProviderError>>(e: E) -> TestkitError {
75 TestkitError::Inner(e.into())
76}
77
78pub struct RecordingProvider<M> {
80 inner: M,
81 recorder: Arc<Recorder>,
82 model_name: String,
83 tools: Option<Vec<ToolDefinition>>,
85}
86
87impl<M> RecordingProvider<M>
88where
89 M: BaseChatModel + Send + Sync + 'static,
90 M::Error: Into<ProviderError>,
91{
92 pub fn new(inner: M, path: impl AsRef<Path>) -> std::io::Result<Self> {
94 let model_name = format!("{}-recorded", inner.model_name());
95 let recorder = Arc::new(Recorder::new(path)?);
96 Ok(Self {
97 inner,
98 recorder,
99 model_name,
100 tools: None,
101 })
102 }
103
104 pub fn inner(&self) -> &M {
106 &self.inner
107 }
108
109 pub fn bind_tools(&self, tools: Vec<ToolDefinition>) -> Self
114 where
115 M: Clone,
116 {
117 Self {
118 inner: self.inner.clone(),
119 recorder: self.recorder.clone(),
120 model_name: self.model_name.clone(),
121 tools: Some(tools),
122 }
123 }
124}
125
126#[async_trait]
127impl<M> Runnable<Vec<Message>, LLMResult> for RecordingProvider<M>
128where
129 M: BaseChatModel + Clone + Send + Sync + 'static,
130 M::Error: Into<ProviderError>,
131{
132 type Error = TestkitError;
133
134 async fn invoke(
135 &self,
136 input: Vec<Message>,
137 config: Option<RunnableConfig>,
138 ) -> Result<LLMResult, Self::Error> {
139 self.chat(input, config).await
140 }
141}
142
143impl<M> BaseLanguageModel<Vec<Message>, LLMResult> for RecordingProvider<M>
144where
145 M: BaseChatModel + Clone + Send + Sync + 'static,
146 M::Error: Into<ProviderError>,
147{
148 fn model_name(&self) -> &str {
149 &self.model_name
150 }
151
152 fn get_num_tokens(&self, text: &str) -> usize {
153 self.inner.get_num_tokens(text)
154 }
155
156 fn temperature(&self) -> Option<f32> {
157 self.inner.temperature()
158 }
159
160 fn max_tokens(&self) -> Option<usize> {
161 self.inner.max_tokens()
162 }
163
164 fn with_temperature(mut self, temp: f32) -> Self {
165 self.inner = self.inner.with_temperature(temp);
166 self
167 }
168
169 fn with_max_tokens(mut self, max: usize) -> Self {
170 self.inner = self.inner.with_max_tokens(max);
171 self
172 }
173}
174
175#[async_trait]
176impl<M> BaseChatModel for RecordingProvider<M>
177where
178 M: BaseChatModel + Clone + Send + Sync + 'static,
179 M::Error: Into<ProviderError>,
180{
181 async fn chat(
182 &self,
183 messages: Vec<Message>,
184 config: Option<RunnableConfig>,
185 ) -> Result<LLMResult, Self::Error> {
186 let response = self
187 .inner
188 .chat(messages.clone(), config)
189 .await
190 .map_err(to_testkit)?;
191 self.recorder.record(&RecordedExchange {
192 messages,
193 response: response.clone(),
194 tools: self.tools.clone(),
195 });
196 Ok(response)
197 }
198
199 async fn stream_chat(
200 &self,
201 messages: Vec<Message>,
202 config: Option<RunnableConfig>,
203 ) -> Result<Pin<Box<dyn Stream<Item = Result<String, Self::Error>> + Send>>, Self::Error> {
204 let mut stream = self
205 .inner
206 .stream_chat(messages.clone(), config)
207 .await
208 .map_err(to_testkit)?;
209 let mut chunks = Vec::new();
210 while let Some(chunk) = stream.next().await {
211 chunks.push(chunk.map_err(to_testkit)?);
212 }
213 let full = chunks.concat();
214 let response = LLMResult {
215 content: full.clone(),
216 model: self.model_name.clone(),
217 ..Default::default()
218 };
219 self.recorder.record(&RecordedExchange {
220 messages,
221 response,
222 tools: self.tools.clone(),
223 });
224 let stream = futures_util::stream::iter(vec![Ok(full)]);
225 Ok(Box::pin(stream))
226 }
227
228 fn bind_tools(
229 &self,
230 tools: Vec<ToolDefinition>,
231 ) -> Option<Box<dyn BaseChatModel<Error = Self::Error> + Send + Sync>> {
232 Some(Box::new(self.bind_tools(tools)))
234 }
235}