. clippy clean

. remove example 'images' alias to example c07-image
. MessageContent::text_into_string/str return None when Parts (to avoid leak)
This commit is contained in:
Jeremy Chone
2025-01-01 08:51:21 -08:00
parent 59f0b149b0
commit 510c0aa903
3 changed files with 24 additions and 39 deletions

View File

@ -9,10 +9,6 @@ keywords = ["generative-ai","openai","chatgpt","gemini","ollama"]
homepage = "https://github.com/jeremychone/rust-genai"
repository = "https://github.com/jeremychone/rust-genai"
[[example]]
name = "images"
path = "examples/c07-image.rs"
[lints.rust]
unsafe_code = "forbid"
# unused = { level = "allow", priority = -1 } # For exploratory dev.

View File

@ -5,6 +5,7 @@ use genai::chat::{ChatMessage, ChatRequest, ContentPart, ImageSource};
use genai::Client;
const MODEL: &str = "gpt-4o-mini";
const IMAGE_URL: &str = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
@ -14,22 +15,20 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut chat_req = ChatRequest::default().with_system("Answer in one sentence");
// This is similar to sending initial system chat messages (which will be cumulative with system chat messages)
chat_req = chat_req.append_message(ChatMessage::user(
vec![
ContentPart::Text(question.to_string()),
ContentPart::Image {
content: "https://upload.wikimedia.org/wikipedia/commons/thumb/d/dd/Gfp-wisconsin-madison-the-nature-boardwalk.jpg/2560px-Gfp-wisconsin-madison-the-nature-boardwalk.jpg".to_string(),
content_type: "image/png".to_string(),
source: ImageSource::Url,
}
]
));
chat_req = chat_req.append_message(ChatMessage::user(vec![
ContentPart::Text(question.to_string()),
ContentPart::Image {
content: IMAGE_URL.to_string(),
content_type: "image/jpg".to_string(),
source: ImageSource::Url,
},
]));
println!("\n--- Question:\n{question}");
let chat_res = client.exec_chat_stream(MODEL, chat_req.clone(), None).await?;
println!("\n--- Answer: (streaming)");
let assistant_answer = print_chat_stream(chat_res, None).await?;
let _assistant_answer = print_chat_stream(chat_res, None).await?;
Ok(())
}

View File

@ -27,7 +27,9 @@ impl MessageContent {
}
/// Create a new MessageContent from provided content parts
pub fn from_parts(parts: impl Into<Vec<ContentPart>>) -> Self { MessageContent::Parts(parts.into()) }
pub fn from_parts(parts: impl Into<Vec<ContentPart>>) -> Self {
MessageContent::Parts(parts.into())
}
/// Create a new MessageContent with the ToolCalls variant
pub fn from_tool_calls(tool_calls: Vec<ToolCall>) -> Self {
@ -39,17 +41,12 @@ impl MessageContent {
impl MessageContent {
/// Returns the MessageContent as &str, only if it is MessageContent::Text
/// Otherwise, it returns None.
/// NOTE: As of now, it always returns Some(..) because MessageContent has only the Text variant.
/// However, this is in preparation for future expansions.
///
/// NOTE: When multi parts content, this will return None and won't concatenate the text parts.
pub fn text_as_str(&self) -> Option<&str> {
match self {
MessageContent::Text(content) => Some(content.as_str()),
MessageContent::Parts(parts) => {
Some(parts.iter().filter_map(|part| match part {
ContentPart::Text(content) => Some(content.clone()),
_ => None,
}).collect::<Vec<String>>().join("\n").leak()) // TODO revisit this, should we leak &str?
},
MessageContent::Parts(_) => None,
MessageContent::ToolCalls(_) => None,
MessageContent::ToolResponses(_) => None,
}
@ -58,17 +55,11 @@ impl MessageContent {
/// Consumes the MessageContent and returns it as &str,
/// only if it is MessageContent::Text; otherwise, it returns None.
///
/// NOTE: As of now, it always returns Some(..) because MessageContent has only the Text variant.
/// However, this is in preparation for future expansions.
/// NOTE: When multi parts content, this will return None and won't concatenate the text parts.
pub fn text_into_string(self) -> Option<String> {
match self {
MessageContent::Text(content) => Some(content),
MessageContent::Parts(parts) => {
Some(parts.into_iter().filter_map(|part| match part {
ContentPart::Text(content) => Some(content),
_ => None,
}).collect::<Vec<String>>().join("\n"))
},
MessageContent::Parts(_) => None,
MessageContent::ToolCalls(_) => None,
MessageContent::ToolResponses(_) => None,
}
@ -112,7 +103,9 @@ impl From<ToolResponse> for MessageContent {
}
impl From<Vec<ContentPart>> for MessageContent {
fn from(parts: Vec<ContentPart>) -> Self { MessageContent::Parts(parts) }
fn from(parts: Vec<ContentPart>) -> Self {
MessageContent::Parts(parts)
}
}
// endregion: --- Froms
@ -137,13 +130,10 @@ impl<'a> From<&'a str> for ContentPart {
// endregion: --- Froms
#[derive(Debug, Clone, Serialize, Deserialize, From)]
pub enum ImageSource {
Url,
Base64
// No `Local` location, this would require handling errors like "file not found" etc.
// Such file can be easily provided by user as Base64, also can implement convenient
// TryFrom<File> to Base64 version. All LLMs accepts local Images only as Base64
Base64, // No `Local` location, this would require handling errors like "file not found" etc.
// Such file can be easily provided by user as Base64, also can implement convenient
// TryFrom<File> to Base64 version. All LLMs accepts local Images only as Base64
}