Files
Genarrative/server-rs/crates/shared-contracts/src/editor_agent.rs
T

367 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 画布Agent对话契约:与 packages/shared/src/contracts/editorAgent.ts 保持同形。
//! 会话元数据存 SpacetimeDB,消息正文整体存 OSS(editor-agent/{conversationId}.json)。
use serde::{Deserialize, Deserializer, Serialize};
pub const EDITOR_AGENT_MAX_ATTACHMENTS: usize = 9;
pub const EDITOR_AGENT_TITLE_MAX_CHARS: usize = 20;
pub const EDITOR_AGENT_DEFAULT_CONVERSATION_TITLE: &str = "新对话";
pub const EDITOR_AGENT_MESSAGES_DOCUMENT_VERSION: u32 = 2;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum EditorAgentMessageRole {
User,
Assistant,
System,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EditorAgentAttachmentSource {
CanvasResource,
LibraryAsset,
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum EditorAgentToolCallStatus {
#[serde(alias = "pending_confirmation", alias = "executing")]
NotCompleted,
Completed,
Failed,
Cancelled,
}
fn default_editor_agent_tool_call_status() -> EditorAgentToolCallStatus {
EditorAgentToolCallStatus::NotCompleted
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentAttachmentRef {
pub source: EditorAgentAttachmentSource,
pub reference_id: String,
#[serde(default)]
pub object_key: Option<String>,
pub image_src: String,
#[serde(default)]
pub thumbnail_src: Option<String>,
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub width: Option<u32>,
#[serde(default)]
pub height: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentGeneratedImage {
#[serde(default)]
pub resource_id: Option<String>,
#[serde(default)]
pub object_key: Option<String>,
#[serde(default)]
pub asset_object_id: Option<String>,
pub image_src: String,
#[serde(default)]
pub thumbnail_src: Option<String>,
#[serde(default)]
pub width: Option<u32>,
#[serde(default)]
pub height: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentGeneratedVideo {
#[serde(default)]
pub resource_id: Option<String>,
#[serde(default)]
pub object_key: Option<String>,
#[serde(default)]
pub asset_object_id: Option<String>,
pub video_src: String,
#[serde(default)]
pub thumbnail_src: Option<String>,
#[serde(default)]
pub width: Option<u32>,
#[serde(default)]
pub height: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentGeneratedAudio {
#[serde(default)]
pub resource_id: Option<String>,
#[serde(default)]
pub object_key: Option<String>,
#[serde(default)]
pub asset_object_id: Option<String>,
pub audio_src: String,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentToolCallDisplayArgs {
pub string_args: Vec<EditorAgentToolCallStringArg>,
pub image_args: Vec<EditorAgentToolCallImageArg>,
pub extras: EditorAgentToolCallDisplayExtras,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentToolCallDisplayExtras {
pub price_mud_points: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentToolCallStringArg {
pub name: String,
pub label: String,
pub value: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentToolCallImageArg {
pub name: String,
pub label: String,
pub refs: Vec<EditorAgentToolCallImageRef>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentToolCallImageRef {
pub image_id: String,
pub image_src: String,
#[serde(default)]
pub object_key: Option<String>,
#[serde(default)]
pub thumbnail_src: Option<String>,
#[serde(default)]
pub label: Option<String>,
#[serde(default)]
pub width: Option<u32>,
#[serde(default)]
pub height: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentToolCall {
pub tool_name: String,
#[serde(default = "default_editor_agent_tool_call_status")]
pub status: EditorAgentToolCallStatus,
pub args: serde_json::Value,
pub display_args: EditorAgentToolCallDisplayArgs,
#[serde(default)]
pub external_job_id: Option<String>,
#[serde(default)]
pub images: Vec<EditorAgentGeneratedImage>,
#[serde(default)]
pub videos: Vec<EditorAgentGeneratedVideo>,
#[serde(default)]
pub audios: Vec<EditorAgentGeneratedAudio>,
#[serde(default)]
pub error: Option<String>,
}
fn deserialize_id_or_zero<'de, D>(deserializer: D) -> Result<usize, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum IdOrZero {
Num(usize),
Str(String),
Other(serde_json::Value),
}
match IdOrZero::deserialize(deserializer)? {
IdOrZero::Num(n) => Ok(n),
_ => Ok(0),
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentMessage {
// to compatible for legacy version
#[serde(deserialize_with = "deserialize_id_or_zero")]
pub id: usize,
pub role: EditorAgentMessageRole,
pub text: String,
#[serde(default)]
pub attachments: Vec<EditorAgentAttachmentRef>,
// one tool call is inside one system message.
pub tool_call: Option<EditorAgentToolCall>,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentConversationSummary {
pub conversation_id: String,
pub project_id: String,
pub title: String,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentConversationDetail {
pub conversation_id: String,
pub project_id: String,
pub title: String,
pub created_at: String,
pub updated_at: String,
pub messages: Vec<EditorAgentMessage>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentConversationMessagesDocument {
pub version: u32,
pub conversation_id: String,
pub messages: Vec<EditorAgentMessage>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreateEditorAgentConversationRequest {
#[serde(default)]
pub title: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentConversationListResponse {
pub conversations: Vec<EditorAgentConversationSummary>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentConversationResponse {
pub conversation: EditorAgentConversationDetail,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentMessageRequest {
pub text: String,
#[serde(default)]
pub attachments: Vec<EditorAgentAttachmentRef>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct EditorAgentMessageResponse {
pub conversation: EditorAgentConversationSummary,
pub delta_messages: Vec<EditorAgentMessage>,
pub error_message: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn tool_call_display_args_and_media_use_camel_case() {
let missing_display_args = serde_json::from_value::<EditorAgentToolCall>(json!({
"toolName": "generate-video",
"args": {},
"images": []
}));
assert!(missing_display_args.is_err());
let tool_call = EditorAgentToolCall {
tool_name: "generate-video".to_string(),
status: EditorAgentToolCallStatus::Completed,
args: json!({}),
display_args: EditorAgentToolCallDisplayArgs {
string_args: vec![EditorAgentToolCallStringArg {
name: "prompt".to_string(),
label: "提示词".to_string(),
value: "生成像素风角色".to_string(),
}],
image_args: vec![EditorAgentToolCallImageArg {
name: "reference_image_ids".to_string(),
label: "参考图片".to_string(),
refs: vec![EditorAgentToolCallImageRef {
image_id: "sha256:reference-1".to_string(),
image_src: "/reference.png".to_string(),
object_key: Some("generated/reference.png".to_string()),
thumbnail_src: Some("/reference-thumbnail.png".to_string()),
label: Some("角色参考".to_string()),
width: Some(512),
height: Some(512),
}],
}],
extras: EditorAgentToolCallDisplayExtras {
price_mud_points: 5,
},
},
external_job_id: Some("task-editor-agent-1".to_string()),
images: Vec::new(),
videos: vec![EditorAgentGeneratedVideo {
resource_id: Some("resource-video-1".to_string()),
object_key: Some("generated/video.mp4".to_string()),
asset_object_id: Some("asset-video-1".to_string()),
video_src: "/generated-video.mp4".to_string(),
thumbnail_src: Some("/generated-video.png".to_string()),
width: Some(1280),
height: Some(720),
}],
audios: vec![EditorAgentGeneratedAudio {
resource_id: Some("resource-audio-1".to_string()),
object_key: Some("generated/audio.mp3".to_string()),
asset_object_id: Some("asset-audio-1".to_string()),
audio_src: "/generated-audio.mp3".to_string(),
}],
error: None,
};
let payload = serde_json::to_value(tool_call).expect("tool call should serialize");
assert_eq!(payload["videos"][0]["videoSrc"], "/generated-video.mp4");
assert_eq!(payload["externalJobId"], "task-editor-agent-1");
assert_eq!(payload["status"], "completed");
assert_eq!(payload["audios"][0]["audioSrc"], "/generated-audio.mp3");
assert_eq!(
payload["displayArgs"]["stringArgs"][0]["value"],
"生成像素风角色"
);
assert_eq!(
payload["displayArgs"]["imageArgs"][0]["refs"][0]["imageId"],
"sha256:reference-1"
);
assert_eq!(
payload["displayArgs"]["imageArgs"][0]["refs"][0]["objectKey"],
"generated/reference.png"
);
assert_eq!(payload["displayArgs"]["extras"]["priceMudPoints"], 5);
}
#[test]
fn legacy_tool_call_status_maps_to_not_completed_on_read() {
let tool_call = serde_json::from_value::<EditorAgentToolCall>(json!({
"toolName": "generate-image",
"status": "executing",
"args": {},
"displayArgs": {
"stringArgs": [],
"imageArgs": [],
"extras": { "priceMudPoints": 3 }
},
"images": []
}))
.expect("legacy status should not break persisted OSS documents");
let payload = serde_json::to_value(tool_call).expect("tool call should serialize");
assert_eq!(payload["status"], "not_completed");
}
}