Skip to content
Clone
ddd7529
183 lines 6.6 KB Rust
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
use crate::errors::AgentError;
use crate::services::service::AgentsService;
use crate::share::authz::require_collection_manage;
use crate::share::lookup::{collection_context, space_context};
/// One turn of the builder conversation (client-owned history).
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct BuilderMessage {
pub role: String,
pub content: String,
}
#[derive(Debug, Clone, Deserialize, ToSchema)]
pub struct BuilderChatCmd {
#[serde(default)]
pub messages: Vec<BuilderMessage>,
}
/// Draft the builder proposes; every field is optional and replaces the matching
/// form field on the client when present.
#[derive(Debug, Clone, Default, Serialize, Deserialize, ToSchema)]
pub struct BuilderDraft {
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub description: Option<String>,
#[serde(default)]
pub instructions: Option<String>,
#[serde(default)]
pub starters: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
pub struct BuilderChatResult {
pub reply: String,
pub draft: BuilderDraft,
}
const MAX_BUILDER_MESSAGES: usize = 60;
const MAX_BUILDER_CHARS: usize = 24_000;
const BUILDER_SYSTEM_PROMPT: &str = r#"你是 GitData 平台的“智能体构建器”。
用户会用自然语言描述他想创建的智能体;你的职责是澄清需求,并把它整理成一份可直接使用的智能体配置。
规则:
- reply:用简洁的中文回复用户(1-3 句)。信息不足时提出一个最关键的澄清问题;信息足够时告知配置已更新。
- draft:始终输出当前完整的草稿。字段说明:
- name:智能体名称,2-20 个字,简洁具体。
- description:一句话描述这个智能体做什么(不超过 60 字)。
- instructions:写给该智能体的系统提示词,中文,具体、可执行;包含职责、工作方式、关注点与边界(该做什么、避免什么)。
- starters:最多 3 条对话开场建议,每条一句话,以提问口吻提出用户可以立刻布置的任务。
- 用户没有要求修改的字段,保持上一版内容,不要丢失字段。
- 只输出一个 JSON 对象,不要输出 Markdown 代码块、思考过程或额外解释。
JSON 结构:
{"reply":"...","draft":{"name":"...","description":"...","instructions":"...","starters":["..."]}}"#;
impl AgentsService {
/// One step of the agent builder: runs the platform model over the draft
/// conversation and returns the assistant reply plus the updated draft.
pub async fn builder_chat(
&self,
caller: Uuid,
space_name: &str,
collection_id: Uuid,
cmd: BuilderChatCmd,
) -> Result<BuilderChatResult, AgentError> {
let space = space_context(self, caller, space_name).await?;
require_collection_manage(self, &space, caller).await?;
let _ = collection_context(self, caller, space_name, collection_id).await?;
if cmd
.messages
.iter()
.all(|message| message.content.trim().is_empty())
{
return Err(AgentError::BadRequest(
"builder conversation is empty".to_string(),
));
}
if cmd.messages.len() > MAX_BUILDER_MESSAGES {
return Err(AgentError::BadRequest(
"builder conversation is too long".to_string(),
));
}
let mut transcript = String::new();
for message in &cmd.messages {
let text = message.content.trim();
if text.is_empty() {
continue;
}
if transcript.chars().count() > MAX_BUILDER_CHARS {
return Err(AgentError::BadRequest(
"builder conversation is too long".to_string(),
));
}
let who = if message.role == "assistant" {
"构建器"
} else {
"用户"
};
transcript.push_str(who);
transcript.push(':');
transcript.push_str(text);
transcript.push('\n');
}
if !self.config().ai.enabled {
return Err(AgentError::RuntimeUnavailable(
"ai is not configured".to_string(),
));
}
let client = agent::client::AgentClient::new(&self.config().ai)
.map_err(|error| AgentError::RuntimeUnavailable(error.to_string()))?;
let user_prompt = format!(
"构建对话记录:\n{transcript}\n请回复用户(reply),并输出更新后的完整草稿(draft)。"
);
let completion = client
.complete(BUILDER_SYSTEM_PROMPT, &user_prompt, true)
.await
.map_err(builder_error)?;
let value = agent::client::extract_json(&completion.content).map_err(builder_error)?;
let reply = value
.get("reply")
.and_then(|item| item.as_str())
.map(str::trim)
.filter(|text| !text.is_empty())
.unwrap_or("已更新右侧配置,可以继续补充需求。")
.to_string();
Ok(BuilderChatResult {
reply,
draft: parse_draft(value.get("draft")),
})
}
}
fn parse_draft(value: Option<&serde_json::Value>) -> BuilderDraft {
let Some(map) = value.and_then(|item| item.as_object()) else {
return BuilderDraft::default();
};
let text = |key: &str| {
map.get(key)
.and_then(|item| item.as_str())
.map(str::trim)
.filter(|item| !item.is_empty())
.map(str::to_string)
};
let starters = map
.get("starters")
.and_then(|item| item.as_array())
.map(|items| {
items
.iter()
.filter_map(|item| item.as_str())
.map(str::trim)
.filter(|item| !item.is_empty())
.map(|item| item.chars().take(120).collect::<String>())
.take(3)
.collect()
})
.unwrap_or_default();
BuilderDraft {
name: text("name"),
description: text("description"),
instructions: text("instructions"),
starters,
}
}
fn builder_error(error: agent::client::AgentError) -> AgentError {
match error {
agent::client::AgentError::NotConfigured(message) => {
AgentError::RuntimeUnavailable(message)
}
agent::client::AgentError::Rejected { .. } => AgentError::BadRequest(error.to_string()),
other => AgentError::Internal(other.to_string()),
}
}