Files
LaodingBot/internal/knowledge/drafts.go

142 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}