use std::collections::HashMap;
use error_stack::{Report, ResultExt};
use thiserror::Error;
use indoc::indoc;
use crate::prompt::model::{Content, Info, Prompt, Template};
use crate::schema::tool_schema::ChatToolSchemaError;
#[derive(Debug, Error)]
pub enum OutputDescriptionError {
#[error("Missing 'json_schema' field")]
MissingJsonSchemaField,
#[error("Missing or invalid 'name' field")]
MissingNameField,
#[error("Missing or invalid 'description' field")]
MissingDescriptionField,
#[error("Missing 'schema' field")]
MissingSchemaField,
#[error("Missing 'properties' field")]
MissingPropertiesField,
}
pub fn assemble(template: &Template, info_with_contents: &HashMap<Info, Content>) -> HashMap<String, Prompt> {
let mut result = HashMap::with_capacity(info_with_contents.len());
for (info, content) in info_with_contents {
let character_prompts = assemble_character_prompt(template, content);
let stage_prompts = assemble_stage_prompt(content);
result.insert(info.name.clone(), Prompt {
character_prompts,
stage_prompts,
});
}
result
}
fn assemble_character_prompt(template: &Template, content: &Content) -> HashMap<String, String> {
let tcp = &template.character_prompts; let ccp = &content.character_prompts; let num_chars = content.character_prompts.character_names.len();
let mut result = HashMap::with_capacity(num_chars);
for character_name in &content.character_prompts.character_names {
let mut character_prompt_parts = Vec::with_capacity(7);
let field_pairs = [
(&tcp.task_description, &ccp.task_description),
(&tcp.principle, &ccp.principle),
(&tcp.how_to_think, &ccp.how_to_think),
(&tcp.examples, &ccp.examples),
];
for (template_field, content_field) in field_pairs.iter() {
if let Some(value) = content_field.get(character_name)
.filter(|value| !value.is_empty())
.or_else(|| content_field.get("assistant"))
.filter(|value| !value.is_empty())
{
character_prompt_parts.push(build_element(
&template_field.element_name,
&template_field.description,
value,
));
}
}
let mut stage_content = String::with_capacity(content.stage_prompt.len() * 50);
for stage_prompt in &content.stage_prompt {
stage_content.push_str(&format!("{}: {}\n", stage_prompt.name, stage_prompt.description));
}
character_prompt_parts.push(build_element(
&template.character_prompts.stage_description.element_name,
&template.character_prompts.stage_description.description,
&stage_content,
));
result.insert(character_name.clone(), character_prompt_parts.join(""));
}
result
}
#[inline]
fn build_element(element_name: &str, element_description: &str, content: &str) -> String {
if content.is_empty() {
String::new()
} else {
let capacity = element_name.len() * 2 + element_description.len() + content.len() + 20;
let mut result = String::with_capacity(capacity);
result.push_str("<");
result.push_str(element_name);
result.push_str(">\n <!-- ");
result.push_str(element_description);
result.push_str(" -->\n");
result.push_str(content);
result.push_str("</");
result.push_str(element_name);
result.push_str(">\n");
result
}
}
#[inline]
fn assemble_stage_prompt(content: &Content) -> HashMap<String, String>{
let mut result = HashMap::with_capacity(content.stage_prompt.len());
for stage_prompt in &content.stage_prompt {
result.insert(stage_prompt.name.clone(), stage_prompt.content.clone());
}
result
}
pub fn assemble_output_description(
json_schema: serde_json::Value,
) -> error_stack::Result<String, OutputDescriptionError> {
let json_schema = json_schema
.get("json_schema")
.ok_or(Report::new(OutputDescriptionError::MissingJsonSchemaField))?;
let name = json_schema
.get("name")
.and_then(serde_json::Value::as_str)
.ok_or(Report::new(OutputDescriptionError::MissingNameField))?;
let description = json_schema
.get("description")
.and_then(serde_json::Value::as_str)
.ok_or(Report::new(OutputDescriptionError::MissingDescriptionField))?;
let schema = json_schema
.get("schema")
.ok_or(Report::new(OutputDescriptionError::MissingSchemaField))?;
let properties = schema
.get("properties")
.ok_or(Report::new(OutputDescriptionError::MissingPropertiesField))?;
let mut result = String::with_capacity(1024);
result.push_str("你的回答需要包含以下内容。\n");
result.push_str(name);
result.push_str(": ");
result.push_str(description);
result.push_str("\n");
result.push_str(&extract_properties(properties, 1));
Ok(result)
}
pub fn assemble_tools_prompt(json_schema_vec: Vec<serde_json::Value>) -> error_stack::Result<String, ChatToolSchemaError> {
let mut tools = String::with_capacity(json_schema_vec.len() * 256);
for json_schema in json_schema_vec {
tools.push_str(
&assemble_tool_prompt(json_schema)
.change_context(ChatToolSchemaError::AssembleToolPrompt)?
);
}
let mut indented_tools = String::with_capacity(tools.len() + tools.lines().count() * 8);
for line in tools.lines() {
indented_tools.push_str(" ");
indented_tools.push_str(line);
indented_tools.push('\n');
}
let result = format!(
indoc! {"
<ToolUse>
当你需要调用某个工具时,请在回答中使用 <ToolUse></ToolUse> 标签,遵循以下要求:
1. 每个标签仅包含一个工具调用,且工具的调用必须按照参数要求提供完整信息。
2. 每个标签内的内容应包含:
- 工具名称:如 send_email。
- 工具描述:简要描述该工具的功能。
- 参数:提供工具所需的所有参数,并确保格式正确(如类型、命名等)。
3. 你可以在同一回答中使用多个<ToolUse></ToolUse>标签,每个标签对应任意你想要的工具调用。
4. 我会根据你提供的调用信息执行相应的操作,并将结果返回给你。
5. 不要在回答中仅包含<ToolUse></ToolUse>标签, 带有一些其他的文字, 可以是你的想法或是其他想表述的内容。\n
你可以使用以下工具:\n\n{}\n
</ToolUse>
"},
indented_tools );
Ok(result)
}
fn assemble_tool_prompt(json_schema: serde_json::Value) -> error_stack::Result<String, ChatToolSchemaError> {
let function = json_schema.get("function")
.ok_or(Report::new(ChatToolSchemaError::MissingFunctionField))?;
let function_name = function.get("name")
.and_then(serde_json::Value::as_str)
.ok_or(Report::new(ChatToolSchemaError::MissingFunctionName))?;
let function_desc = function.get("description")
.and_then(serde_json::Value::as_str)
.ok_or(Report::new(ChatToolSchemaError::MissingFunctionDescription))?;
let parameters = function.get("parameters")
.ok_or(Report::new(ChatToolSchemaError::MissingFunctionParameters))?;
let properties = parameters.get("properties")
.ok_or(Report::new(ChatToolSchemaError::MissingFunctionProperties))?;
let mut result = String::with_capacity(512);
result.push_str("函数名: ");
result.push_str(function_name);
result.push_str("\n函数描述: ");
result.push_str(function_desc);
result.push_str("\n");
result.push_str(&extract_properties(properties, 1));
Ok(result)
}
pub fn extract_properties(properties: &serde_json::Value, indent: usize) -> String {
let props_len = properties.as_object().map_or(0, |obj| obj.len());
let mut result = String::with_capacity(props_len * 128);
let indent_str = " ".repeat(indent);
if let Some(props) = properties.as_object() {
for (prop_name, prop_value) in props {
if prop_name == "cot" {
continue;
}
let mut line = String::with_capacity(prop_name.len() + 100);
line.push_str(&indent_str);
line.push_str(prop_name);
let prop_type = prop_value.get("type");
let prop_desc = prop_value.get("description").and_then(|d| d.as_str());
let prop_enum = prop_value.get("enum");
if let Some(type_val) = prop_type {
match type_val {
serde_json::Value::String(type_str) => {
line.push_str(" (");
line.push_str(type_str);
line.push_str(")");
}
serde_json::Value::Array(type_array) => {
let mut types = Vec::with_capacity(type_array.len());
for v in type_array {
if let Some(s) = v.as_str() {
types.push(s.to_string());
}
}
if !types.is_empty() {
line.push_str(" ([");
line.push_str(&types.join(", "));
line.push_str("])");
}
}
_ => {}
}
}
if let Some(desc) = prop_desc {
line.push_str(": ");
line.push_str(desc);
}
if let Some(enum_val) = prop_enum {
if let Some(enum_values) = enum_val.as_array() {
let mut enum_strings = Vec::with_capacity(enum_values.len());
for v in enum_values {
if let Some(s) = v.as_str() {
enum_strings.push(s.to_string());
}
}
if !enum_strings.is_empty() {
line.push_str(" (Enum: [");
line.push_str(&enum_strings.join(", "));
line.push_str("])");
}
}
}
line.push('\n');
result.push_str(&line);
if prop_type == Some(&serde_json::Value::String("object".to_string())) {
if let Some(sub_properties) = prop_value.get("properties") {
result.push_str(&extract_properties(sub_properties, indent + 1));
}
}
}
}
result
}