mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
feat: 插件工具按需拉取(get_plugin_tools) + 设备命令类型拆分(shell-cmd/homeagent-cmd)
1. 提示词去污染: buildToolCatalog 从全量工具定义改为按插件分组摘要 (插件名 + 工具数 + 能力概览), 完整工具定义由新工具 get_plugin_tools(plugin_name) 动态拉取; 新增 executeGetPluginTools 支持按插件过滤 stageHost/io 工具 2. 设备命令类型: device_ctl_cmdrun 的 command 支持前缀区分 - shell-cmd <cmd> -> 设备端执行原生 shell - homeagent-<cap> -> 设备端 HomeAgent 内置能力(如 camerasue/screensue) - homeagent-cmd <cap> -> 同上(兼容写法) PushCmd 增加 cmd_type 字段下发给设备端分发 3. 测试: TestExecuteGetPluginTools 验证按插件拉取/全部摘要/未知插件
This commit is contained in:
33
internal/agent/core/gpttest_test.go
Normal file
33
internal/agent/core/gpttest_test.go
Normal file
@ -0,0 +1,33 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/sdk"
|
||||
)
|
||||
|
||||
func TestExecuteGetPluginTools(tmp *testing.T) {
|
||||
sh := NewStageHost()
|
||||
sh.RegisterTool("weather_current", sdk.ToolDef{Name: "weather_current", Plugin: "weather", Description: "当前天气", Parameters: map[string]interface{}{"type": "object"}}, nil)
|
||||
sh.RegisterTool("weather_forecast", sdk.ToolDef{Name: "weather_forecast", Plugin: "weather", Description: "天气预报"}, nil)
|
||||
sh.RegisterTool("device_ctl_cmdrun", sdk.ToolDef{Name: "device_ctl_cmdrun", Plugin: "remotedevice", Description: "下发命令"}, nil)
|
||||
|
||||
a := New(AgentConfig{ID: "t", StageHost: sh})
|
||||
|
||||
out := a.executeGetPluginTools("weather")
|
||||
if !strings.Contains(out, "weather_current") || !strings.Contains(out, "weather_forecast") {
|
||||
tmp.Fatalf("weather tools missing:\n%s", out)
|
||||
}
|
||||
if strings.Contains(out, "device_ctl_cmdrun") {
|
||||
tmp.Fatalf("should not contain other plugin:\n%s", out)
|
||||
}
|
||||
all := a.executeGetPluginTools("")
|
||||
if !strings.Contains(all, "weather_current") || !strings.Contains(all, "device_ctl_cmdrun") {
|
||||
tmp.Fatalf("all tools missing:\n%s", all)
|
||||
}
|
||||
none := a.executeGetPluginTools("nope")
|
||||
if !strings.Contains(none, "没有可用的工具") {
|
||||
tmp.Fatalf("unknown plugin msg:\n%s", none)
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
package core
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
@ -50,3 +51,61 @@ func (a *Agent) resolveToolPlugin(name string) string {
|
||||
}
|
||||
return "core"
|
||||
}
|
||||
|
||||
// executeGetPluginTools 返回指定插件的完整工具定义(名称/参数/用途)。
|
||||
// 支持按插件名拉取, 未指定时返回全部插件的工具摘要。
|
||||
func (a *Agent) executeGetPluginTools(pluginName string) string {
|
||||
type toolItem struct {
|
||||
name, desc string
|
||||
params interface{}
|
||||
}
|
||||
var items []toolItem
|
||||
|
||||
// 收集 StageHost(SDK 插件)工具
|
||||
if a.stageHost != nil {
|
||||
for _, d := range a.stageHost.GetToolDefs() {
|
||||
plg := d.Plugin
|
||||
if plg == "" {
|
||||
plg = a.resolveToolPlugin(d.Name)
|
||||
}
|
||||
if pluginName != "" && plg != pluginName {
|
||||
continue
|
||||
}
|
||||
items = append(items, toolItem{d.Name, d.Description, d.Parameters})
|
||||
}
|
||||
}
|
||||
// 收集 IOManager(设备/通道)工具
|
||||
if a.io != nil {
|
||||
for _, d := range a.io.GetAllTools() {
|
||||
plg := a.resolveToolPlugin(d.Name)
|
||||
if pluginName != "" && plg != pluginName {
|
||||
continue
|
||||
}
|
||||
items = append(items, toolItem{d.Name, d.Description, d.Parameters})
|
||||
}
|
||||
}
|
||||
if len(items) == 0 {
|
||||
if pluginName != "" {
|
||||
return fmt.Sprintf("插件 %s 没有可用的工具定义", pluginName)
|
||||
}
|
||||
return "当前没有可用的工具定义"
|
||||
}
|
||||
var sb strings.Builder
|
||||
if pluginName == "" {
|
||||
sb.WriteString(fmt.Sprintf("共 %d 个工具:\n", len(items)))
|
||||
} else {
|
||||
sb.WriteString(fmt.Sprintf("插件 %s 共 %d 个工具:\n", pluginName, len(items)))
|
||||
}
|
||||
for _, it := range items {
|
||||
sb.WriteString(fmt.Sprintf("\n### %s\n", it.name))
|
||||
if it.desc != "" {
|
||||
sb.WriteString(it.desc + "\n")
|
||||
}
|
||||
if it.params != nil {
|
||||
if b, err := json.Marshal(it.params); err == nil && len(b) < 600 {
|
||||
sb.WriteString("参数: " + string(b) + "\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@ -61,6 +61,9 @@ func (a *Agent) executeToolCallInner(tc agentAPI.ToolCall) string {
|
||||
return a.executeOutputListChannels()
|
||||
case tc.Name == "plgreload":
|
||||
return a.executePluginReload()
|
||||
case tc.Name == "get_plugin_tools":
|
||||
pluginName, _ := tc.Arguments["plugin_name"].(string)
|
||||
return a.executeGetPluginTools(pluginName)
|
||||
case tc.Name == "spawn_child":
|
||||
return a.executeSpawnChild(tc)
|
||||
case tc.Name == "child_result":
|
||||
|
||||
@ -98,31 +98,42 @@ func (a *Agent) buildToolCatalog() string {
|
||||
if len(defs) == 0 {
|
||||
return ""
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString("\n\n【可用工具列表】")
|
||||
seen := make(map[string]bool)
|
||||
for _, d := range defs {
|
||||
t, ok := d.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
fn, ok := t["function"].(map[string]interface{})
|
||||
// 仅注入插件/通道能力摘要,避免全量工具定义污染 system prompt。
|
||||
// 每个插件列:名称 + 能力描述 + 工具数。完整工具定义由 get_plugin_tools 按需拉取。
|
||||
byPlugin := map[string]int{} // plugin -> 工具数
|
||||
pluginDesc := map[string]string{} // plugin -> 首个工具描述(作能力概览)
|
||||
var order []string
|
||||
for _, t := range defs {
|
||||
fn, ok := t.(map[string]interface{})["function"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
name, _ := fn["name"].(string)
|
||||
if name == "" || seen[name] {
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
seen[name] = true
|
||||
desc, _ := fn["description"].(string)
|
||||
sb.WriteString(fmt.Sprintf("\n- %s", name))
|
||||
if desc != "" {
|
||||
if len(desc) > 80 {
|
||||
desc = desc[:80] + "..."
|
||||
}
|
||||
sb.WriteString(": " + desc)
|
||||
plg := a.resolveToolPlugin(name)
|
||||
if _, seen := byPlugin[plg]; !seen {
|
||||
order = append(order, plg)
|
||||
}
|
||||
byPlugin[plg]++
|
||||
if pluginDesc[plg] == "" {
|
||||
desc, _ := fn["description"].(string)
|
||||
if len(desc) > 60 {
|
||||
desc = desc[:60] + "..."
|
||||
}
|
||||
pluginDesc[plg] = desc
|
||||
}
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString("\n\n【可用工具能力】\n")
|
||||
sb.WriteString("工具按插件分组注册。需要某个插件的具体工具时,调用 get_plugin_tools(\"{插件名}\") 获取该插件的完整工具定义(名称/参数/用途)。\n")
|
||||
for _, plg := range order {
|
||||
sb.WriteString(fmt.Sprintf("- %s (%d 个工具)", plg, byPlugin[plg]))
|
||||
if d := pluginDesc[plg]; d != "" {
|
||||
sb.WriteString(": " + d)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
@ -422,6 +433,21 @@ func (a *Agent) buildToolDefs() []interface{} {
|
||||
})
|
||||
}
|
||||
|
||||
// 按插件动态拉取工具定义(避免全量注入提示词污染)
|
||||
tools = append(tools, map[string]interface{}{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": "get_plugin_tools",
|
||||
"description": "获取指定插件的完整工具定义(名称/参数/用途)。参数 plugin_name 传插件名(见系统提示的【可用工具能力】列表)。省略时返回全部插件的工具摘要。",
|
||||
"parameters": map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"plugin_name": map[string]interface{}{"type": "string", "description": "插件名,如 qq / remotedevice / weather", "default": ""},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
tools = append(tools, map[string]interface{}{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
|
||||
@ -56,7 +56,11 @@ func (d *devicectlDevice) Tools() []agentIO.ToolDef {
|
||||
},
|
||||
{
|
||||
Name: "device_ctl_cmdrun",
|
||||
Description: "向一台已授权且在线设备发送命令执行请求(如开机/重启/播放/自定义命令)。" +
|
||||
Description: "向设备下发命令/操作(异步,accepted=true 后用 device_ctl_cmdresult 轮询结果)。" +
|
||||
"command 支持两类(前缀区分):\n" +
|
||||
"- shell-cmd: 在设备上执行原生 shell 命令,如 shell-cmd ls -la /tmp\n" +
|
||||
"- homeagent-cmd: 调用设备端 HomeAgent 内置能力,如 homeagent-camerasue(调用用户侧摄像头)、" +
|
||||
"homeagent-screensue(用户侧屏幕显示内容)\n" +
|
||||
"⚡ 高危:设备必须已授权,且该操作会改变设备行为。" +
|
||||
"返回 accepted=true 表示已下发并等待设备执行,之后可用 device_ctl_cmdresult 查询结果。" +
|
||||
"若设备未授权或离线,返回错误信息。",
|
||||
@ -64,7 +68,7 @@ func (d *devicectlDevice) Tools() []agentIO.ToolDef {
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"device_id": map[string]interface{}{"type": "string", "description": "目标设备 ID"},
|
||||
"command": map[string]interface{}{"type": "string", "description": "要执行的命令(设备自定义语义,如 power_on、play:xxx 或 shell 命令)"},
|
||||
"command": map[string]interface{}{"type": "string", "description": "以 shell-cmd 或 homeagent-cmd 前缀开头。如 shell-cmd pwd、homeagent-camerasue"},
|
||||
},
|
||||
"required": []interface{}{"device_id", "command"},
|
||||
},
|
||||
@ -191,8 +195,23 @@ func (d *devicectlDevice) cmdrun(args map[string]interface{}) (interface{}, erro
|
||||
if !m.Online {
|
||||
return nil, fmt.Errorf("device %s 不在线,无法执行命令", id)
|
||||
}
|
||||
// 命令类型:shell-cmd / homeagent-* 前缀区分;无前缀按 shell 处理(兼容旧格式)
|
||||
cmdType := "shell"
|
||||
switch {
|
||||
case strings.HasPrefix(cmd, "shell-cmd"):
|
||||
cmdType = "shell"
|
||||
cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "shell-cmd"))
|
||||
case strings.HasPrefix(cmd, "homeagent-cmd"):
|
||||
cmdType = "homeagent"
|
||||
cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "homeagent-cmd"))
|
||||
case strings.HasPrefix(cmd, "homeagent-"):
|
||||
cmdType = "homeagent"
|
||||
cmd = strings.TrimSpace(strings.TrimPrefix(cmd, "homeagent-"))
|
||||
default:
|
||||
cmdType = "shell"
|
||||
}
|
||||
reqID := newReqID()
|
||||
if err := d.reg.PushCmd(id, reqID, cmd); err != nil {
|
||||
if err := d.reg.PushCmd(id, reqID, cmd, cmdType); err != nil {
|
||||
return nil, fmt.Errorf("下发命令失败: %w", err)
|
||||
}
|
||||
// 阻塞等待设备结果(带超时);结果同时由 registry 留档。
|
||||
|
||||
@ -248,11 +248,15 @@ func (r *Registry) PushJSON(deviceID string, payload map[string]interface{}) err
|
||||
}
|
||||
|
||||
// PushCmd 向设备发送命令执行请求。
|
||||
func (r *Registry) PushCmd(deviceID, reqID, command string) error {
|
||||
func (r *Registry) PushCmd(deviceID, reqID, command, cmdType string) error {
|
||||
if cmdType == "" {
|
||||
cmdType = "shell"
|
||||
}
|
||||
return r.PushJSON(deviceID, map[string]interface{}{
|
||||
"op": "cmd",
|
||||
"req_id": reqID,
|
||||
"command": command,
|
||||
"op": "cmd",
|
||||
"req_id": reqID,
|
||||
"command": command,
|
||||
"cmd_type": cmdType,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user