split code
This commit is contained in:
Generated
+4
@@ -3153,8 +3153,12 @@ dependencies = [
|
||||
name = "module-editor-agent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"hmac",
|
||||
"platform-llm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"shared-contracts",
|
||||
"shared-kernel",
|
||||
"spacetimedb",
|
||||
]
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
use module_editor_agent::agent::agent::{Agent, LlmApiAdaptor};
|
||||
use module_editor_agent::agent::agent_builder::AgentBuilder;
|
||||
use module_editor_agent::agent::error::PromptError;
|
||||
use module_editor_agent::agent::hook::Hook;
|
||||
use module_editor_agent::agent::memory::AgentMemory;
|
||||
use module_editor_agent::agent::tool::{Tool, ToolDyn};
|
||||
use platform_llm::{LlmClient, LlmMessage};
|
||||
use serde_json::Value;
|
||||
|
||||
pub(crate) struct LlmCompletionModel {
|
||||
client: LlmClient,
|
||||
}
|
||||
|
||||
impl LlmApiAdaptor<LlmMessage> for LlmCompletionModel {
|
||||
async fn complete<'a>(
|
||||
&self,
|
||||
messages: impl Iterator<Item = &'a LlmMessage> + Send,
|
||||
) -> Result<String, PromptError> {
|
||||
use platform_llm::LlmTextRequest;
|
||||
let request =
|
||||
LlmTextRequest::new(messages.cloned().collect()).with_request_timeout_ms(30_000);
|
||||
let response = self
|
||||
.client
|
||||
.request_text(request)
|
||||
.await
|
||||
.map_err(|e| PromptError::CompletionError(e.to_string()))?;
|
||||
Ok(response.content)
|
||||
}
|
||||
|
||||
fn tool_result_message(&self, tool_name: &str, output: &str) -> LlmMessage {
|
||||
LlmMessage::system(format!("Tool '{tool_name}' returned: {output}"))
|
||||
}
|
||||
|
||||
fn build_assistant_message(&self, text: &str) -> LlmMessage {
|
||||
LlmMessage::assistant(text)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LlmChatAgentBuilder {
|
||||
client: Option<LlmClient>,
|
||||
system_prompt_parts: Vec<String>,
|
||||
tools: Vec<Box<dyn ToolDyn>>,
|
||||
hooks: Vec<Box<dyn Hook>>,
|
||||
max_turns: usize,
|
||||
memory_data: Option<Box<dyn AgentMemory<LlmMessage>>>,
|
||||
}
|
||||
|
||||
impl AgentBuilder<LlmMessage, LlmCompletionModel> for LlmChatAgentBuilder {
|
||||
type Client = LlmClient;
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
client: None,
|
||||
system_prompt_parts: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
hooks: Vec::new(),
|
||||
max_turns: 10,
|
||||
memory_data: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn with_client(mut self, client: LlmClient) -> Self {
|
||||
self.client = Some(client);
|
||||
self
|
||||
}
|
||||
|
||||
fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
|
||||
self.system_prompt_parts.push(system_prompt.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn tool(mut self, tool: impl Tool + Send + Sync + 'static) -> Self {
|
||||
self.tools.push(Box::new(tool));
|
||||
self
|
||||
}
|
||||
|
||||
fn add_hook(mut self, hook: impl Hook + 'static) -> Self {
|
||||
self.hooks.push(Box::new(hook));
|
||||
self
|
||||
}
|
||||
|
||||
fn max_turns(mut self, n: usize) -> Self {
|
||||
self.max_turns = n;
|
||||
self
|
||||
}
|
||||
|
||||
fn memory(mut self, memory: impl AgentMemory<LlmMessage> + 'static) -> Self {
|
||||
self.memory_data = Some(Box::new(memory));
|
||||
self
|
||||
}
|
||||
|
||||
fn build(self) -> Agent<LlmCompletionModel, LlmMessage> {
|
||||
let model = LlmCompletionModel {
|
||||
client: self.client.expect("call .with_client() first"),
|
||||
};
|
||||
let mut agent = Agent::new(model);
|
||||
let tool_specs = self
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| ToolPromptSpec {
|
||||
name: tool.tool_name().to_string(),
|
||||
description: tool.description(),
|
||||
parameters: tool.parameters(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let base_prompt = self.system_prompt_parts.join("\n\n");
|
||||
let system_prompt = build_tools_system_prompt(&base_prompt, &tool_specs);
|
||||
agent.tools = self.tools;
|
||||
agent.hooks = self.hooks;
|
||||
agent.default_max_turns = self.max_turns;
|
||||
agent.memory = self.memory_data;
|
||||
agent.system_prompt = Some(LlmMessage::system(&system_prompt));
|
||||
agent
|
||||
}
|
||||
}
|
||||
|
||||
struct ToolPromptSpec {
|
||||
name: String,
|
||||
description: String,
|
||||
parameters: Value,
|
||||
}
|
||||
|
||||
fn build_tools_system_prompt(base_prompt: &str, tool_specs: &[ToolPromptSpec]) -> String {
|
||||
let mut prompt = String::new();
|
||||
prompt.push_str(base_prompt);
|
||||
prompt.push_str("\n\nYou have access to the following tools.\n\n");
|
||||
|
||||
if tool_specs.is_empty() {
|
||||
prompt.push_str("(No tools available.)\n");
|
||||
} else {
|
||||
prompt.push_str("## JSON Response Format\n");
|
||||
prompt.push_str(
|
||||
"When you need to use a tool, respond with valid JSON only (no markdown fences):\n",
|
||||
);
|
||||
prompt.push_str("{\n");
|
||||
prompt.push_str(" \"reply_text\": \"your message to the user\",\n");
|
||||
prompt.push_str(" \"tool_calls\": [\n {\n");
|
||||
prompt.push_str(" \"tool_name\": \"tool_name_here\",\n");
|
||||
prompt.push_str(" \"args\": { \"argument_name\": \"argument_value\" }\n");
|
||||
prompt.push_str(" }\n ]\n");
|
||||
prompt.push_str("}\n\n");
|
||||
prompt.push_str("If you don't need to use a tool, respond with:\n");
|
||||
prompt.push_str("{\n");
|
||||
prompt.push_str(" \"reply_text\": \"your message\",\n");
|
||||
prompt.push_str(" \"tool_calls\": []\n");
|
||||
prompt.push_str("}\n\n");
|
||||
prompt.push_str("## Available Tools\n\n");
|
||||
|
||||
for tool in tool_specs {
|
||||
prompt.push_str(&format!("- {}\n", tool.name));
|
||||
prompt.push_str(&format!(" Description: {}\n", tool.description));
|
||||
prompt.push_str(" Arguments JSON Schema:\n");
|
||||
let parameters = serde_json::to_string_pretty(&tool.parameters)
|
||||
.unwrap_or_else(|_| tool.parameters.to_string());
|
||||
prompt.push_str(¶meters);
|
||||
prompt.push_str("\n");
|
||||
}
|
||||
|
||||
prompt.push_str(
|
||||
"return your response immediately without expecting further tool execution. ",
|
||||
);
|
||||
prompt.push_str(
|
||||
"as you see, tool_calls is an array, several tools calls can be executed in one turn concurrently. ",
|
||||
);
|
||||
prompt.push_str("For example: `\"reply_text\": \"Task complete. <end/>\"`.");
|
||||
prompt.push_str("\n");
|
||||
prompt.push_str("IMPORTANT: Always respond with valid JSON only. ");
|
||||
prompt.push_str(
|
||||
"Do not wrap the JSON in markdown code fences or add extra text outside the JSON.",
|
||||
);
|
||||
}
|
||||
|
||||
prompt
|
||||
}
|
||||
@@ -1,75 +1,76 @@
|
||||
use axum::extract::{Path, State};
|
||||
use axum::{Extension, Json};
|
||||
use module_editor_agent::agent::agent_builder::AgentBuilder;
|
||||
use module_editor_agent::agent::error::PromptError;
|
||||
use module_editor_agent::agent::memory::VecMemory;
|
||||
use module_editor_agent::agent::run::PromptOutput;
|
||||
use module_editor_agent::agent::tool::Tool;
|
||||
use module_editor_agent::framework::agent_builder::AgentBuilder;
|
||||
use module_editor_agent::framework::error::PromptError;
|
||||
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::{
|
||||
EDITOR_AGENT_CONVERSATION_ID_PREFIX, EDITOR_AGENT_DEFAULT_CONVERSATION_TITLE,
|
||||
derive_conversation_title, editor_agent_messages_object_key,
|
||||
derive_conversation_title, editor_agent_messages_object_key,
|
||||
EDITOR_AGENT_CONVERSATION_ID_PREFIX, EDITOR_AGENT_DEFAULT_CONVERSATION_TITLE,
|
||||
};
|
||||
use platform_llm::LlmMessage;
|
||||
use serde::Serialize;
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{json, Value};
|
||||
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 crate::editor_agent::agent::LlmChatAgentBuilder;
|
||||
use crate::editor_agent::editor_tools::common::{EditorAgentPricedTool, EditorToolContext};
|
||||
use crate::editor_agent::editor_tools::edit_image::{EditImageTool, EditImageToolArgs};
|
||||
use crate::editor_agent::editor_tools::generate_background_music::{
|
||||
GenerateBackgroundMusicTool, GenerateBackgroundMusicToolArgs,
|
||||
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 crate::editor_agent::editor_tools::generate_character::GenerateCharacterTool;
|
||||
use crate::editor_agent::editor_tools::generate_icon_spritesheet::{
|
||||
GenerateIconSpritesheetTool, GenerateIconSpritesheetToolArgs,
|
||||
use module_editor_agent::agent::tools::generate_character::GenerateCharacterTool;
|
||||
use module_editor_agent::agent::tools::generate_icon_spritesheet::{
|
||||
GenerateIconSpritesheetTool, GenerateIconSpritesheetToolArgs,
|
||||
};
|
||||
use crate::editor_agent::editor_tools::generate_image::{GenerateImageTool, GenerateImageToolArgs};
|
||||
use crate::editor_agent::editor_tools::generate_sound_effect::{
|
||||
GenerateSoundEffectTool, GenerateSoundEffectToolArgs,
|
||||
use module_editor_agent::agent::tools::generate_image::{GenerateImageTool, GenerateImageToolArgs};
|
||||
use module_editor_agent::agent::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 module_editor_agent::agent::tools::generate_ui_design::GenerateUiDesignTool;
|
||||
use module_editor_agent::agent::tools::generate_video::{GenerateVideoTool, GenerateVideoToolArgs};
|
||||
use crate::editor_agent::utils::{
|
||||
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,
|
||||
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,
|
||||
};
|
||||
use crate::editor_agent::{context, display_args, reconcile};
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use crate::editor_generation_queue::{
|
||||
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,
|
||||
enqueue_editor_generation_job_with_identity, 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,
|
||||
};
|
||||
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::tools::context::EditorToolContext;
|
||||
|
||||
pub async fn editor_agent_message(
|
||||
State(state): State<AppState>,
|
||||
@@ -219,7 +220,7 @@ pub async fn editor_agent_message(
|
||||
delta_messages: Vec::new(),
|
||||
error_message: Some(err.to_string()),
|
||||
})),
|
||||
Ok(mut delta_messages) => {
|
||||
Ok(delta_messages) => {
|
||||
for msg in &delta_messages {
|
||||
document.messages.push(msg.clone());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::editor_agent::editor_tools::common::EditorToolContext;
|
||||
use crate::editor_agent::utils::{ImageId, ImageMetadata, IntoDataKey};
|
||||
use module_editor_agent::agent::tools::context::EditorToolContext;
|
||||
use crate::editor_agent::utils::IntoDataKey;
|
||||
use shared_contracts::editor_agent::EditorAgentConversationMessagesDocument;
|
||||
use std::collections::HashMap;
|
||||
use module_editor_agent::agent::asset::{ImageId, ImageMetadata};
|
||||
|
||||
pub fn build_tool_context(document: &EditorAgentConversationMessagesDocument) -> EditorToolContext {
|
||||
let mut images: HashMap<ImageId, ImageMetadata> = HashMap::new();
|
||||
@@ -19,7 +20,7 @@ pub fn build_tool_context(document: &EditorAgentConversationMessagesDocument) ->
|
||||
images.insert(image_id, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// user pointed
|
||||
for a in &msg.attachments {
|
||||
let data_key = a.clone().into_data_key();
|
||||
@@ -33,4 +34,4 @@ pub fn build_tool_context(document: &EditorAgentConversationMessagesDocument) ->
|
||||
}
|
||||
|
||||
EditorToolContext { images }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
use serde_json::Value;
|
||||
use module_editor_agent::agent::error::PromptError;
|
||||
use module_editor_agent::agent::tool::Tool;
|
||||
use module_editor_agent::framework::error::PromptError;
|
||||
use module_editor_agent::framework::tool::Tool;
|
||||
use shared_contracts::editor_agent::{EditorAgentConversationMessagesDocument, EditorAgentToolCallDisplayArgs, EditorAgentToolCallDisplayExtras, EditorAgentToolCallImageArg, EditorAgentToolCallImageRef, EditorAgentToolCallStringArg};
|
||||
use crate::editor_agent::editor_tools::common::{EditorAgentPricedTool, EditorToolContext};
|
||||
use crate::editor_agent::editor_tools::edit_image::{EditImageTool, EditImageToolArgs};
|
||||
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::{GenerateIconSpritesheetTool, GenerateIconSpritesheetToolArgs};
|
||||
use crate::editor_agent::editor_tools::generate_image::{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::utils::{ImageId, IntoDataKey, IntoImageId};
|
||||
use crate::editor_agent::pricing::EditorAgentPricedTool;
|
||||
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 module_editor_agent::agent::asset::ImageId;
|
||||
use crate::editor_agent::utils::{IntoDataKey, IntoImageId};
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
|
||||
pub fn build_tool_call_display_args(
|
||||
@@ -311,4 +313,4 @@ pub fn resolve_tool_call_image_ref(
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
use crate::editor_agent::utils::{ImageId, ImageMetadata};
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use crate::http_error::AppError;
|
||||
use crate::openai_image_generation::GPT_IMAGE_2_MODEL;
|
||||
use axum::http::StatusCode;
|
||||
use module_editor_agent::agent::tool::Tool;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct EditorToolContext {
|
||||
pub images: HashMap<ImageId, ImageMetadata>,
|
||||
}
|
||||
|
||||
impl EditorToolContext {
|
||||
/// Check if an image with the given ID exists in the context.
|
||||
pub fn contains_image(&self, image_id: &ImageId) -> bool {
|
||||
self.images.contains_key(image_id)
|
||||
}
|
||||
|
||||
pub fn image_data_key(&self, image_id: &ImageId) -> Option<&str> {
|
||||
self.images
|
||||
.get(image_id)
|
||||
.map(|metadata| metadata.data_key.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
/// api-server 侧的画布 Agent 工具计价扩展。
|
||||
///
|
||||
/// 通用 `Tool` 仍只负责参数校验;价格依赖 api-server 的运行时配置,不能下沉到
|
||||
/// `module-editor-agent`。实际执行和扣费仍由既有生成 BFF 负责。
|
||||
pub(crate) trait EditorAgentPricedTool: Tool {
|
||||
fn pricing(&self, pricing: &EditorGenerationPricingConfig, args: &<Self as Tool>::Args) -> u32;
|
||||
}
|
||||
|
||||
pub(crate) fn editor_agent_image_mud_points(
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
kind: Option<&str>,
|
||||
image_size: Option<&str>,
|
||||
) -> u32 {
|
||||
// 这些 Agent 工具当前向既有 BFF 传 model=None;BFF 会先归一为 gpt-image-2。
|
||||
// 尺寸同样只把精确的 2K 识别为 2K,其余值回落到 1K。
|
||||
let normalized_image_size = match image_size.map(str::trim) {
|
||||
Some("2K") => "2K",
|
||||
_ => "1K",
|
||||
};
|
||||
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,15 +1,13 @@
|
||||
mod editor_tools;
|
||||
mod utils;
|
||||
pub mod api;
|
||||
mod agent;
|
||||
mod display_args;
|
||||
mod context;
|
||||
mod resp_to_asset;
|
||||
mod reconcile;
|
||||
pub mod pricing;
|
||||
|
||||
pub use api::{
|
||||
create_editor_agent_conversation, delete_editor_agent_conversation,
|
||||
get_editor_agent_conversation, list_editor_agent_conversations,
|
||||
confirm_editor_agent_tool_call, cancel_editor_agent_tool_call,
|
||||
cancel_editor_agent_tool_call, confirm_editor_agent_tool_call,
|
||||
create_editor_agent_conversation, delete_editor_agent_conversation,
|
||||
get_editor_agent_conversation, list_editor_agent_conversations,
|
||||
};
|
||||
pub use utils::build_editor_agent_canvas_completion;
|
||||
|
||||
+134
-15
@@ -1,22 +1,22 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::editor_agent::editor_tools::common::{EditorAgentPricedTool, EditorToolContext};
|
||||
use crate::editor_agent::editor_tools::edit_image::{EditImageTool, EditImageToolArgs};
|
||||
use crate::editor_agent::editor_tools::generate_background_music::{
|
||||
GenerateBackgroundMusicTool, GenerateBackgroundMusicToolArgs,
|
||||
use module_editor_agent::framework::tool::Tool;
|
||||
use platform_image::GPT_IMAGE_2_MODEL;
|
||||
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 crate::editor_agent::editor_tools::generate_character::GenerateCharacterTool;
|
||||
use crate::editor_agent::editor_tools::generate_icon_spritesheet::{
|
||||
GenerateIconSpritesheetTool, GenerateIconSpritesheetToolArgs,
|
||||
use module_editor_agent::agent::tools::generate_character::GenerateCharacterTool;
|
||||
use module_editor_agent::agent::tools::generate_icon_spritesheet::{
|
||||
GenerateIconSpritesheetTool, GenerateIconSpritesheetToolArgs,
|
||||
};
|
||||
use crate::editor_agent::editor_tools::generate_image::{GenerateImageTool, GenerateImageToolArgs};
|
||||
use crate::editor_agent::editor_tools::generate_sound_effect::{
|
||||
GenerateSoundEffectTool, GenerateSoundEffectToolArgs,
|
||||
use module_editor_agent::agent::tools::generate_image::{GenerateImageTool, GenerateImageToolArgs};
|
||||
use module_editor_agent::agent::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::utils::ImageId;
|
||||
use crate::editor_generation_config::load_editor_generation_pricing_from_paths;
|
||||
use module_editor_agent::agent::tools::generate_ui_design::GenerateUiDesignTool;
|
||||
use module_editor_agent::agent::tools::generate_video::{GenerateVideoTool, GenerateVideoToolArgs};
|
||||
use crate::editor_generation_config::{load_editor_generation_pricing_from_paths, EditorGenerationPricingConfig};
|
||||
|
||||
fn context() -> EditorToolContext {
|
||||
EditorToolContext {
|
||||
@@ -188,3 +188,122 @@ fn pricing_uses_the_supplied_runtime_snapshot() {
|
||||
19
|
||||
);
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateVideoTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateVideoToolArgs,
|
||||
) -> u32 {
|
||||
let model = args
|
||||
.model
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(Self::DEFAULT_VIDEO_MODEL);
|
||||
let resolution = args
|
||||
.resolution
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(Self::DEFAULT_VIDEO_RESOLUTION);
|
||||
let duration_seconds = args
|
||||
.duration_seconds
|
||||
.unwrap_or(Self::DEFAULT_VIDEO_DURATION_SECONDS);
|
||||
pricing.video_model_mud_points(Some(model), resolution, duration_seconds)
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateUiDesignTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateImageToolArgs,
|
||||
) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, Some("ui-design"), args.image_size.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateSoundEffectTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateSoundEffectToolArgs,
|
||||
) -> u32 {
|
||||
let model = args
|
||||
.model
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(Self::DEFAULT_MODEL);
|
||||
pricing.sound_effect_model_mud_points(Some(model))
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateImageTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateImageToolArgs,
|
||||
) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, None, args.image_size.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateIconSpritesheetTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateIconSpritesheetToolArgs,
|
||||
) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, Some("icon"), args.image_size.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateCharacterTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateImageToolArgs,
|
||||
) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, Some("character"), args.image_size.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
/// api-server 侧的画布 Agent 工具计价扩展。
|
||||
///
|
||||
/// 通用 `Tool` 仍只负责参数校验;价格依赖 api-server 的运行时配置,不能下沉到
|
||||
/// `module-editor-agent`。实际执行和扣费仍由既有生成 BFF 负责。
|
||||
pub(crate) trait EditorAgentPricedTool: Tool {
|
||||
fn pricing(&self, pricing: &EditorGenerationPricingConfig, args: &<Self as Tool>::Args) -> u32;
|
||||
}
|
||||
|
||||
pub(crate) fn editor_agent_image_mud_points(
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
kind: Option<&str>,
|
||||
image_size: Option<&str>,
|
||||
) -> u32 {
|
||||
// 这些 Agent 工具当前向既有 BFF 传 model=None;BFF 会先归一为 gpt-image-2。
|
||||
// 尺寸同样只把精确的 2K 识别为 2K,其余值回落到 1K。
|
||||
let normalized_image_size = match image_size.map(str::trim) {
|
||||
Some("2K") => "2K",
|
||||
_ => "1K",
|
||||
};
|
||||
pricing.image_generation_mud_points(kind, Some(GPT_IMAGE_2_MODEL), Some(normalized_image_size))
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateBackgroundMusicTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
_args: &GenerateBackgroundMusicToolArgs,
|
||||
) -> u32 {
|
||||
pricing.background_music_model_mud_points(Some(Self::DEFAULT_MODEL))
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for EditImageTool {
|
||||
fn pricing(&self, pricing: &EditorGenerationPricingConfig, _args: &EditImageToolArgs) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, Some("quick-edit"), Some("1K"))
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,26 @@
|
||||
use crate::editor_agent::editor_tools::common::EditorToolContext;
|
||||
use crate::editor_agent::editor_tools::edit_image::{
|
||||
use module_editor_agent::agent::tools::context::EditorToolContext;
|
||||
use module_editor_agent::agent::tools::edit_image::{
|
||||
EditImageTool, EditImageToolArgs, EditorImageEditResult,
|
||||
};
|
||||
use crate::editor_agent::editor_tools::generate_background_music::{
|
||||
use module_editor_agent::agent::tools::generate_background_music::{
|
||||
GenerateBackgroundMusicTool, GenerateBackgroundMusicToolArgs,
|
||||
};
|
||||
use crate::editor_agent::editor_tools::generate_character::GenerateCharacterTool;
|
||||
use crate::editor_agent::editor_tools::generate_icon_spritesheet::{
|
||||
use module_editor_agent::agent::tools::generate_character::GenerateCharacterTool;
|
||||
use module_editor_agent::agent::tools::generate_icon_spritesheet::{
|
||||
EditorIconSpritesheetResult, GenerateIconSpritesheetTool, GenerateIconSpritesheetToolArgs,
|
||||
};
|
||||
use crate::editor_agent::editor_tools::generate_image::{
|
||||
use module_editor_agent::agent::tools::generate_image::{
|
||||
EditorImageGenerationResult, GenerateImageTool, GenerateImageToolArgs,
|
||||
};
|
||||
use crate::editor_agent::editor_tools::generate_sound_effect::{
|
||||
use module_editor_agent::agent::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 module_editor_agent::agent::tools::generate_ui_design::GenerateUiDesignTool;
|
||||
use module_editor_agent::agent::tools::generate_video::{GenerateVideoTool, GenerateVideoToolArgs};
|
||||
use crate::editor_agent::resp_to_asset;
|
||||
use crate::http_error::AppError;
|
||||
use crate::state::AppState;
|
||||
use module_editor_agent::agent::tool::Tool;
|
||||
use module_editor_agent::framework::tool::Tool;
|
||||
use serde_json::Value;
|
||||
use shared_contracts::assets::{EditorAudioGenerateResponse, EditorVideoGenerateResponse};
|
||||
use shared_contracts::editor_agent::{
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use serde_json::Value;
|
||||
use shared_contracts::assets::{EditorAudioGenerateResponse, EditorVideoGenerateResponse};
|
||||
use shared_contracts::editor_agent::{EditorAgentGeneratedAudio, EditorAgentGeneratedImage, EditorAgentGeneratedVideo};
|
||||
use crate::editor_agent::editor_tools::edit_image::EditorImageEditResult;
|
||||
use crate::editor_agent::editor_tools::generate_icon_spritesheet::EditorIconSpritesheetResult;
|
||||
use crate::editor_agent::editor_tools::generate_image::EditorImageGenerationResult;
|
||||
use module_editor_agent::agent::tools::edit_image::EditorImageEditResult;
|
||||
use module_editor_agent::agent::tools::generate_icon_spritesheet::EditorIconSpritesheetResult;
|
||||
use module_editor_agent::agent::tools::generate_image::EditorImageGenerationResult;
|
||||
|
||||
fn value_string(value: Option<&Value>, field: &str) -> Option<String> {
|
||||
value
|
||||
|
||||
@@ -4,27 +4,25 @@ use crate::platform_errors::map_oss_error;
|
||||
use crate::state::AppState;
|
||||
use axum::http::StatusCode;
|
||||
use platform_oss::{
|
||||
LegacyAssetPrefix, OssObjectAccess, OssPutObjectRequest, OssSignedGetObjectUrlRequest,
|
||||
LegacyAssetPrefix, OssObjectAccess, OssPutObjectRequest, OssSignedGetObjectUrlRequest,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use sha2::{Digest, Sha256};
|
||||
use serde_json::{json, Value};
|
||||
use shared_contracts::assets::{
|
||||
EditorCanvasGenerationCompletionPayload, EditorCanvasGenerationPlaceholderPayload,
|
||||
EditorCanvasGenerationCompletionPayload, EditorCanvasGenerationPlaceholderPayload,
|
||||
};
|
||||
use shared_contracts::editor_agent::{
|
||||
EDITOR_AGENT_MESSAGES_DOCUMENT_VERSION, EditorAgentAttachmentRef, EditorAgentAttachmentSource,
|
||||
EditorAgentConversationDetail, EditorAgentConversationMessagesDocument, EditorAgentGeneratedImage,
|
||||
EditorAgentConversationSummary, EditorAgentMessage,
|
||||
EditorAgentAttachmentRef, EditorAgentAttachmentSource, EditorAgentConversationDetail,
|
||||
EditorAgentConversationMessagesDocument, EditorAgentConversationSummary, EditorAgentGeneratedImage,
|
||||
EditorAgentMessage, EDITOR_AGENT_MESSAGES_DOCUMENT_VERSION,
|
||||
};
|
||||
use shared_kernel::{normalize_optional_string, normalize_required_string};
|
||||
use spacetime_client::{
|
||||
EditorAgentConversationRecord, EditorAssetLibraryRecord, EditorAssetRecord,
|
||||
EditorProjectGetRecordInput, EditorProjectRecord, EditorProjectResourceRecord,
|
||||
EditorAgentConversationRecord, EditorAssetLibraryRecord, EditorAssetRecord,
|
||||
EditorProjectGetRecordInput, EditorProjectRecord, EditorProjectResourceRecord,
|
||||
};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt::Display;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
use module_editor_agent::agent::asset::ImageId;
|
||||
|
||||
pub trait IntoDataKey {
|
||||
fn into_data_key(self) -> String;
|
||||
@@ -107,48 +105,9 @@ impl IntoResourceId for EditorAssetRecord {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ImageId {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
impl ImageId {
|
||||
/// Keep data keys server-side while giving the model a stable image reference.
|
||||
pub fn from_data_key(data_key: impl AsRef<str>) -> Self {
|
||||
let digest = Sha256::digest(data_key.as_ref().as_bytes());
|
||||
Self {
|
||||
id: format!("sha256:{digest:x}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ImageId {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
self.id.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ImageId {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
String::deserialize(deserializer).map(|id| ImageId { id })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ImageMetadata {
|
||||
pub tag: String,
|
||||
pub data_key: String,
|
||||
}
|
||||
|
||||
impl Display for ImageId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.id)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod image_id_tests {
|
||||
use super::ImageId;
|
||||
use module_editor_agent::agent::asset::ImageId;
|
||||
|
||||
#[test]
|
||||
fn image_id_is_a_stable_hash_of_the_data_key() {
|
||||
|
||||
@@ -13,3 +13,7 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
shared-kernel = { workspace = true }
|
||||
spacetimedb = { workspace = true, optional = true }
|
||||
platform-llm = { workspace = true }
|
||||
shared-contracts = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
|
||||
@@ -1,80 +1,174 @@
|
||||
use crate::agent::error::PromptError;
|
||||
use crate::agent::hook::Hook;
|
||||
use crate::agent::memory::AgentMemory;
|
||||
use crate::agent::run::PromptRequest;
|
||||
use crate::agent::{Tool, ToolDyn};
|
||||
use crate::framework::agent::{Agent, LlmApiAdaptor};
|
||||
use crate::framework::agent_builder::AgentBuilder;
|
||||
use crate::framework::error::PromptError;
|
||||
use crate::framework::hook::Hook;
|
||||
use crate::framework::memory::AgentMemory;
|
||||
use crate::framework::tool::{Tool, ToolDyn};
|
||||
use platform_llm::{LlmClient, LlmMessage};
|
||||
use serde_json::Value;
|
||||
|
||||
pub struct Agent<M: LlmApiAdaptor<Message>, Message> {
|
||||
pub model: M,
|
||||
pub tools: Vec<Box<dyn ToolDyn>>,
|
||||
pub hooks: Vec<Box<dyn Hook>>,
|
||||
pub default_max_turns: usize,
|
||||
pub system_prompt: Option<Message>,
|
||||
pub memory: Option<Box<dyn AgentMemory<Message>>>,
|
||||
pub struct LlmCompletionModel {
|
||||
client: LlmClient,
|
||||
}
|
||||
|
||||
impl<M, Message> Agent<M, Message>
|
||||
where
|
||||
M: LlmApiAdaptor<Message> + 'static,
|
||||
Message: Send + 'static,
|
||||
{
|
||||
pub fn new(model: M) -> Self {
|
||||
impl LlmApiAdaptor<LlmMessage> for LlmCompletionModel {
|
||||
async fn complete<'a>(
|
||||
&self,
|
||||
messages: impl Iterator<Item = &'a LlmMessage> + Send,
|
||||
) -> Result<String, PromptError> {
|
||||
use platform_llm::LlmTextRequest;
|
||||
let request =
|
||||
LlmTextRequest::new(messages.cloned().collect()).with_request_timeout_ms(30_000);
|
||||
let response = self
|
||||
.client
|
||||
.request_text(request)
|
||||
.await
|
||||
.map_err(|e| PromptError::CompletionError(e.to_string()))?;
|
||||
Ok(response.content)
|
||||
}
|
||||
|
||||
fn tool_result_message(&self, tool_name: &str, output: &str) -> LlmMessage {
|
||||
LlmMessage::system(format!("Tool '{tool_name}' returned: {output}"))
|
||||
}
|
||||
|
||||
fn build_assistant_message(&self, text: &str) -> LlmMessage {
|
||||
LlmMessage::assistant(text)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LlmChatAgentBuilder {
|
||||
client: Option<LlmClient>,
|
||||
system_prompt_parts: Vec<String>,
|
||||
tools: Vec<Box<dyn ToolDyn>>,
|
||||
hooks: Vec<Box<dyn Hook>>,
|
||||
max_turns: usize,
|
||||
memory_data: Option<Box<dyn AgentMemory<LlmMessage>>>,
|
||||
}
|
||||
|
||||
impl AgentBuilder<LlmMessage, LlmCompletionModel> for LlmChatAgentBuilder {
|
||||
type Client = LlmClient;
|
||||
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
model,
|
||||
client: None,
|
||||
system_prompt_parts: Vec::new(),
|
||||
tools: Vec::new(),
|
||||
hooks: Vec::new(),
|
||||
default_max_turns: 10,
|
||||
system_prompt: None,
|
||||
memory: None,
|
||||
max_turns: 10,
|
||||
memory_data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tool(mut self, tool: impl Tool + Send + Sync + 'static) -> Self {
|
||||
fn with_client(mut self, client: LlmClient) -> Self {
|
||||
self.client = Some(client);
|
||||
self
|
||||
}
|
||||
|
||||
fn system_prompt(mut self, system_prompt: impl Into<String>) -> Self {
|
||||
self.system_prompt_parts.push(system_prompt.into());
|
||||
self
|
||||
}
|
||||
|
||||
fn tool(mut self, tool: impl Tool + Send + Sync + 'static) -> Self {
|
||||
self.tools.push(Box::new(tool));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn system_prompt(mut self, msg: Message) -> Self {
|
||||
self.system_prompt = Some(msg);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn memory(mut self, mem: impl AgentMemory<Message> + 'static) -> Self {
|
||||
self.memory = Some(Box::new(mem));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn hook(mut self, hook: impl Hook + 'static) -> Self {
|
||||
fn add_hook(mut self, hook: impl Hook + 'static) -> Self {
|
||||
self.hooks.push(Box::new(hook));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_turns(mut self, n: usize) -> Self {
|
||||
self.default_max_turns = n;
|
||||
fn max_turns(mut self, n: usize) -> Self {
|
||||
self.max_turns = n;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn tools(&self) -> &[Box<dyn ToolDyn>] {
|
||||
&self.tools
|
||||
fn memory(mut self, memory: impl AgentMemory<LlmMessage> + 'static) -> Self {
|
||||
self.memory_data = Some(Box::new(memory));
|
||||
self
|
||||
}
|
||||
|
||||
|
||||
pub fn prompt(&mut self, message: impl Into<Message> + Send) -> PromptRequest<'_, M, Message>
|
||||
where
|
||||
Message: 'static,
|
||||
{
|
||||
PromptRequest::new(self, message.into())
|
||||
fn build(self) -> Agent<LlmCompletionModel, LlmMessage> {
|
||||
let model = LlmCompletionModel {
|
||||
client: self.client.expect("call .with_client() first"),
|
||||
};
|
||||
let mut agent = Agent::new(model);
|
||||
let tool_specs = self
|
||||
.tools
|
||||
.iter()
|
||||
.map(|tool| ToolPromptSpec {
|
||||
name: tool.tool_name().to_string(),
|
||||
description: tool.description(),
|
||||
parameters: tool.parameters(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let base_prompt = self.system_prompt_parts.join("\n\n");
|
||||
let system_prompt = build_tools_system_prompt(&base_prompt, &tool_specs);
|
||||
agent.tools = self.tools;
|
||||
agent.hooks = self.hooks;
|
||||
agent.default_max_turns = self.max_turns;
|
||||
agent.memory = self.memory_data;
|
||||
agent.system_prompt = Some(LlmMessage::system(&system_prompt));
|
||||
agent
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LlmApiAdaptor<Message>: Send + Sync {
|
||||
fn complete<'a>(
|
||||
&self,
|
||||
messages: impl Iterator<Item = &'a Message> + Send,
|
||||
) -> impl Future<Output = Result<String, PromptError>> + Send
|
||||
where
|
||||
Message: 'a;
|
||||
fn tool_result_message(&self, tool_name: &str, output: &str) -> Message;
|
||||
|
||||
fn build_assistant_message(&self, text: &str) -> Message;
|
||||
struct ToolPromptSpec {
|
||||
name: String,
|
||||
description: String,
|
||||
parameters: Value,
|
||||
}
|
||||
|
||||
fn build_tools_system_prompt(base_prompt: &str, tool_specs: &[ToolPromptSpec]) -> String {
|
||||
let mut prompt = String::new();
|
||||
prompt.push_str(base_prompt);
|
||||
prompt.push_str("\n\nYou have access to the following tools.\n\n");
|
||||
|
||||
if tool_specs.is_empty() {
|
||||
prompt.push_str("(No tools available.)\n");
|
||||
} else {
|
||||
prompt.push_str("## JSON Response Format\n");
|
||||
prompt.push_str(
|
||||
"When you need to use a tool, respond with valid JSON only (no markdown fences):\n",
|
||||
);
|
||||
prompt.push_str("{\n");
|
||||
prompt.push_str(" \"reply_text\": \"your message to the user\",\n");
|
||||
prompt.push_str(" \"tool_calls\": [\n {\n");
|
||||
prompt.push_str(" \"tool_name\": \"tool_name_here\",\n");
|
||||
prompt.push_str(" \"args\": { \"argument_name\": \"argument_value\" }\n");
|
||||
prompt.push_str(" }\n ]\n");
|
||||
prompt.push_str("}\n\n");
|
||||
prompt.push_str("If you don't need to use a tool, respond with:\n");
|
||||
prompt.push_str("{\n");
|
||||
prompt.push_str(" \"reply_text\": \"your message\",\n");
|
||||
prompt.push_str(" \"tool_calls\": []\n");
|
||||
prompt.push_str("}\n\n");
|
||||
prompt.push_str("## Available Tools\n\n");
|
||||
|
||||
for tool in tool_specs {
|
||||
prompt.push_str(&format!("- {}\n", tool.name));
|
||||
prompt.push_str(&format!(" Description: {}\n", tool.description));
|
||||
prompt.push_str(" Arguments JSON Schema:\n");
|
||||
let parameters = serde_json::to_string_pretty(&tool.parameters)
|
||||
.unwrap_or_else(|_| tool.parameters.to_string());
|
||||
prompt.push_str(¶meters);
|
||||
prompt.push_str("\n");
|
||||
}
|
||||
|
||||
prompt.push_str(
|
||||
"return your response immediately without expecting further tool execution. ",
|
||||
);
|
||||
prompt.push_str(
|
||||
"as you see, tool_calls is an array, several tools calls can be executed in one turn concurrently. ",
|
||||
);
|
||||
prompt.push_str("For example: `\"reply_text\": \"Task complete. <end/>\"`.");
|
||||
prompt.push_str("\n");
|
||||
prompt.push_str("IMPORTANT: Always respond with valid JSON only. ");
|
||||
prompt.push_str(
|
||||
"Do not wrap the JSON in markdown code fences or add extra text outside the JSON.",
|
||||
);
|
||||
}
|
||||
|
||||
prompt
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
use std::fmt::Display;
|
||||
use sha2::Sha256;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use hmac::digest::Digest;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ImageId {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
impl ImageId {
|
||||
/// Keep data keys server-side while giving the model a stable image reference.
|
||||
pub fn from_data_key(data_key: impl AsRef<str>) -> Self {
|
||||
let digest = Sha256::digest(data_key.as_ref().as_bytes());
|
||||
Self {
|
||||
id: format!("sha256:{digest:x}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ImageId {
|
||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
self.id.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ImageId {
|
||||
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
|
||||
String::deserialize(deserializer).map(|id| ImageId { id })
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ImageId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ImageMetadata {
|
||||
pub tag: String,
|
||||
pub data_key: String,
|
||||
}
|
||||
@@ -1,9 +1,3 @@
|
||||
use tool::{Tool, ToolDyn};
|
||||
|
||||
pub mod agent;
|
||||
pub mod agent_builder;
|
||||
pub mod error;
|
||||
pub mod hook;
|
||||
pub mod memory;
|
||||
pub mod run;
|
||||
pub mod tool;
|
||||
pub mod tools;
|
||||
pub mod asset;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
use std::collections::HashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use crate::agent::asset::ImageId;
|
||||
use crate::agent::asset::ImageMetadata;
|
||||
|
||||
impl EditorToolContext {
|
||||
/// Check if an image with the given ID exists in the context.
|
||||
pub fn contains_image(&self, image_id: &ImageId) -> bool {
|
||||
self.images.contains_key(image_id)
|
||||
}
|
||||
|
||||
pub fn image_data_key(&self, image_id: &ImageId) -> Option<&str> {
|
||||
self.images
|
||||
.get(image_id)
|
||||
.map(|metadata| metadata.data_key.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct EditorToolContext {
|
||||
pub images: HashMap<ImageId, ImageMetadata>,
|
||||
}
|
||||
+3
-13
@@ -1,14 +1,10 @@
|
||||
use crate::editor_agent::editor_tools::common::{
|
||||
EditorAgentPricedTool, EditorToolContext, editor_agent_image_mud_points,
|
||||
};
|
||||
use crate::editor_agent::utils::ImageId;
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use module_editor_agent::agent::tool::{Tool, ToolFailure, ToolFailureKind};
|
||||
use crate::agent::tools::context::EditorToolContext;
|
||||
use crate::agent::asset::ImageId;
|
||||
use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub struct EditImageTool {
|
||||
pub context: EditorToolContext,
|
||||
}
|
||||
@@ -115,12 +111,6 @@ impl Tool for EditImageTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for EditImageTool {
|
||||
fn pricing(&self, pricing: &EditorGenerationPricingConfig, _args: &EditImageToolArgs) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, Some("quick-edit"), Some("1K"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Typed result deserialized from `edit_editor_image_for_owner` response.
|
||||
///
|
||||
/// Mirrors `EditorImageGenerationResponse` but uses `String` instead of `&'static str`
|
||||
+3
-15
@@ -1,8 +1,4 @@
|
||||
use crate::editor_agent::editor_tools::common::EditorAgentPricedTool;
|
||||
use crate::editor_generation_config::{
|
||||
EditorGenerationPricingConfig, EDITOR_BACKGROUND_MUSIC_MODEL_SUNO,
|
||||
};
|
||||
use module_editor_agent::agent::tool::{Tool, ToolFailure};
|
||||
use crate::framework::tool::{Tool, ToolFailure};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use shared_contracts::assets::EditorAudioGenerateResponse;
|
||||
@@ -71,17 +67,9 @@ impl Tool for GenerateBackgroundMusicTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateBackgroundMusicTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
_args: &GenerateBackgroundMusicToolArgs,
|
||||
) -> u32 {
|
||||
pricing.background_music_model_mud_points(Some(EDITOR_BACKGROUND_MUSIC_MODEL_SUNO))
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateBackgroundMusicTool {
|
||||
pub const DEFAULT_MODEL: &'static str = "chirp-v5";
|
||||
|
||||
pub fn format_execute_message(
|
||||
&self,
|
||||
args: &GenerateBackgroundMusicToolArgs,
|
||||
+4
-20
@@ -1,13 +1,7 @@
|
||||
use crate::editor_agent::editor_tools::common::{
|
||||
editor_agent_image_mud_points, EditorAgentPricedTool, EditorToolContext,
|
||||
};
|
||||
use crate::editor_agent::editor_tools::generate_image::{
|
||||
EditorImageGenerationResult, GenerateImageError, GenerateImageTool, GenerateImageToolArgs,
|
||||
GenerateImageToolOutput,
|
||||
};
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use module_editor_agent::agent::tool::{Tool, ToolFailure};
|
||||
use crate::framework::tool::{Tool, ToolFailure};
|
||||
use serde_json::{json, Value};
|
||||
use crate::agent::tools::context::EditorToolContext;
|
||||
use crate::agent::tools::generate_image::{EditorImageGenerationResult, GenerateImageError, GenerateImageTool, GenerateImageToolArgs, GenerateImageToolOutput};
|
||||
|
||||
pub struct GenerateCharacterTool {
|
||||
pub context: EditorToolContext,
|
||||
@@ -59,16 +53,6 @@ impl Tool for GenerateCharacterTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateCharacterTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateImageToolArgs,
|
||||
) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, Some("character"), args.image_size.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateCharacterTool {
|
||||
pub fn format_execute_message(
|
||||
&self,
|
||||
@@ -84,7 +68,7 @@ impl GenerateCharacterTool {
|
||||
.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);
|
||||
let image_id = crate::agent::asset::ImageId::from_data_key(data_key);
|
||||
format!(
|
||||
"[tool_call:{tool_name}] args: {args} output: generated result saved as image: {image_id}"
|
||||
)
|
||||
+3
-25
@@ -1,11 +1,6 @@
|
||||
use crate::editor_agent::editor_tools::common::{
|
||||
editor_agent_image_mud_points, EditorAgentPricedTool, EditorToolContext,
|
||||
};
|
||||
use crate::editor_agent::utils::ImageId;
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use crate::http_error::AppError;
|
||||
use axum::http::StatusCode;
|
||||
use module_editor_agent::agent::tool::{Tool, ToolFailure, ToolFailureKind};
|
||||
use crate::agent::tools::context::EditorToolContext;
|
||||
use crate::agent::asset::ImageId;
|
||||
use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::error::Error;
|
||||
@@ -113,16 +108,6 @@ impl Tool for GenerateIconSpritesheetTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateIconSpritesheetTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateIconSpritesheetToolArgs,
|
||||
) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, Some("icon"), args.image_size.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateIconSpritesheetTool {
|
||||
fn validate_args(
|
||||
&self,
|
||||
@@ -174,10 +159,3 @@ impl GenerateIconSpritesheetTool {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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"),
|
||||
}))
|
||||
}
|
||||
+4
-17
@@ -1,9 +1,6 @@
|
||||
use crate::editor_agent::editor_tools::common::{
|
||||
editor_agent_image_mud_points, EditorAgentPricedTool, EditorToolContext,
|
||||
};
|
||||
use crate::editor_agent::utils::ImageId;
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use module_editor_agent::agent::tool::{Tool, ToolFailure, ToolFailureKind};
|
||||
use crate::agent::tools::context::EditorToolContext;
|
||||
use crate::agent::asset::ImageId;
|
||||
use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use std::error::Error;
|
||||
@@ -107,16 +104,6 @@ impl Tool for GenerateImageTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateImageTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateImageToolArgs,
|
||||
) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, None, args.image_size.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct EditorImageGenerationResult {
|
||||
@@ -137,7 +124,7 @@ pub struct EditorImageGenerationResult {
|
||||
}
|
||||
|
||||
impl GenerateImageTool {
|
||||
pub(crate) fn validate_args(
|
||||
pub fn validate_args(
|
||||
&self,
|
||||
args: &GenerateImageToolArgs,
|
||||
) -> Result<(), GenerateImageError> {
|
||||
+3
-21
@@ -1,8 +1,4 @@
|
||||
use crate::editor_agent::editor_tools::common::EditorAgentPricedTool;
|
||||
use crate::editor_generation_config::{
|
||||
EditorGenerationPricingConfig, EDITOR_SOUND_EFFECT_MODEL_VIDU,
|
||||
};
|
||||
use module_editor_agent::agent::tool::{Tool, ToolFailure};
|
||||
use crate::framework::tool::{Tool, ToolFailure};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use shared_contracts::assets::EditorAudioGenerateResponse;
|
||||
@@ -80,23 +76,9 @@ impl Tool for GenerateSoundEffectTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateSoundEffectTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateSoundEffectToolArgs,
|
||||
) -> u32 {
|
||||
let model = args
|
||||
.model
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(EDITOR_SOUND_EFFECT_MODEL_VIDU);
|
||||
pricing.sound_effect_model_mud_points(Some(model))
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateSoundEffectTool {
|
||||
pub const DEFAULT_MODEL: &'static str = "audio1.0";
|
||||
|
||||
pub fn format_execute_message(
|
||||
&self,
|
||||
args: &GenerateSoundEffectToolArgs,
|
||||
+7
-20
@@ -1,13 +1,10 @@
|
||||
use crate::editor_agent::editor_tools::common::{
|
||||
EditorAgentPricedTool, EditorToolContext, editor_agent_image_mud_points,
|
||||
use crate::agent::tools::generate_image::{
|
||||
EditorImageGenerationResult, GenerateImageError, GenerateImageTool, GenerateImageToolArgs,
|
||||
GenerateImageToolOutput,
|
||||
};
|
||||
use crate::editor_agent::editor_tools::generate_image::{
|
||||
EditorImageGenerationResult, GenerateImageError, GenerateImageTool, GenerateImageToolArgs,
|
||||
GenerateImageToolOutput,
|
||||
};
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use module_editor_agent::agent::tool::{Tool, ToolFailure};
|
||||
use serde_json::{Value, json};
|
||||
use crate::framework::tool::{Tool, ToolFailure};
|
||||
use serde_json::{json, Value};
|
||||
use crate::agent::tools::context::EditorToolContext;
|
||||
|
||||
pub struct GenerateUiDesignTool {
|
||||
pub context: EditorToolContext,
|
||||
@@ -58,16 +55,6 @@ impl Tool for GenerateUiDesignTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateUiDesignTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateImageToolArgs,
|
||||
) -> u32 {
|
||||
editor_agent_image_mud_points(pricing, Some("ui-design"), args.image_size.as_deref())
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateUiDesignTool {
|
||||
pub fn format_execute_message(
|
||||
&self,
|
||||
@@ -83,7 +70,7 @@ impl GenerateUiDesignTool {
|
||||
.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);
|
||||
let image_id = crate::agent::asset::ImageId::from_data_key(data_key);
|
||||
format!(
|
||||
"[tool_call:{tool_name}] args: {args} output: generated result saved as image: {image_id}"
|
||||
)
|
||||
+9
-34
@@ -1,20 +1,16 @@
|
||||
use crate::editor_agent::editor_tools::common::EditorAgentPricedTool;
|
||||
use crate::editor_agent::utils::ImageId;
|
||||
use crate::editor_generation_config::EditorGenerationPricingConfig;
|
||||
use module_editor_agent::agent::tool::{Tool, ToolFailure, ToolFailureKind};
|
||||
use crate::agent::asset::ImageId;
|
||||
use crate::framework::tool::{Tool, ToolFailure, ToolFailureKind};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use shared_contracts::assets::EditorVideoGenerateResponse;
|
||||
use std::error::Error;
|
||||
use std::fmt::Display;
|
||||
use crate::agent::tools::context::EditorToolContext;
|
||||
|
||||
pub struct GenerateVideoTool {
|
||||
pub context: crate::editor_agent::editor_tools::common::EditorToolContext,
|
||||
pub context: EditorToolContext,
|
||||
}
|
||||
|
||||
const DEFAULT_VIDEO_MODEL: &str = "seedance2.0-fast";
|
||||
const DEFAULT_VIDEO_RESOLUTION: &str = "720p";
|
||||
const DEFAULT_VIDEO_DURATION_SECONDS: u32 = 4;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum GenerateVideoError {
|
||||
@@ -101,32 +97,11 @@ impl Tool for GenerateVideoTool {
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorAgentPricedTool for GenerateVideoTool {
|
||||
fn pricing(
|
||||
&self,
|
||||
pricing: &EditorGenerationPricingConfig,
|
||||
args: &GenerateVideoToolArgs,
|
||||
) -> u32 {
|
||||
let model = args
|
||||
.model
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(DEFAULT_VIDEO_MODEL);
|
||||
let resolution = args
|
||||
.resolution
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or(DEFAULT_VIDEO_RESOLUTION);
|
||||
let duration_seconds = args
|
||||
.duration_seconds
|
||||
.unwrap_or(DEFAULT_VIDEO_DURATION_SECONDS);
|
||||
pricing.video_model_mud_points(Some(model), resolution, duration_seconds)
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateVideoTool {
|
||||
pub const DEFAULT_VIDEO_MODEL: &'static str = "seedance2.0-fast";
|
||||
pub const DEFAULT_VIDEO_RESOLUTION: &'static str = "720p";
|
||||
pub const DEFAULT_VIDEO_DURATION_SECONDS: u32 = 4;
|
||||
|
||||
pub fn format_execute_message(
|
||||
&self,
|
||||
args: &GenerateVideoToolArgs,
|
||||
@@ -140,4 +115,4 @@ impl GenerateVideoTool {
|
||||
video_id
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-4
@@ -1,4 +1,3 @@
|
||||
pub mod common;
|
||||
pub mod edit_image;
|
||||
pub mod generate_background_music;
|
||||
pub mod generate_character;
|
||||
@@ -7,6 +6,4 @@ pub mod generate_image;
|
||||
pub mod generate_sound_effect;
|
||||
pub mod generate_ui_design;
|
||||
pub mod generate_video;
|
||||
|
||||
#[cfg(test)]
|
||||
mod pricing_tests;
|
||||
pub mod context;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user