diff --git a/server-rs/crates/api-server/src/editor_agent/reconcile.rs b/server-rs/crates/api-server/src/editor_agent/reconcile.rs new file mode 100644 index 000000000..8517cfea3 --- /dev/null +++ b/server-rs/crates/api-server/src/editor_agent/reconcile.rs @@ -0,0 +1,176 @@ +use serde_json::Value; +use module_editor_agent::agent::tool::Tool; +use shared_contracts::assets::{EditorAudioGenerateResponse, EditorVideoGenerateResponse}; +use shared_contracts::editor_agent::{EditorAgentConversationMessagesDocument, EditorAgentMessage, EditorAgentToolCallStatus}; +use spacetime_client::{EditorAgentConversationRecord, ExternalGenerationJobGetRecordInput}; +use crate::editor_agent::editor_tools::common::EditorToolContext; +use crate::editor_agent::editor_tools::edit_image::{EditImageTool, EditImageToolArgs, EditorImageEditResult}; +use crate::editor_agent::editor_tools::generate_background_music::{GenerateBackgroundMusicTool, GenerateBackgroundMusicToolArgs}; +use crate::editor_agent::editor_tools::generate_character::GenerateCharacterTool; +use crate::editor_agent::editor_tools::generate_icon_spritesheet::{EditorIconSpritesheetResult, GenerateIconSpritesheetTool, GenerateIconSpritesheetToolArgs}; +use crate::editor_agent::editor_tools::generate_image::{EditorImageGenerationResult, GenerateImageTool, GenerateImageToolArgs}; +use crate::editor_agent::editor_tools::generate_sound_effect::{GenerateSoundEffectTool, GenerateSoundEffectToolArgs}; +use crate::editor_agent::editor_tools::generate_ui_design::GenerateUiDesignTool; +use crate::editor_agent::editor_tools::generate_video::{GenerateVideoTool, GenerateVideoToolArgs}; +use crate::editor_agent::resp_to_asset; +use crate::http_error::AppError; +use crate::state::AppState; + +pub async fn reconcile_editor_agent_tool_calls( + state: &AppState, + conversation: &EditorAgentConversationRecord, + document: &mut EditorAgentConversationMessagesDocument, +) -> Result, AppError> { + let candidates = document + .messages + .iter() + .enumerate() + .filter_map(|(message_index, message)| { + let tool_call = message.tool_call.as_ref()?; + if tool_call.status != EditorAgentToolCallStatus::NotCompleted { + return None; + } + Some((message_index, tool_call.external_job_id.clone()?)) + }) + .collect::>(); + let mut reconciled = Vec::new(); + + for (message_index, job_id) in candidates { + let job = match state + .spacetime_client() + .get_external_generation_job(ExternalGenerationJobGetRecordInput { + job_id, + owner_user_id: conversation.owner_user_id.clone(), + }) + .await + { + Ok(job) => job, + Err(_) => continue, + }; + + let changed = match job.status.as_str() { + "completed" => reconcile_completed_editor_agent_tool_call( + &mut document.messages[message_index], + job.result_payload_json.as_deref(), + ), + "failed" => { + let tool_call = document.messages[message_index] + .tool_call + .as_mut() + .expect("candidate contains a tool call"); + let error = job + .last_error_message + .unwrap_or_else(|| "生成失败".to_string()); + tool_call.error = Some(error.clone()); + tool_call.status = EditorAgentToolCallStatus::Failed; + document.messages[message_index].text = + format!("[tool_call:{}] output: {error}", tool_call.tool_name); + Ok(()) + } + _ => continue, + }; + + match changed { + Ok(()) => reconciled.push(document.messages[message_index].clone()), + Err(error) => { + let tool_call = document.messages[message_index] + .tool_call + .as_mut() + .expect("candidate contains a tool call"); + tool_call.error = Some(error.clone()); + tool_call.status = EditorAgentToolCallStatus::Failed; + document.messages[message_index].text = + format!("[tool_call:{}] output: {error}", tool_call.tool_name); + reconciled.push(document.messages[message_index].clone()); + } + } + } + + Ok(reconciled) +} + +fn reconcile_completed_editor_agent_tool_call( + message: &mut EditorAgentMessage, + result_payload_json: Option<&str>, +) -> Result<(), String> { + let response = result_payload_json + .ok_or_else(|| "生成任务缺少结果数据".to_string()) + .and_then(|payload| serde_json::from_str::(payload).map_err(|e| e.to_string()))? + .get("response") + .cloned() + .ok_or_else(|| "生成任务缺少 response 结果".to_string())?; + let tool_call = message + .tool_call + .as_mut() + .ok_or_else(|| "消息缺少工具调用".to_string())?; + let tool_name = tool_call.tool_name.clone(); + + match tool_name.as_str() { + GenerateImageTool::NAME | GenerateCharacterTool::NAME | GenerateUiDesignTool::NAME => { + let args: GenerateImageToolArgs = parse_reconciled_value(&tool_call.args)?; + let result: EditorImageGenerationResult = parse_reconciled_value(&response)?; + message.text = match tool_name.as_str() { + GenerateCharacterTool::NAME => GenerateCharacterTool { + context: EditorToolContext::default(), + } + .format_execute_message(&args, &result), + GenerateUiDesignTool::NAME => GenerateUiDesignTool { + context: EditorToolContext::default(), + } + .format_execute_message(&args, &result), + _ => GenerateImageTool { + context: EditorToolContext::default(), + } + .format_execute_message(&args, &result), + }; + tool_call.images = vec![resp_to_asset::editor_agent_generated_image(&result)]; + } + EditImageTool::NAME => { + let args: EditImageToolArgs = parse_reconciled_value(&tool_call.args)?; + let result: EditorImageEditResult = parse_reconciled_value(&response)?; + tool_call.images = vec![resp_to_asset::editor_agent_generated_edited_image(&result)]; + message.text = EditImageTool { + context: EditorToolContext::default(), + } + .format_execute_message(args, result); + } + GenerateIconSpritesheetTool::NAME => { + let args: GenerateIconSpritesheetToolArgs = parse_reconciled_value(&tool_call.args)?; + let result: EditorIconSpritesheetResult = parse_reconciled_value(&response)?; + message.text = GenerateIconSpritesheetTool { + context: EditorToolContext::default(), + } + .format_execute_message(&args, &result); + tool_call.images = vec![resp_to_asset::editor_agent_generated_spritesheet(&result)]; + } + GenerateVideoTool::NAME => { + let args: GenerateVideoToolArgs = parse_reconciled_value(&tool_call.args)?; + let result: EditorVideoGenerateResponse = parse_reconciled_value(&response)?; + message.text = GenerateVideoTool { + context: EditorToolContext::default(), + } + .format_execute_message(&args, &result); + tool_call.videos = vec![resp_to_asset::editor_agent_generated_video(&result)]; + } + GenerateSoundEffectTool::NAME | GenerateBackgroundMusicTool::NAME => { + let result: EditorAudioGenerateResponse = parse_reconciled_value(&response)?; + message.text = if tool_name == GenerateSoundEffectTool::NAME { + let args: GenerateSoundEffectToolArgs = parse_reconciled_value(&tool_call.args)?; + GenerateSoundEffectTool.format_execute_message(&args, &result) + } else { + let args: GenerateBackgroundMusicToolArgs = + parse_reconciled_value(&tool_call.args)?; + GenerateBackgroundMusicTool.format_execute_message(&args, &result) + }; + tool_call.audios = vec![resp_to_asset::editor_agent_generated_audio(&result)]; + } + _ => return Err(format!("不支持回填工具 {tool_name}")), + } + tool_call.error = None; + tool_call.status = EditorAgentToolCallStatus::Completed; + Ok(()) +} + +fn parse_reconciled_value(value: &Value) -> Result { + serde_json::from_value(value.clone()).map_err(|error| error.to_string()) +} \ No newline at end of file