mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 09:58:06 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fddefc78a1 | |||
| 3f431063e2 | |||
| 18d7ad3a36 |
@ -1,32 +0,0 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
|
||||
sdkmeta "gitcode.com/JianFeeeee/homeagent-sdk/meta"
|
||||
)
|
||||
|
||||
func TestExpandPromptVars(t *testing.T) {
|
||||
got := expandPromptVars("型号 {{kernel_version}}({{kernel_commit}}),SDK {{sdk_version}}")
|
||||
if !strings.Contains(got, meta.Version) || !strings.Contains(got, sdkmeta.Version) {
|
||||
t.Fatalf("占位符未展开: %q", got)
|
||||
}
|
||||
if strings.Contains(got, "{{") {
|
||||
t.Fatalf("仍有未展开的内置占位符: %q", got)
|
||||
}
|
||||
// 人格卡实测原文:写死了 v1.0.3,应能被占位符取代
|
||||
live := expandPromptVars("你是 HomeAgent 的看板娘「小宅」(Xiao Zhai),HΔ-Kernel v{{kernel_version}} 型号的家政型 AI 管家助手。")
|
||||
if strings.Contains(live, "1.0.3") || !strings.Contains(live, "v"+meta.Version) {
|
||||
t.Fatalf("人格卡版本未跟随内核: %q", live)
|
||||
}
|
||||
// 未知占位符必须原样保留(写错要看得见,不能被静默吞掉)
|
||||
if unk := expandPromptVars("版本 {{kernel_verison}}"); !strings.Contains(unk, "{{kernel_verison}}") {
|
||||
t.Fatalf("未知占位符被吞: %q", unk)
|
||||
}
|
||||
// 无占位符时原样返回(人格卡热路径,不做无谓拷贝)
|
||||
if plain := "无占位符"; expandPromptVars(plain) != plain {
|
||||
t.Fatal("无占位符时不应改写")
|
||||
}
|
||||
}
|
||||
@ -5,8 +5,6 @@ import (
|
||||
"strings"
|
||||
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/meta"
|
||||
sdkmeta "gitcode.com/JianFeeeee/homeagent-sdk/meta"
|
||||
)
|
||||
|
||||
func (a *Agent) buildMemoryContext(input string, maxTokens int) string {
|
||||
@ -39,30 +37,8 @@ func (a *Agent) buildMemoryContext(input string, maxTokens int) string {
|
||||
return s
|
||||
}
|
||||
|
||||
// expandPromptVars 展开自定义提示词(人格卡)里的版本占位符。
|
||||
//
|
||||
// 为什么需要:人格卡是**配置项**,一旦写死版本号就会随内核发版而说谎 ——
|
||||
// 实测线上人格卡写着 "HΔ-Kernel v1.0.3 型号",内核早已 1.3.x,agent 向用户
|
||||
// 自报版本时就照抄 1.0.3。占位符让这类文本永远跟随真实构建:
|
||||
//
|
||||
// {{kernel_version}} → 内核版本(如 1.3.5)
|
||||
// {{kernel_commit}} → 构建 commit
|
||||
// {{sdk_version}} → 所兼容的 SDK 版本(如 1.3.0)
|
||||
//
|
||||
// 未知占位符**原样保留**:写错了要看得见,而不是被静默换成空串。
|
||||
func expandPromptVars(s string) string {
|
||||
if !strings.Contains(s, "{{") {
|
||||
return s
|
||||
}
|
||||
return strings.NewReplacer(
|
||||
"{{kernel_version}}", meta.Version,
|
||||
"{{kernel_commit}}", meta.Commit,
|
||||
"{{sdk_version}}", sdkmeta.Version,
|
||||
).Replace(s)
|
||||
}
|
||||
|
||||
func (a *Agent) buildSystemPrompt(memContext string, userInput string) string {
|
||||
prompt := expandPromptVars(a.systemPrompt)
|
||||
prompt := a.systemPrompt
|
||||
if prompt == "" {
|
||||
prompt = "你是小宅,HomeAgent 的看板娘,一个家政型 AI 管家助手。绝不用 Unicode emoji,只用颜文字表达情感,句尾带语气词。WebUI 概览页展示你的立绘。"
|
||||
}
|
||||
|
||||
@ -742,7 +742,7 @@ func (r *ConfigRegistry) seedCoreDefs(dataDir string) {
|
||||
reg(ConfigDef{Key: "core.agent.workdir", Default: "", Type: "string", DisplayName: "工作目录", Description: "Agent 命令执行的默认工作目录(如 cmd_run 工具的 fallback),留空使用内核所在目录", Category: "agent"})
|
||||
reg(ConfigDef{Key: "core.agent.embedding_model_path", Default: "", Type: "string", DisplayName: "预训练词嵌入模型路径", Description: "预训练词嵌入模型路径(word2vec 文本格式),支持逗号分隔多个模型。路径后可加 #topN 规格只加载前 N 个词向量(如 /data/cc.zh.300.vec#top50000)以控制常驻内存,词频降序命中覆盖绝大部分文本。空则使用 TF-IDF 回退。修改后需重启生效。", Category: "agent"})
|
||||
reg(ConfigDef{Key: "core.agent.onnx_model_path", Default: "", Type: "string", DisplayName: "ONNX 模型路径", Description: "依存句法分析 ONNX 模型文件路径。留空使用二进制内嵌模型/规则引擎。修改后需重启生效。", Category: "agent"})
|
||||
reg(ConfigDef{Key: "core.agent.system_prompt", Default: "", Type: "text", DisplayName: "系统身份提示词", Description: "Agent 的系统提示词,定义身份和行为规则。留空则使用编译时内置默认值。支持版本占位符(随构建实时展开,避免写死版本号随发版说谎):{{kernel_version}}、{{kernel_commit}}、{{sdk_version}}。修改后需重启生效。", Category: "agent"})
|
||||
reg(ConfigDef{Key: "core.agent.system_prompt", Default: "", Type: "text", DisplayName: "系统身份提示词", Description: "Agent 的系统提示词,定义身份和行为规则。留空则使用编译时内置默认值。修改后需重启生效。", Category: "agent"})
|
||||
|
||||
reg(ConfigDef{Key: "core.input_processing.image.fallback_provider", Default: "", Type: "string", DisplayName: "图片回退提供商", Description: "当主 LLM 不支持图片处理时使用的提供商(留空则自动降级为文字描述)", Category: "input"})
|
||||
reg(ConfigDef{Key: "core.input_processing.image.fallback_model", Default: "", Type: "string", DisplayName: "图片回退模型", Description: "图片回退提供商使用的模型名", Category: "input"})
|
||||
|
||||
@ -34,15 +34,11 @@ type StaticEmbedder struct {
|
||||
jieba *gojieba.Jieba
|
||||
stopWords map[string]bool
|
||||
|
||||
// words 是词向量表。**用 float32 存**:源文件(fastText 文本格式)本身就是 float32,
|
||||
// 用 float64 存等于把 578 万……不,是 57.8 万词 × 300 维的常驻内存凭空翻倍
|
||||
// (实测生产:float64 → 1.29GB,float32 → 0.65GB)。相似度计算仍在 float64 里累加,
|
||||
// 精度不受影响。改回 float64 会被 TestStaticEmbedder_VectorMemIsFloat32 拦住。
|
||||
words map[string][]float32
|
||||
words map[string][]float64
|
||||
dim int
|
||||
loaded bool
|
||||
|
||||
unkVec []float32
|
||||
unkVec []float64
|
||||
unkNorm float64
|
||||
}
|
||||
|
||||
@ -154,7 +150,7 @@ func NewStaticEmbedder(modelPaths ...string) *StaticEmbedder {
|
||||
e := &StaticEmbedder{
|
||||
jieba: GetJieba(),
|
||||
stopWords: sw,
|
||||
words: make(map[string][]float32),
|
||||
words: make(map[string][]float64),
|
||||
}
|
||||
|
||||
if len(modelPaths) == 0 {
|
||||
@ -258,16 +254,15 @@ func (e *StaticEmbedder) load(spec string, primary bool) error {
|
||||
continue
|
||||
}
|
||||
|
||||
vec := make([]float32, dim)
|
||||
vec := make([]float64, dim)
|
||||
for i := 0; i < dim; i++ {
|
||||
// 源文件是 float32 精度的文本向量:用 32 位解析,与源数据一致。
|
||||
v, _ := strconv.ParseFloat(fields[i+1], 32)
|
||||
vec[i] = float32(v)
|
||||
v, _ := strconv.ParseFloat(fields[i+1], 64)
|
||||
vec[i] = v
|
||||
}
|
||||
e.words[word] = vec
|
||||
if primary {
|
||||
for i := range vecSum {
|
||||
vecSum[i] += float64(vec[i])
|
||||
vecSum[i] += vec[i]
|
||||
}
|
||||
count++
|
||||
}
|
||||
@ -281,13 +276,11 @@ func (e *StaticEmbedder) load(spec string, primary bool) error {
|
||||
for i := range vecSum {
|
||||
vecSum[i] /= float64(count)
|
||||
}
|
||||
e.unkVec = make([]float32, dim)
|
||||
for i, v := range vecSum {
|
||||
e.unkVec[i] = float32(v)
|
||||
}
|
||||
e.unkVec = make([]float64, dim)
|
||||
copy(e.unkVec, vecSum)
|
||||
var normSq float64
|
||||
for _, v := range e.unkVec {
|
||||
normSq += float64(v) * float64(v)
|
||||
normSq += v * v
|
||||
}
|
||||
e.unkNorm = float64(math.Sqrt(normSq))
|
||||
e.loaded = true
|
||||
@ -373,11 +366,11 @@ func (e *StaticEmbedder) Vectorize(text string) vector.Vector {
|
||||
|
||||
if !ok {
|
||||
for i, v := range unkVec {
|
||||
sum[i] += w * float64(v)
|
||||
sum[i] += w * v
|
||||
}
|
||||
} else {
|
||||
for i, v := range vec {
|
||||
sum[i] += w * float64(v)
|
||||
sum[i] += w * v
|
||||
}
|
||||
}
|
||||
weightSum += w
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// 词向量必须用 float32 存。
|
||||
//
|
||||
// 这条判据是拿生产内存换来的:向量本体 = 词数 × 维数 × 每元素字节数。
|
||||
// 生产配置加载了 200000(zh) + 378151(en) = 57.8 万词 × 300 维 ⇒
|
||||
// float64 = 1.29GB、float32 = 0.65GB(差 0.65GB 常驻)。
|
||||
// 源数据(fastText 文本格式)本身就是 float32 精度,用 float64 存没有任何收益。
|
||||
//
|
||||
// 若有人把类型改回 float64,本测试**编译失败**(`var vec []float32` 的类型断言),
|
||||
// 这正是想要的效果。
|
||||
func TestStaticEmbedder_VectorMemIsFloat32(t *testing.T) {
|
||||
e := newSynthEmbedder(t, 300)
|
||||
|
||||
words := 0
|
||||
bytes := 0
|
||||
for _, vec := range e.words {
|
||||
var typed []float32 = vec // 编译期断言:存储必须是 []float32
|
||||
if len(typed) != e.dim {
|
||||
t.Fatalf("维度不符: %d != %d", len(typed), e.dim)
|
||||
}
|
||||
words++
|
||||
bytes += len(typed) * int(unsafe.Sizeof(typed[0]))
|
||||
}
|
||||
if words == 0 {
|
||||
t.Fatal("合成模型应至少加载一个词")
|
||||
}
|
||||
// float32:每词 300×4 = 1200 字节;float64 会是 2400
|
||||
if want := words * e.dim * 4; bytes != want {
|
||||
t.Fatalf("向量本体字节数应 %d(float32),实际 %d", want, bytes)
|
||||
}
|
||||
}
|
||||
@ -28,10 +28,7 @@ var (
|
||||
//
|
||||
// ❗main 上此值始终是**下一个未发布中版本**,不随 patch 发布变动
|
||||
//(见 docs/git-branching.md §2.1);已发布的版本号看对应的 release/vX.Y.x 与 tag。
|
||||
// 1.3.5:系统提示词(人格卡)支持版本占位符 —— 人格卡是配置项,写死版本号
|
||||
// 会随发版说谎(线上写 v1.0.3、内核 1.3.x,agent 就自报 1.0.3)。
|
||||
// 支持 {{kernel_version}} / {{kernel_commit}} / {{sdk_version}}。
|
||||
Version = "1.3.5"
|
||||
Version = "1.3.0"
|
||||
|
||||
// Commit 是构建时的 Git commit hash。
|
||||
Commit = "unknown"
|
||||
|
||||
@ -138,39 +138,28 @@ func (p *Plugin) Stop() error {
|
||||
func (p *Plugin) registerTools(s *sdk.PluginSDK) {
|
||||
s.RegisterTool("plugin_install", sdk.ToolDef{
|
||||
Name: "plugin_install",
|
||||
Description: "安装 HomeAgent 插件包(.hmap)。两种来源:url(http/https 下载)或 path(本机路径,配合 plugindev_build 的产物用这个)。插件已存在时传 overwrite=true 原地更新(升级/降级/重装,保留配置表,无需卸载重装)。更新后需调用 plgreload 或重启生效。",
|
||||
Description: "从 URL 安装 HomeAgent 插件包(.hmap 文件)。插件已存在时传 overwrite=true 原地更新(升级/降级/重装,保留配置表,无需卸载重装)。更新后需调用 plgreload 或重启生效。",
|
||||
Parameters: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"url": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "插件包的下载 URL(http/https)",
|
||||
},
|
||||
"path": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "插件包在**本机**的路径(.hmap)。与 url 二选一;同时给出时以 path 为准",
|
||||
"description": "插件包的下载 URL",
|
||||
},
|
||||
"overwrite": map[string]interface{}{
|
||||
"type": "boolean",
|
||||
"description": "已存在时原地更新(保留配置)。默认 false",
|
||||
},
|
||||
},
|
||||
"required": []string{"url"},
|
||||
},
|
||||
}, func(args map[string]interface{}) (interface{}, error) {
|
||||
overwrite, _ := args["overwrite"].(bool)
|
||||
// path 优先:它对应"agent 自己构建出产物再装"的场景(plugindev_build → plugin_install)。
|
||||
if path, _ := args["path"].(string); strings.TrimSpace(path) != "" {
|
||||
pth := strings.TrimSpace(path)
|
||||
if st, err := os.Stat(pth); err != nil || st.IsDir() {
|
||||
return map[string]interface{}{"error": fmt.Sprintf("path 无效(必须是存在的 .hmap 文件): %s", pth)}, nil
|
||||
}
|
||||
return p.installFromPath(pth, overwrite)
|
||||
}
|
||||
url, _ := args["url"].(string)
|
||||
if strings.TrimSpace(url) == "" {
|
||||
return map[string]interface{}{"error": "需要 url 或 path(二选一)"}, nil
|
||||
if url == "" {
|
||||
return map[string]interface{}{"error": "url is required"}, nil
|
||||
}
|
||||
return p.installFromURL(strings.TrimSpace(url), overwrite)
|
||||
overwrite, _ := args["overwrite"].(bool)
|
||||
return p.installFromURL(url, overwrite)
|
||||
})
|
||||
|
||||
s.RegisterTool("plugin_list", sdk.ToolDef{
|
||||
@ -466,12 +455,12 @@ func (p *Plugin) installFromData(data []byte, overwrite bool) (interface{}, erro
|
||||
|
||||
if existing && !overwrite {
|
||||
return map[string]interface{}{
|
||||
"error": "plugin already exists",
|
||||
"name": pkg.Name,
|
||||
"version": pkg.Version,
|
||||
"current": oldVersion,
|
||||
"action": "remove_first",
|
||||
"hint": `传 "overwrite": true 可原地更新(保留配置)`,
|
||||
"error": "plugin already exists",
|
||||
"name": pkg.Name,
|
||||
"version": pkg.Version,
|
||||
"current": oldVersion,
|
||||
"action": "remove_first",
|
||||
"hint": `传 "overwrite": true 可原地更新(保留配置)`,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -486,7 +475,7 @@ func (p *Plugin) installFromData(data []byte, overwrite bool) (interface{}, erro
|
||||
os.RemoveAll(backup)
|
||||
if err := os.Rename(target, backup); err != nil {
|
||||
return map[string]interface{}{
|
||||
"error": "backup old plugin dir failed",
|
||||
"error": "backup old plugin dir failed",
|
||||
"details": err.Error(),
|
||||
}, nil
|
||||
}
|
||||
@ -495,8 +484,8 @@ func (p *Plugin) installFromData(data []byte, overwrite bool) (interface{}, erro
|
||||
os.RemoveAll(target)
|
||||
if rbErr := os.Rename(backup, target); rbErr != nil {
|
||||
return map[string]interface{}{
|
||||
"error": "extract failed AND rollback failed",
|
||||
"details": err.Error(),
|
||||
"error": "extract failed AND rollback failed",
|
||||
"details": err.Error(),
|
||||
"rollback": rbErr.Error(),
|
||||
}, nil
|
||||
}
|
||||
@ -518,15 +507,15 @@ func (p *Plugin) installFromData(data []byte, overwrite bool) (interface{}, erro
|
||||
action = "reinstalled"
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"status": "installed",
|
||||
"name": pkg.Name,
|
||||
"version": pkg.Version,
|
||||
"status": "installed",
|
||||
"name": pkg.Name,
|
||||
"version": pkg.Version,
|
||||
"previous_version": oldVersion,
|
||||
"entry": pkg.Entry,
|
||||
"checksum": checksum,
|
||||
"action": action,
|
||||
"reload_required": true,
|
||||
"config_kept": true,
|
||||
"entry": pkg.Entry,
|
||||
"checksum": checksum,
|
||||
"action": action,
|
||||
"reload_required": true,
|
||||
"config_kept": true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user