mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-08 00:25:05 +00:00
v4 architecture: pipeline stages, SDK, event bus, LLM-driven memory consolidation
- SDK PluginAPI (internal/plugin/sdk/): RegisterTool/RegisterStage/Subscribe/Publish - EventBus (internal/events/): system-level pub/sub with wildcard support - StageHost (internal/agent/core/stages.go): 7-stage message pipeline - Agent core: on_input/pre_action/post_action/before_toolcall/after_toolcall/before_output/after_output - Plugin Registry: SDK plugin registration and tool routing - GraphDB.MergeEntities: entity consolidation with relation redirection - memory_merge tool: allows LLM to merge similar entities - Consolidation task: heartbeat detects conflicts, enqueues via IO for LLM decision - _consolidation_ internal channel for system-level memory maintenance - Comprehensive documentation: ARCHITECTURE.md, PLAN.md, DESIGN.md, README.md - 54 tests across all packages, all passing
This commit is contained in:
@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
agentIO "gitcode.com/JianFeeeee/HomeAgent/internal/agent/io"
|
||||
sdk "gitcode.com/JianFeeeee/HomeAgent/internal/plugin/sdk"
|
||||
)
|
||||
|
||||
type PluginType string
|
||||
@ -147,15 +148,90 @@ type Registry struct {
|
||||
plugins map[string]Plugin
|
||||
ioMgr *agentIO.IOManager
|
||||
factories map[string]NativeFactory // 名称匹配的插件使用原生实现
|
||||
sdkAPIs map[string]*sdkAPI // SDK 插件 API 实例
|
||||
}
|
||||
|
||||
type sdkAPI struct {
|
||||
api *sdk.PluginAPI
|
||||
tools map[string]sdk.ToolHandler
|
||||
stages map[sdk.Stage][]sdk.StageHandler
|
||||
}
|
||||
|
||||
func NewRegistry() *Registry {
|
||||
return &Registry{
|
||||
plugins: make(map[string]Plugin),
|
||||
factories: make(map[string]NativeFactory),
|
||||
sdkAPIs: make(map[string]*sdkAPI),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterPluginAPI 注册一个 SDK 插件 API 实例
|
||||
func (r *Registry) RegisterPluginAPI(api *sdk.PluginAPI) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if _, ok := r.sdkAPIs[api.Name]; ok {
|
||||
return fmt.Errorf("sdk api %s already registered", api.Name)
|
||||
}
|
||||
r.sdkAPIs[api.Name] = &sdkAPI{
|
||||
api: api,
|
||||
tools: api.Tools(),
|
||||
stages: make(map[sdk.Stage][]sdk.StageHandler),
|
||||
}
|
||||
for stage := range sdk.AllStages() {
|
||||
if handlers := api.StageHandlers(stage); len(handlers) > 0 {
|
||||
r.sdkAPIs[api.Name].stages[stage] = handlers
|
||||
}
|
||||
}
|
||||
log.Printf("[plugin] registered SDK plugin: %s (tools=%d, stages=%d)",
|
||||
api.Name, len(api.Tools()), len(r.sdkAPIs[api.Name].stages))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAllSDKToolDefs 收集所有 SDK 插件的工具定义
|
||||
func (r *Registry) GetAllSDKToolDefs() []sdk.ToolDef {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
var defs []sdk.ToolDef
|
||||
for _, sa := range r.sdkAPIs {
|
||||
for name := range sa.tools {
|
||||
defs = append(defs, sdk.ToolDef{Name: name})
|
||||
}
|
||||
}
|
||||
return defs
|
||||
}
|
||||
|
||||
// ExecuteSDKTool 执行 SDK 插件工具
|
||||
func (r *Registry) ExecuteSDKTool(name string, args map[string]interface{}) (interface{}, error) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
for _, sa := range r.sdkAPIs {
|
||||
if handler, ok := sa.tools[name]; ok {
|
||||
return handler(args)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("sdk tool %s not found", name)
|
||||
}
|
||||
|
||||
// GetStageHandlers 获取所有 SDK 插件在指定阶段的处理器
|
||||
func (r *Registry) GetStageHandlers(stage sdk.Stage) []sdk.StageHandler {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
var handlers []sdk.StageHandler
|
||||
for _, sa := range r.sdkAPIs {
|
||||
if h, ok := sa.stages[stage]; ok {
|
||||
handlers = append(handlers, h...)
|
||||
}
|
||||
}
|
||||
return handlers
|
||||
}
|
||||
|
||||
// SDKPluginCount 返回已注册的 SDK 插件数量
|
||||
func (r *Registry) SDKPluginCount() int {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return len(r.sdkAPIs)
|
||||
}
|
||||
|
||||
// RegisterNative 注册内置原生插件工厂。当从 plugins/ 加载插件时,
|
||||
// 如果插件名称匹配已注册的工厂,优先使用原生设备注册。
|
||||
// 例如: r.RegisterNative("qq", onebot.NewDeviceFactory)
|
||||
|
||||
169
internal/plugin/sdk/api.go
Normal file
169
internal/plugin/sdk/api.go
Normal file
@ -0,0 +1,169 @@
|
||||
package sdk
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Stage string
|
||||
|
||||
const (
|
||||
StageOnInput Stage = "on_input"
|
||||
StagePreAction Stage = "pre_action"
|
||||
StagePostAction Stage = "post_action"
|
||||
StageBeforeToolcall Stage = "before_toolcall"
|
||||
StageAfterToolcall Stage = "after_toolcall"
|
||||
StageBeforeOutput Stage = "before_output"
|
||||
StageAfterOutput Stage = "after_output"
|
||||
)
|
||||
|
||||
type EventType string
|
||||
|
||||
const (
|
||||
EventRawInput EventType = "raw_input"
|
||||
EventAgentOutput EventType = "agent_output"
|
||||
EventToolCall EventType = "tool_call"
|
||||
EventReasoning EventType = "reasoning"
|
||||
EventSystem EventType = "system"
|
||||
EventAll EventType = "*"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Type EventType `json:"type"`
|
||||
Source string `json:"source"`
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type MemItem struct {
|
||||
Content string `json:"content"`
|
||||
Score float64 `json:"score"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type StageContext struct {
|
||||
RawMessage string
|
||||
UserID string
|
||||
GroupID string
|
||||
ContextMsgs []map[string]interface{}
|
||||
LLMText string
|
||||
ToolCalls []ToolCall
|
||||
ToolResults []ToolResult
|
||||
FinalText string
|
||||
Response *string
|
||||
Phase Stage
|
||||
Memory []MemItem
|
||||
Extra map[string]interface{}
|
||||
}
|
||||
|
||||
type ToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
}
|
||||
|
||||
type ToolResult struct {
|
||||
CallID string `json:"call_id"`
|
||||
Name string `json:"name"`
|
||||
Success bool `json:"success"`
|
||||
Result interface{} `json:"result"`
|
||||
}
|
||||
|
||||
type ToolDef struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
}
|
||||
|
||||
type MemoryAPI interface {
|
||||
Recall(query string, topK int) ([]MemItem, error)
|
||||
Commit(triples []map[string]string) error
|
||||
Introspect() (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
type KnowledgeAPI interface {
|
||||
Search(query string, topK int) ([]MemItem, error)
|
||||
Create(name, content string) error
|
||||
List() ([]string, error)
|
||||
}
|
||||
|
||||
type EventHandler func(event *Event)
|
||||
type StageHandler func(ctx *StageContext) error
|
||||
type ToolHandler func(args map[string]interface{}) (interface{}, error)
|
||||
|
||||
type PluginAPI struct {
|
||||
Name string
|
||||
Version string
|
||||
|
||||
tools map[string]ToolHandler
|
||||
stages map[Stage][]StageHandler
|
||||
events map[EventType][]EventHandler
|
||||
eventBus EventBus
|
||||
memAPI MemoryAPI
|
||||
knowAPI KnowledgeAPI
|
||||
}
|
||||
|
||||
func NewPluginAPI(name, version string, bus EventBus, mem MemoryAPI, know KnowledgeAPI) *PluginAPI {
|
||||
return &PluginAPI{
|
||||
Name: name,
|
||||
Version: version,
|
||||
tools: make(map[string]ToolHandler),
|
||||
stages: make(map[Stage][]StageHandler),
|
||||
events: make(map[EventType][]EventHandler),
|
||||
eventBus: bus,
|
||||
memAPI: mem,
|
||||
knowAPI: know,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginAPI) RegisterTool(name string, handler ToolHandler) error {
|
||||
if _, ok := p.tools[name]; ok {
|
||||
return fmt.Errorf("tool %s already registered by plugin %s", name, p.Name)
|
||||
}
|
||||
p.tools[name] = handler
|
||||
if p.eventBus != nil {
|
||||
p.eventBus.Publish(&Event{
|
||||
Type: EventSystem,
|
||||
Source: p.Name,
|
||||
Payload: map[string]interface{}{"action": "register_tool", "tool": name},
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PluginAPI) RegisterStage(stage Stage, handler StageHandler) {
|
||||
p.stages[stage] = append(p.stages[stage], handler)
|
||||
}
|
||||
|
||||
func (p *PluginAPI) Subscribe(eventType EventType, handler EventHandler) {
|
||||
p.events[eventType] = append(p.events[eventType], handler)
|
||||
if p.eventBus != nil {
|
||||
p.eventBus.Subscribe(eventType, handler)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginAPI) Publish(evt *Event) {
|
||||
if p.eventBus != nil {
|
||||
p.eventBus.Publish(evt)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PluginAPI) Tools() map[string]ToolHandler {
|
||||
return p.tools
|
||||
}
|
||||
|
||||
func (p *PluginAPI) StageHandlers(stage Stage) []StageHandler {
|
||||
return p.stages[stage]
|
||||
}
|
||||
|
||||
func (p *PluginAPI) Memory() MemoryAPI { return p.memAPI }
|
||||
func (p *PluginAPI) Knowledge() KnowledgeAPI { return p.knowAPI }
|
||||
|
||||
func AllStages() map[Stage]bool {
|
||||
return map[Stage]bool{
|
||||
StageOnInput: true,
|
||||
StagePreAction: true,
|
||||
StagePostAction: true,
|
||||
StageBeforeToolcall: true,
|
||||
StageAfterToolcall: true,
|
||||
StageBeforeOutput: true,
|
||||
StageAfterOutput: true,
|
||||
}
|
||||
}
|
||||
42
internal/plugin/sdk/bus.go
Normal file
42
internal/plugin/sdk/bus.go
Normal file
@ -0,0 +1,42 @@
|
||||
package sdk
|
||||
|
||||
import "fmt"
|
||||
|
||||
type EventBus interface {
|
||||
Publish(event *Event)
|
||||
Subscribe(eventType EventType, handler EventHandler) func()
|
||||
}
|
||||
|
||||
type InProcessBus struct {
|
||||
subs map[EventType][]EventHandler
|
||||
}
|
||||
|
||||
func NewInProcessBus() *InProcessBus {
|
||||
return &InProcessBus{
|
||||
subs: make(map[EventType][]EventHandler),
|
||||
}
|
||||
}
|
||||
|
||||
func (b *InProcessBus) Publish(evt *Event) {
|
||||
for _, h := range b.subs[EventAll] {
|
||||
h(evt)
|
||||
}
|
||||
if evt.Type != EventAll {
|
||||
for _, h := range b.subs[evt.Type] {
|
||||
h(evt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *InProcessBus) Subscribe(eventType EventType, handler EventHandler) func() {
|
||||
b.subs[eventType] = append(b.subs[eventType], handler)
|
||||
return func() {
|
||||
list := b.subs[eventType]
|
||||
for i, h := range list {
|
||||
if fmt.Sprintf("%p", h) == fmt.Sprintf("%p", handler) {
|
||||
b.subs[eventType] = append(list[:i], list[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
133
internal/plugin/sdk/bus_test.go
Normal file
133
internal/plugin/sdk/bus_test.go
Normal file
@ -0,0 +1,133 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInProcessBus(t *testing.T) {
|
||||
bus := NewInProcessBus()
|
||||
var called bool
|
||||
|
||||
bus.Subscribe(EventRawInput, func(evt *Event) {
|
||||
called = true
|
||||
if evt.Source != "test" {
|
||||
t.Errorf("expected source test, got %s", evt.Source)
|
||||
}
|
||||
})
|
||||
|
||||
bus.Publish(&Event{
|
||||
Type: EventRawInput,
|
||||
Source: "test",
|
||||
})
|
||||
|
||||
if !called {
|
||||
t.Error("handler was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInProcessBusWildcard(t *testing.T) {
|
||||
bus := NewInProcessBus()
|
||||
count := 0
|
||||
|
||||
bus.Subscribe(EventAll, func(evt *Event) {
|
||||
count++
|
||||
})
|
||||
|
||||
bus.Publish(&Event{Type: EventRawInput, Source: "s1"})
|
||||
bus.Publish(&Event{Type: EventToolCall, Source: "s2"})
|
||||
|
||||
if count != 2 {
|
||||
t.Errorf("expected 2, got %d", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginAPI(t *testing.T) {
|
||||
bus := NewInProcessBus()
|
||||
api := NewPluginAPI("test", "1.0.0", bus, nil, nil)
|
||||
|
||||
if api.Name != "test" {
|
||||
t.Errorf("expected test, got %s", api.Name)
|
||||
}
|
||||
if api.Version != "1.0.0" {
|
||||
t.Errorf("expected 1.0.0, got %s", api.Version)
|
||||
}
|
||||
|
||||
var stageCalled bool
|
||||
api.RegisterStage(StageOnInput, func(ctx *StageContext) error {
|
||||
stageCalled = true
|
||||
if ctx.RawMessage != "hello" {
|
||||
t.Errorf("expected hello, got %s", ctx.RawMessage)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
ctx := &StageContext{RawMessage: "hello"}
|
||||
for _, handler := range api.StageHandlers(StageOnInput) {
|
||||
handler(ctx)
|
||||
}
|
||||
|
||||
if !stageCalled {
|
||||
t.Error("stage handler was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginAPITool(t *testing.T) {
|
||||
api := NewPluginAPI("test", "1.0.0", nil, nil, nil)
|
||||
|
||||
err := api.RegisterTool("test_tool", func(args map[string]interface{}) (interface{}, error) {
|
||||
return "ok", nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("register tool: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := api.Tools()["test_tool"]; !ok {
|
||||
t.Error("tool not found")
|
||||
}
|
||||
|
||||
// duplicate registration should fail
|
||||
err = api.RegisterTool("test_tool", func(args map[string]interface{}) (interface{}, error) {
|
||||
return "ok", nil
|
||||
})
|
||||
if err == nil {
|
||||
t.Error("expected error on duplicate tool registration")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginAPIStageShortCircuit(t *testing.T) {
|
||||
api := NewPluginAPI("test", "1.0.0", nil, nil, nil)
|
||||
|
||||
api.RegisterStage(StageOnInput, func(ctx *StageContext) error {
|
||||
resp := "intercepted"
|
||||
ctx.Response = &resp
|
||||
return nil
|
||||
})
|
||||
|
||||
ctx := &StageContext{RawMessage: "hello"}
|
||||
handlers := api.StageHandlers(StageOnInput)
|
||||
if len(handlers) != 1 {
|
||||
t.Fatalf("expected 1 handler, got %d", len(handlers))
|
||||
}
|
||||
handlers[0](ctx)
|
||||
|
||||
if ctx.Response == nil || *ctx.Response != "intercepted" {
|
||||
t.Errorf("expected intercepted, got %v", ctx.Response)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllStages(t *testing.T) {
|
||||
stages := AllStages()
|
||||
expected := []Stage{
|
||||
StageOnInput, StagePreAction, StagePostAction,
|
||||
StageBeforeToolcall, StageAfterToolcall,
|
||||
StageBeforeOutput, StageAfterOutput,
|
||||
}
|
||||
for _, s := range expected {
|
||||
if !stages[s] {
|
||||
t.Errorf("missing stage: %s", s)
|
||||
}
|
||||
}
|
||||
if len(stages) != len(expected) {
|
||||
t.Errorf("expected %d stages, got %d", len(expected), len(stages))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user