diff --git a/server-rs/crates/api-server/src/editor_agent/api.rs b/server-rs/crates/api-server/src/editor_agent/api.rs index 7588c4245..b6fd1d89b 100644 --- a/server-rs/crates/api-server/src/editor_agent/api.rs +++ b/server-rs/crates/api-server/src/editor_agent/api.rs @@ -6,70 +6,71 @@ use module_editor_agent::framework::memory::VecMemory; use module_editor_agent::framework::run::PromptOutput; use module_editor_agent::framework::tool::Tool; use module_editor_agent::{ - derive_conversation_title, editor_agent_messages_object_key, - EDITOR_AGENT_CONVERSATION_ID_PREFIX, EDITOR_AGENT_DEFAULT_CONVERSATION_TITLE, + EDITOR_AGENT_CONVERSATION_ID_PREFIX, EDITOR_AGENT_DEFAULT_CONVERSATION_TITLE, + derive_conversation_title, editor_agent_messages_object_key, }; use platform_llm::LlmMessage; use serde::Serialize; -use serde_json::{json, Value}; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; use shared_contracts::assets::{ - EditorBackgroundMusicGenerateRequest, EditorSoundEffectGenerateRequest, - EditorVideoGenerateRequest, + EditorBackgroundMusicGenerateRequest, EditorSoundEffectGenerateRequest, + EditorVideoGenerateRequest, }; use shared_contracts::editor_agent::{ - CreateEditorAgentConversationRequest, EditorAgentConversationListResponse, - EditorAgentConversationMessagesDocument, EditorAgentConversationResponse, - EditorAgentConversationSummary, EditorAgentMessage, EditorAgentMessageRequest, - EditorAgentMessageResponse, EditorAgentMessageRole, EditorAgentToolCall, - EditorAgentToolCallStatus, + CreateEditorAgentConversationRequest, EditorAgentConversationListResponse, + EditorAgentConversationMessagesDocument, EditorAgentConversationResponse, + EditorAgentConversationSummary, EditorAgentMessage, EditorAgentMessageRequest, + EditorAgentMessageResponse, EditorAgentMessageRole, EditorAgentToolCall, + EditorAgentToolCallStatus, }; use spacetime_client::{ - EditorAgentConversationCreateRecordInput, EditorAgentConversationDeleteRecordInput, - EditorAgentConversationRecord, EditorAgentConversationTouchRecordInput, - EditorProjectGetRecordInput, + EditorAgentConversationCreateRecordInput, EditorAgentConversationDeleteRecordInput, + EditorAgentConversationRecord, EditorAgentConversationTouchRecordInput, + EditorProjectGetRecordInput, }; use crate::api_response::json_success_body; use crate::auth::AuthenticatedAccessToken; -use module_editor_agent::agent::agent::LlmChatAgentBuilder; use crate::editor_agent::pricing::EditorAgentPricedTool; -use module_editor_agent::agent::tools::edit_image::{EditImageTool, EditImageToolArgs}; -use module_editor_agent::agent::tools::generate_background_music::{ - GenerateBackgroundMusicTool, GenerateBackgroundMusicToolArgs, -}; -use module_editor_agent::agent::tools::generate_character::GenerateCharacterTool; -use module_editor_agent::agent::tools::generate_icon_spritesheet::{ - GenerateIconSpritesheetTool, GenerateIconSpritesheetToolArgs, -}; -use module_editor_agent::agent::tools::generate_image::{GenerateImageTool, GenerateImageToolArgs}; -use module_editor_agent::agent::tools::generate_sound_effect::{ - GenerateSoundEffectTool, GenerateSoundEffectToolArgs, -}; -use module_editor_agent::agent::tools::generate_ui_design::GenerateUiDesignTool; -use module_editor_agent::agent::tools::generate_video::{GenerateVideoTool, GenerateVideoToolArgs}; use crate::editor_agent::utils::{ - build_editor_agent_canvas_completion, conversation_detail_from_record, conversation_summary_from_record, - editor_agent_bad_request, empty_messages_document, ensure_editor_project_access, - normalize_editor_agent_attachments, now_rfc3339, read_messages_document, - require_editor_agent_sidebar_enabled, write_messages_document, IntoImageId, + IntoImageId, build_editor_agent_canvas_completion, conversation_detail_from_record, + conversation_summary_from_record, editor_agent_bad_request, empty_messages_document, + ensure_editor_project_access, normalize_editor_agent_attachments, now_rfc3339, + read_messages_document, require_editor_agent_sidebar_enabled, write_messages_document, }; use crate::editor_agent::{context, display_args, reconcile}; use crate::editor_generation_config::EditorGenerationPricingConfig; use crate::editor_generation_queue::{ - enqueue_editor_generation_job, EDITOR_BACKGROUND_MUSIC_GENERATION_JOB_KIND, - EDITOR_ICON_SPRITESHEET_GENERATION_JOB_KIND, EDITOR_IMAGE_EDIT_JOB_KIND, - EDITOR_IMAGE_GENERATION_JOB_KIND, EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND, - EDITOR_VIDEO_GENERATION_JOB_KIND, + EDITOR_BACKGROUND_MUSIC_GENERATION_JOB_KIND, EDITOR_ICON_SPRITESHEET_GENERATION_JOB_KIND, + EDITOR_IMAGE_EDIT_JOB_KIND, EDITOR_IMAGE_GENERATION_JOB_KIND, + EDITOR_SOUND_EFFECT_GENERATION_JOB_KIND, EDITOR_VIDEO_GENERATION_JOB_KIND, + enqueue_editor_generation_job_with_identity, }; use crate::editor_project::{ - EditorIconSpritesheetGenerationRequest, EditorImageEditRequest, EditorImageGenerationRequest, + EditorIconSpritesheetGenerationRequest, EditorImageEditRequest, EditorImageGenerationRequest, }; use crate::editor_project::{current_utc_micros, map_editor_project_error}; use crate::http_error::AppError; use crate::request_context::RequestContext; use crate::state::AppState; -use shared_kernel::{build_prefixed_uuid_id, normalize_optional_string}; +use module_editor_agent::agent::agent::LlmChatAgentBuilder; use module_editor_agent::agent::tools::context::EditorToolContext; +use module_editor_agent::agent::tools::edit_image::{EditImageTool, EditImageToolArgs}; +use module_editor_agent::agent::tools::generate_background_music::{ + GenerateBackgroundMusicTool, GenerateBackgroundMusicToolArgs, +}; +use module_editor_agent::agent::tools::generate_character::GenerateCharacterTool; +use module_editor_agent::agent::tools::generate_icon_spritesheet::{ + GenerateIconSpritesheetTool, GenerateIconSpritesheetToolArgs, +}; +use module_editor_agent::agent::tools::generate_image::{GenerateImageTool, GenerateImageToolArgs}; +use module_editor_agent::agent::tools::generate_sound_effect::{ + GenerateSoundEffectTool, GenerateSoundEffectToolArgs, +}; +use module_editor_agent::agent::tools::generate_ui_design::GenerateUiDesignTool; +use module_editor_agent::agent::tools::generate_video::{GenerateVideoTool, GenerateVideoToolArgs}; +use shared_kernel::{build_prefixed_uuid_id, normalize_optional_string}; pub async fn editor_agent_message( State(state): State, @@ -844,15 +845,21 @@ pub async fn confirm_editor_agent_tool_call( AppError::from_status(axum::http::StatusCode::INTERNAL_SERVER_ERROR) .with_details(json!({"message": format!("failed to serialize tool payload: {error}")})) })?; - let job = enqueue_editor_generation_job( + let (job_id, dedupe_key) = editor_agent_tool_job_identity( + conversation.conversation_id.as_str(), + message_id, + tool_name.as_str(), + ); + let job = enqueue_editor_generation_job_with_identity( &state, - &request_context, conversation.owner_user_id.as_str(), job_kind, conversation.project_id.clone(), request_label, u64::from(price_mud_points), &payload, + job_id, + dedupe_key, ) .await?; let message = &mut document.messages[message_id]; @@ -874,6 +881,16 @@ fn parse_confirm_tool_args(value: &Value) -> Res .map_err(|error| editor_agent_bad_request(format!("invalid tool call args: {error}"))) } +fn editor_agent_tool_job_identity( + conversation_id: &str, + message_id: usize, + tool_name: &str, +) -> (String, String) { + let dedupe_key = format!("editor-agent:{conversation_id}:{message_id}:{tool_name}"); + let digest = Sha256::digest(dedupe_key.as_bytes()); + (format!("task-editor-agent-{digest:x}"), dedupe_key) +} + async fn load_editor_agent_project( state: &AppState, conversation: &EditorAgentConversationRecord, diff --git a/server-rs/crates/api-server/src/editor_generation_queue.rs b/server-rs/crates/api-server/src/editor_generation_queue.rs index e58ea8dc9..640aa6dc9 100644 --- a/server-rs/crates/api-server/src/editor_generation_queue.rs +++ b/server-rs/crates/api-server/src/editor_generation_queue.rs @@ -47,12 +47,41 @@ where T: Serialize, { let job_id = build_prefixed_uuid_id("task-"); + enqueue_editor_generation_job_with_identity( + state, + owner_user_id, + job_kind, + source_entity_id, + request_label, + price_mud_points, + payload, + job_id.clone(), + format!("editor-canvas:{job_kind}:{job_id}"), + ) + .await +} + +#[allow(clippy::too_many_arguments)] +pub(crate) async fn enqueue_editor_generation_job_with_identity( + state: &AppState, + owner_user_id: &str, + job_kind: &str, + source_entity_id: impl Into, + request_label: impl Into, + price_mud_points: u64, + payload: &T, + job_id: String, + dedupe_key: String, +) -> Result +where + T: Serialize, +{ let request_payload_json = serialize_editor_generation_job_payload(payload)?; let now_micros = current_utc_micros(); state .spacetime_client() .enqueue_external_generation_job(ExternalGenerationJobEnqueueRecordInput { - dedupe_key: format!("editor-canvas:{job_kind}:{job_id}"), + dedupe_key, job_id, job_kind: job_kind.to_string(), owner_user_id: owner_user_id.to_string(),