619 lines
25 KiB
C++
619 lines
25 KiB
C++
#include "conv_template.h"
|
|
|
|
#include <tvm/ffi/function.h>
|
|
|
|
#include "../support/json_parser.h"
|
|
#include "image_utils.h"
|
|
|
|
namespace mlc {
|
|
namespace llm {
|
|
namespace json_ffi {
|
|
|
|
using namespace mlc::llm;
|
|
|
|
/****************** Model vision config ******************/
|
|
|
|
ModelVisionConfig ModelVisionConfig::FromJSON(const tvm::ffi::json::Object& json_obj) {
|
|
ModelVisionConfig config;
|
|
|
|
Result<int64_t> hidden_size_res = json::LookupWithResultReturn<int64_t>(json_obj, "hidden_size");
|
|
if (hidden_size_res.IsOk()) {
|
|
config.hidden_size = static_cast<int>(hidden_size_res.Unwrap());
|
|
}
|
|
|
|
Result<int64_t> image_size_res = json::LookupWithResultReturn<int64_t>(json_obj, "image_size");
|
|
if (image_size_res.IsOk()) {
|
|
config.image_size = static_cast<int>(image_size_res.Unwrap());
|
|
}
|
|
|
|
Result<int64_t> intermediate_size_res =
|
|
json::LookupWithResultReturn<int64_t>(json_obj, "intermediate_size");
|
|
if (intermediate_size_res.IsOk()) {
|
|
config.intermediate_size = static_cast<int>(intermediate_size_res.Unwrap());
|
|
}
|
|
|
|
Result<int64_t> num_attention_heads_res =
|
|
json::LookupWithResultReturn<int64_t>(json_obj, "num_attention_heads");
|
|
if (num_attention_heads_res.IsOk()) {
|
|
config.num_attention_heads = static_cast<int>(num_attention_heads_res.Unwrap());
|
|
}
|
|
|
|
Result<int64_t> num_hidden_layers_res =
|
|
json::LookupWithResultReturn<int64_t>(json_obj, "num_hidden_layers");
|
|
if (num_hidden_layers_res.IsOk()) {
|
|
config.num_hidden_layers = static_cast<int>(num_hidden_layers_res.Unwrap());
|
|
}
|
|
|
|
Result<int64_t> patch_size_res = json::LookupWithResultReturn<int64_t>(json_obj, "patch_size");
|
|
if (patch_size_res.IsOk()) {
|
|
config.patch_size = static_cast<int>(patch_size_res.Unwrap());
|
|
}
|
|
|
|
Result<int64_t> projection_dim_res =
|
|
json::LookupWithResultReturn<int64_t>(json_obj, "projection_dim");
|
|
if (projection_dim_res.IsOk()) {
|
|
config.projection_dim = static_cast<int>(projection_dim_res.Unwrap());
|
|
}
|
|
|
|
Result<int64_t> vocab_size_res = json::LookupWithResultReturn<int64_t>(json_obj, "vocab_size");
|
|
if (vocab_size_res.IsOk()) {
|
|
config.vocab_size = static_cast<int>(vocab_size_res.Unwrap());
|
|
}
|
|
|
|
Result<std::string> dtype_res = json::LookupWithResultReturn<std::string>(json_obj, "dtype");
|
|
if (dtype_res.IsOk()) {
|
|
config.dtype = dtype_res.Unwrap();
|
|
}
|
|
|
|
Result<int64_t> num_channels_res =
|
|
json::LookupWithResultReturn<int64_t>(json_obj, "num_channels");
|
|
if (num_channels_res.IsOk()) {
|
|
config.num_channels = static_cast<int>(num_channels_res.Unwrap());
|
|
}
|
|
|
|
Result<double> layer_norm_eps_res =
|
|
json::LookupWithResultReturn<double>(json_obj, "layer_norm_eps");
|
|
if (layer_norm_eps_res.IsOk()) {
|
|
config.layer_norm_eps = layer_norm_eps_res.Unwrap();
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
/****************** Model config ******************/
|
|
|
|
ModelConfig ModelConfig::FromJSON(const tvm::ffi::json::Object& json_obj) {
|
|
ModelConfig config;
|
|
|
|
Result<int64_t> vocab_size_res = json::LookupWithResultReturn<int64_t>(json_obj, "vocab_size");
|
|
if (vocab_size_res.IsOk()) {
|
|
config.vocab_size = static_cast<int>(vocab_size_res.Unwrap());
|
|
}
|
|
|
|
Result<int64_t> context_window_size_res =
|
|
json::LookupWithResultReturn<int64_t>(json_obj, "context_window_size");
|
|
if (context_window_size_res.IsOk()) {
|
|
config.context_window_size = static_cast<int>(context_window_size_res.Unwrap());
|
|
}
|
|
|
|
Result<int64_t> sliding_window_size_res =
|
|
json::LookupWithResultReturn<int64_t>(json_obj, "sliding_window_size");
|
|
if (sliding_window_size_res.IsOk()) {
|
|
config.sliding_window_size = static_cast<int>(sliding_window_size_res.Unwrap());
|
|
}
|
|
|
|
Result<int64_t> prefill_chunk_size_res =
|
|
json::LookupWithResultReturn<int64_t>(json_obj, "prefill_chunk_size");
|
|
if (prefill_chunk_size_res.IsOk()) {
|
|
config.prefill_chunk_size = static_cast<int>(prefill_chunk_size_res.Unwrap());
|
|
}
|
|
|
|
Result<int64_t> tensor_parallel_shards_res =
|
|
json::LookupWithResultReturn<int64_t>(json_obj, "tensor_parallel_shards");
|
|
if (tensor_parallel_shards_res.IsOk()) {
|
|
config.tensor_parallel_shards = static_cast<int>(tensor_parallel_shards_res.Unwrap());
|
|
}
|
|
|
|
Result<int64_t> pipeline_parallel_stages_res =
|
|
json::LookupWithResultReturn<int64_t>(json_obj, "pipeline_parallel_stages");
|
|
if (pipeline_parallel_stages_res.IsOk()) {
|
|
config.pipeline_parallel_stages = static_cast<int>(pipeline_parallel_stages_res.Unwrap());
|
|
}
|
|
|
|
Result<int64_t> max_batch_size_res =
|
|
json::LookupWithResultReturn<int64_t>(json_obj, "max_batch_size");
|
|
if (max_batch_size_res.IsOk()) {
|
|
config.max_batch_size = static_cast<int>(max_batch_size_res.Unwrap());
|
|
}
|
|
|
|
if (json_obj.count("vision_config")) {
|
|
const tvm::ffi::json::Object& vision_config_obj =
|
|
json_obj.at("vision_config").cast<tvm::ffi::json::Object>();
|
|
config.vision_config = ModelVisionConfig::FromJSON(vision_config_obj);
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
/****************** Conversation template ******************/
|
|
|
|
std::unordered_map<MessagePlaceholders, std::string> PLACEHOLDERS = {
|
|
{MessagePlaceholders::SYSTEM, "{system_message}"},
|
|
{MessagePlaceholders::USER, "{user_message}"},
|
|
{MessagePlaceholders::ASSISTANT, "{assistant_message}"},
|
|
{MessagePlaceholders::TOOL, "{tool_message}"},
|
|
{MessagePlaceholders::FUNCTION, "{function_string}"}};
|
|
|
|
MessagePlaceholders MessagePlaceholderFromString(const std::string& role) {
|
|
static const std::unordered_map<std::string, MessagePlaceholders> enum_map = {
|
|
{"system", MessagePlaceholders::SYSTEM}, {"user", MessagePlaceholders::USER},
|
|
{"assistant", MessagePlaceholders::ASSISTANT}, {"tool", MessagePlaceholders::TOOL},
|
|
{"function", MessagePlaceholders::FUNCTION},
|
|
};
|
|
|
|
return enum_map.at(role);
|
|
}
|
|
|
|
Conversation::Conversation()
|
|
: role_templates({{"user", PLACEHOLDERS[MessagePlaceholders::USER]},
|
|
{"assistant", PLACEHOLDERS[MessagePlaceholders::ASSISTANT]},
|
|
{"tool", PLACEHOLDERS[MessagePlaceholders::TOOL]}}) {}
|
|
|
|
std::string Conversation::GetSystemText(const std::string& system_msg) const {
|
|
std::string system_text = this->system_template;
|
|
static std::string system_placeholder = PLACEHOLDERS[MessagePlaceholders::SYSTEM];
|
|
size_t pos = system_text.find(system_placeholder);
|
|
if (pos != std::string::npos) {
|
|
system_text.replace(pos, system_placeholder.length(), system_msg);
|
|
}
|
|
return system_text;
|
|
}
|
|
|
|
std::string Conversation::GetRoleText(const std::string& role, const std::string& content,
|
|
const std::optional<std::string>& fn_call_string) const {
|
|
std::string role_text = this->role_templates.at(role);
|
|
std::string placeholder = PLACEHOLDERS[MessagePlaceholderFromString(role)];
|
|
size_t pos = role_text.find(placeholder);
|
|
if (pos != std::string::npos) {
|
|
role_text.replace(pos, placeholder.length(), content);
|
|
}
|
|
if (fn_call_string) {
|
|
// replace placeholder[FUNCTION] with function_string
|
|
// this assumes function calling is used for a single request scenario only
|
|
pos = role_text.find(PLACEHOLDERS[MessagePlaceholders::FUNCTION]);
|
|
if (pos != std::string::npos) {
|
|
role_text.replace(pos, PLACEHOLDERS[MessagePlaceholders::FUNCTION].length(),
|
|
fn_call_string.value());
|
|
}
|
|
}
|
|
return role_text;
|
|
}
|
|
|
|
/// Try to detect if function calling is needed, if so, return the function calling string
|
|
Result<std::optional<std::string>> TryGetFunctionCallingString(
|
|
const Conversation& conv, const ChatCompletionRequest& request) {
|
|
using TResult = Result<std::optional<std::string>>;
|
|
if (!request.tools.has_value() ||
|
|
(request.tool_choice.has_value() && request.tool_choice.value() == "none")) {
|
|
return TResult::Ok(std::nullopt);
|
|
}
|
|
std::vector<ChatTool> tools_ = request.tools.value();
|
|
std::string tool_choice_ = request.tool_choice.value();
|
|
|
|
// TODO: support with tool choice as dict
|
|
for (const auto& tool : tools_) {
|
|
if (tool.function.name == tool_choice_) {
|
|
tvm::ffi::json::Value function_str(tool.function.AsJSON());
|
|
return TResult::Ok(tvm::ffi::json::Stringify(function_str));
|
|
}
|
|
}
|
|
|
|
if (tool_choice_ != "auto") {
|
|
return TResult::Error("Invalid tool_choice value in the request: " + tool_choice_);
|
|
}
|
|
|
|
tvm::ffi::json::Array function_list;
|
|
for (const auto& tool : tools_) {
|
|
function_list.push_back(tool.function.AsJSON());
|
|
}
|
|
|
|
tvm::ffi::json::Value function_list_json(function_list);
|
|
return TResult::Ok(tvm::ffi::json::Stringify(function_list_json));
|
|
};
|
|
|
|
Result<std::vector<Data>> CreatePrompt(const Conversation& conv,
|
|
const ChatCompletionRequest& request,
|
|
const ModelConfig& config, DLDevice device) {
|
|
using TResult = Result<std::vector<Data>>;
|
|
|
|
Result<std::optional<std::string>> fn_call_str_tmp = TryGetFunctionCallingString(conv, request);
|
|
if (fn_call_str_tmp.IsErr()) {
|
|
return TResult::Error(fn_call_str_tmp.UnwrapErr());
|
|
}
|
|
std::optional<std::string> fn_call_string = fn_call_str_tmp.Unwrap();
|
|
|
|
// Handle system message
|
|
// concz
|
|
bool has_custom_system = false;
|
|
std::string custom_system_inputs;
|
|
|
|
auto f_populate_system_message = [&](const std::vector<ChatCompletionMessage>& msg_vec) {
|
|
for (ChatCompletionMessage msg : msg_vec) {
|
|
if (msg.role == "system") {
|
|
TVM_FFI_ICHECK(msg.content.IsText()) << "System message must be text";
|
|
custom_system_inputs += msg.content.Text();
|
|
has_custom_system = true;
|
|
}
|
|
}
|
|
};
|
|
// go through messages in template and passed in.
|
|
f_populate_system_message(conv.messages);
|
|
f_populate_system_message(request.messages);
|
|
|
|
// pending text records the text to be put into data
|
|
// we lazily accumulate the pending text
|
|
// to reduce amount of segments in the Data vector
|
|
std::string pending_text =
|
|
conv.GetSystemText(has_custom_system ? custom_system_inputs : conv.system_message);
|
|
|
|
// Get the message strings
|
|
std::vector<Data> message_list;
|
|
size_t non_system_msg_count = 0;
|
|
|
|
// returns error if error happens
|
|
auto f_process_messages =
|
|
[&](const std::vector<ChatCompletionMessage>& msg_vec) -> std::optional<TResult> {
|
|
for (size_t i = 0; i < msg_vec.size(); ++i) {
|
|
const ChatCompletionMessage& msg = msg_vec[i];
|
|
// skip system message as it is already processed
|
|
if (msg.role == "system") continue;
|
|
|
|
auto role_it = conv.roles.find(msg.role);
|
|
if (role_it == conv.roles.end()) {
|
|
return TResult::Error("Role \"" + msg.role + "\" is not supported");
|
|
}
|
|
const std::string& role_name = role_it->second;
|
|
// skip when content is empty
|
|
if (msg.content.IsNull()) {
|
|
pending_text += role_name + conv.role_empty_sep;
|
|
continue;
|
|
}
|
|
++non_system_msg_count;
|
|
// assistant uses conv.seps[1] if there are two seps
|
|
int sep_offset = msg.role == "assistant" ? 1 : 0;
|
|
const std::string& seperator = conv.seps[sep_offset % conv.seps.size()];
|
|
// setup role prefix
|
|
std::string role_prefix = "";
|
|
// Do not append role prefix if this is the first message and there is already a system
|
|
// message
|
|
if (conv.add_role_after_system_message || pending_text.empty() || non_system_msg_count != 1) {
|
|
role_prefix = role_name + conv.role_content_sep;
|
|
}
|
|
pending_text += role_prefix;
|
|
|
|
if (msg.content.IsParts()) {
|
|
for (const auto& item : msg.content.Parts()) {
|
|
auto it_type = item.find("type");
|
|
if (it_type == item.end()) {
|
|
return TResult::Error("The content of a message does not have \"type\" field");
|
|
}
|
|
if (it_type->second == "text") {
|
|
auto it_text = item.find("text");
|
|
if (it_text == item.end()) {
|
|
return TResult::Error(
|
|
"The text type content of a message does not have \"text\" field");
|
|
}
|
|
// replace placeholder[ROLE] with input message from role
|
|
pending_text += conv.GetRoleText(msg.role, it_text->second, fn_call_string);
|
|
} else if (it_type->second == "image_url") {
|
|
if (item.find("image_url") == item.end()) {
|
|
return TResult::Error("Content should have an image_url field");
|
|
}
|
|
std::string image_url =
|
|
item.at("image_url"); // TODO(mlc-team): According to OpenAI API reference this
|
|
// should be a map, with a "url" key containing the URL, but
|
|
// we are just assuming this as the URL for now
|
|
std::string base64_image = image_url.substr(image_url.find(",") + 1);
|
|
Result<Tensor> image_data_res = LoadImageFromBase64(base64_image);
|
|
if (image_data_res.IsErr()) {
|
|
return TResult::Error(image_data_res.UnwrapErr());
|
|
}
|
|
if (!config.vision_config.has_value()) {
|
|
return TResult::Error("Vision config is required for image input");
|
|
}
|
|
int image_size = config.vision_config.value().image_size;
|
|
int patch_size = config.vision_config.value().patch_size;
|
|
|
|
int embed_size = (image_size * image_size) / (patch_size * patch_size);
|
|
|
|
Tensor image_data = image_data_res.Unwrap();
|
|
std::vector<int64_t> new_shape = {1, image_size, image_size, 3};
|
|
Tensor image_tensor = image_data.CreateView(new_shape, image_data.DataType());
|
|
// TODO: Not sure if commenting will affect other functions. But
|
|
// python part will do clip preprocessing. auto image_tensor =
|
|
// ClipPreprocessor(image_data_res.Unwrap(), image_size, device);
|
|
// lazily commit text data
|
|
if (pending_text.length() != 0) {
|
|
message_list.push_back(TextData(pending_text));
|
|
pending_text = "";
|
|
}
|
|
message_list.push_back(ImageData(image_tensor, embed_size));
|
|
} else {
|
|
return TResult::Error("Unsupported content type: " + it_type->second);
|
|
}
|
|
}
|
|
} else {
|
|
TVM_FFI_ICHECK(msg.content.IsText());
|
|
pending_text += conv.GetRoleText(msg.role, msg.content.Text(), fn_call_string);
|
|
}
|
|
pending_text += seperator;
|
|
}
|
|
return std::nullopt;
|
|
};
|
|
|
|
// Optionally strip `<think>...</think>` blocks from historical assistant
|
|
// messages (those before the last user message), mirroring Qwen3's HF chat
|
|
// template. See mlc-ai/mlc-llm#3482.
|
|
const std::vector<ChatCompletionMessage>* conv_messages_ptr = &conv.messages;
|
|
const std::vector<ChatCompletionMessage>* request_messages_ptr = &request.messages;
|
|
std::vector<ChatCompletionMessage> stripped_conv_messages;
|
|
std::vector<ChatCompletionMessage> stripped_request_messages;
|
|
if (conv.strip_reasoning_in_history) {
|
|
const size_t conv_size = conv.messages.size();
|
|
int64_t last_user_idx = -1;
|
|
for (size_t i = 0; i < conv_size; ++i) {
|
|
if (conv.messages[i].role == "user") last_user_idx = static_cast<int64_t>(i);
|
|
}
|
|
for (size_t i = 0; i < request.messages.size(); ++i) {
|
|
if (request.messages[i].role == "user") last_user_idx = static_cast<int64_t>(conv_size + i);
|
|
}
|
|
const std::string kCloseTag = "</think>";
|
|
auto strip_range = [&](const std::vector<ChatCompletionMessage>& in, size_t offset,
|
|
std::vector<ChatCompletionMessage>& out) {
|
|
out.reserve(in.size());
|
|
for (size_t i = 0; i < in.size(); ++i) {
|
|
ChatCompletionMessage msg = in[i];
|
|
const int64_t global_idx = static_cast<int64_t>(offset + i);
|
|
if (msg.role == "assistant" && global_idx < last_user_idx && msg.content.IsText()) {
|
|
const std::string& text = msg.content.Text();
|
|
const size_t close_pos = text.rfind(kCloseTag);
|
|
if (close_pos != std::string::npos) {
|
|
size_t start = close_pos + kCloseTag.size();
|
|
while (start < text.size() && text[start] == '\n') ++start;
|
|
msg.content = ChatCompletionMessageContent(text.substr(start));
|
|
}
|
|
}
|
|
out.push_back(std::move(msg));
|
|
}
|
|
};
|
|
strip_range(conv.messages, 0, stripped_conv_messages);
|
|
strip_range(request.messages, conv_size, stripped_request_messages);
|
|
conv_messages_ptr = &stripped_conv_messages;
|
|
request_messages_ptr = &stripped_request_messages;
|
|
}
|
|
|
|
if (auto err = f_process_messages(*conv_messages_ptr)) {
|
|
return err.value();
|
|
}
|
|
if (auto err = f_process_messages(*request_messages_ptr)) {
|
|
return err.value();
|
|
}
|
|
// append last assistant begin message
|
|
ChatCompletionMessage last_assistant_begin;
|
|
last_assistant_begin.role = "assistant";
|
|
last_assistant_begin.content = std::nullopt;
|
|
if (auto err = f_process_messages({last_assistant_begin})) {
|
|
return err.value();
|
|
}
|
|
if (pending_text.length() != 0) {
|
|
message_list.push_back(TextData(pending_text));
|
|
}
|
|
// Handle system_prefix_token_ids
|
|
if (conv.system_prefix_token_ids.has_value()) {
|
|
message_list.insert(message_list.begin(), TokenData(conv.system_prefix_token_ids.value()));
|
|
}
|
|
return TResult::Ok(message_list);
|
|
}
|
|
|
|
Result<Conversation> Conversation::FromJSON(const tvm::ffi::json::Object& json_obj) {
|
|
using TResult = Result<Conversation>;
|
|
Conversation conv;
|
|
|
|
Result<std::optional<std::string>> name_res =
|
|
json::LookupOptionalWithResultReturn<std::string>(json_obj, "name");
|
|
if (name_res.IsErr()) {
|
|
return TResult::Error(name_res.UnwrapErr());
|
|
}
|
|
conv.name = name_res.Unwrap();
|
|
|
|
Result<std::string> system_template_res =
|
|
json::LookupWithResultReturn<std::string>(json_obj, "system_template");
|
|
if (system_template_res.IsErr()) {
|
|
return TResult::Error(system_template_res.UnwrapErr());
|
|
}
|
|
conv.system_template = system_template_res.Unwrap();
|
|
|
|
Result<std::string> system_message_res =
|
|
json::LookupWithResultReturn<std::string>(json_obj, "system_message");
|
|
if (system_message_res.IsErr()) {
|
|
return TResult::Error(system_message_res.UnwrapErr());
|
|
}
|
|
conv.system_message = system_message_res.Unwrap();
|
|
|
|
Result<std::optional<tvm::ffi::json::Array>> system_prefix_token_ids_arr_res =
|
|
json::LookupOptionalWithResultReturn<tvm::ffi::json::Array>(json_obj,
|
|
"system_prefix_token_ids");
|
|
if (system_prefix_token_ids_arr_res.IsErr()) {
|
|
return TResult::Error(system_prefix_token_ids_arr_res.UnwrapErr());
|
|
}
|
|
std::optional<tvm::ffi::json::Array> system_prefix_token_ids_arr =
|
|
system_prefix_token_ids_arr_res.Unwrap();
|
|
if (system_prefix_token_ids_arr.has_value()) {
|
|
std::vector<int> system_prefix_token_ids;
|
|
system_prefix_token_ids.reserve(system_prefix_token_ids_arr.value().size());
|
|
for (const auto& token_id : system_prefix_token_ids_arr.value()) {
|
|
if (!token_id.try_cast<int64_t>().has_value()) {
|
|
return TResult::Error("A system prefix token id is not integer.");
|
|
}
|
|
system_prefix_token_ids.push_back(static_cast<int>(token_id.cast<int64_t>()));
|
|
}
|
|
conv.system_prefix_token_ids = std::move(system_prefix_token_ids);
|
|
}
|
|
|
|
Result<bool> add_role_after_system_message_res =
|
|
json::LookupWithResultReturn<bool>(json_obj, "add_role_after_system_message");
|
|
if (add_role_after_system_message_res.IsErr()) {
|
|
return TResult::Error(add_role_after_system_message_res.UnwrapErr());
|
|
}
|
|
conv.add_role_after_system_message = add_role_after_system_message_res.Unwrap();
|
|
|
|
Result<tvm::ffi::json::Object> roles_object_res =
|
|
json::LookupWithResultReturn<tvm::ffi::json::Object>(json_obj, "roles");
|
|
if (roles_object_res.IsErr()) {
|
|
return TResult::Error(roles_object_res.UnwrapErr());
|
|
}
|
|
for (const auto& role : roles_object_res.Unwrap()) {
|
|
if (!role.second.try_cast<std::string>().has_value()) {
|
|
return TResult::Error("A role value in the conversation template is not a string.");
|
|
}
|
|
conv.roles[role.first.cast<tvm::ffi::String>()] = role.second.cast<std::string>();
|
|
}
|
|
|
|
Result<std::optional<tvm::ffi::json::Object>> role_templates_object_res =
|
|
json::LookupOptionalWithResultReturn<tvm::ffi::json::Object>(json_obj, "role_templates");
|
|
if (role_templates_object_res.IsErr()) {
|
|
return TResult::Error(role_templates_object_res.UnwrapErr());
|
|
}
|
|
std::optional<tvm::ffi::json::Object> role_templates_object = role_templates_object_res.Unwrap();
|
|
if (role_templates_object.has_value()) {
|
|
for (const auto& [role, msg] : role_templates_object.value()) {
|
|
if (!msg.try_cast<std::string>().has_value()) {
|
|
return TResult::Error("A value in \"role_templates\" is not a string.");
|
|
}
|
|
conv.role_templates[role.cast<tvm::ffi::String>()] = msg.cast<std::string>();
|
|
}
|
|
}
|
|
|
|
Result<tvm::ffi::json::Array> messages_arr_res =
|
|
json::LookupWithResultReturn<tvm::ffi::json::Array>(json_obj, "messages");
|
|
if (messages_arr_res.IsErr()) {
|
|
return TResult::Error(messages_arr_res.UnwrapErr());
|
|
}
|
|
for (const auto& message : messages_arr_res.Unwrap()) {
|
|
if (!message.try_cast<tvm::ffi::json::Array>().has_value() ||
|
|
message.cast<tvm::ffi::json::Array>().size() != 2) {
|
|
return TResult::Error(
|
|
"A message in the conversation template is not an array of [role, content].");
|
|
}
|
|
tvm::ffi::json::Array message_arr = message.cast<tvm::ffi::json::Array>();
|
|
if (!message_arr[0].try_cast<std::string>().has_value()) {
|
|
return TResult::Error("The role of a message in the conversation template is not a string.");
|
|
}
|
|
std::string role = message_arr[0].cast<std::string>();
|
|
// content can be a string or an array of objects
|
|
if (message_arr[1].try_cast<std::string>().has_value()) {
|
|
ChatCompletionMessage msg;
|
|
msg.role = role;
|
|
msg.content = message_arr[1].cast<std::string>();
|
|
conv.messages.push_back(msg);
|
|
continue;
|
|
} else if (message_arr[1].try_cast<tvm::ffi::json::Array>().has_value()) {
|
|
tvm::ffi::json::Array content_arr = message_arr[1].cast<tvm::ffi::json::Array>();
|
|
std::vector<std::unordered_map<std::string, std::string>> content;
|
|
content.reserve(content_arr.size());
|
|
for (const auto& item : content_arr) {
|
|
if (!item.try_cast<tvm::ffi::json::Object>().has_value()) {
|
|
return TResult::Error("The content of conversation template message is not an object");
|
|
}
|
|
std::unordered_map<std::string, std::string> item_map;
|
|
for (const auto& [key, value] : item.cast<tvm::ffi::json::Object>()) {
|
|
item_map[key.cast<tvm::ffi::String>()] = tvm::ffi::json::Stringify(value);
|
|
}
|
|
content.push_back(std::move(item_map));
|
|
}
|
|
ChatCompletionMessage msg;
|
|
msg.role = role;
|
|
msg.content = content;
|
|
conv.messages.push_back(msg);
|
|
continue;
|
|
} else {
|
|
return TResult::Error(
|
|
"The content of a message in the conversation template is not a string or an array.");
|
|
}
|
|
}
|
|
|
|
Result<tvm::ffi::json::Array> seps_arr_res =
|
|
json::LookupWithResultReturn<tvm::ffi::json::Array>(json_obj, "seps");
|
|
if (seps_arr_res.IsErr()) {
|
|
return TResult::Error(seps_arr_res.UnwrapErr());
|
|
}
|
|
std::vector<std::string> seps;
|
|
for (const auto& sep : seps_arr_res.Unwrap()) {
|
|
if (!sep.try_cast<std::string>().has_value()) {
|
|
return TResult::Error("A separator (\"seps\") of the conversation template is not a string");
|
|
}
|
|
conv.seps.push_back(sep.cast<std::string>());
|
|
}
|
|
|
|
Result<std::string> role_content_sep_res =
|
|
json::LookupWithResultReturn<std::string>(json_obj, "role_content_sep");
|
|
if (role_content_sep_res.IsErr()) {
|
|
return TResult::Error(role_content_sep_res.UnwrapErr());
|
|
}
|
|
conv.role_content_sep = role_content_sep_res.Unwrap();
|
|
|
|
Result<std::string> role_empty_sep_res =
|
|
json::LookupWithResultReturn<std::string>(json_obj, "role_empty_sep");
|
|
if (role_empty_sep_res.IsErr()) {
|
|
return TResult::Error(role_empty_sep_res.UnwrapErr());
|
|
}
|
|
conv.role_empty_sep = role_empty_sep_res.Unwrap();
|
|
|
|
Result<tvm::ffi::json::Array> stop_str_arr_res =
|
|
json::LookupWithResultReturn<tvm::ffi::json::Array>(json_obj, "stop_str");
|
|
if (stop_str_arr_res.IsErr()) {
|
|
return TResult::Error(stop_str_arr_res.UnwrapErr());
|
|
}
|
|
for (const auto& stop : stop_str_arr_res.Unwrap()) {
|
|
if (!stop.try_cast<std::string>().has_value()) {
|
|
return TResult::Error(
|
|
"A stop string (\"stop_str\") of the conversation template is not a string.");
|
|
}
|
|
conv.stop_str.push_back(stop.cast<std::string>());
|
|
}
|
|
|
|
Result<tvm::ffi::json::Array> stop_token_ids_arr_res =
|
|
json::LookupWithResultReturn<tvm::ffi::json::Array>(json_obj, "stop_token_ids");
|
|
if (stop_token_ids_arr_res.IsErr()) {
|
|
return TResult::Error(stop_token_ids_arr_res.UnwrapErr());
|
|
}
|
|
for (const auto& stop : stop_token_ids_arr_res.Unwrap()) {
|
|
if (!stop.try_cast<int64_t>().has_value()) {
|
|
return TResult::Error(
|
|
"A stop token id (\"stop_token_ids\") of the conversation template is not an integer.");
|
|
}
|
|
conv.stop_token_ids.push_back(static_cast<int>(stop.cast<int64_t>()));
|
|
}
|
|
|
|
Result<std::optional<bool>> strip_reasoning_res =
|
|
json::LookupOptionalWithResultReturn<bool>(json_obj, "strip_reasoning_in_history");
|
|
if (strip_reasoning_res.IsErr()) {
|
|
return TResult::Error(strip_reasoning_res.UnwrapErr());
|
|
}
|
|
conv.strip_reasoning_in_history = strip_reasoning_res.Unwrap().value_or(false);
|
|
|
|
return TResult::Ok(conv);
|
|
}
|
|
|
|
Result<Conversation> Conversation::FromJSON(const std::string& json_str) {
|
|
Result<tvm::ffi::json::Object> json_obj = json::ParseToJSONObjectWithResultReturn(json_str);
|
|
if (json_obj.IsErr()) {
|
|
return Result<Conversation>::Error(json_obj.UnwrapErr());
|
|
}
|
|
return Conversation::FromJSON(json_obj.Unwrap());
|
|
}
|
|
|
|
} // namespace json_ffi
|
|
} // namespace llm
|
|
} // namespace mlc
|