blob: fe4a5cfa7d7ab17a45eed291a5bb3757ab60be0e [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use crate::prompt::{ContentPart, Message, ToolOutput};
use maka_agent_facts::language::Language;
/// Chat has text-only tool messages. Keep the entire result group together and
/// carry media in the following user message, never between parallel results.
pub(super) fn project(messages: Vec<Message>, language: Language) -> Vec<Message> {
let mut projected = Vec::with_capacity(messages.len());
let mut images = Vec::new();
for mut message in messages {
if let Message::Tool { content, .. } = &mut message {
for result in content {
let ToolOutput::Content(parts) = &mut result.output else {
continue;
};
let mut labelled = false;
for part in parts {
if matches!(part, ContentPart::File { media_type, .. } if media_type.starts_with("image/") || media_type.starts_with("audio/"))
{
let audio = matches!(part, ContentPart::File { media_type, .. } if media_type.starts_with("audio/"));
let kind = match (language, audio) {
(Language::En, true) => "Audio",
(Language::En, false) => "Image",
(Language::ZhCn, true) => "音频",
(Language::ZhCn, false) => "图像",
(Language::ZhTw, true) => "音訊",
(Language::ZhTw, false) => "影像",
};
if !labelled {
let name = &result.tool_name;
let id = &result.tool_call_id;
images.push(ContentPart::text(match language {
Language::En => format!("{kind} from tool {name} ({id}):"),
Language::ZhCn => format!("来自工具 {name}({id})的{kind}:"),
Language::ZhTw => format!("來自工具 {name}({id})的{kind}:"),
}));
labelled = true;
}
images.push(std::mem::replace(
part,
ContentPart::text(match language {
Language::En => format!("{kind} supplied below."),
Language::ZhCn => format!("{kind}见下方。"),
Language::ZhTw => format!("{kind}見下方。"),
}),
));
}
}
}
} else if !images.is_empty() {
projected.push(Message::User {
content: std::mem::take(&mut images),
provider_options: None,
});
}
projected.push(message);
}
if !images.is_empty() {
projected.push(Message::User {
content: images,
provider_options: None,
});
}
projected
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prompt::FileData;
#[test]
fn tool_media_projection_uses_captured_language_and_keeps_payload() {
let media = ContentPart::File {
data: FileData::Data("aW1hZ2U=".into()),
media_type: "image/png".into(),
provider_options: None,
};
for (language, expected) in [
(Language::ZhCn, "来自工具 read(call-原文)的图像:"),
(Language::ZhTw, "來自工具 read(call-原文)的影像:"),
] {
let messages = project(
vec![Message::tool(
"call-原文",
"read",
ToolOutput::Content(vec![ContentPart::text("Tool result 原文"), media.clone()]),
)],
language,
);
let Message::User { content, .. } = &messages[1] else {
panic!("media user message")
};
assert_eq!(content, &[ContentPart::text(expected), media.clone()]);
let Message::Tool { content, .. } = &messages[0] else {
panic!("settled tool result")
};
let ToolOutput::Content(parts) = &content[0].output else {
panic!("tool content")
};
assert_eq!(parts[0], ContentPart::text("Tool result 原文"));
assert_eq!(content[0].tool_call_id, "call-原文");
}
}
}