mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-04 00:03:59 +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
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
@ -50,3 +51,61 @@ func (a *Agent) resolveToolPlugin(name string) string {
|
|||||||
}
|
}
|
||||||
return "core"
|
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()
|
return a.executeOutputListChannels()
|
||||||
case tc.Name == "plgreload":
|
case tc.Name == "plgreload":
|
||||||
return a.executePluginReload()
|
return a.executePluginReload()
|
||||||
|
case tc.Name == "get_plugin_tools":
|
||||||
|
pluginName, _ := tc.Arguments["plugin_name"].(string)
|
||||||
|
return a.executeGetPluginTools(pluginName)
|
||||||
case tc.Name == "spawn_child":
|
case tc.Name == "spawn_child":
|
||||||
return a.executeSpawnChild(tc)
|
return a.executeSpawnChild(tc)
|
||||||
case tc.Name == "child_result":
|
case tc.Name == "child_result":
|
||||||
|
|||||||
@ -98,31 +98,42 @@ func (a *Agent) buildToolCatalog() string {
|
|||||||
if len(defs) == 0 {
|
if len(defs) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
var sb strings.Builder
|
// 仅注入插件/通道能力摘要,避免全量工具定义污染 system prompt。
|
||||||
sb.WriteString("\n\n【可用工具列表】")
|
// 每个插件列:名称 + 能力描述 + 工具数。完整工具定义由 get_plugin_tools 按需拉取。
|
||||||
seen := make(map[string]bool)
|
byPlugin := map[string]int{} // plugin -> 工具数
|
||||||
for _, d := range defs {
|
pluginDesc := map[string]string{} // plugin -> 首个工具描述(作能力概览)
|
||||||
t, ok := d.(map[string]interface{})
|
var order []string
|
||||||
if !ok {
|
for _, t := range defs {
|
||||||
continue
|
fn, ok := t.(map[string]interface{})["function"].(map[string]interface{})
|
||||||
}
|
|
||||||
fn, ok := t["function"].(map[string]interface{})
|
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name, _ := fn["name"].(string)
|
name, _ := fn["name"].(string)
|
||||||
if name == "" || seen[name] {
|
if name == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
seen[name] = true
|
plg := a.resolveToolPlugin(name)
|
||||||
desc, _ := fn["description"].(string)
|
if _, seen := byPlugin[plg]; !seen {
|
||||||
sb.WriteString(fmt.Sprintf("\n- %s", name))
|
order = append(order, plg)
|
||||||
if desc != "" {
|
|
||||||
if len(desc) > 80 {
|
|
||||||
desc = desc[:80] + "..."
|
|
||||||
}
|
|
||||||
sb.WriteString(": " + desc)
|
|
||||||
}
|
}
|
||||||
|
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()
|
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{}{
|
tools = append(tools, map[string]interface{}{
|
||||||
"type": "function",
|
"type": "function",
|
||||||
"function": map[string]interface{}{
|
"function": map[string]interface{}{
|
||||||
|
|||||||
@ -56,7 +56,11 @@ func (d *devicectlDevice) Tools() []agentIO.ToolDef {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "device_ctl_cmdrun",
|
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 查询结果。" +
|
"返回 accepted=true 表示已下发并等待设备执行,之后可用 device_ctl_cmdresult 查询结果。" +
|
||||||
"若设备未授权或离线,返回错误信息。",
|
"若设备未授权或离线,返回错误信息。",
|
||||||
@ -64,7 +68,7 @@ func (d *devicectlDevice) Tools() []agentIO.ToolDef {
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]interface{}{
|
||||||
"device_id": map[string]interface{}{"type": "string", "description": "目标设备 ID"},
|
"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"},
|
"required": []interface{}{"device_id", "command"},
|
||||||
},
|
},
|
||||||
@ -191,8 +195,23 @@ func (d *devicectlDevice) cmdrun(args map[string]interface{}) (interface{}, erro
|
|||||||
if !m.Online {
|
if !m.Online {
|
||||||
return nil, fmt.Errorf("device %s 不在线,无法执行命令", id)
|
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()
|
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)
|
return nil, fmt.Errorf("下发命令失败: %w", err)
|
||||||
}
|
}
|
||||||
// 阻塞等待设备结果(带超时);结果同时由 registry 留档。
|
// 阻塞等待设备结果(带超时);结果同时由 registry 留档。
|
||||||
|
|||||||
@ -248,11 +248,15 @@ func (r *Registry) PushJSON(deviceID string, payload map[string]interface{}) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PushCmd 向设备发送命令执行请求。
|
// 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{}{
|
return r.PushJSON(deviceID, map[string]interface{}{
|
||||||
"op": "cmd",
|
"op": "cmd",
|
||||||
"req_id": reqID,
|
"req_id": reqID,
|
||||||
"command": command,
|
"command": command,
|
||||||
|
"cmd_type": cmdType,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user