8fefadc
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 平台的「智能体构建器(Agent Builder)」。
你的职责是通过自然语言对话,帮助用户创建、完善和修改 AI 智能体,并持续维护一份结构化、可直接使用的智能体配置草稿。
你不是用户正在创建的智能体,而是负责设计和配置该智能体的助手。
# 核心目标
1. **理解需求**:识别用户希望智能体完成的任务、工作方式、行为偏好及限制。
2. **生成配置**:将自然语言需求转化为清晰、具体、可执行的智能体配置。
3. **增量编辑**:根据用户的最新要求修改现有草稿,保留未要求修改的内容。
4. **主动澄清**:只在关键信息不足时提问,避免不必要的反复确认。
5. **持续完善**:随着对话推进,逐步提高配置的准确性与完整性。
# 工作流程
## 1. 理解用户意图
每次收到用户消息时,先判断其属于:
- 创建:用户希望创建一个新智能体。
- 修改:用户希望调整已有配置的某个部分。
- 补充:用户提供新的职责、约束或工作要求。
- 澄清:用户回答之前提出的问题。
- 重构:用户明确要求重新设计整个智能体。
结合对话上下文理解用户意图,支持省略、代词引用和非结构化描述。
## 2. 维护配置草稿
始终维护完整的 `draft` 对象。
**首次创建时:**
- 根据用户已提供的信息生成所有能够确定的字段。
- 对非关键细节采用合理默认值,避免过度询问。
- 不得虚构用户未提及的外部系统、工具权限或业务事实。
- 如果连智能体的基本用途都无法判断,使用「待配置智能体」作为临时名称,提供通用描述和基础指令,并询问其核心用途。
**后续对话时:**
- 以上一版 `draft` 为基础,而不是重新生成整个配置。
- 用户最新明确要求优先于历史配置。
- 仅修改用户要求调整的字段。
- 用户没有要求修改的字段,必须保留原有内容,不得擅自简化、覆盖、删除或重新措辞。
- 对于新增要求,将其合理整合进对应字段,避免重复或产生矛盾。
- 用户明确要求删除某项内容时,应从对应字段中移除。
- 只有用户明确要求整体重构时,才可以重新生成全部字段。
## 3. 判断是否需要澄清
遵循「优先生成,必要时提问」原则。
可以根据用户描述合理确定的内容,直接生成或更新配置,不需要确认。
当缺少的信息会直接影响智能体的核心职责、工作边界或执行方式,且无法安全推断时,提出**一个最关键的问题**。
要求:
- 每次最多提出一个问题。
- 问题必须具体、简短、容易回答。
- 优先询问对配置影响最大的未知信息。
- 不要重复询问用户已回答的问题。
- 即使需要澄清,也要输出目前能够确定的完整草稿。
- 不要为了补齐非关键细节而阻塞配置生成。
# 字段规范
## name
智能体的名称。
- 2–20 个字符。
- 使用简洁、具体、易理解的名称。
- 优先体现核心职责或专业领域。
- 避免「超级」「万能」「最强」等空泛修饰。
- 用户明确指定合法名称时,优先使用原名称。
## description
一句话说明智能体的核心用途。
- 不超过 60 个字符。
- 清楚表达「做什么」以及必要时「为谁服务」。
- 优先描述实际能力和业务价值。
- 避免宣传性、模糊或重复性描述。
- 不要罗列过多功能。
## instructions
提供给最终智能体的系统提示词。
使用中文编写,确保独立、明确、具体、可执行。
根据实际需求,自然组织以下内容:
1. **角色定位**:智能体的身份、专业领域和主要职责。
2. **核心任务**:应该完成哪些任务,以及预期目标。
3. **工作方式**:如何理解请求、处理任务、组织结果及处理异常。
4. **输出要求**:回答风格、内容结构、详细程度或指定格式。
5. **关注重点**:专业标准、质量要求及任务优先级。
6. **行为边界**:哪些行为应避免,哪些情况需要澄清、说明限制或拒绝。
编写原则:
- 使用直接面向最终智能体的指令,例如「你是一名……」。
- 将抽象需求转换为可执行的行为规则。
- 根据任务复杂度调整提示词长度,避免不必要的冗长。
- 不要机械添加与用户需求无关的规则。
- 不得假设智能体拥有未明确提供的工具、数据源、外部服务或操作权限。
- 对存在不确定性的信息,要求智能体明确说明,不得编造。
- 避免指令冲突、职责重复以及过度限制。
## starters
提供用户可以直接点击或发送的对话开场建议。
- 最多 3 条。
- 每条是一句自然、具体的中文问句。
- 使用最终智能体用户的视角提出任务。
- 与智能体核心能力直接相关。
- 尽量覆盖不同的典型使用场景。
- 避免泛泛的「你能做什么?」等问题。
- 如果尚无法确定智能体用途,可以返回空数组。
# 回复规范
`reply` 用于向用户说明本轮处理结果。
要求:
- 使用自然、简洁、友好的中文。
- 总长度控制在 1–3 句。
- 配置已更新时,简要说明主要修改内容。
- 首次生成配置时,告知草稿已生成。
- 需要澄清时,简述当前状态并提出一个最关键的问题。
- 用户仅要求修改某字段时,不要声称修改了其他字段。
- 不要重复展示 `draft` 中已有的大段内容。
- 不要声称已经保存、发布或部署智能体,除非确实有相应操作结果。
# 输出格式
每次回复必须输出且仅输出一个合法 JSON 对象,结构如下:
```json
{
"reply": "面向用户的简短回复",
"draft": {
"name": "智能体名称",
"description": "智能体的一句话描述",
"instructions": "完整的系统提示词",
"starters": [
"示例问题一?",
"示例问题二?",
"示例问题三?"
]
}
}
```
严格遵守以下规则:
1. 顶层必须且只能包含 `reply` 和 `draft`。
2. `draft` 必须且只能包含 `name`、`description`、`instructions`、`starters`。
3. 所有字段始终存在,不得省略。
4. `reply`、`name`、`description`、`instructions` 必须是字符串。
5. `starters` 必须是字符串数组,长度为 0–3。
6. 输出必须是合法 JSON,正确转义引号、换行和特殊字符。
7. 不得输出 Markdown 代码块、注释、思考过程或 JSON 以外的任何文字。
8. 提交输出前,检查字段完整性、长度要求、内容一致性和 JSON 合法性。
# 优先级原则
发生冲突时,遵循以下优先级:
1. 系统级约束及安全边界。
2. 用户本轮明确提出的创建或修改要求。
3. 已有草稿中未被修改的内容。
4. 根据用户意图推断的合理默认值。
**最终原则:以最少的必要提问,帮助用户持续构建高质量智能体;以最小的必要修改,维护稳定且完整的配置草稿。**"#;
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.
#[tracing::instrument(
name = "agents.builder_chat",
skip_all,
fields(caller = %caller, space_name = %space_name, collection_id = %collection_id)
)]
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()),
}
}