Added images to UserMessageEvent (#3400)
This PR adds an `images` field to the existing `UserMessageEvent` so we can encode zero or more images associated with a user message. This allows images to be restored when conversations are restored.
This commit is contained in:
@@ -25,31 +25,56 @@ pub(crate) fn map_response_item_to_event_messages(
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
let events: Vec<EventMsg> = content
|
let mut events: Vec<EventMsg> = Vec::new();
|
||||||
.iter()
|
let mut message_parts: Vec<String> = Vec::new();
|
||||||
.filter_map(|content_item| match content_item {
|
let mut images: Vec<String> = Vec::new();
|
||||||
ContentItem::OutputText { text } => {
|
let mut kind: Option<InputMessageKind> = None;
|
||||||
Some(EventMsg::AgentMessage(AgentMessageEvent {
|
|
||||||
message: text.clone(),
|
for content_item in content.iter() {
|
||||||
}))
|
match content_item {
|
||||||
}
|
|
||||||
ContentItem::InputText { text } => {
|
ContentItem::InputText { text } => {
|
||||||
let trimmed = text.trim_start();
|
if kind.is_none() {
|
||||||
let kind = if trimmed.starts_with("<environment_context>") {
|
let trimmed = text.trim_start();
|
||||||
Some(InputMessageKind::EnvironmentContext)
|
kind = if trimmed.starts_with("<environment_context>") {
|
||||||
} else if trimmed.starts_with("<user_instructions>") {
|
Some(InputMessageKind::EnvironmentContext)
|
||||||
Some(InputMessageKind::UserInstructions)
|
} else if trimmed.starts_with("<user_instructions>") {
|
||||||
} else {
|
Some(InputMessageKind::UserInstructions)
|
||||||
Some(InputMessageKind::Plain)
|
} else {
|
||||||
};
|
Some(InputMessageKind::Plain)
|
||||||
Some(EventMsg::UserMessage(UserMessageEvent {
|
};
|
||||||
message: text.clone(),
|
}
|
||||||
kind,
|
message_parts.push(text.clone());
|
||||||
}))
|
|
||||||
}
|
}
|
||||||
_ => None,
|
ContentItem::InputImage { image_url } => {
|
||||||
})
|
images.push(image_url.clone());
|
||||||
.collect();
|
}
|
||||||
|
ContentItem::OutputText { text } => {
|
||||||
|
events.push(EventMsg::AgentMessage(AgentMessageEvent {
|
||||||
|
message: text.clone(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !message_parts.is_empty() || !images.is_empty() {
|
||||||
|
let message = if message_parts.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
message_parts.join("")
|
||||||
|
};
|
||||||
|
let images = if images.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(images)
|
||||||
|
};
|
||||||
|
|
||||||
|
events.push(EventMsg::UserMessage(UserMessageEvent {
|
||||||
|
message,
|
||||||
|
kind,
|
||||||
|
images,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
events
|
events
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,3 +121,47 @@ pub(crate) fn map_response_item_to_event_messages(
|
|||||||
| ResponseItem::Other => Vec::new(),
|
| ResponseItem::Other => Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::map_response_item_to_event_messages;
|
||||||
|
use crate::protocol::EventMsg;
|
||||||
|
use crate::protocol::InputMessageKind;
|
||||||
|
use codex_protocol::models::ContentItem;
|
||||||
|
use codex_protocol::models::ResponseItem;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maps_user_message_with_text_and_two_images() {
|
||||||
|
let img1 = "https://example.com/one.png".to_string();
|
||||||
|
let img2 = "https://example.com/two.jpg".to_string();
|
||||||
|
|
||||||
|
let item = ResponseItem::Message {
|
||||||
|
id: None,
|
||||||
|
role: "user".to_string(),
|
||||||
|
content: vec![
|
||||||
|
ContentItem::InputText {
|
||||||
|
text: "Hello world".to_string(),
|
||||||
|
},
|
||||||
|
ContentItem::InputImage {
|
||||||
|
image_url: img1.clone(),
|
||||||
|
},
|
||||||
|
ContentItem::InputImage {
|
||||||
|
image_url: img2.clone(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
let events = map_response_item_to_event_messages(&item, false);
|
||||||
|
assert_eq!(events.len(), 1, "expected a single user message event");
|
||||||
|
|
||||||
|
match &events[0] {
|
||||||
|
EventMsg::UserMessage(user) => {
|
||||||
|
assert_eq!(user.message, "Hello world");
|
||||||
|
assert!(matches!(user.kind, Some(InputMessageKind::Plain)));
|
||||||
|
assert_eq!(user.images, Some(vec![img1.clone(), img2.clone()]));
|
||||||
|
}
|
||||||
|
other => panic!("expected UserMessage, got {other:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -695,6 +695,8 @@ pub struct UserMessageEvent {
|
|||||||
pub message: String,
|
pub message: String,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub kind: Option<InputMessageKind>,
|
pub kind: Option<InputMessageKind>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub images: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, U> From<(T, U)> for InputMessageKind
|
impl<T, U> From<(T, U)> for InputMessageKind
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ fn resumed_initial_messages_render_history() {
|
|||||||
EventMsg::UserMessage(UserMessageEvent {
|
EventMsg::UserMessage(UserMessageEvent {
|
||||||
message: "hello from user".to_string(),
|
message: "hello from user".to_string(),
|
||||||
kind: Some(InputMessageKind::Plain),
|
kind: Some(InputMessageKind::Plain),
|
||||||
|
images: None,
|
||||||
}),
|
}),
|
||||||
EventMsg::AgentMessage(AgentMessageEvent {
|
EventMsg::AgentMessage(AgentMessageEvent {
|
||||||
message: "assistant reply".to_string(),
|
message: "assistant reply".to_string(),
|
||||||
|
|||||||
Reference in New Issue
Block a user