langchain_rust/language_models/
mod.rs1use std::{collections::HashMap, pin::Pin};
2
3use futures::Stream;
4use serde::{Deserialize, Serialize};
5
6use crate::schemas::ToolCall;
7
8pub mod llm;
9pub mod options;
10
11mod error;
12pub use error::*;
13
14#[derive(Debug, Serialize, Deserialize, Clone, Default)]
20pub struct GenerateResult {
21 pub tokens: Option<TokenUsage>,
22 pub generation: String,
23 #[serde(default, skip_serializing_if = "Vec::is_empty")]
24 pub tool_calls: Vec<ToolCall>,
25}
26
27impl GenerateResult {
28 pub fn with_tool_calls(mut self, calls: Vec<ToolCall>) -> Self {
31 if !calls.is_empty() {
32 self.generation = ToolCall::list_to_string(&calls);
33 self.tool_calls = calls;
34 }
35 self
36 }
37
38 pub fn has_tool_calls(&self) -> bool {
39 !self.tool_calls.is_empty()
40 }
41
42 pub fn to_hashmap(&self) -> HashMap<String, String> {
43 let mut map = HashMap::new();
44
45 map.insert("generation".to_string(), self.generation.clone());
47
48 if let Some(ref tokens) = self.tokens {
50 map.insert(
51 "prompt_tokens".to_string(),
52 tokens.prompt_tokens.to_string(),
53 );
54 map.insert(
55 "completion_tokens".to_string(),
56 tokens.completion_tokens.to_string(),
57 );
58 map.insert("total_tokens".to_string(), tokens.total_tokens.to_string());
59 }
60
61 map
62 }
63}
64
65#[derive(Debug, Clone)]
71#[non_exhaustive]
72pub enum LLMEvent {
73 Text(String),
75 Done(GenerateResult),
77}
78
79pub type LLMEventStream<'a> = Pin<Box<dyn Stream<Item = Result<LLMEvent, LLMError>> + Send + 'a>>;
81
82pub(crate) fn box_llm_events<'a, S>(stream: S) -> LLMEventStream<'a>
84where
85 S: Stream<Item = Result<LLMEvent, LLMError>> + Send + 'a,
86{
87 Box::pin(stream)
88}
89
90pub(crate) fn result_events(result: GenerateResult) -> Vec<LLMEvent> {
93 let mut events = Vec::with_capacity(2);
94 if !result.has_tool_calls() && !result.generation.is_empty() {
95 events.push(LLMEvent::Text(result.generation.clone()));
96 }
97 events.push(LLMEvent::Done(result));
98 events
99}
100
101#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)]
102pub struct TokenUsage {
103 pub prompt_tokens: u32,
104 pub completion_tokens: u32,
105 pub total_tokens: u32,
106}
107
108impl TokenUsage {
109 pub fn sum(&self, other: &TokenUsage) -> TokenUsage {
110 TokenUsage {
111 prompt_tokens: self.prompt_tokens + other.prompt_tokens,
112 completion_tokens: self.completion_tokens + other.completion_tokens,
113 total_tokens: self.total_tokens + other.total_tokens,
114 }
115 }
116
117 pub fn add(&mut self, other: &TokenUsage) {
118 self.prompt_tokens += other.prompt_tokens;
119 self.completion_tokens += other.completion_tokens;
120 self.total_tokens += other.total_tokens;
121 }
122}
123
124impl TokenUsage {
125 pub fn new(prompt_tokens: u32, completion_tokens: u32) -> Self {
126 Self {
127 prompt_tokens,
128 completion_tokens,
129 total_tokens: prompt_tokens + completion_tokens,
130 }
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn token_usage_new_sum_add() {
140 let a = TokenUsage::new(3, 4);
141 assert_eq!(a.total_tokens, 7);
142 let b = TokenUsage::new(1, 2);
143 let s = a.sum(&b);
144 assert_eq!(
145 (s.prompt_tokens, s.completion_tokens, s.total_tokens),
146 (4, 6, 10)
147 );
148 let mut c = a.clone();
149 c.add(&b);
150 assert_eq!(c.total_tokens, 10);
151 assert_eq!(a.total_tokens, 7);
153 }
154
155 #[test]
156 fn generate_result_to_hashmap() {
157 let without = GenerateResult {
158 generation: "hi".into(),
159 ..Default::default()
160 };
161 let m = without.to_hashmap();
162 assert_eq!(m.len(), 1);
163 assert_eq!(m["generation"], "hi");
164
165 let with = GenerateResult {
166 generation: "hi".into(),
167 tokens: Some(TokenUsage::new(1, 2)),
168 ..Default::default()
169 };
170 let m = with.to_hashmap();
171 assert_eq!(m["prompt_tokens"], "1");
172 assert_eq!(m["completion_tokens"], "2");
173 assert_eq!(m["total_tokens"], "3");
174 }
175
176 #[test]
177 fn with_tool_calls_mirrors_into_generation() {
178 let calls = vec![ToolCall::new("c1", "weather", r#"{"city":"Lima"}"#)];
179 let r = GenerateResult {
180 generation: "let me check".into(),
181 ..Default::default()
182 }
183 .with_tool_calls(calls.clone());
184 assert!(r.has_tool_calls());
185 assert_eq!(r.tool_calls, calls);
186 assert_eq!(ToolCall::parse_list(&r.generation).unwrap(), calls);
187
188 let plain = GenerateResult {
189 generation: "hi".into(),
190 ..Default::default()
191 }
192 .with_tool_calls(vec![]);
193 assert!(!plain.has_tool_calls());
194 assert_eq!(plain.generation, "hi");
195 }
196
197 #[test]
198 fn generate_result_serde_is_backward_compatible() {
199 let old: GenerateResult =
201 serde_json::from_str(r#"{"tokens":null,"generation":"hi"}"#).unwrap();
202 assert!(old.tool_calls.is_empty());
203 let v = serde_json::to_value(&old).unwrap();
205 assert!(v.get("tool_calls").is_none());
206
207 let with = GenerateResult::default().with_tool_calls(vec![ToolCall::new("a", "f", "{}")]);
208 let v = serde_json::to_value(&with).unwrap();
209 assert_eq!(v["tool_calls"][0]["function"]["name"], "f");
210 let back: GenerateResult = serde_json::from_value(v).unwrap();
211 assert_eq!(back.tool_calls, with.tool_calls);
212 }
213
214 #[test]
215 fn result_events_skip_text_for_tool_calls_and_empty_output() {
216 let text = |events: &[LLMEvent]| {
217 events
218 .iter()
219 .filter_map(|e| match e {
220 LLMEvent::Text(t) => Some(t.clone()),
221 _ => None,
222 })
223 .collect::<Vec<_>>()
224 };
225 let plain = result_events(GenerateResult {
226 generation: "hi".into(),
227 ..Default::default()
228 });
229 assert_eq!(text(&plain), vec!["hi"]);
230 assert!(matches!(plain.last(), Some(LLMEvent::Done(r)) if r.generation == "hi"));
231
232 let tools = result_events(
233 GenerateResult::default().with_tool_calls(vec![ToolCall::new("a", "f", "{}")]),
234 );
235 assert!(text(&tools).is_empty(), "tool-call JSON is not answer text");
236 assert!(matches!(tools.as_slice(), [LLMEvent::Done(r)] if r.has_tool_calls()));
237
238 assert!(matches!(
239 result_events(GenerateResult::default()).as_slice(),
240 [LLMEvent::Done(_)]
241 ));
242 }
243
244 #[derive(Clone)]
246 struct OnlyGenerate(Result<&'static str, &'static str>);
247
248 #[async_trait::async_trait]
249 impl llm::LLM for OnlyGenerate {
250 async fn generate(
251 &self,
252 _: &[crate::schemas::Message],
253 ) -> Result<GenerateResult, LLMError> {
254 match self.0 {
255 Ok(s) => Ok(GenerateResult {
256 generation: s.into(),
257 ..Default::default()
258 }),
259 Err(e) => Err(LLMError::OtherError(e.into())),
260 }
261 }
262 async fn stream(
263 &self,
264 _: &[crate::schemas::Message],
265 ) -> Result<
266 Pin<Box<dyn Stream<Item = Result<crate::schemas::StreamData, LLMError>> + Send>>,
267 LLMError,
268 > {
269 unimplemented!()
270 }
271 }
272
273 #[tokio::test]
274 async fn default_generate_stream_wraps_generate() {
275 use futures::StreamExt;
276 use llm::LLM;
277
278 let events: Vec<_> = OnlyGenerate(Ok("hello"))
279 .generate_stream(&[])
280 .map(Result::unwrap)
281 .collect()
282 .await;
283 assert!(matches!(&events[0], LLMEvent::Text(t) if t == "hello"));
284 assert!(matches!(&events[1], LLMEvent::Done(r) if r.generation == "hello"));
285 assert_eq!(events.len(), 2);
286
287 let events: Vec<_> = OnlyGenerate(Err("down"))
288 .generate_stream(&[])
289 .collect()
290 .await;
291 assert_eq!(events.len(), 1);
292 assert!(events[0].as_ref().unwrap_err().to_string().contains("down"));
293 }
294}