142 lines
3.8 KiB
Go
142 lines
3.8 KiB
Go
package knowledge
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
|
||
"laodingbot/internal/memory"
|
||
)
|
||
|
||
func GenerateSkillDraft(cluster memory.CapabilityGapCluster, draftRoot string) (string, bool, error) {
|
||
draftRoot = strings.TrimSpace(draftRoot)
|
||
if draftRoot == "" {
|
||
draftRoot = "./skills"
|
||
}
|
||
if err := os.MkdirAll(draftRoot, 0o755); err != nil {
|
||
return "", false, err
|
||
}
|
||
|
||
skillDirName := "auto_" + slugFromIntent(cluster.IntentKey)
|
||
if skillDirName == "" {
|
||
skillDirName = "auto_gap_skill"
|
||
}
|
||
dir := filepath.Join(draftRoot, skillDirName)
|
||
file := filepath.Join(dir, "skill.md")
|
||
|
||
if _, err := os.Stat(file); err == nil {
|
||
return file, false, nil
|
||
}
|
||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||
return "", false, err
|
||
}
|
||
|
||
skillTitle := titleFromIntent(cluster.SampleIntent)
|
||
if skillTitle == "" {
|
||
skillTitle = "能力缺口补全技能"
|
||
}
|
||
content := buildDraftMarkdown(skillTitle, cluster)
|
||
if err := os.WriteFile(file, []byte(content), 0o644); err != nil {
|
||
return "", false, err
|
||
}
|
||
return file, true, nil
|
||
}
|
||
|
||
func buildDraftMarkdown(skillTitle string, cluster memory.CapabilityGapCluster) string {
|
||
createdAt := time.Now().Format(time.RFC3339)
|
||
return strings.TrimSpace(fmt.Sprintf(`---
|
||
name: %s
|
||
description: 由 capability_gap 自动生成并用于自动补全缺失能力。
|
||
source: capability_gap
|
||
generated_at: %s
|
||
cluster_intent_key: %s
|
||
cluster_reason: %s
|
||
cluster_count: %d
|
||
---
|
||
|
||
# Skill: %s
|
||
|
||
## 背景
|
||
- 该技能由系统根据高频能力缺口自动生成并已纳入技能目录。
|
||
- 最近高频缺口聚类:`+"`%s`"+`
|
||
- 缺口原因:`+"`%s`"+`
|
||
- 出现次数:`+"`%d`"+`
|
||
|
||
## 目标能力
|
||
- 明确该类问题应如何判断是否需要调用工具。
|
||
- 约束输入输出,避免泛化过度。
|
||
- 在失败时提供可操作回退路径。
|
||
|
||
## 建议触发信号
|
||
- 用户提问与下述意图高度相关:`+"`%s`"+`
|
||
- 现有技能未命中,或命中后无法完成。
|
||
|
||
## 建议工具
|
||
- 优先使用现有工具:`+"`shell`"+`、`+"`file`"+`。
|
||
- 若能力不足,需要创建新工具时:
|
||
1. 在 `+"`internal/tools/<tool_name>/`"+` 下生成 Go 代码;
|
||
2. 在 `+"`cmd/bot/main.go`"+` 或 toolhost 注册逻辑中完成注册;
|
||
3. 生成/补充 `+"`*_test.go`"+`;
|
||
4. 调用 `+"`go test ./...`"+` 验证。
|
||
|
||
## ReAct 指南
|
||
1. 先确认用户目标和输入约束。
|
||
2. 判断是否可直接回答;若不行,再选择工具。
|
||
3. 工具调用前先最小化探测范围。
|
||
4. 工具失败时输出原因与下一步建议。
|
||
5. 若缺少 skill:使用 `+"`file`"+` 与 `+"`shell`"+` 创建新的 `+"`skills/<skill_name>/skill.md`"+`。
|
||
6. 若缺少 tool:生成工具代码与测试后执行 `+"`go test ./...`"+`。
|
||
|
||
## 输出规范
|
||
- 结论:一句话给出当前阶段结论。
|
||
- 依据:列出关键观察与证据。
|
||
- 限制:说明当前不确定性。
|
||
- 下一步:给用户可执行动作。
|
||
`, skillTitle, createdAt, cluster.IntentKey, cluster.Reason, cluster.Count, skillTitle, cluster.IntentKey, cluster.Reason, cluster.Count, cluster.SampleIntent))
|
||
}
|
||
|
||
func slugFromIntent(intent string) string {
|
||
intent = strings.TrimSpace(strings.ToLower(intent))
|
||
if intent == "" {
|
||
return ""
|
||
}
|
||
b := strings.Builder{}
|
||
lastDash := false
|
||
for _, r := range intent {
|
||
isAlphaNum := (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9')
|
||
isCJK := r >= 0x4e00 && r <= 0x9fff
|
||
if isAlphaNum || isCJK {
|
||
b.WriteRune(r)
|
||
lastDash = false
|
||
continue
|
||
}
|
||
if !lastDash {
|
||
b.WriteRune('-')
|
||
lastDash = true
|
||
}
|
||
}
|
||
out := strings.Trim(b.String(), "-")
|
||
if out == "" {
|
||
return ""
|
||
}
|
||
runes := []rune(out)
|
||
if len(runes) > 48 {
|
||
out = string(runes[:48])
|
||
}
|
||
return out
|
||
}
|
||
|
||
func titleFromIntent(intent string) string {
|
||
intent = strings.TrimSpace(intent)
|
||
if intent == "" {
|
||
return ""
|
||
}
|
||
runes := []rune(intent)
|
||
if len(runes) > 32 {
|
||
intent = string(runes[:32])
|
||
}
|
||
return intent
|
||
}
|