clean code

This commit is contained in:
2026-07-13 15:24:19 +08:00
parent 4e0c0c8cd9
commit af6b8021ac
9 changed files with 60 additions and 588 deletions
@@ -4,6 +4,9 @@ use crate::openai_image_generation::GPT_IMAGE_2_MODEL;
use module_editor_agent::agent::tool::Tool;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use axum::http::StatusCode;
use serde_json::json;
use crate::http_error::AppError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EditorToolContext {
@@ -44,3 +47,10 @@ pub(crate) fn editor_agent_image_mud_points(
};
pricing.image_generation_mud_points(kind, Some(GPT_IMAGE_2_MODEL), Some(normalized_image_size))
}
pub fn image_not_found(image_id: &ImageId) -> AppError {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "editor-agent",
"message": format!("asset {image_id} not found in context"),
}))
}
@@ -1,10 +1,10 @@
use crate::editor_agent::editor_tools::common::{
EditorAgentPricedTool, EditorToolContext, editor_agent_image_mud_points,
editor_agent_image_mud_points, EditorAgentPricedTool, EditorToolContext,
};
use crate::editor_agent::utils::ImageId;
use crate::editor_generation_config::EditorGenerationPricingConfig;
use crate::editor_project::{
EditorGenerationCaller, EditorImageEditRequest, edit_editor_image_for_owner,
edit_editor_image_for_owner, EditorGenerationCaller, EditorImageEditRequest,
};
use crate::http_error::AppError;
use crate::request_context::RequestContext;
@@ -12,11 +12,12 @@ use crate::state::AppState;
use axum::http::StatusCode;
use module_editor_agent::agent::tool::{Tool, ToolFailure, ToolFailureKind};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use serde_json::{json, Value};
use shared_contracts::api::ApiSuccessEnvelope;
use shared_contracts::assets::EditorCanvasGenerationCompletionPayload;
use std::error::Error;
use std::fmt::Display;
use crate::editor_agent::editor_tools::common;
pub struct EditImageTool {
pub context: EditorToolContext,
@@ -178,7 +179,6 @@ impl EditImageTool {
Ok(())
}
// TODO should share with format in run.rs
pub fn format_execute_message(
&self,
args: <Self as Tool>::Args,
@@ -199,83 +199,5 @@ impl EditImageTool {
"[tool_call:{name}] args: {arg_json} output: edited result saved as image: {image_id}"
)
}
pub async fn execute(
&self,
state: &AppState,
request_context: &RequestContext,
caller: EditorGenerationCaller,
args: EditImageToolArgs,
model: Option<String>,
project_id: Option<String>,
generation_inputs: Option<Value>,
asset_label: Option<String>,
source_resource_id: Option<String>,
canvas_completion: Option<EditorCanvasGenerationCompletionPayload>,
) -> Result<EditorImageEditResult, AppError> {
let source_image_src = self
.context
.image_data_key(&args.object_image_id)
.ok_or_else(|| image_not_found(&args.object_image_id))?
.to_string();
let reference_image_srcs: Result<Vec<String>, AppError> = args
.reference_image_ids
.into_iter()
.map(|image_id| {
self.context
.image_data_key(&image_id)
.map(ToOwned::to_owned)
.ok_or_else(|| image_not_found(&image_id))
})
.collect();
let reference_image_srcs = reference_image_srcs?;
let result = edit_editor_image_for_owner(
state,
request_context,
caller,
EditorImageEditRequest {
prompt: args.prompt,
source_image_src,
size: None,
model,
reference_image_srcs: Some(reference_image_srcs),
project_id,
asset_kind: Some("editor_agent_edit_image".to_string()),
generation_inputs,
asset_folder_id: Some("project".to_string()),
asset_label,
source_resource_id,
target_layer_id: None,
canvas_completion,
},
)
.await?
.0;
let data = if request_context.wants_envelope() {
serde_json::from_value::<ApiSuccessEnvelope<Value>>(result)
.map_err(|e| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"message": format!("failed to parse success envelope: {e}"),
}))
})?
.data
} else {
result
};
serde_json::from_value::<EditorImageEditResult>(data).map_err(|e| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"message": format!("failed to deserialize edit image result: {e}"),
}))
})
}
}
fn image_not_found(image_id: &ImageId) -> AppError {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "editor-agent",
"message": format!("asset {image_id} not found in context"),
}))
}
@@ -1,23 +1,11 @@
use crate::editor_agent::editor_tools::common::EditorAgentPricedTool;
use crate::editor_agent::editor_tools::generate_sound_effect::{
map_media_response_error, parse_media_response, pending_message,
};
use crate::editor_generation_config::{
EDITOR_BACKGROUND_MUSIC_MODEL_SUNO, EditorGenerationPricingConfig,
EditorGenerationPricingConfig, EDITOR_BACKGROUND_MUSIC_MODEL_SUNO,
};
use crate::http_error::AppError;
use crate::request_context::RequestContext;
use crate::state::AppState;
use crate::vector_engine_audio_generation::generate_editor_background_music_for_owner;
use axum::Json;
use axum::http::StatusCode;
use module_editor_agent::agent::tool::{Tool, ToolFailure};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use shared_contracts::assets::{
EditorAudioGenerateResponse, EditorBackgroundMusicGenerateRequest,
EditorCanvasGenerationCompletionPayload,
};
use serde_json::{json, Value};
use shared_contracts::assets::EditorAudioGenerateResponse;
use std::error::Error;
use std::fmt::Display;
@@ -74,7 +62,7 @@ impl Tool for GenerateBackgroundMusicTool {
return Err(GenerateBackgroundMusicError::PromptNotProvided);
}
Ok(GenerateBackgroundMusicToolOutput {
message: pending_message(),
message: "this tool call is pending user confirmation. if all is pending, just end this turn".to_string(),
})
}
}
@@ -94,41 +82,6 @@ impl EditorAgentPricedTool for GenerateBackgroundMusicTool {
}
impl GenerateBackgroundMusicTool {
#[allow(clippy::too_many_arguments)]
pub async fn execute(
&self,
state: &AppState,
request_context: &RequestContext,
owner_user_id: String,
args: GenerateBackgroundMusicToolArgs,
project_id: String,
generation_inputs: Option<Value>,
asset_label: Option<String>,
canvas_completion: EditorCanvasGenerationCompletionPayload,
) -> Result<EditorAudioGenerateResponse, AppError> {
if args.prompt.trim().is_empty() {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_details(
json!({ "provider": "editor-agent", "message": "背景音乐提示词不能为空" }),
));
}
let response = generate_editor_background_music_for_owner(
state.clone(),
request_context.clone(),
owner_user_id,
Ok(Json(EditorBackgroundMusicGenerateRequest {
gpt_description_prompt: args.prompt,
make_instrumental: args.make_instrumental,
project_id: Some(project_id),
canvas_completion: Some(canvas_completion),
generation_inputs,
asset_folder_id: Some("project".to_string()),
asset_label,
})),
)
.await
.map_err(map_media_response_error)?;
parse_media_response(request_context, response.0)
}
pub fn format_execute_message(
&self,
args: &GenerateBackgroundMusicToolArgs,
@@ -1,18 +1,13 @@
use crate::editor_agent::editor_tools::common::{
EditorAgentPricedTool, EditorToolContext, editor_agent_image_mud_points,
editor_agent_image_mud_points, EditorAgentPricedTool, EditorToolContext,
};
use crate::editor_agent::editor_tools::generate_image::{
EditorImageGenerationResult, GenerateImageError, GenerateImageTool, GenerateImageToolArgs,
GenerateImageToolOutput,
EditorImageGenerationResult, GenerateImageError, GenerateImageTool, GenerateImageToolArgs,
GenerateImageToolOutput,
};
use crate::editor_generation_config::EditorGenerationPricingConfig;
use crate::editor_project::EditorGenerationCaller;
use crate::http_error::AppError;
use crate::request_context::RequestContext;
use crate::state::AppState;
use module_editor_agent::agent::tool::{Tool, ToolFailure};
use serde_json::{Value, json};
use shared_contracts::assets::EditorCanvasGenerationCompletionPayload;
use serde_json::{json, Value};
pub struct GenerateCharacterTool {
pub context: EditorToolContext,
@@ -75,60 +70,23 @@ impl EditorAgentPricedTool for GenerateCharacterTool {
}
impl GenerateCharacterTool {
#[allow(clippy::too_many_arguments)]
pub async fn execute(
&self,
state: &AppState,
request_context: &RequestContext,
caller: EditorGenerationCaller,
args: GenerateImageToolArgs,
project_id: String,
generation_inputs: Option<Value>,
asset_label: Option<String>,
canvas_completion: EditorCanvasGenerationCompletionPayload,
) -> Result<EditorImageGenerationResult, AppError> {
GenerateImageTool {
context: self.context.clone(),
}
.execute_with_kind(
state,
request_context,
caller,
args,
project_id,
generation_inputs,
asset_label,
canvas_completion,
Some("character"),
"character",
)
.await
}
pub fn format_execute_message(
&self,
args: &GenerateImageToolArgs,
result: &EditorImageGenerationResult,
) -> String {
format_tool_message(Self::NAME, args, result)
let tool_name = Self::NAME;
let args = serde_json::to_string(args).unwrap_or_default();
let data_key = result
.object_key
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| value.trim_start_matches('/').to_string())
.unwrap_or_else(|| result.image_src.clone());
let image_id = crate::editor_agent::utils::ImageId::from_data_key(data_key);
format!(
"[tool_call:{tool_name}] args: {args} output: generated result saved as image: {image_id}"
)
}
}
pub(crate) fn format_tool_message(
tool_name: &str,
args: &GenerateImageToolArgs,
result: &EditorImageGenerationResult,
) -> String {
let args = serde_json::to_string(args).unwrap_or_default();
let data_key = result
.object_key
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| value.trim_start_matches('/').to_string())
.unwrap_or_else(|| result.image_src.clone());
let image_id = crate::editor_agent::utils::ImageId::from_data_key(data_key);
format!(
"[tool_call:{tool_name}] args: {args} output: generated result saved as image: {image_id}"
)
}
@@ -1,21 +1,13 @@
use crate::editor_agent::editor_tools::common::{
EditorAgentPricedTool, EditorToolContext, editor_agent_image_mud_points,
editor_agent_image_mud_points, EditorAgentPricedTool, EditorToolContext,
};
use crate::editor_agent::utils::ImageId;
use crate::editor_generation_config::EditorGenerationPricingConfig;
use crate::editor_project::{
EditorGenerationCaller, EditorIconSpritesheetGenerationRequest,
generate_editor_icon_spritesheet_for_owner,
};
use crate::http_error::AppError;
use crate::request_context::RequestContext;
use crate::state::AppState;
use axum::http::StatusCode;
use module_editor_agent::agent::tool::{Tool, ToolFailure, ToolFailureKind};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use shared_contracts::api::ApiSuccessEnvelope;
use shared_contracts::assets::EditorCanvasGenerationCompletionPayload;
use serde_json::{json, Value};
use std::error::Error;
use std::fmt::Display;
@@ -160,79 +152,12 @@ impl GenerateIconSpritesheetTool {
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub async fn execute(
&self,
state: &AppState,
request_context: &RequestContext,
caller: EditorGenerationCaller,
args: GenerateIconSpritesheetToolArgs,
project_id: String,
generation_inputs: Option<Value>,
canvas_completion: EditorCanvasGenerationCompletionPayload,
) -> Result<EditorIconSpritesheetResult, AppError> {
self.validate_args(&args).map_err(|error| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "editor-agent",
"message": error.to_string(),
}))
})?;
let result = generate_editor_icon_spritesheet_for_owner(
state,
request_context,
caller,
EditorIconSpritesheetGenerationRequest {
reference_image_src: self
.context
.image_data_key(&args.reference_image_id)
.ok_or_else(|| image_not_found(&args.reference_image_id))?
.to_string(),
reference_image_srcs: Some(
args.reference_image_ids
.into_iter()
.map(|image_id| {
self.context
.image_data_key(&image_id)
.map(ToOwned::to_owned)
.ok_or_else(|| image_not_found(&image_id))
})
.collect::<Result<Vec<_>, _>>()?,
),
icon_descriptions: args.icon_descriptions,
model: None,
screen_color: Some("auto".to_string()),
seg_model: Some("birefnet".to_string()),
aspect_ratio: args.aspect_ratio,
image_size: args.image_size,
project_id: Some(project_id),
generation_inputs,
asset_folder_id: Some("project".to_string()),
canvas_completion: Some(canvas_completion),
},
)
.await?
.0;
let data = if request_context.wants_envelope() {
serde_json::from_value::<ApiSuccessEnvelope<Value>>(result)
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(
json!({ "message": format!("failed to parse success envelope: {error}") }),
)
})?
.data
} else {
result
};
serde_json::from_value(data).map_err(|error| AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR)
.with_details(json!({ "message": format!("failed to deserialize icon spritesheet result: {error}") })))
}
pub fn format_execute_message(
&self,
args: &GenerateIconSpritesheetToolArgs,
result: &EditorIconSpritesheetResult,
) -> String {
let tool_name = Self::NAME;
let args = serde_json::to_string(args).unwrap_or_default();
let data_key = result
.spritesheet_resource
@@ -245,9 +170,7 @@ impl GenerateIconSpritesheetTool {
.unwrap_or_else(|| result.spritesheet_image_src.clone());
let image_id = ImageId::from_data_key(data_key);
format!(
"[tool_call:{}] args: {args} output: generated spritesheet saved as image: {}",
Self::NAME,
image_id
"[tool_call:{tool_name}] args: {args} output: generated spritesheet saved as image: {image_id}",
)
}
}
@@ -180,119 +180,6 @@ impl GenerateImageTool {
Self::NAME,
)
}
pub async fn execute(
&self,
state: &AppState,
request_context: &RequestContext,
caller: EditorGenerationCaller,
args: GenerateImageToolArgs,
project_id: String,
generation_inputs: Option<Value>,
asset_label: Option<String>,
canvas_completion: EditorCanvasGenerationCompletionPayload,
) -> Result<EditorImageGenerationResult, AppError> {
self.execute_with_kind(
state,
request_context,
caller,
args,
project_id,
generation_inputs,
asset_label,
canvas_completion,
None,
"editor_agent_generated_image",
)
.await
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn execute_with_kind(
&self,
state: &AppState,
request_context: &RequestContext,
caller: EditorGenerationCaller,
args: GenerateImageToolArgs,
project_id: String,
generation_inputs: Option<Value>,
asset_label: Option<String>,
canvas_completion: EditorCanvasGenerationCompletionPayload,
kind: Option<&str>,
asset_kind: &str,
) -> Result<EditorImageGenerationResult, AppError> {
self.validate_args(&args).map_err(|error| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "editor-agent",
"message": error.to_string(),
}))
})?;
let result = generate_editor_image_for_owner(
state,
request_context,
caller,
EditorImageGenerationRequest {
prompt: args.prompt,
size: None,
kind: kind.map(ToOwned::to_owned),
model: None,
screen_color: kind
.is_some_and(|value| value == "character")
.then(|| "auto".to_string()),
seg_model: kind
.is_some_and(|value| value == "character")
.then(|| "birefnet".to_string()),
aspect_ratio: args.aspect_ratio,
image_size: args.image_size,
reference_image_srcs: Some(self.image_data_keys(args.reference_image_ids)?),
project_id: Some(project_id),
asset_kind: Some(asset_kind.to_string()),
generation_inputs,
asset_folder_id: Some("project".to_string()),
asset_label,
source_resource_id: None,
canvas_completion: Some(canvas_completion),
},
)
.await?
.0;
let data = if request_context.wants_envelope() {
serde_json::from_value::<ApiSuccessEnvelope<Value>>(result)
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"message": format!("failed to parse success envelope: {error}"),
}))
})?
.data
} else {
result
};
serde_json::from_value::<EditorImageGenerationResult>(data).map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"message": format!("failed to deserialize image generation result: {error}"),
}))
})
}
fn image_data_keys(&self, image_ids: Vec<ImageId>) -> Result<Vec<String>, AppError> {
image_ids
.into_iter()
.map(|image_id| {
self.context
.image_data_key(&image_id)
.map(ToOwned::to_owned)
.ok_or_else(|| {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "editor-agent",
"message": format!("asset {image_id} not found in context"),
}))
})
})
.collect()
}
}
#[cfg(test)]
@@ -1,22 +1,11 @@
use crate::editor_agent::editor_tools::common::EditorAgentPricedTool;
use crate::editor_generation_config::{
EDITOR_SOUND_EFFECT_MODEL_VIDU, EditorGenerationPricingConfig,
EditorGenerationPricingConfig, EDITOR_SOUND_EFFECT_MODEL_VIDU,
};
use crate::http_error::AppError;
use crate::request_context::RequestContext;
use crate::state::AppState;
use crate::vector_engine_audio_generation::generate_editor_sound_effect_for_owner;
use axum::Json;
use axum::http::StatusCode;
use axum::response::Response;
use module_editor_agent::agent::tool::{Tool, ToolFailure};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use shared_contracts::api::ApiSuccessEnvelope;
use shared_contracts::assets::{
EditorAudioGenerateResponse, EditorCanvasGenerationCompletionPayload,
EditorSoundEffectGenerateRequest,
};
use serde_json::{json, Value};
use shared_contracts::assets::EditorAudioGenerateResponse;
use std::error::Error;
use std::fmt::Display;
@@ -81,7 +70,7 @@ impl Tool for GenerateSoundEffectTool {
return Err(GenerateSoundEffectError::PromptNotProvided);
}
Ok(GenerateSoundEffectToolOutput {
message: pending_message(),
message: "this tool call is pending user confirmation. if all is pending, just end this turn".to_string(),
})
}
}
@@ -108,43 +97,6 @@ impl EditorAgentPricedTool for GenerateSoundEffectTool {
}
impl GenerateSoundEffectTool {
#[allow(clippy::too_many_arguments)]
pub async fn execute(
&self,
state: &AppState,
request_context: &RequestContext,
owner_user_id: String,
args: GenerateSoundEffectToolArgs,
project_id: String,
generation_inputs: Option<Value>,
asset_label: Option<String>,
canvas_completion: EditorCanvasGenerationCompletionPayload,
) -> Result<EditorAudioGenerateResponse, AppError> {
if args.prompt.trim().is_empty() {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_details(
json!({ "provider": "editor-agent", "message": "音效提示词不能为空" }),
));
}
let response = generate_editor_sound_effect_for_owner(
state.clone(),
request_context.clone(),
owner_user_id,
Ok(Json(EditorSoundEffectGenerateRequest {
prompt: args.prompt,
model: args.model,
duration: args.duration.unwrap_or(3),
project_id: Some(project_id),
canvas_completion: Some(canvas_completion),
generation_inputs,
asset_folder_id: Some("project".to_string()),
asset_label,
})),
)
.await
.map_err(map_media_response_error)?;
parse_media_response(request_context, response.0)
}
pub fn format_execute_message(
&self,
args: &GenerateSoundEffectToolArgs,
@@ -158,36 +110,3 @@ impl GenerateSoundEffectTool {
)
}
}
pub(crate) fn pending_message() -> String {
"this tool call is pending user confirmation. if all is pending, just end this turn".to_string()
}
pub(crate) fn map_media_response_error(response: Response) -> AppError {
AppError::from_status(response.status()).with_details(json!({
"provider": "editor-agent",
"message": "媒体生成请求失败",
}))
}
pub(crate) fn parse_media_response<T: for<'de> Deserialize<'de>>(
request_context: &RequestContext,
value: Value,
) -> Result<T, AppError> {
let data = if request_context.wants_envelope() {
serde_json::from_value::<ApiSuccessEnvelope<Value>>(value)
.map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(
json!({ "message": format!("failed to parse success envelope: {error}") }),
)
})?
.data
} else {
value
};
serde_json::from_value(data).map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(
json!({ "message": format!("failed to deserialize media generation result: {error}") }),
)
})
}
@@ -1,19 +1,13 @@
use crate::editor_agent::editor_tools::common::{
EditorAgentPricedTool, EditorToolContext, editor_agent_image_mud_points,
};
use crate::editor_agent::editor_tools::generate_character::format_tool_message;
use crate::editor_agent::editor_tools::generate_image::{
EditorImageGenerationResult, GenerateImageError, GenerateImageTool, GenerateImageToolArgs,
GenerateImageToolOutput,
};
use crate::editor_generation_config::EditorGenerationPricingConfig;
use crate::editor_project::EditorGenerationCaller;
use crate::http_error::AppError;
use crate::request_context::RequestContext;
use crate::state::AppState;
use module_editor_agent::agent::tool::{Tool, ToolFailure};
use serde_json::{Value, json};
use shared_contracts::assets::EditorCanvasGenerationCompletionPayload;
pub struct GenerateUiDesignTool {
pub context: EditorToolContext,
@@ -75,41 +69,23 @@ impl EditorAgentPricedTool for GenerateUiDesignTool {
}
impl GenerateUiDesignTool {
#[allow(clippy::too_many_arguments)]
pub async fn execute(
&self,
state: &AppState,
request_context: &RequestContext,
caller: EditorGenerationCaller,
args: GenerateImageToolArgs,
project_id: String,
generation_inputs: Option<Value>,
asset_label: Option<String>,
canvas_completion: EditorCanvasGenerationCompletionPayload,
) -> Result<EditorImageGenerationResult, AppError> {
GenerateImageTool {
context: self.context.clone(),
}
.execute_with_kind(
state,
request_context,
caller,
args,
project_id,
generation_inputs,
asset_label,
canvas_completion,
Some("ui-design"),
"ui-design",
)
.await
}
pub fn format_execute_message(
&self,
args: &GenerateImageToolArgs,
result: &EditorImageGenerationResult,
) -> String {
format_tool_message(Self::NAME, args, result)
let tool_name = Self::NAME;
let args = serde_json::to_string(args).unwrap_or_default();
let data_key = result
.object_key
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(|value| value.trim_start_matches('/').to_string())
.unwrap_or_else(|| result.image_src.clone());
let image_id = crate::editor_agent::utils::ImageId::from_data_key(data_key);
format!(
"[tool_call:{tool_name}] args: {args} output: generated result saved as image: {image_id}"
)
}
}
@@ -1,22 +1,10 @@
use crate::character_animation_assets::generate_editor_video_for_owner;
use crate::editor_agent::editor_tools::common::EditorAgentPricedTool;
use crate::editor_agent::editor_tools::generate_sound_effect::{
map_media_response_error, parse_media_response, pending_message,
};
use crate::editor_agent::utils::ImageId;
use crate::editor_generation_config::EditorGenerationPricingConfig;
use crate::http_error::AppError;
use crate::request_context::RequestContext;
use crate::state::AppState;
use axum::Json;
use axum::http::StatusCode;
use module_editor_agent::agent::tool::{Tool, ToolFailure, ToolFailureKind};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use shared_contracts::assets::{
EditorCanvasGenerationCompletionPayload, EditorVideoGenerateRequest,
EditorVideoGenerateResponse,
};
use serde_json::{json, Value};
use shared_contracts::assets::EditorVideoGenerateResponse;
use std::error::Error;
use std::fmt::Display;
@@ -99,7 +87,7 @@ impl Tool for GenerateVideoTool {
}
}
Ok(GenerateVideoToolOutput {
message: pending_message(),
message: "this tool call is pending user confirmation. if all is pending, just end this turn".to_string(),
})
}
}
@@ -139,63 +127,6 @@ impl EditorAgentPricedTool for GenerateVideoTool {
}
impl GenerateVideoTool {
#[allow(clippy::too_many_arguments)]
pub async fn execute(
&self,
state: &AppState,
request_context: &RequestContext,
owner_user_id: String,
args: GenerateVideoToolArgs,
project_id: String,
generation_inputs: Option<Value>,
asset_label: Option<String>,
canvas_completion: EditorCanvasGenerationCompletionPayload,
) -> Result<EditorVideoGenerateResponse, AppError> {
if args.prompt.trim().is_empty() {
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_details(
json!({ "provider": "editor-agent", "message": "视频提示词不能为空" }),
));
}
let reference_image_srcs: Result<Vec<String>, AppError> = args
.reference_image_ids
.into_iter()
.map(|image_id| {
self.context
.image_data_key(&image_id)
.map(ToOwned::to_owned)
.ok_or_else(|| image_not_found(&image_id))
})
.collect();
let reference_image_srcs = reference_image_srcs?;
let response = generate_editor_video_for_owner(
state.clone(),
request_context.clone(),
owner_user_id,
Ok(Json(EditorVideoGenerateRequest {
prompt: args.prompt,
model: args.model.unwrap_or_else(|| "seedance2.0-fast".to_string()),
aspect_ratio: args.aspect_ratio.unwrap_or_else(|| "16:9".to_string()),
duration_seconds: args.duration_seconds.unwrap_or(4),
resolution: args.resolution.unwrap_or_else(|| "720p".to_string()),
mode: "std".to_string(),
sound: args.sound.unwrap_or_else(|| "off".to_string()),
web_search_enabled: false,
reference_image_srcs,
reference_video_srcs: Vec::new(),
reference_audio_srcs: Vec::new(),
project_id: Some(project_id),
canvas_completion: Some(canvas_completion),
generation_inputs,
source_resource_id: None,
asset_kind: Some("video".to_string()),
asset_folder_id: Some("project".to_string()),
asset_label,
})),
)
.await
.map_err(map_media_response_error)?;
parse_media_response(request_context, response.0)
}
pub fn format_execute_message(
&self,
args: &GenerateVideoToolArgs,
@@ -209,11 +140,4 @@ impl GenerateVideoTool {
video_id
)
}
}
fn image_not_found(image_id: &ImageId) -> AppError {
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
"provider": "editor-agent",
"message": format!("asset {image_id} not found in context"),
}))
}
}