langchain_rust/llm/openai/
mod.rs1use std::pin::Pin;
2
3pub use async_openai::config::{AzureConfig, Config, OpenAIConfig};
4
5use async_openai::types::chat::{
6 ChatChoiceStream, ChatCompletionMessageToolCalls, ChatCompletionRequestAssistantMessageArgs,
7 ChatCompletionRequestMessage, ChatCompletionRequestMessageContentPartImageArgs,
8 ChatCompletionRequestMessageContentPartTextArgs, ChatCompletionRequestSystemMessageArgs,
9 ChatCompletionRequestToolMessageArgs, ChatCompletionRequestUserMessageArgs,
10 ChatCompletionRequestUserMessageContent, ChatCompletionRequestUserMessageContentPart,
11 ChatCompletionStreamOptions, ChatCompletionToolChoiceOption, ChatCompletionTools,
12 CreateChatCompletionRequest, CreateChatCompletionRequestArgs,
13 CreateChatCompletionStreamResponse, ImageDetail, ImageUrl, ResponseFormat, StopConfiguration,
14};
15use async_openai::{error::OpenAIError, Client};
16use async_trait::async_trait;
17use futures::{Stream, StreamExt};
18
19use crate::observability::{traced_event_stream, traced_generate};
20use crate::schemas::convert::{LangchainIntoOpenAI, TryLangchainIntoOpenAI};
21use crate::{
22 language_models::{
23 box_llm_events, llm::LLM, options::CallOptions, GenerateResult, LLMError, LLMEvent,
24 LLMEventStream, TokenUsage,
25 },
26 schemas::{
27 messages::{ImageContent, Message, MessageType},
28 StreamData, ToolCall,
29 },
30};
31
32#[derive(Clone)]
33pub enum OpenAIModel {
34 Gpt35,
35 Gpt4,
36 Gpt4Turbo,
37 Gpt4o,
38 Gpt4oMini,
39}
40
41impl ToString for OpenAIModel {
42 fn to_string(&self) -> String {
43 match self {
44 OpenAIModel::Gpt35 => "gpt-3.5-turbo".to_string(),
45 OpenAIModel::Gpt4 => "gpt-4".to_string(),
46 OpenAIModel::Gpt4Turbo => "gpt-4-turbo-preview".to_string(),
47 OpenAIModel::Gpt4o => "gpt-4o".to_string(),
48 OpenAIModel::Gpt4oMini => "gpt-4o-mini".to_string(),
49 }
50 }
51}
52
53impl Into<String> for OpenAIModel {
54 fn into(self) -> String {
55 self.to_string()
56 }
57}
58
59#[derive(Clone)]
60pub struct OpenAI<C: Config> {
61 config: C,
62 options: CallOptions,
63 model: String,
64}
65
66const PROVIDER: &str = "openai";
68
69impl<C: Config> OpenAI<C> {
70 pub fn new(config: C) -> Self {
71 Self {
72 config,
73 options: CallOptions::default(),
74 model: OpenAIModel::Gpt4oMini.to_string(),
75 }
76 }
77
78 pub fn with_model<S: Into<String>>(mut self, model: S) -> Self {
79 self.model = model.into();
80 self
81 }
82
83 pub fn with_config(mut self, config: C) -> Self {
84 self.config = config;
85 self
86 }
87
88 pub fn with_options(mut self, options: CallOptions) -> Self {
89 self.options = options;
90 self
91 }
92}
93
94impl Default for OpenAI<OpenAIConfig> {
95 fn default() -> Self {
96 Self::new(OpenAIConfig::default())
97 }
98}
99
100impl<C: Config + Clone + Send + Sync + 'static> OpenAI<C> {
101 async fn generate_untraced(&self, prompt: &[Message]) -> Result<GenerateResult, LLMError> {
102 let client = Client::with_config(self.config.clone());
103 let request = self.generate_request(prompt, self.options.streaming_func.is_some())?;
104 match &self.options.streaming_func {
105 Some(func) => {
106 let mut stream = client.chat().create_stream(request).await?;
107 let mut generate_result = GenerateResult::default();
108 while let Some(result) = stream.next().await {
109 match result {
110 Ok(response) => {
111 if let Some(usage) = response.usage {
112 generate_result.tokens = Some(TokenUsage {
113 prompt_tokens: usage.prompt_tokens,
114 completion_tokens: usage.completion_tokens,
115 total_tokens: usage.total_tokens,
116 });
117 }
118 for chat_choice in response.choices.iter() {
119 let chat_choice: ChatChoiceStream = chat_choice.clone();
120 {
121 let mut func = func.lock().await;
122 let _ = func(
123 serde_json::to_string(&chat_choice).unwrap_or("".into()),
124 )
125 .await;
126 }
127 if let Some(content) = chat_choice.delta.content {
128 generate_result.generation.push_str(&content);
129 }
130 }
131 }
132 Err(err) => {
133 return Err(err.into());
135 }
136 }
137 }
138 Ok(generate_result)
139 }
140 None => {
141 let response = client.chat().create(request).await?;
142 let mut generate_result = GenerateResult::default();
143
144 if let Some(usage) = response.usage {
145 generate_result.tokens = Some(TokenUsage {
146 prompt_tokens: usage.prompt_tokens,
147 completion_tokens: usage.completion_tokens,
148 total_tokens: usage.total_tokens,
149 });
150 }
151
152 if let Some(choice) = &response.choices.first() {
153 generate_result.generation = choice.message.content.clone().unwrap_or_default();
154 if let Some(calls) = &choice.message.tool_calls {
155 let typed: Vec<ToolCall> = calls
156 .iter()
157 .filter_map(|c| match c {
158 ChatCompletionMessageToolCalls::Function(f) => Some(ToolCall::new(
159 &f.id,
160 &f.function.name,
161 &f.function.arguments,
162 )),
163 _ => None,
164 })
165 .collect();
166 if typed.is_empty() {
167 generate_result.generation =
169 serde_json::to_string(&calls).unwrap_or_default();
170 } else {
171 generate_result = generate_result.with_tool_calls(typed);
172 }
173 }
174 } else {
175 generate_result.generation = "".to_string();
176 }
177
178 Ok(generate_result)
179 }
180 }
181 }
182}
183
184#[async_trait]
185impl<C: Config + Clone + Send + Sync + 'static> LLM for OpenAI<C> {
186 async fn generate(&self, prompt: &[Message]) -> Result<GenerateResult, LLMError> {
187 traced_generate(PROVIDER, &self.model, self.generate_untraced(prompt)).await
188 }
189
190 async fn invoke(&self, prompt: &str) -> Result<String, LLMError> {
191 self.generate(&[Message::new_human_message(prompt)])
192 .await
193 .map(|res| res.generation)
194 }
195
196 async fn stream(
197 &self,
198 messages: &[Message],
199 ) -> Result<Pin<Box<dyn Stream<Item = Result<StreamData, LLMError>> + Send>>, LLMError> {
200 let client = Client::with_config(self.config.clone());
201 let request = self.generate_request(messages, true)?;
202
203 let original_stream = client.chat().create_stream(request).await?;
204
205 let new_stream = original_stream.map(|result| match result {
206 Ok(completion) => {
207 let value_completion = serde_json::to_value(completion).map_err(LLMError::from)?;
208 let tokens = match value_completion.pointer("/usage") {
209 Some(usage) if !usage.is_null() => Some(
210 serde_json::from_value::<TokenUsage>(usage.clone())
211 .map_err(LLMError::from)?,
212 ),
213 _ => None,
214 };
215 let content = value_completion.pointer("/choices/0/delta/content");
218 if tokens.is_none() && content.is_none() {
219 return Err(LLMError::ContentNotFound(
220 "/choices/0/delta/content".to_string(),
221 ));
222 }
223 let content = content
224 .and_then(serde_json::Value::as_str)
225 .unwrap_or("")
226 .to_string();
227
228 Ok(StreamData::new(value_completion, tokens, content))
229 }
230 Err(e) => Err(LLMError::from(e)),
231 });
232
233 Ok(Box::pin(new_stream))
234 }
235
236 fn generate_stream<'a>(&'a self, messages: &'a [Message]) -> LLMEventStream<'a> {
237 let events = box_llm_events(async_stream::try_stream! {
238 let client = Client::with_config(self.config.clone());
239 let request = self.generate_request(messages, true)?;
240 let mut stream = client.chat().create_stream(request).await?;
241
242 let mut acc = StreamAccumulator::default();
243 while let Some(chunk) = stream.next().await {
244 if let Some(text) = acc.push(chunk?) {
245 yield LLMEvent::Text(text);
246 }
247 }
248 yield LLMEvent::Done(acc.finish());
249 });
250 traced_event_stream(PROVIDER, &self.model, events)
251 }
252
253 fn add_options(&mut self, options: CallOptions) {
254 self.options.merge_options(options)
255 }
256}
257
258#[derive(Default)]
262struct StreamAccumulator {
263 text: String,
264 calls: std::collections::BTreeMap<u32, ToolCall>,
265 tokens: Option<TokenUsage>,
266}
267
268impl StreamAccumulator {
269 fn push(&mut self, chunk: CreateChatCompletionStreamResponse) -> Option<String> {
271 if let Some(usage) = chunk.usage {
272 self.tokens = Some(TokenUsage {
273 prompt_tokens: usage.prompt_tokens,
274 completion_tokens: usage.completion_tokens,
275 total_tokens: usage.total_tokens,
276 });
277 }
278 let delta = chunk.choices.into_iter().find(|c| c.index == 0)?.delta;
279 for part in delta.tool_calls.unwrap_or_default() {
280 let call = self.calls.entry(part.index).or_default();
281 if let Some(id) = part.id {
282 call.id.push_str(&id);
283 }
284 if let Some(function) = part.function {
285 call.name
286 .push_str(function.name.as_deref().unwrap_or_default());
287 call.arguments
288 .push_str(function.arguments.as_deref().unwrap_or_default());
289 }
290 }
291 let text = delta.content.filter(|t| !t.is_empty())?;
292 self.text.push_str(&text);
293 Some(text)
294 }
295
296 fn finish(self) -> GenerateResult {
297 GenerateResult {
298 tokens: self.tokens,
299 generation: self.text,
300 ..Default::default()
301 }
302 .with_tool_calls(self.calls.into_values().collect())
303 }
304}
305
306fn openai_image_url(image: &ImageContent) -> Result<ImageUrl, OpenAIError> {
309 let detail = match image.detail.as_deref().map(str::to_ascii_lowercase) {
310 None => None,
311 Some(d) if d == "auto" => Some(ImageDetail::Auto),
312 Some(d) if d == "low" => Some(ImageDetail::Low),
313 Some(d) if d == "high" => Some(ImageDetail::High),
314 Some(other) => {
315 return Err(OpenAIError::InvalidArgument(format!(
316 "unknown image detail {other:?}, expected auto, low or high"
317 )))
318 }
319 };
320 Ok(ImageUrl {
321 url: image.source().to_url(),
322 detail,
323 })
324}
325
326impl<C: Config> OpenAI<C> {
327 fn to_openai_messages(
328 &self,
329 messages: &[Message],
330 ) -> Result<Vec<ChatCompletionRequestMessage>, LLMError> {
331 let mut openai_messages: Vec<ChatCompletionRequestMessage> = Vec::new();
332 for m in messages {
333 match m.message_type {
334 MessageType::AIMessage => openai_messages.push(match &m.tool_calls {
335 Some(value) => {
336 let function: Vec<ChatCompletionMessageToolCalls> =
337 serde_json::from_value(value.clone())?;
338 ChatCompletionRequestAssistantMessageArgs::default()
339 .tool_calls(function)
340 .content(m.content.clone())
341 .build()?
342 .into()
343 }
344 None => ChatCompletionRequestAssistantMessageArgs::default()
345 .content(m.content.clone())
346 .build()?
347 .into(),
348 }),
349 MessageType::HumanMessage => {
350 let content: ChatCompletionRequestUserMessageContent = match &m.images {
351 Some(images) if !images.is_empty() => {
352 let mut parts: Vec<ChatCompletionRequestUserMessageContentPart> =
353 Vec::with_capacity(images.len() + 1);
354 if !m.content.is_empty() {
355 parts.push(
356 ChatCompletionRequestMessageContentPartTextArgs::default()
357 .text(m.content.clone())
358 .build()?
359 .into(),
360 );
361 }
362 for image in images {
363 parts.push(
364 ChatCompletionRequestMessageContentPartImageArgs::default()
365 .image_url(openai_image_url(image)?)
366 .build()?
367 .into(),
368 );
369 }
370 parts.into()
371 }
372 _ => m.content.clone().into(),
373 };
374
375 openai_messages.push(
376 ChatCompletionRequestUserMessageArgs::default()
377 .content(content)
378 .build()?
379 .into(),
380 )
381 }
382 MessageType::SystemMessage => openai_messages.push(
383 ChatCompletionRequestSystemMessageArgs::default()
384 .content(m.content.clone())
385 .build()?
386 .into(),
387 ),
388 MessageType::ToolMessage => {
389 openai_messages.push(
390 ChatCompletionRequestToolMessageArgs::default()
391 .content(m.content.clone())
392 .tool_call_id(m.id.clone().unwrap_or_default())
393 .build()?
394 .into(),
395 );
396 }
397 }
398 }
399 Ok(openai_messages)
400 }
401
402 fn generate_request(
403 &self,
404 messages: &[Message],
405 stream: bool,
406 ) -> Result<CreateChatCompletionRequest, LLMError> {
407 let messages: Vec<ChatCompletionRequestMessage> = self.to_openai_messages(messages)?;
408 let mut request_builder = CreateChatCompletionRequestArgs::default();
409 if let Some(temperature) = self.options.temperature {
410 request_builder.temperature(temperature);
411 }
412 if let Some(max_tokens) = self.options.max_tokens {
413 request_builder.max_tokens(max_tokens);
414 }
415 if stream {
416 request_builder.stream(true);
417 if let Some(include_usage) = self.options.stream_usage {
418 request_builder.stream_options(ChatCompletionStreamOptions {
419 include_usage: Some(include_usage),
420 include_obfuscation: None,
421 });
422 }
423 }
424 request_builder.model(self.model.to_string());
425 if let Some(stop_words) = &self.options.stop_words {
426 request_builder.stop(StopConfiguration::StringArray(stop_words.clone()));
427 }
428
429 if let Some(functions) = &self.options.functions {
430 let functions: Result<Vec<ChatCompletionTools>, OpenAIError> = functions
431 .clone()
432 .into_iter()
433 .map(|f| f.try_into_openai())
434 .collect();
435 request_builder.tools(functions?);
436 }
437
438 if let Some(behavior) = &self.options.function_call_behavior {
439 request_builder
440 .tool_choice::<ChatCompletionToolChoiceOption>(behavior.clone().into_openai());
441 }
442
443 if let Some(response_format) = &self.options.response_format {
444 request_builder
445 .response_format::<ResponseFormat>(response_format.clone().into_openai());
446 }
447
448 request_builder.messages(messages);
449 Ok(request_builder.build()?)
450 }
451}
452
453#[cfg(test)]
454mod mock_tests;
455
456#[cfg(test)]
457mod tests {
458 use crate::schemas::FunctionDefinition;
459
460 use super::*;
461
462 use base64::prelude::*;
463 use serde_json::json;
464 use std::sync::Arc;
465 use tokio::sync::Mutex;
466 use tokio::test;
467
468 #[test]
469 #[ignore]
470 async fn test_invoke() {
471 let message_complete = Arc::new(Mutex::new(String::new()));
472
473 let streaming_func = {
476 let message_complete = message_complete.clone();
477 move |content: String| {
478 let message_complete = message_complete.clone();
479 async move {
480 let mut message_complete_lock = message_complete.lock().await;
481 println!("Content: {:?}", content);
482 message_complete_lock.push_str(&content);
483 Ok(())
484 }
485 }
486 };
487 let options = CallOptions::new().with_streaming_func(streaming_func);
488 let open_ai = OpenAI::new(OpenAIConfig::default())
490 .with_model(OpenAIModel::Gpt35.to_string()) .with_options(options);
492
493 match open_ai.invoke("hola").await {
497 Ok(result) => {
498 println!("Generate Result: {:?}", result);
500 println!("Message Complete: {:?}", message_complete.lock().await);
501 }
502 Err(e) => {
503 eprintln!("Error calling generate: {:?}", e);
505 }
506 }
507 }
508
509 #[test]
510 #[ignore]
511 async fn test_generate_function() {
512 let message_complete = Arc::new(Mutex::new(String::new()));
513
514 let streaming_func = {
517 let message_complete = message_complete.clone();
518 move |content: String| {
519 let message_complete = message_complete.clone();
520 async move {
521 let content = serde_json::from_str::<ChatChoiceStream>(&content).unwrap();
522 if content.finish_reason.is_some() {
523 return Ok(());
524 }
525 let mut message_complete_lock = message_complete.lock().await;
526 println!("Content: {:?}", content);
527 message_complete_lock.push_str(&content.delta.content.unwrap());
528 Ok(())
529 }
530 }
531 };
532 let options = CallOptions::new().with_streaming_func(streaming_func);
534 let open_ai = OpenAI::new(OpenAIConfig::default())
536 .with_model(OpenAIModel::Gpt35.to_string()) .with_options(options);
538
539 let messages = vec![Message::new_human_message("Hello, how are you?")];
541
542 match open_ai.generate(&messages).await {
544 Ok(result) => {
545 println!("Generate Result: {:?}", result);
547 println!("Message Complete: {:?}", message_complete.lock().await);
548 }
549 Err(e) => {
550 eprintln!("Error calling generate: {:?}", e);
552 }
553 }
554 }
555
556 #[test]
557 #[ignore]
558 async fn test_openai_stream() {
559 let open_ai = OpenAI::default().with_model(OpenAIModel::Gpt35.to_string());
561
562 let messages = vec![Message::new_human_message("Hello, how are you?")];
564
565 open_ai
566 .stream(&messages)
567 .await
568 .unwrap()
569 .for_each(|result| async {
570 match result {
571 Ok(stream_data) => {
572 println!("Stream Data: {:?}", stream_data.content);
573 }
574 Err(e) => {
575 eprintln!("Error calling generate: {:?}", e);
576 }
577 }
578 })
579 .await;
580 }
581
582 #[test]
583 #[ignore]
584 async fn test_function() {
585 let mut functions = Vec::new();
586 functions.push(FunctionDefinition {
587 name: "cli".to_string(),
588 description: "Use the Ubuntu command line to preform any action you wish.".to_string(),
589 parameters: json!({
590 "type": "object",
591 "properties": {
592 "command": {
593 "type": "string",
594 "description": "The raw command you want executed"
595 }
596 },
597 "required": ["command"]
598 }),
599 });
600
601 let llm = OpenAI::default()
602 .with_model(OpenAIModel::Gpt35)
603 .with_config(OpenAIConfig::new())
604 .with_options(CallOptions::new().with_functions(functions));
605 let response = llm
606 .invoke("Use the command line to create a new rust project. Execute the first command.")
607 .await
608 .unwrap();
609 println!("{}", response)
610 }
611
612 #[test]
613 #[ignore]
614 async fn test_generate_with_image_message() {
615 let open_ai =
617 OpenAI::new(OpenAIConfig::default()).with_model(OpenAIModel::Gpt4o.to_string());
618
619 let image = std::fs::read("./src/llm/test_data/example.jpg").unwrap();
621 let image_base64 = BASE64_STANDARD.encode(image);
622
623 let image_urls = vec![format!("data:image/jpeg;base64,{image_base64}")];
625 let messages = vec![
626 Message::new_human_message("Describe this image"),
627 Message::new_human_message_with_images(image_urls),
628 ];
629
630 let response = open_ai.generate(&messages).await.unwrap();
632 println!("Response: {:?}", response);
633 }
634}