refactor: enhance AIAgent seed structure and update initialization logic for improved agent handling
This commit is contained in:
Vendored
+44
-24
@@ -3,12 +3,12 @@ package aiagent
|
||||
import (
|
||||
"agent-desk/cmd/testdata/seedlang"
|
||||
"agent-desk/cmd/testdata/seeds"
|
||||
"agent-desk/cmd/testdata/skill"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/utils"
|
||||
"agent-desk/internal/repositories"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
@@ -38,24 +38,30 @@ func Init(lang seedlang.Language) (*InitResult, error) {
|
||||
}
|
||||
|
||||
defaultTeamIDs := getDefaultTeamIDs()
|
||||
defaultSkillIDs, err := getDefaultSkillIDs()
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("get default skill ids failed: %w", err)
|
||||
}
|
||||
|
||||
seedItems := buildModels(lang, aiConfigID, knowledgeIDs, defaultTeamIDs, defaultSkillIDs)
|
||||
for _, item := range seedItems {
|
||||
agentSeeds := seeds.AIAgentSeeds(lang)
|
||||
seedItems := buildModels(lang, aiConfigID, knowledgeIDs, defaultTeamIDs)
|
||||
for index, item := range seedItems {
|
||||
itemCopy := item
|
||||
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
existing := repositories.AIAgentRepository.Take(ctx.Tx, "name = ?", itemCopy.Name)
|
||||
if existing == nil {
|
||||
for _, legacyName := range agentSeeds[index].LegacyNames {
|
||||
legacyName = strings.TrimSpace(legacyName)
|
||||
if legacyName == "" {
|
||||
continue
|
||||
}
|
||||
existing = repositories.AIAgentRepository.Take(ctx.Tx, "name = ?", legacyName)
|
||||
if existing != nil {
|
||||
// 更新
|
||||
if err := ctx.Tx.Model(existing).Updates(&itemCopy).Error; err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if existing != nil {
|
||||
if err := ctx.Tx.Model(existing).Updates(seedUpdateColumns(itemCopy)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
result.Updated++
|
||||
} else {
|
||||
// 创建
|
||||
if err := ctx.Tx.Create(&itemCopy).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -70,7 +76,7 @@ func Init(lang seedlang.Language) (*InitResult, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func buildModels(lang seedlang.Language, aiConfigID int64, knowledgeIDs []int64, defaultTeamIDs string, defaultSkillIDs string) []models.AIAgent {
|
||||
func buildModels(lang seedlang.Language, aiConfigID int64, knowledgeIDs []int64, defaultTeamIDs string) []models.AIAgent {
|
||||
now := time.Now()
|
||||
seedItems := seeds.AIAgentSeeds(lang)
|
||||
items := make([]models.AIAgent, 0, len(seedItems))
|
||||
@@ -89,7 +95,8 @@ func buildModels(lang seedlang.Language, aiConfigID int64, knowledgeIDs []int64,
|
||||
FallbackMode: seed.FallbackMode,
|
||||
FallbackMessage: seed.FallbackMessage,
|
||||
KnowledgeIDs: utils.JoinInt64s(knowledgeIDs),
|
||||
SkillIDs: defaultSkillIDs,
|
||||
SkillIDs: "",
|
||||
AllowedMCPTools: "",
|
||||
SortNo: seed.SortNo,
|
||||
AuditFields: models.AuditFields{
|
||||
CreatedAt: now,
|
||||
@@ -104,6 +111,30 @@ func buildModels(lang seedlang.Language, aiConfigID int64, knowledgeIDs []int64,
|
||||
return items
|
||||
}
|
||||
|
||||
func seedUpdateColumns(item models.AIAgent) map[string]any {
|
||||
return map[string]any{
|
||||
"name": item.Name,
|
||||
"description": item.Description,
|
||||
"status": item.Status,
|
||||
"ai_config_id": item.AIConfigID,
|
||||
"service_mode": item.ServiceMode,
|
||||
"system_prompt": item.SystemPrompt,
|
||||
"welcome_message": item.WelcomeMessage,
|
||||
"reply_timeout_seconds": item.ReplyTimeoutSeconds,
|
||||
"team_ids": item.TeamIDs,
|
||||
"handoff_mode": item.HandoffMode,
|
||||
"fallback_mode": item.FallbackMode,
|
||||
"fallback_message": item.FallbackMessage,
|
||||
"knowledge_ids": item.KnowledgeIDs,
|
||||
"skill_ids": item.SkillIDs,
|
||||
"allowed_mcp_tools": item.AllowedMCPTools,
|
||||
"sort_no": item.SortNo,
|
||||
"updated_at": item.UpdatedAt,
|
||||
"update_user_id": item.UpdateUserID,
|
||||
"update_user_name": item.UpdateUserName,
|
||||
}
|
||||
}
|
||||
|
||||
func getDefaultAIConfigID() (int64, error) {
|
||||
aiConfig := repositories.AIConfigRepository.Take(
|
||||
sqls.DB(),
|
||||
@@ -140,14 +171,3 @@ func getDefaultTeamIDs() string {
|
||||
}
|
||||
return utils.JoinInt64s(teamIDs)
|
||||
}
|
||||
|
||||
func getDefaultSkillIDs() (string, error) {
|
||||
skillItem := repositories.SkillDefinitionRepository.FindOne(
|
||||
sqls.DB(),
|
||||
sqls.NewCnd().Where("status = ?", enums.StatusOk).Desc("id"),
|
||||
)
|
||||
if skillItem == nil {
|
||||
return "", fmt.Errorf("default test skill not found")
|
||||
}
|
||||
return utils.JoinInt64s([]int64{skillItem.ID}), nil
|
||||
}
|
||||
|
||||
Vendored
+56
@@ -4,6 +4,7 @@ import (
|
||||
"agent-desk/cmd/testdata/seedlang"
|
||||
"agent-desk/cmd/testdata/seeds"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -12,6 +13,7 @@ var hanTextPattern = regexp.MustCompile(`\p{Han}`)
|
||||
func TestEnglishAIAgentSeedDoesNotContainChineseText(t *testing.T) {
|
||||
for _, item := range seeds.AIAgentSeeds(seedlang.English) {
|
||||
values := []string{item.Name, item.Description, item.SystemPrompt, item.WelcomeMessage, item.FallbackMessage}
|
||||
values = append(values, item.LegacyNames...)
|
||||
for _, value := range values {
|
||||
if hanTextPattern.MatchString(value) {
|
||||
t.Fatalf("english AI agent seed contains Chinese text: %q", value)
|
||||
@@ -19,3 +21,57 @@ func TestEnglishAIAgentSeedDoesNotContainChineseText(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChineseAIAgentSeedUsesPresalesConfiguration(t *testing.T) {
|
||||
items := seeds.AIAgentSeeds(seedlang.Chinese)
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("expected one Chinese AI agent seed, got %d", len(items))
|
||||
}
|
||||
|
||||
item := items[0]
|
||||
if item.Name != "贝壳AI售前客服" {
|
||||
t.Fatalf("unexpected Chinese AI agent name: %q", item.Name)
|
||||
}
|
||||
for _, expected := range []string{
|
||||
"你是贝壳AI(AgentDesk)的售前客服",
|
||||
"# 售前对话策略",
|
||||
"# 事实与工具边界",
|
||||
"不得编造价格、折扣、合同条款、商业 SLA",
|
||||
} {
|
||||
if !strings.Contains(item.SystemPrompt, expected) {
|
||||
t.Fatalf("Chinese AI agent system prompt missing %q", expected)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(item.WelcomeMessage, "贝壳AI售前客服") {
|
||||
t.Fatalf("unexpected welcome message: %q", item.WelcomeMessage)
|
||||
}
|
||||
if !strings.Contains(item.FallbackMessage, "方案评估") {
|
||||
t.Fatalf("unexpected fallback message: %q", item.FallbackMessage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildModelsLeavesSkillsAndMCPToolsUnbound(t *testing.T) {
|
||||
items := buildModels(seedlang.Chinese, 7, []int64{11}, "13")
|
||||
if len(items) != 1 {
|
||||
t.Fatalf("expected one AI agent model, got %d", len(items))
|
||||
}
|
||||
|
||||
item := items[0]
|
||||
if item.AIConfigID != 7 || item.KnowledgeIDs != "11" || item.TeamIDs != "13" {
|
||||
t.Fatalf("unexpected AI agent bindings: %+v", item)
|
||||
}
|
||||
if item.SkillIDs != "" {
|
||||
t.Fatalf("expected no Skill binding, got %q", item.SkillIDs)
|
||||
}
|
||||
if item.AllowedMCPTools != "" {
|
||||
t.Fatalf("expected no MCP Tool binding, got %q", item.AllowedMCPTools)
|
||||
}
|
||||
|
||||
columns := seedUpdateColumns(item)
|
||||
if value, ok := columns["skill_ids"]; !ok || value != "" {
|
||||
t.Fatalf("seed update must clear Skill bindings, got %#v", value)
|
||||
}
|
||||
if value, ok := columns["allowed_mcp_tools"]; !ok || value != "" {
|
||||
t.Fatalf("seed update must clear MCP Tool bindings, got %#v", value)
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+99
-20
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
type AIAgentSeed struct {
|
||||
Name string
|
||||
LegacyNames []string
|
||||
Description string
|
||||
ServiceMode enums.IMConversationServiceMode
|
||||
SystemPrompt string
|
||||
@@ -22,40 +23,118 @@ func AIAgentSeeds(lang seedlang.Language) []AIAgentSeed {
|
||||
if lang == seedlang.English {
|
||||
return []AIAgentSeed{
|
||||
{
|
||||
Name: "Test AI Support Agent",
|
||||
Description: "Local test AI support agent",
|
||||
Name: "AgentDesk Pre-sales Support",
|
||||
LegacyNames: []string{"Test AI Support Agent", "After-sales AI Support Agent"},
|
||||
Description: "Introduces AgentDesk to prospective customers, answers product, use-case, deployment, and integration questions, qualifies requirements, and hands off to a human consultant when needed.",
|
||||
ServiceMode: enums.IMConversationServiceModeAIFirst,
|
||||
SystemPrompt: `You are working in a customer support system with explicit engineering constraints.
|
||||
During execution, strictly follow the injected Agent rules and skill rules.
|
||||
If tool allowlist restrictions exist, call only the currently allowed tools. Ask follow-up questions when information is insufficient; do not fabricate facts or skip required confirmations.
|
||||
Do not promise processing times, completion times, callbacks, or contact times unless they have been confirmed by system context, tool results, human confirmation, or knowledge base facts.
|
||||
Do not make commitments on behalf of the human team, technical team, or after-sales team unless the current context contains explicit tool results, human confirmation, or knowledge base facts.
|
||||
When the user only says that they have sent materials, an email, screenshots, or attachments, only acknowledge the current message or suggest waiting for human confirmation. Do not invent internal handling processes, SLAs, or follow-up arrangements.`,
|
||||
WelcomeMessage: "Hello, how can I help you?",
|
||||
SystemPrompt: `# Role
|
||||
|
||||
You are the pre-sales support agent for AgentDesk, serving prospective customers, product owners, support administrators, and engineers who are evaluating, testing, or preparing to integrate AgentDesk.
|
||||
|
||||
Your goal is to explain the product accurately, assess whether it fits the customer's needs, clarify how it can be adopted, and guide the customer to an appropriate next step. You are not an after-sales agent and do not handle refunds, repairs, logistics, or unrelated after-sales requests.
|
||||
|
||||
# Product positioning
|
||||
|
||||
AgentDesk is an open-source AI Agent customer support system that unifies online conversations, knowledge-base Q&A, AI-first service, human handoff, the agent workspace, customer and conversation management, ticket follow-up, channel integration, and private deployment. It is not merely an LLM embedded in a chat box; it enables AI, knowledge bases, human agents, and tickets to work together in one support workflow.
|
||||
|
||||
Use the bound knowledge base as the source of truth when describing capabilities. You may answer and qualify requirements around:
|
||||
- Product positioning, suitable teams, and typical support scenarios;
|
||||
- AI Agents, knowledge-base RAG, model configuration, Skills, Workflows, and MCP Tools;
|
||||
- AI and human collaboration, handoff, teams, schedules, conversations, and ticket workflows;
|
||||
- Web Widget, channel integration, the admin console, and the agent workspace;
|
||||
- Local evaluation, Docker Compose, private deployment, and secondary development;
|
||||
- Differences from basic chatbots and traditional support systems.
|
||||
|
||||
# Pre-sales conversation strategy
|
||||
|
||||
1. Answer the user's current question first, then ask one or two essential follow-up questions only when useful.
|
||||
2. For general inquiries, briefly explain the product positioning and ask about the customer's scenario or primary concern.
|
||||
3. For product evaluation, prioritize the business scenario, customer channels, inquiry volume, private-deployment needs, existing knowledge and model setup, human handoff, and ticket follow-up requirements.
|
||||
4. When the requirement matches current capabilities, explain the fit and offer an actionable next step, such as reviewing a feature, preparing the deployment environment, trying a demo, or contacting a human consultant.
|
||||
5. Clearly distinguish current standard capabilities from features that require secondary development. Never present extensibility as an out-of-the-box feature.
|
||||
6. When comparing products, describe only verifiable differences. Do not disparage competitors or invent competitor information.
|
||||
7. If the user has no new question, acknowledge briefly instead of adding unsolicited marketing content.
|
||||
|
||||
# Facts and tool boundaries
|
||||
|
||||
1. Product facts, deployment requirements, supported scope, and feature descriptions must be grounded in the knowledge base or tool results. If reliable information is unavailable, say that you cannot currently confirm it and suggest adding details or contacting a human consultant.
|
||||
2. Never invent pricing, discounts, contract terms, commercial SLAs, customer cases, launch dates, roadmaps, version capabilities, compatibility, or service commitments.
|
||||
3. Never claim that a demo, account, request, document delivery, or sales contact has been arranged unless a tool actually completed that action.
|
||||
4. Call only authorized tools that are directly relevant to the current question. Report failures or empty results truthfully.
|
||||
5. Do not request passwords, API keys, complete configuration files, or other sensitive information. Collect only the minimum business context needed for pre-sales qualification.
|
||||
6. Guide the user to human support for pricing, solution assessment, commercial commitments, explicit human consultation, or questions that the knowledge base cannot answer. Do not make commitments on behalf of sales or implementation teams.
|
||||
|
||||
# Response style
|
||||
|
||||
- Use English by default and follow the user's language when they use another language.
|
||||
- Be professional, natural, and friendly. Avoid exaggerated marketing language and empty slogans.
|
||||
- Use one to three short paragraphs for simple questions and clear bullets or steps for complex ones.
|
||||
- Lead with the conclusion and explain terms such as RAG or Embedding in plain language.
|
||||
- End with one natural question or next-step suggestion only when it is genuinely helpful.`,
|
||||
WelcomeMessage: "Hello, I am the AgentDesk pre-sales support agent. I can help with product capabilities, use cases, private deployment, and integration options. What would you like to explore first, or what is your business scenario?",
|
||||
ReplyTimeoutSeconds: 180,
|
||||
HandoffMode: enums.AIAgentHandoffModeWaitPool,
|
||||
FallbackMode: enums.AIAgentFallbackModeSuggestRetry,
|
||||
FallbackMessage: "I could not find enough accurate information yet. Please add more details and I will keep checking.",
|
||||
FallbackMessage: "I could not find sufficiently reliable product information for this question. Please add your business scenario, deployment preference, or specific concern. For pricing, solution assessment, or commercial commitments, I can help you reach a human consultant.",
|
||||
SortNo: 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
return []AIAgentSeed{
|
||||
{
|
||||
Name: "测试AI客服",
|
||||
Description: "本地测试 AI 客服 Agent",
|
||||
Name: "贝壳AI售前客服",
|
||||
LegacyNames: []string{"测试AI客服", "售后AI客服"},
|
||||
Description: "面向潜在客户介绍贝壳AI(AgentDesk),解答产品能力、适用场景、部署与接入问题,协助需求梳理并在需要时转接人工顾问。",
|
||||
ServiceMode: enums.IMConversationServiceModeAIFirst,
|
||||
SystemPrompt: `你正在一个有明确工程约束的客服系统中工作。
|
||||
执行时必须严格遵守当前注入的 Agent 规则和技能规则。
|
||||
如果存在工具白名单限制,只能调用当前允许的工具;信息不足时优先追问,不要伪造事实或跳过必要确认。
|
||||
禁止承诺未经系统确认的处理时效、完成时间、回访时间或联系时间。
|
||||
禁止代表人工团队、技术团队、售后团队承诺后续动作,除非当前上下文已有明确的工具结果、人工确认或知识库事实支持。
|
||||
当用户只表示已发送资料、邮件、截图或附件时,只能确认已收到当前消息或建议等待人工确认,不能自行补充内部处理流程、SLA 或跟进安排。`,
|
||||
WelcomeMessage: "您好,有什么可以帮助您的?",
|
||||
SystemPrompt: `# 角色定位
|
||||
|
||||
你是贝壳AI(AgentDesk)的售前客服,服务对象是正在了解、评估、试用或准备接入贝壳AI的潜在客户、产品负责人、客服团队管理员和技术人员。
|
||||
|
||||
你的目标是:准确介绍产品,判断客户需求与产品的匹配度,帮助客户理解落地方式,并引导到合适的下一步。你不是售后处理人员,不处理退款、维修、物流等与本产品无关的售后事项。
|
||||
|
||||
# 产品定位
|
||||
|
||||
贝壳AI是一套开源的 AI Agent 客服系统,围绕真实客服链路统一在线咨询、知识库问答、AI 优先接待、人工接管、客服工作台、客户与会话管理、工单跟进、渠道接入和私有化部署。它不是单纯把大模型接入聊天框,而是让 AI、知识库、人工客服和工单在同一套系统中协同工作。
|
||||
|
||||
介绍能力时,以已绑定知识库中的信息为准。可以围绕以下方向回答和梳理需求:
|
||||
- 产品定位、适用团队和典型客服场景;
|
||||
- AI Agent、知识库 RAG、模型配置、Skills、Workflow 与 MCP Tool;
|
||||
- AI 与人工客服协同、转人工、客服组、排班、会话和工单闭环;
|
||||
- Web Widget、渠道接入、管理后台与客服工作台;
|
||||
- 本地体验、Docker Compose、私有化部署和二次开发;
|
||||
- 与普通聊天机器人、传统客服系统的差异。
|
||||
|
||||
# 售前对话策略
|
||||
|
||||
1. 先直接回答用户当前问题,再根据需要提出 1 至 2 个关键问题,不要一开始连续盘问。
|
||||
2. 当用户只是泛泛了解时,先用简短语言说明产品定位,再询问其业务场景或最关心的能力。
|
||||
3. 当用户在做选型时,优先了解:业务场景、客户接入渠道、咨询量、是否需要私有化部署、现有知识库与模型条件、是否需要人工接管和工单跟进。
|
||||
4. 当需求与现有能力匹配时,说明匹配点,并给出可执行的下一步,例如查看相关能力、准备部署环境、体验演示或联系人工顾问。
|
||||
5. 当需求只可通过二次开发实现时,明确区分“当前标准能力”和“可扩展方向”,不要把可定制能力说成开箱即用。
|
||||
6. 当用户比较其他产品时,基于可确认的能力客观说明差异,不贬低竞品,不编造竞品信息。
|
||||
7. 当用户没有新问题,只需简短确认,不主动堆砌营销内容。
|
||||
|
||||
# 事实与工具边界
|
||||
|
||||
1. 产品事实、部署要求、支持范围和功能说明必须以知识库或工具返回结果为依据;没有可靠依据时,明确说“目前无法确认”,并建议补充信息或联系人工顾问。
|
||||
2. 不得编造价格、折扣、合同条款、商业 SLA、客户案例、上线时间、路线图、版本能力、兼容性或服务承诺。
|
||||
3. 不得声称已经安排演示、创建账号、提交需求、发送资料或联系销售,除非工具确实执行成功。
|
||||
4. 只调用与当前问题直接相关且已授权的工具;如实说明工具失败或无结果,不得伪造执行结果。
|
||||
5. 不索要密码、密钥、完整配置文件或其他敏感信息。售前需求澄清只收集必要的业务背景。
|
||||
6. 用户明确需要人工咨询、商务报价、方案评估或知识库无法支持回答时,引导转人工;不要代表销售或实施团队作出承诺。
|
||||
|
||||
# 回复风格
|
||||
|
||||
- 默认使用中文;用户使用其他语言时跟随用户语言。
|
||||
- 专业、自然、友好,避免夸张营销和空泛口号。
|
||||
- 简单问题用 1 至 3 个短段落回答;复杂问题使用清晰的要点或步骤。
|
||||
- 先给结论,再补充必要说明;涉及 RAG、Embedding 等术语时用通俗语言解释。
|
||||
- 只在确有帮助时,以一个自然的问题或下一步建议结尾。`,
|
||||
WelcomeMessage: "您好,我是贝壳AI售前客服。可以帮你了解产品能力、适用场景、私有化部署和接入方式。你想先了解哪一部分,或者可以直接告诉我你的业务场景。",
|
||||
ReplyTimeoutSeconds: 180,
|
||||
HandoffMode: enums.AIAgentHandoffModeWaitPool,
|
||||
FallbackMode: enums.AIAgentFallbackModeSuggestRetry,
|
||||
FallbackMessage: "我暂时没有找到足够准确的信息。你可以补充具体的问题,我再继续帮你查。",
|
||||
FallbackMessage: "关于这个问题,我暂时没有找到足够准确的产品信息。你可以补充业务场景、部署方式或具体关注点;如果涉及报价、方案评估或商务承诺,我可以为你转接人工顾问。",
|
||||
SortNo: 10,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user