storage generated assets metadata in external job payload (only for editor agent source)
This commit is contained in:
@@ -19,7 +19,9 @@ use platform_editor_agent::agent::tools::generate_sound_effect::{
|
||||
GenerateSoundEffectTool, GenerateSoundEffectToolArgs,
|
||||
};
|
||||
use platform_editor_agent::agent::tools::generate_ui_design::GenerateUiDesignTool;
|
||||
use platform_editor_agent::agent::tools::generate_video::{GenerateVideoTool, GenerateVideoToolArgs};
|
||||
use platform_editor_agent::agent::tools::generate_video::{
|
||||
GenerateVideoTool, GenerateVideoToolArgs,
|
||||
};
|
||||
use platform_editor_agent::framework::tool::Tool;
|
||||
use serde_json::Value;
|
||||
use shared_contracts::assets::{EditorAudioGenerateResponse, EditorVideoGenerateResponse};
|
||||
@@ -108,9 +110,9 @@ fn reconcile_completed_editor_agent_tool_call(
|
||||
let response = result_payload_json
|
||||
.ok_or_else(|| "生成任务缺少结果数据".to_string())
|
||||
.and_then(|payload| serde_json::from_str::<Value>(payload).map_err(|e| e.to_string()))?
|
||||
.get("response")
|
||||
.get("editor-agent-tool-call-result")
|
||||
.cloned()
|
||||
.ok_or_else(|| "生成任务缺少 response 结果".to_string())?;
|
||||
.ok_or_else(|| "生成任务缺少 Agent 工具调用结果".to_string())?;
|
||||
let tool_call = message
|
||||
.tool_call
|
||||
.as_mut()
|
||||
@@ -153,7 +155,7 @@ fn reconcile_completed_editor_agent_tool_call(
|
||||
context: EditorToolContext::default(),
|
||||
}
|
||||
.format_execute_message(&args, &result);
|
||||
tool_call.images = vec![resp_to_asset::editor_agent_generated_spritesheet(&result)];
|
||||
tool_call.images = resp_to_asset::editor_agent_generated_spritesheet_images(&result);
|
||||
}
|
||||
GenerateVideoTool::NAME => {
|
||||
let args: GenerateVideoToolArgs = parse_reconciled_value(&tool_call.args)?;
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use serde_json::Value;
|
||||
use shared_contracts::assets::{EditorAudioGenerateResponse, EditorVideoGenerateResponse};
|
||||
use shared_contracts::editor_agent::{EditorAgentGeneratedAudio, EditorAgentGeneratedImage, EditorAgentGeneratedVideo};
|
||||
use platform_editor_agent::agent::tools::edit_image::EditorImageEditResult;
|
||||
use platform_editor_agent::agent::tools::generate_icon_spritesheet::EditorIconSpritesheetResult;
|
||||
use platform_editor_agent::agent::tools::generate_image::EditorImageGenerationResult;
|
||||
use serde_json::Value;
|
||||
use shared_contracts::assets::{EditorAudioGenerateResponse, EditorVideoGenerateResponse};
|
||||
use shared_contracts::editor_agent::{
|
||||
EditorAgentGeneratedAudio, EditorAgentGeneratedImage, EditorAgentGeneratedVideo,
|
||||
};
|
||||
|
||||
fn value_string(value: Option<&Value>, field: &str) -> Option<String> {
|
||||
value
|
||||
@@ -12,7 +14,9 @@ fn value_string(value: Option<&Value>, field: &str) -> Option<String> {
|
||||
.map(ToOwned::to_owned)
|
||||
}
|
||||
|
||||
pub fn editor_agent_generated_image(result: &EditorImageGenerationResult) -> EditorAgentGeneratedImage {
|
||||
pub fn editor_agent_generated_image(
|
||||
result: &EditorImageGenerationResult,
|
||||
) -> EditorAgentGeneratedImage {
|
||||
EditorAgentGeneratedImage {
|
||||
resource_id: value_string(result.resource.as_ref(), "resourceId"),
|
||||
object_key: result.object_key.clone(),
|
||||
@@ -52,7 +56,32 @@ pub fn editor_agent_generated_spritesheet(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn editor_agent_generated_video(result: &EditorVideoGenerateResponse) -> EditorAgentGeneratedVideo {
|
||||
pub fn editor_agent_generated_spritesheet_images(
|
||||
result: &EditorIconSpritesheetResult,
|
||||
) -> Vec<EditorAgentGeneratedImage> {
|
||||
let images = result
|
||||
.icon_image_srcs
|
||||
.iter()
|
||||
.map(|image| EditorAgentGeneratedImage {
|
||||
resource_id: value_string(image.resource.as_ref(), "resourceId"),
|
||||
object_key: value_string(image.resource.as_ref(), "objectKey"),
|
||||
asset_object_id: value_string(image.resource.as_ref(), "assetObjectId"),
|
||||
image_src: image.image_src.clone(),
|
||||
thumbnail_src: None,
|
||||
width: Some(image.width),
|
||||
height: Some(image.height),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
if images.is_empty() {
|
||||
vec![editor_agent_generated_spritesheet(result)]
|
||||
} else {
|
||||
images
|
||||
}
|
||||
}
|
||||
|
||||
pub fn editor_agent_generated_video(
|
||||
result: &EditorVideoGenerateResponse,
|
||||
) -> EditorAgentGeneratedVideo {
|
||||
EditorAgentGeneratedVideo {
|
||||
resource_id: value_string(result.resource.as_ref(), "resourceId"),
|
||||
object_key: result.object_key.clone(),
|
||||
@@ -64,7 +93,9 @@ pub fn editor_agent_generated_video(result: &EditorVideoGenerateResponse) -> Edi
|
||||
}
|
||||
}
|
||||
|
||||
pub fn editor_agent_generated_audio(result: &EditorAudioGenerateResponse) -> EditorAgentGeneratedAudio {
|
||||
pub fn editor_agent_generated_audio(
|
||||
result: &EditorAudioGenerateResponse,
|
||||
) -> EditorAgentGeneratedAudio {
|
||||
EditorAgentGeneratedAudio {
|
||||
resource_id: value_string(result.resource.as_ref(), "resourceId"),
|
||||
object_key: result.object_key.clone(),
|
||||
|
||||
@@ -675,7 +675,9 @@ async fn process_external_generation_job_once(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await,
|
||||
Ok(result) => {
|
||||
complete_editor_generation_job(&state, &worker_id, &job, result.0).await
|
||||
}
|
||||
Err(error) => {
|
||||
let message = error.body_text();
|
||||
fail_job(&state, &worker_id, &job, message.clone()).await?;
|
||||
@@ -703,7 +705,9 @@ async fn process_external_generation_job_once(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await,
|
||||
Ok(result) => {
|
||||
complete_editor_generation_job(&state, &worker_id, &job, result.0).await
|
||||
}
|
||||
Err(error) => {
|
||||
let message = error.body_text();
|
||||
fail_job(&state, &worker_id, &job, message.clone()).await?;
|
||||
@@ -732,7 +736,9 @@ async fn process_external_generation_job_once(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await,
|
||||
Ok(result) => {
|
||||
complete_editor_generation_job(&state, &worker_id, &job, result.0).await
|
||||
}
|
||||
Err(error) => {
|
||||
let message = error.body_text();
|
||||
fail_job(&state, &worker_id, &job, message.clone()).await?;
|
||||
@@ -833,7 +839,9 @@ async fn process_external_generation_job_once(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await,
|
||||
Ok(result) => {
|
||||
complete_editor_generation_job(&state, &worker_id, &job, result.0).await
|
||||
}
|
||||
Err(response) => {
|
||||
let message = response_error_message(response).await;
|
||||
fail_job(&state, &worker_id, &job, message.clone()).await?;
|
||||
@@ -862,7 +870,9 @@ async fn process_external_generation_job_once(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await,
|
||||
Ok(result) => {
|
||||
complete_editor_generation_job(&state, &worker_id, &job, result.0).await
|
||||
}
|
||||
Err(response) => {
|
||||
let message = response_error_message(response).await;
|
||||
fail_job(&state, &worker_id, &job, message.clone()).await?;
|
||||
@@ -891,7 +901,9 @@ async fn process_external_generation_job_once(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await,
|
||||
Ok(result) => {
|
||||
complete_editor_generation_job(&state, &worker_id, &job, result.0).await
|
||||
}
|
||||
Err(response) => {
|
||||
let message = response_error_message(response).await;
|
||||
fail_job(&state, &worker_id, &job, message.clone()).await?;
|
||||
@@ -920,7 +932,9 @@ async fn process_external_generation_job_once(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => complete_editor_generation_job(&state, &worker_id, &job).await,
|
||||
Ok(result) => {
|
||||
complete_editor_generation_job(&state, &worker_id, &job, result.0).await
|
||||
}
|
||||
Err(response) => {
|
||||
let message = response_error_message(response).await;
|
||||
fail_job(&state, &worker_id, &job, message.clone()).await?;
|
||||
@@ -1025,18 +1039,13 @@ async fn complete_editor_generation_job(
|
||||
state: &AppState,
|
||||
worker_id: &str,
|
||||
job: &ExternalGenerationJobRecord,
|
||||
response: Value,
|
||||
) -> Result<(), String> {
|
||||
complete_job(
|
||||
state,
|
||||
worker_id,
|
||||
job,
|
||||
Some(
|
||||
json!({
|
||||
"sourceModule": job.source_module.clone(),
|
||||
"sourceEntityId": job.source_entity_id.clone(),
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
Some(editor_generation_result_payload_json(job, &response)),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -1064,6 +1073,16 @@ fn editor_generation_result_payload_json(
|
||||
"sourceModule": job.source_module.clone(),
|
||||
"sourceEntityId": job.source_entity_id.clone(),
|
||||
});
|
||||
if is_editor_agent_generation_job(job)
|
||||
&& let Some(object) = payload.as_object_mut()
|
||||
{
|
||||
// The Agent needs this compact result to restore its tool-call card. Other jobs keep
|
||||
// master's metadata-only completion payload to avoid turning the queue into an asset API.
|
||||
object.insert(
|
||||
"editor-agent-tool-call-result".to_string(),
|
||||
compact_editor_generation_result(response.clone()),
|
||||
);
|
||||
}
|
||||
if let Some(warning) = extract_editor_generation_slice_warning(response)
|
||||
&& let Some(object) = payload.as_object_mut()
|
||||
{
|
||||
@@ -1072,6 +1091,55 @@ fn editor_generation_result_payload_json(
|
||||
payload.to_string()
|
||||
}
|
||||
|
||||
fn is_editor_agent_generation_job(job: &ExternalGenerationJobRecord) -> bool {
|
||||
serde_json::from_str::<Value>(job.request_payload_json.as_str())
|
||||
.ok()
|
||||
.is_some_and(|payload| {
|
||||
payload
|
||||
.pointer("/generationInputs/source")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|source| source.trim() == "editor-agent-tool-call-result")
|
||||
})
|
||||
}
|
||||
|
||||
fn compact_editor_generation_result(mut result: Value) -> Value {
|
||||
let Some(object) = result.as_object_mut() else {
|
||||
return result;
|
||||
};
|
||||
object.remove("project");
|
||||
object.remove("asset");
|
||||
object.remove("spritesheetAsset");
|
||||
for field in ["resource", "spritesheetResource"] {
|
||||
let Some(resource) = object.get_mut(field).and_then(Value::as_object_mut) else {
|
||||
continue;
|
||||
};
|
||||
resource
|
||||
.retain(|key, _| matches!(key.as_str(), "resourceId" | "objectKey" | "assetObjectId"));
|
||||
}
|
||||
if let Some(icon_image_srcs) = object
|
||||
.get_mut("iconImageSrcs")
|
||||
.and_then(Value::as_array_mut)
|
||||
{
|
||||
for icon in icon_image_srcs {
|
||||
let Some(icon) = icon.as_object_mut() else {
|
||||
continue;
|
||||
};
|
||||
if let Some(resource) = icon.get_mut("resource").and_then(Value::as_object_mut) {
|
||||
resource.retain(|key, _| {
|
||||
matches!(key.as_str(), "resourceId" | "objectKey" | "assetObjectId")
|
||||
});
|
||||
}
|
||||
icon.retain(|key, _| {
|
||||
matches!(
|
||||
key.as_str(),
|
||||
"name" | "imageSrc" | "width" | "height" | "resource"
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn extract_editor_generation_slice_warning(response: &Value) -> Option<Value> {
|
||||
let data = response.get("data").unwrap_or(response);
|
||||
let warning = data.get("sliceWarning")?;
|
||||
@@ -1357,6 +1425,128 @@ mod tests {
|
||||
);
|
||||
assert!(payload.get("spritesheetImageSrc").is_none());
|
||||
assert!(payload.get("iconImageSrcs").is_none());
|
||||
assert!(payload.get("editor-agent-tool-call-result").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_agent_result_payload_keeps_compact_response() {
|
||||
let mut job = external_generation_job_record_fixture(Some("lease-1"));
|
||||
job.request_payload_json = json!({
|
||||
"generationInputs": { "source": "editor-agent-tool-call-result" },
|
||||
})
|
||||
.to_string();
|
||||
let response = json!({
|
||||
"imageSrc": "/api/assets/object/generated.png",
|
||||
"objectKey": "users/user-1/generated.png",
|
||||
"assetObjectId": "asset-object-1",
|
||||
"width": 1024,
|
||||
"height": 1024,
|
||||
"sourceType": "generated",
|
||||
"prompt": "castle",
|
||||
"actualPrompt": null,
|
||||
"model": "gpt-image-2",
|
||||
"provider": "VectorEngine",
|
||||
"taskId": "provider-task-1",
|
||||
"resource": {
|
||||
"resourceId": "resource-1",
|
||||
"objectKey": "users/user-1/generated.png",
|
||||
"assetObjectId": "asset-object-1",
|
||||
"imageSrc": "data:image/png;base64,SHOULD_NOT_PERSIST",
|
||||
},
|
||||
"asset": { "assetId": "asset-1" },
|
||||
"project": { "projectId": "project-1" },
|
||||
});
|
||||
|
||||
let payload: Value =
|
||||
serde_json::from_str(&editor_generation_result_payload_json(&job, &response))
|
||||
.expect("worker result should be valid JSON");
|
||||
|
||||
assert_eq!(
|
||||
payload["editor-agent-tool-call-result"]["imageSrc"],
|
||||
json!("/api/assets/object/generated.png")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["editor-agent-tool-call-result"]["resource"],
|
||||
json!({
|
||||
"resourceId": "resource-1",
|
||||
"objectKey": "users/user-1/generated.png",
|
||||
"assetObjectId": "asset-object-1",
|
||||
})
|
||||
);
|
||||
assert!(payload["editor-agent-tool-call-result"].get("asset").is_none());
|
||||
assert!(payload["editor-agent-tool-call-result"].get("project").is_none());
|
||||
assert!(!payload.to_string().contains("data:image"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_agent_spritesheet_result_keeps_all_persisted_slices() {
|
||||
let mut job = external_generation_job_record_fixture(Some("lease-1"));
|
||||
job.request_payload_json = json!({
|
||||
"generationInputs": { "source": "editor-agent-tool-call-result" },
|
||||
})
|
||||
.to_string();
|
||||
let response = json!({
|
||||
"spritesheetImageSrc": "/api/assets/object/sheet.png",
|
||||
"spritesheetWidth": 512,
|
||||
"spritesheetHeight": 512,
|
||||
"taskId": "provider-task-1",
|
||||
"spritesheetResource": {
|
||||
"resourceId": "sheet-resource",
|
||||
"objectKey": "users/user-1/sheet.png",
|
||||
"assetObjectId": "sheet-object",
|
||||
},
|
||||
"iconImageSrcs": [
|
||||
{
|
||||
"name": "backpack",
|
||||
"imageSrc": "/api/assets/object/backpack.png",
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"resource": {
|
||||
"resourceId": "icon-resource-1",
|
||||
"objectKey": "users/user-1/backpack.png",
|
||||
"assetObjectId": "icon-object-1",
|
||||
"imageSrc": "data:image/png;base64,SHOULD_NOT_PERSIST",
|
||||
},
|
||||
"asset": { "assetId": "icon-asset-1" },
|
||||
},
|
||||
{
|
||||
"name": "map",
|
||||
"imageSrc": "/api/assets/object/map.png",
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"resource": {
|
||||
"resourceId": "icon-resource-2",
|
||||
"objectKey": "users/user-1/map.png",
|
||||
"assetObjectId": "icon-object-2",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
let payload: Value =
|
||||
serde_json::from_str(&editor_generation_result_payload_json(&job, &response))
|
||||
.expect("worker result should be valid JSON");
|
||||
|
||||
assert_eq!(
|
||||
payload["editor-agent-tool-call-result"]["iconImageSrcs"]
|
||||
.as_array()
|
||||
.map(Vec::len),
|
||||
Some(2)
|
||||
);
|
||||
assert_eq!(
|
||||
payload["editor-agent-tool-call-result"]["iconImageSrcs"][0]["resource"],
|
||||
json!({
|
||||
"resourceId": "icon-resource-1",
|
||||
"objectKey": "users/user-1/backpack.png",
|
||||
"assetObjectId": "icon-object-1",
|
||||
})
|
||||
);
|
||||
assert!(
|
||||
payload["editor-agent-tool-call-result"]["iconImageSrcs"][0]
|
||||
.get("asset")
|
||||
.is_none()
|
||||
);
|
||||
assert!(!payload.to_string().contains("data:image"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user