sdk: embed non-toolchain SDK in third_party, add NoMemory/Cleaner support

- Embed sdk/, example/, meta/, go.mod from homeagent-sdk (no .git)
- Core .gitignore excludes SDK toolchain: bin/, tools/, package/
- RegisterInputChannel + ChannelDef(NoMemory, Cleaner) in SDK
- IOManager input channel registry with GetInputChannelDef
- eventloop: apply channel Cleaner/NoMemory to interrupt text
- context engine: channelDefLookup applied in textForVector
- document store: ChannelCleaner param for archive functions
- All callers/adapters updated with ChannelDef{} default
This commit is contained in:
root
2026-07-29 14:48:14 +08:00
parent 4218890aed
commit a899d777c3
29 changed files with 1265 additions and 94 deletions

8
.gitignore vendored
View File

@ -21,3 +21,11 @@ login.json
cmd/gui/node_modules/
cmd/gui/dist/
.gopath/
# SDK 工具链 — 核心仓不追踪
third_party/homeagent-sdk/bin/
third_party/homeagent-sdk/tools/
third_party/homeagent-sdk/package/
third_party/homeagent-sdk/.gitignore
third_party/homeagent-sdk/README*

2
go.mod
View File

@ -13,3 +13,5 @@ require github.com/yanyiwu/gojieba v1.4.7
require github.com/yalue/onnxruntime_go v1.13.0
require gitcode.com/JianFeeeee/homeagent-sdk v0.7.2
replace gitcode.com/JianFeeeee/homeagent-sdk => ./third_party/homeagent-sdk

View File

@ -178,6 +178,9 @@ func New(cfg AgentConfig) *Agent {
if cfg.StageHost != nil {
rc.SetToolDefLookup(cfg.StageHost.ToolDef)
}
if cfg.IO != nil {
rc.SetChannelDefLookup(cfg.IO.GetInputChannelDef)
}
return &Agent{
id: cfg.ID,

View File

@ -22,6 +22,7 @@ func (d *mockOutputDevice) Tools() []agentIO.ToolDef { return d.too
func (d *mockOutputDevice) Start() error { return nil }
func (d *mockOutputDevice) Stop() error { return nil }
func (d *mockOutputDevice) OutputCapabilities() agentIO.OutputCapability { return d.caps }
func (d *mockOutputDevice) ChannelDef() agentIO.ChannelDef { return agentIO.ChannelDef{} }
func (d *mockOutputDevice) Execute(tool string, args map[string]interface{}) (interface{}, error) {
if d.toolFn != nil {
return d.toolFn(tool, args)

View File

@ -34,13 +34,14 @@ type ContextEvent struct {
const contextFlushInterval = 5 * time.Second
type RelevanceContext struct {
mu sync.Mutex
events []*ContextEvent
embedder *memory.StaticEmbedder
savePath string
saveTimer *time.Timer
dirty bool
toolDefLookup func(name string) *sdk.ToolDef
mu sync.Mutex
events []*ContextEvent
embedder *memory.StaticEmbedder
savePath string
saveTimer *time.Timer
dirty bool
toolDefLookup func(name string) *sdk.ToolDef
channelDefLookup func(name string) (sdk.ChannelDef, bool)
}
func NewRelevanceContext(savePath string, embedder *memory.StaticEmbedder) *RelevanceContext {
@ -60,6 +61,12 @@ func (c *RelevanceContext) SetToolDefLookup(fn func(name string) *sdk.ToolDef) {
c.toolDefLookup = fn
}
func (c *RelevanceContext) SetChannelDefLookup(fn func(name string) (sdk.ChannelDef, bool)) {
c.mu.Lock()
defer c.mu.Unlock()
c.channelDefLookup = fn
}
func (c *RelevanceContext) load() {
data, err := os.ReadFile(c.savePath)
if err != nil {
@ -75,7 +82,7 @@ func (c *RelevanceContext) load() {
c.events = events
}
func textForVector(evt *ContextEvent, toolDefLookup func(name string) *sdk.ToolDef) string {
func textForVector(evt *ContextEvent, toolDefLookup func(name string) *sdk.ToolDef, channelDefLookup func(name string) (sdk.ChannelDef, bool)) string {
var text string
switch {
case evt.Source == "agent" && evt.Response != "":
@ -86,6 +93,16 @@ func textForVector(evt *ContextEvent, toolDefLookup func(name string) *sdk.ToolD
text = evt.Input
}
// 计算层:应用输入通道的 Cleaner不改原文仅在计算层清洗
if channelDefLookup != nil {
if chDef, ok := channelDefLookup(evt.Source); ok && chDef.Cleaner != nil {
text = chDef.Cleaner(text)
}
if chDef, ok := channelDefLookup(evt.Source); ok && chDef.NoMemory {
return ""
}
}
// 计算层附加工具输出NoMemory 跳过,其余经 Cleaner 过滤
if toolDefLookup != nil {
noMemory := make(map[string]bool)
@ -130,8 +147,41 @@ func (c *RelevanceContext) toolOutputClean(name, output string) string {
return output
}
// inputChannelClean 根据输入通道的 Def 清洗输入文本,用于计算层。
func (c *RelevanceContext) inputChannelClean(source, input string) string {
if c.channelDefLookup == nil {
return input
}
chDef, ok := c.channelDefLookup(source)
if !ok {
return input
}
if chDef.Cleaner != nil {
return chDef.Cleaner(input)
}
return input
}
// channelCleanerForDoc 返回 ChannelCleaner使 document 包在存档时能按来源查找 Cleaner。
func (c *RelevanceContext) channelCleanerForDoc() document.ChannelCleaner {
if c.channelDefLookup == nil {
return nil
}
return func(source string) func(string) string {
chDef, ok := c.channelDefLookup(source)
if !ok {
return nil
}
return chDef.Cleaner
}
}
func (c *RelevanceContext) computeVector(evt *ContextEvent) vector.Vector {
return c.embedder.Vectorize(textForVector(evt, c.toolDefLookup))
text := textForVector(evt, c.toolDefLookup, c.channelDefLookup)
if text == "" {
return nil
}
return c.embedder.Vectorize(text)
}
func (c *RelevanceContext) Save() error {
@ -274,7 +324,7 @@ func (c *RelevanceContext) Prune(currentInput string, topK int, docStore *docume
ToolResults: convertToolResults(s.event.ToolResults),
}
}
doc, err := docStore.ContextToDoc("context_archived", entries, c.embedder, nil, c.toolOutputClean)
doc, err := docStore.ContextToDoc("context_archived", entries, c.embedder, nil, c.toolOutputClean, c.channelCleanerForDoc())
if err == nil && doc != nil {
archived = len(entries)
}

View File

@ -281,6 +281,11 @@ func (a *Agent) processTextInput(evt *agentIO.InputEvent, input string) {
if v, ok := evt.Payload["no_memory"].(bool); ok {
noMemory = v
}
if !noMemory && a.io != nil {
if chDef, ok := a.io.GetInputChannelDef(evt.Source); ok && chDef.NoMemory {
noMemory = true
}
}
stageCtx := a.stageCtxFromInput(input, evt.Source, "")
stageCtx.Extra["input_source"] = evt.Source
@ -297,12 +302,20 @@ func (a *Agent) processTextInput(evt *agentIO.InputEvent, input string) {
input = stageCtx.RawMessage
// 计算层用的清洗文本(不改原文):通道 Cleaner 提取语义内容后用于向量化/提关键词
cleanInput := input
if a.io != nil {
if chDef, ok := a.io.GetInputChannelDef(evt.Source); ok && chDef.Cleaner != nil {
cleanInput = chDef.Cleaner(input)
}
}
a.publishEvent(events.EventRawInput, map[string]interface{}{
"content": input,
"source": evt.Source,
})
archived := a.context.Prune(input, a.maxContextSize-1, a.docStore)
archived := a.context.Prune(cleanInput, a.maxContextSize-1, a.docStore)
if archived > 0 {
log.Printf("[agent] pruned %d low-relevance events to document memory", archived)
}
@ -328,7 +341,7 @@ func (a *Agent) processTextInput(evt *agentIO.InputEvent, input string) {
a.context.Append(ContextEvent{
Timestamp: time.Now(),
Source: "agent",
Input: input,
Input: cleanInput,
Response: response,
ToolsUsed: toolsUsed,
ToolResults: toolResults,
@ -337,7 +350,7 @@ func (a *Agent) processTextInput(evt *agentIO.InputEvent, input string) {
a.emitResponse(evt, response)
if !stageCtx.NoMemory {
a.emitMemoryCandidate(evt.Source, input, response, toolResults, toolsUsed)
a.emitMemoryCandidate(evt.Source, cleanInput, response, toolResults, toolsUsed)
}
}

View File

@ -6,8 +6,13 @@ import (
"runtime/debug"
"sync"
"time"
pubsdk "gitcode.com/JianFeeeee/homeagent-sdk/sdk"
)
// ChannelDef 描述通道在记忆计算层的行为,与 ToolDef.NoMemory/Cleaner 语义一致。
type ChannelDef = pubsdk.ChannelDef
type DeviceType int
const (
@ -60,6 +65,7 @@ type Device interface {
Start() error
Stop() error
OutputCapabilities() OutputCapability
ChannelDef() ChannelDef
}
type ToolHandler func(args map[string]interface{}) (interface{}, error)
@ -90,20 +96,22 @@ type OutputEvent struct {
}
type IOManager struct {
mu sync.RWMutex
devices map[string]Device
inputCh chan *InputEvent
interruptCh chan *InputEvent
outputCh chan *OutputEvent
nextReqID int64
mu sync.RWMutex
devices map[string]Device
inputCh chan *InputEvent
interruptCh chan *InputEvent
outputCh chan *OutputEvent
nextReqID int64
inputChannels map[string]ChannelDef
}
func NewIOManager() *IOManager {
return &IOManager{
devices: make(map[string]Device),
inputCh: make(chan *InputEvent, 256),
interruptCh: make(chan *InputEvent, 64),
outputCh: make(chan *OutputEvent, 256),
devices: make(map[string]Device),
inputCh: make(chan *InputEvent, 256),
interruptCh: make(chan *InputEvent, 64),
outputCh: make(chan *OutputEvent, 256),
inputChannels: make(map[string]ChannelDef),
}
}
@ -333,6 +341,28 @@ func (m *IOManager) EmitTextTo(target, outputChannel, text string) {
func (m *IOManager) InputChan() <-chan *InputEvent { return m.inputCh }
func (m *IOManager) OutputChan() <-chan *OutputEvent { return m.outputCh }
// RegisterInputChannel 注册输入通道的记忆行为
func (m *IOManager) RegisterInputChannel(name string, def ChannelDef) {
m.mu.Lock()
defer m.mu.Unlock()
m.inputChannels[name] = def
}
// UnregisterInputChannel 注销输入通道
func (m *IOManager) UnregisterInputChannel(name string) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.inputChannels, name)
}
// GetInputChannelDef 查询输入通道的记忆行为定义
func (m *IOManager) GetInputChannelDef(name string) (ChannelDef, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
def, ok := m.inputChannels[name]
return def, ok
}
func (m *IOManager) GetAllTools() []ToolDef {
m.mu.RLock()
defer m.mu.RUnlock()
@ -434,6 +464,7 @@ func (d *Microphone) OutputCapabilities() OutputCapability { return 0 } // 纯
func (d *Microphone) Description() string { return fmt.Sprintf("麦克风 (%s, %dHz)", d.name, d.sampleRate) }
func (d *Microphone) Start() error { return nil }
func (d *Microphone) Stop() error { return nil }
func (d *Microphone) ChannelDef() ChannelDef { return ChannelDef{} }
func (d *Microphone) Tools() []ToolDef {
return []ToolDef{{
@ -468,6 +499,7 @@ func (d *Speaker) OutputCapabilities() OutputCapability { return CapText | CapAu
func (d *Speaker) Description() string { return fmt.Sprintf("扬声器 (%s)", d.name) }
func (d *Speaker) Start() error { return nil }
func (d *Speaker) Stop() error { return nil }
func (d *Speaker) ChannelDef() ChannelDef { return ChannelDef{} }
func (d *Speaker) Tools() []ToolDef {
return []ToolDef{{
@ -504,6 +536,7 @@ func (d *Camera) OutputCapabilities() OutputCapability { return CapImage } //
func (d *Camera) Description() string { return fmt.Sprintf("摄像头 (%s)", d.name) }
func (d *Camera) Start() error { return nil }
func (d *Camera) Stop() error { return nil }
func (d *Camera) ChannelDef() ChannelDef { return ChannelDef{} }
func (d *Camera) Tools() []ToolDef {
return []ToolDef{
@ -551,6 +584,7 @@ func (d *RobotArm) OutputCapabilities() OutputCapability { return CapStructured
func (d *RobotArm) Description() string { return fmt.Sprintf("机械臂 (%s)", d.name) }
func (d *RobotArm) Start() error { return nil }
func (d *RobotArm) Stop() error { return nil }
func (d *RobotArm) ChannelDef() ChannelDef { return ChannelDef{} }
func (d *RobotArm) Tools() []ToolDef {
return []ToolDef{
@ -602,6 +636,7 @@ func (d *GPIODevice) OutputCapabilities() OutputCapability { return CapStructure
func (d *GPIODevice) Description() string { return "GPIO 通用引脚" }
func (d *GPIODevice) Start() error { return nil }
func (d *GPIODevice) Stop() error { return nil }
func (d *GPIODevice) ChannelDef() ChannelDef { return ChannelDef{} }
func (d *GPIODevice) Tools() []ToolDef {
return []ToolDef{

View File

@ -43,6 +43,7 @@ func (d *mockDevice) Stop() error {
return nil
}
func (d *mockDevice) OutputCapabilities() OutputCapability { return d.caps }
func (d *mockDevice) ChannelDef() ChannelDef { return ChannelDef{} }
func (d *mockDevice) Execute(tool string, args map[string]interface{}) (interface{}, error) {
if d.executeFn != nil {
return d.executeFn(tool, args)

View File

@ -6,10 +6,39 @@ import (
"path/filepath"
"strings"
"sync"
"unicode/utf8"
"github.com/yanyiwu/gojieba"
)
// contentPOS 有实义的词性标签:只保留名词/动词/形容词/专名等
var contentPOS = map[string]bool{
"n": true, // 普通名词
"nr": true, // 人名
"ns": true, // 地名
"nt": true, // 机构名
"nw": true, // 作品名/URL
"nz": true, // 其他专名
"v": true, // 动词
"vd": true, // 副动词
"vn": true, // 名动词
"a": true, // 形容词
"ad": true, // 副形词
"an": true, // 名形词
"i": true, // 成语
"l": true, // 习用语
"j": true, // 简称
"s": true, // 处所词
"f": true, // 方位词
"b": true, // 区别词
"z": true, // 状态词
"t": true, // 时间词
"eng": true, // 英文
"x": true, // 非语素字
"zg": true, // 其他
"un": true, // 未知词性——保守保留
}
var (
jiebaOnce sync.Once
jiebaInst *gojieba.Jieba
@ -69,32 +98,109 @@ func jiebaDictDir() string {
}
var stopWords = map[string]bool{
"的": true, "了": true, "是": true, "在": true, "有": true,
"": true, "": true, "": true, "": true, "": true,
"": true, "一个": true, "": true, "": true, "": true,
"": true, "": true, "要": true, "去": true, "你": true,
"": true, "": true, "没有": true, "看": true, "好": true,
"自己": true, "这": true, "他": true, "": true, "": true,
"": true, "我们": true, "你们": true, "他们": true,
"": true, "": true, "": true,
"嗯": true, "哦": true, "": true, "": true, "": true,
"然后": true, "因为": true, "所以": true, "如果": true, "但是": true,
"可能": true, "还是": true, "已经": true,
"就是": true, "不是": true, "是的": true,
"非常": true, "比较": true, "应该": true, "需要": true,
"能够": true, "目前": true, "现在": true, "今天": true, "昨天": true,
"明天": true, "知道": true, "觉得": true, "认为": true,
"": true, "": true, "": true,
// ── 代词 ──
"": true, "": true, "": true, "": true, "": true,
"我们": true, "你们": true, "他们": true, "她们": true, "它们": true,
"自己": true, "别人": true, "大家": true,
"": true, "": true, "": true,
"这个": true, "那个": true, "哪个": true,
"这里": true, "那里": true, "哪里": true,
"这些": true, "那些": true, "哪些": true,
"什么": true, "怎么": true, "怎样": true, "怎么样": true,
"": true, "为什么": true,
// ── 量词 ──
"一个": true, "每个": true,
"": true, "": true, "": true, "": true,
// ── 介词 ──
"在": true, "到": true, "": true, "": true, "": true,
"": true, "": true, "给": true, "向": true, "往": true,
"跟": true, "和": true, "与": true, "同": true, "比": true,
"关于": true, "对于": true, "按照": true, "根据": true, "通过": true,
"因为": true, "由于": true, "为了": true, "为": true,
// ── 连词 ──
"或": true, "或者": true,
"但是": true, "但": true, "可是": true, "不过": true,
"然而": true, "虽然": true, "尽管": true, "即使": true,
"如果": true, "假如": true, "只要": true, "除非": true,
"而且": true, "并且": true, "同时": true, "此外": true,
"所以": true, "因此": true, "于是": true,
"然后": true, "接着": true, "从而": true,
"不是": true, "就是": true, "而是": true,
// ── 助词 ──
"的": true, "地": true, "得": true,
"了": true, "着": true, "过": true,
"所": true,
"吗": true, "吧": true, "啊": true, "呢": true, "啦": true,
"嗯": true, "哦": true, "哈": true, "呀": true, "嘛": true, "哟": true,
"噢": true, "喔": true, "呵": true, "嘿": true, "喂": true,
"呐": true, "呗": true, "咚": true, "噗": true,
// ── 副词 ──
"不": true, "没": true, "没有": true, "别": true, "不要": true,
"很": true, "太": true, "非常": true, "十分": true, "特别": true,
"比较": true, "相当": true, "更": true, "最": true,
"都": true, "也": true, "还": true, "又": true, "再": true,
"就": true, "才": true, "便": true,
"已经": true, "曾经": true, "刚": true, "刚刚": true,
"正在": true, "正": true,
"将要": true, "将": true,
"一直": true, "总是": true, "从来": true,
"经常": true, "通常": true, "往往": true,
"可能": true, "也许": true, "大概": true, "大约": true,
"一定": true, "肯定": true, "必须": true,
"当然": true, "其实": true, "确实": true,
"一起": true, "一块": true,
"仍然": true, "依然": true, "还是": true,
"互相": true, "分别": true,
"是否": true, "能否": true, "可否": true,
"越": true, "挺": true,
// ── 判断动词 ──
"是": true, "有": true,
"是的": true, "有的": true,
// ── 能愿动词 ──
"能": true, "能够": true, "可以": true, "会": true,
"要": true, "需要": true, "想要": true,
"应该": true, "应当": true, "该": true,
"愿意": true, "肯": true, "敢": true,
// ── 常用弱义动词 ──
"说": true, "看": true, "做": true, "来": true, "去": true,
"用": true, "想": true, "知道": true,
"觉得": true, "认为": true, "发现": true, "看到": true,
"表示": true, "告诉": true, "问": true, "回答": true,
"成为": true, "作为": true, "进行": true, "使用": true,
// ── 时间词(泛化) ──
"今天": true, "昨天": true, "明天": true, "前天": true, "后天": true,
"早上": true, "上午": true, "中午": true, "下午": true, "晚上": true,
"现在": true, "目前": true, "之前": true, "之后": true, "以后": true,
"最近": true, "刚才": true, "以前": true, "原来": true, "将来": true,
// ── 人称/泛指 ──
"人": true, "人们": true, "东西": true, "事情": true,
"问题": true, "情况": true, "时候": true, "地方": true,
"方式": true, "方法": true, "原因": true, "结果": true,
// ── 高频语气组合 ──
"好的": true, "好吧": true, "好": true,
"好了": true, "对了": true, "行了": true,
"没事": true, "没关系": true, "算了": true,
"看起来": true, "看上去": true, "听起来": true,
"可以说": true, "也就是说": true, "的话": true,
"来着": true, "罢了": true, "便是": true,
// ── 对话/消息类弱义词 ──
"消息": true, "回复": true, "发送": true,
"查看": true, "显示": true, "输出": true, "输入": true,
"文件": true, "内容": true, "信息": true,
"来自": true, "收到": true,
// ── 英文停用词 ──
"the": true, "a": true, "an": true, "is": true, "are": true,
"was": true, "were": true, "be": true, "been": true, "being": true,
"have": true, "has": true, "had": true, "do": true, "does": true,
"did": true, "will": true, "would": true, "could": true, "should": true,
"may": true, "might": true, "can": true, "shall": true, "this": true,
"that": true, "these": true, "those": true, "it": true, "its": true,
"may": true, "might": true, "can": true, "shall": true,
"this": true, "that": true, "these": true, "those": true,
"it": true, "its": true,
"and": true, "or": true, "but": true, "in": true, "on": true,
"at": true, "to": true, "for": true, "of": true, "with": true,
"what": true, "how": true, "why": true, "which": true, "where": true,
"when": true, "who": true, "whom": true,
"please": true, "yes": true, "no": true, "not": true,
}
// TokenizeWords 使用 jieba 精确模式分词,返回去重后的所有词 token不过滤停用词
@ -118,30 +224,45 @@ func TokenizeWords(text string) []string {
return result
}
func ExtractKeywords(text string) []string {
// TokenizeContentWords 使用 jieba 精确模式分词 + 词性过滤,只保留名词/动词/形容词/专名等有实义的词
// 过滤停用词 + 短词(<2 字符),返回去重结果。适用于向量化、关键词提取等需要语义质量的任务。
func TokenizeContentWords(text string) []string {
text = CleanText(text)
x := GetJieba()
if x == nil {
return nil
}
words := x.Cut(text, false)
var keywords []string
tagged := x.Tag(text)
var result []string
seen := make(map[string]bool)
for _, w := range words {
if stopWords[w] || seen[w] {
for _, t := range tagged {
idx := strings.LastIndex(t, "/")
if idx < 0 {
continue
}
r := []rune(w)
if len(r) < 2 {
word := t[:idx]
tag := t[idx+1:]
word = strings.TrimSpace(word)
if word == "" || seen[word] {
continue
}
seen[w] = true
keywords = append(keywords, w)
if stopWords[word] {
continue
}
if utf8.RuneCountInString(word) < 2 {
continue
}
if !contentPOS[tag] {
continue
}
seen[word] = true
result = append(result, word)
}
if len(keywords) > 5 {
keywords = keywords[:5]
}
return keywords
return result
}
func ExtractKeywords(text string) []string {
return TokenizeContentWords(text)
}
// CutExact 精确模式分词:返回去停用词后的所有有义项(不限数量),用于 doc→graph 蒸馏

View File

@ -23,8 +23,8 @@ func TestCutExact(t *testing.T) {
{
name: "chinese_sentence",
text: "今天天气怎么样",
min: 2,
not: nil,
min: 1, // "怎么样" 已加入停用词表
not: []string{"怎么样"},
},
{
name: "stop_words_removed",
@ -47,8 +47,8 @@ func TestCutExact(t *testing.T) {
{
name: "qq_conversation",
text: "今天天气怎么样 → 今天天气很好",
min: 2,
not: nil,
min: 1, // "怎么样" "很" 已加入停用词表
not: []string{"怎么样", "很好", "很"},
},
{
name: "all_stop_words",

View File

@ -15,6 +15,10 @@ import (
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
)
// ChannelCleaner 按事件来源查找输入通道的 Cleaner 函数。
// 返回 nil 表示不使用额外清洗。
type ChannelCleaner func(source string) func(string) string
// Doc — 记忆文档:由上下文提炼而来
type Doc struct {
ID string `json:"id"`
@ -128,7 +132,7 @@ func (s *Store) Insert(doc *Doc) error {
// toolCleanFn 可选func(name, output string) string按工具名对输出进行过滤/清洗:
// - 返回 "" → 跳过该工具输出NoMemory
// - 返回清洗后文本 → 用于计算层Cleaner原文不受影响
func (s *Store) ContextToDoc(source string, entries []ContextEntry, vec vector.Vectorizer, cleanFn func(string) string, toolCleanFn func(name, output string) string) (*Doc, error) {
func (s *Store) ContextToDoc(source string, entries []ContextEntry, vec vector.Vectorizer, cleanFn func(string) string, toolCleanFn func(name, output string) string, channelCleaner ChannelCleaner) (*Doc, error) {
if len(entries) == 0 {
return nil, nil
}
@ -151,9 +155,9 @@ func (s *Store) ContextToDoc(source string, entries []ContextEntry, vec vector.V
content := strings.Join(parts, "\n")
contentHash := simpleHash(content)
summary := summarizeEntries(entries, cleanFn, toolCleanFn)
tags := extractTags(entries, cleanFn, toolCleanFn)
entities := extractEntities(entries, cleanFn, toolCleanFn)
summary := summarizeEntries(entries, cleanFn, toolCleanFn, channelCleaner)
tags := extractTags(entries, cleanFn, toolCleanFn, channelCleaner)
entities := extractEntities(entries, cleanFn, toolCleanFn, channelCleaner)
s.mu.Lock()
@ -441,7 +445,7 @@ type ContextEntry struct {
ToolResults []ToolResultItem
}
func summarizeEntries(entries []ContextEntry, cleanText func(string) string, toolCleanFn func(name, output string) string) string {
func summarizeEntries(entries []ContextEntry, cleanText func(string) string, toolCleanFn func(name, output string) string, channelCleaner ChannelCleaner) string {
if len(entries) == 0 {
return ""
}
@ -449,7 +453,13 @@ func summarizeEntries(entries []ContextEntry, cleanText func(string) string, too
var topics []string
for _, e := range entries {
sources[e.Source]++
words := memory.ExtractKeywords(cleanText(e.Content))
content := e.Content
if channelCleaner != nil {
if c := channelCleaner(e.Source); c != nil {
content = c(content)
}
}
words := memory.ExtractKeywords(cleanText(content))
topics = append(topics, words...)
for _, tr := range e.ToolResults {
out := tr.Output
@ -490,10 +500,16 @@ func summarizeEntries(entries []ContextEntry, cleanText func(string) string, too
return summary
}
func extractTags(entries []ContextEntry, cleanText func(string) string, toolCleanFn func(name, output string) string) []string {
func extractTags(entries []ContextEntry, cleanText func(string) string, toolCleanFn func(name, output string) string, channelCleaner ChannelCleaner) []string {
tagSet := make(map[string]bool)
for _, e := range entries {
for _, kw := range memory.ExtractKeywords(cleanText(e.Content)) {
content := e.Content
if channelCleaner != nil {
if c := channelCleaner(e.Source); c != nil {
content = c(content)
}
}
for _, kw := range memory.ExtractKeywords(cleanText(content)) {
tagSet[kw] = true
}
for _, tr := range e.ToolResults {
@ -520,11 +536,17 @@ func extractTags(entries []ContextEntry, cleanText func(string) string, toolClea
return tags
}
func extractEntities(entries []ContextEntry, cleanText func(string) string, toolCleanFn func(name, output string) string) []string {
func extractEntities(entries []ContextEntry, cleanText func(string) string, toolCleanFn func(name, output string) string, channelCleaner ChannelCleaner) []string {
var entities []string
seen := make(map[string]bool)
for _, e := range entries {
for _, kw := range memory.ExtractKeywords(cleanText(e.Content)) {
content := e.Content
if channelCleaner != nil {
if c := channelCleaner(e.Source); c != nil {
content = c(content)
}
}
for _, kw := range memory.ExtractKeywords(cleanText(content)) {
if len(kw) >= 2 && !seen[kw] {
seen[kw] = true
entities = append(entities, kw)

View File

@ -82,7 +82,7 @@ func TestContextToDoc(t *testing.T) {
{Timestamp: time.Now(), Source: "user", Content: "特别是Go语言", Response: "Go很棒"},
}
doc, err := s.ContextToDoc("test", entries, nil, nil, nil)
doc, err := s.ContextToDoc("test", entries, nil, nil, nil, nil)
if err != nil {
t.Fatal(err)
}
@ -178,7 +178,7 @@ func TestSummarizeEntries(t *testing.T) {
{Source: "user", Content: "今天天气如何"},
{Source: "user", Content: "明天会下雨吗"},
}
summary := summarizeEntries(entries, func(s string) string { return s }, nil)
summary := summarizeEntries(entries, func(s string) string { return s }, nil, nil)
if summary == "" {
t.Error("summary should not be empty")
}
@ -198,7 +198,7 @@ func TestExtractTags(t *testing.T) {
entries := []ContextEntry{
{Content: "我喜欢喝咖啡和编程"},
}
tags := extractTags(entries, func(s string) string { return s }, nil)
tags := extractTags(entries, func(s string) string { return s }, nil, nil)
if len(tags) == 0 {
t.Error("should extract tags")
}
@ -384,7 +384,7 @@ func TestSummarizeEntriesWithToolCleanFn(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
summary := summarizeEntries([]ContextEntry{entry}, func(s string) string { return s }, tc.toolCleanFn)
summary := summarizeEntries([]ContextEntry{entry}, func(s string) string { return s }, tc.toolCleanFn, nil)
for _, w := range tc.wantTopics {
if !contains(summary, w) {
t.Errorf("summary should contain %q, got: %s", w, summary)
@ -410,7 +410,7 @@ func TestExtractTagsWithToolCleanFn(t *testing.T) {
}
// toolCleanFn 返回 "" → NoMemory工具输出被跳过
tagsSkip := extractTags(entries, func(s string) string { return s }, func(name, output string) string { return "" })
tagsSkip := extractTags(entries, func(s string) string { return s }, func(name, output string) string { return "" }, nil)
for _, tag := range tagsSkip {
if tag == "编程" || tag == "咖啡" {
t.Errorf("NoMemory tool should not contribute keywords, got tag: %s", tag)
@ -418,7 +418,7 @@ func TestExtractTagsWithToolCleanFn(t *testing.T) {
}
// toolCleanFn 返回清洗文本 → 用清洗后内容提取关键词
tagsClean := extractTags(entries, func(s string) string { return s }, func(name, output string) string { return "咖啡 编程" })
tagsClean := extractTags(entries, func(s string) string { return s }, func(name, output string) string { return "咖啡 编程" }, nil)
found := false
for _, tag := range tagsClean {
if tag == "编程" {
@ -456,7 +456,7 @@ func TestContextToDocContentPreservesRawToolOutput(t *testing.T) {
cleaner := func(name, output string) string {
return "天气 温度"
}
doc, err := s.ContextToDoc("test", entries, nil, nil, cleaner)
doc, err := s.ContextToDoc("test", entries, nil, nil, cleaner, nil)
if err != nil {
t.Fatal(err)
}

View File

@ -271,19 +271,31 @@ func (e *StaticEmbedder) tokenize(text string) []string {
if e.jieba == nil {
return nil
}
words := e.jieba.Cut(text, false)
tagged := e.jieba.Tag(text)
var result []string
seen := make(map[string]bool)
for _, w := range words {
w = strings.TrimSpace(w)
if w == "" || e.stopWords[w] || seen[w] {
for _, t := range tagged {
idx := strings.LastIndex(t, "/")
if idx < 0 {
continue
}
if utf8.RuneCountInString(w) < 2 {
word := t[:idx]
tag := t[idx+1:]
word = strings.TrimSpace(word)
if word == "" || seen[word] {
continue
}
seen[w] = true
result = append(result, w)
if e.stopWords[word] {
continue
}
if utf8.RuneCountInString(word) < 2 {
continue
}
if !contentPOS[tag] {
continue
}
seen[word] = true
result = append(result, word)
}
return result
}

View File

@ -191,12 +191,18 @@ func (v *TFIDFVectorizer) Vectorize(text string) Vector {
vec := make(Vector)
for f, count := range tf {
tfNorm := count / maxTF
idf := 1.0
if v.totalDocs > 0 {
df := v.docFreq[f]
if df > 0 {
idf = math.Log(float64(v.totalDocs+1)/df+1) + 1
}
if v.totalDocs < 3 {
vec[f] = tfNorm
continue
}
df := v.docFreq[f]
if df <= 0 {
continue
}
// 平滑 IDF高频词趋近 0低频词趋近 log(N)
idf := math.Log(float64(v.totalDocs+1) / (df + 1))
if idf < 0.1 {
continue
}
vec[f] = tfNorm * idf
}

View File

@ -149,7 +149,7 @@ func TestStoreInsertAndSearch(t *testing.T) {
func TestStoreRemove(t *testing.T) {
s := NewStore()
v := NewTFIDFVectorizer(NGramTokenizer(1))
v.Train([]string{"a"})
v.Train([]string{"hello world", "hello a", "foo bar", "baz qux", "test doc"})
s.Insert("1", "a", v.Vectorize("a"), nil)
s.Insert("2", "a", v.Vectorize("a"), nil)

View File

@ -320,7 +320,7 @@ func go_core_dispatch(methodID C.int, ctx unsafe.Pointer, s1, s2, s3 *C.char, i1
case 3: // CORE_REGISTER_OUTPUT_CH
pid := pluginID
chName := a1
s.RegisterOutputChannel(chName, n1, a2, func(args map[string]interface{}) (interface{}, error) {
s.RegisterOutputChannel(chName, n1, a2, sdk.ChannelDef{}, func(args map[string]interface{}) (interface{}, error) {
// Output is async: return immediately, send in background
// to avoid nested cgo calls (cgo within cgo can crash)
go func() {

View File

@ -121,6 +121,7 @@ type channelDevice struct {
desc string
caps agentIO.OutputCapability
handler sdk.ToolHandler
chDef agentIO.ChannelDef
}
func (d *channelDevice) Name() string { return d.name }
@ -133,6 +134,7 @@ func (d *channelDevice) Tools() []agentIO.ToolDef { return nil }
func (d *channelDevice) Execute(tool string, args map[string]interface{}) (interface{}, error) {
return d.handler(args)
}
func (d *channelDevice) ChannelDef() agentIO.ChannelDef { return d.chDef }
func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
sett := sdk.NewSettings(name, r.cfgReg)
@ -152,7 +154,7 @@ func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
regAPI = func(name string) error { return nil }
}
regOutput := func(chName string, caps int, desc string, handler sdk.ToolHandler) error {
regOutput := func(chName string, caps int, desc string, def sdk.ChannelDef, handler sdk.ToolHandler) error {
if r.iom == nil {
return nil
}
@ -161,9 +163,18 @@ func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
caps: agentIO.OutputCapability(caps),
desc: desc,
handler: handler,
chDef: agentIO.ChannelDef(def),
})
}
regInput := func(name string, def sdk.ChannelDef) error {
if r.iom == nil {
return nil
}
r.iom.RegisterInputChannel(name, agentIO.ChannelDef(def))
return nil
}
return sdk.New(name, sdk.SDKConfig{
IOManager: r.iom,
EventBus: r.evBus,
@ -177,6 +188,7 @@ func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
RegStage: regStage,
RegAPI: regAPI,
RegOutput: regOutput,
RegInput: regInput,
})
}

View File

@ -190,7 +190,7 @@ func (r *ChannelRegistry) Dispatch(data json.RawMessage, pluginName string, sp *
caps = 1
}
desc := fmt.Sprintf("OC channel %s (from %s)", chName, pn)
s.RegisterOutputChannel(chName, caps, desc, func(args map[string]interface{}) (interface{}, error) {
s.RegisterOutputChannel(chName, caps, desc, sdk.ChannelDef{}, func(args map[string]interface{}) (interface{}, error) {
return sp.CallTool(chName, args)
})

View File

@ -72,7 +72,7 @@ func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
s.SetAutoRestart(true)
s.RegisterOutputChannel("cli", 1, "CLI 终端", func(args map[string]interface{}) (interface{}, error) {
s.RegisterOutputChannel("cli", 1, "CLI 终端", sdk.ChannelDef{}, func(args map[string]interface{}) (interface{}, error) {
payload, _ := args["payload"].(string)
if payload != "" {
fmt.Println(payload)

View File

@ -157,7 +157,7 @@ func (p *Plugin) Name() string { return p.name }
func (p *Plugin) Start(s *sdk.PluginSDK) error {
s.SetAutoRestart(true)
s.RegisterOutputChannel("webui", 1, "Web 控制台", func(args map[string]interface{}) (interface{}, error) {
s.RegisterOutputChannel("webui", 1, "Web 控制台", sdk.ChannelDef{}, func(args map[string]interface{}) (interface{}, error) {
payload, _ := args["payload"].(string)
if payload != "" {
p.evBus.Publish(&events.Event{

View File

@ -48,6 +48,8 @@ const (
)
type APIRegistrar = pubsdk.APIRegistrar
type OutputChannelRegistrar = pubsdk.OutputChannelRegistrar
type InputChannelRegistrar = pubsdk.InputChannelRegistrar
type ChannelDef = pubsdk.ChannelDef
type PluginSDK struct {
*pubsdk.PluginSDK
@ -92,6 +94,7 @@ type SDKConfig struct {
RegStage StageRegistrar
RegAPI APIRegistrar
RegOutput OutputChannelRegistrar
RegInput InputChannelRegistrar
}
func New(name string, cfg SDKConfig) *PluginSDK {
@ -99,6 +102,9 @@ func New(name string, cfg SDKConfig) *PluginSDK {
if cfg.IOManager != nil {
base.SetIOInjector(ioAdapter{iom: cfg.IOManager})
}
if cfg.RegInput != nil {
base.SetInputChannelRegistrar(cfg.RegInput)
}
base.SetMemoryAPI(cfg.Memory)
base.SetTextMemoryAPI(cfg.TextMemory)
base.SetDocMemoryAPI(cfg.DocMemory)

3
third_party/homeagent-sdk/go.mod vendored Normal file
View File

@ -0,0 +1,3 @@
module gitcode.com/JianFeeeee/homeagent-sdk
go 1.21.0

87
third_party/homeagent-sdk/meta/meta.go vendored Normal file
View File

@ -0,0 +1,87 @@
// Package meta 收集 HomeAgent SDK 的全部元数据。
// 版本号应与核心 meta.Version 保持一致。
// ABI 版本与 Dispatch Method ID 应与核心仓 internal/meta/meta.go 保持一致。
package meta
var (
// Version 是 HomeAgent SDK 版本号。
// 通过 `-ldflags="-X gitcode.com/JianFeeeee/homeagent-sdk/meta.Version=vX.Y.Z"` 注入。
Version = "0.7.2"
// Commit 是构建时的 Git commit hash。
Commit = "unknown"
// BuildTime 是构建时间。
BuildTime = "unknown"
// SDKName 是 SDK 名称。
SDKName = "HomeAgent SDK"
// CoreModule 是核心仓的 Go module path供 plugindev 生成 go.mod 时使用。
CoreModule = "gitcode.com/JianFeeeee/HomeAgent"
// CoreVersion 是此 SDK 所兼容的最低核心版本。
CoreVersion = "0.7.2"
)
// FullVersion 返回完整的版本字符串。
func FullVersion() string {
return SDKName + " v" + Version + " (" + Commit + ")"
}
// ---- ABI 版本(与核心仓 internal/meta/meta.go 同步) ----
// 修改时需确保核心仓与 SDK 仓的值一致。
const (
ABIVersion = 1
ABIVersionMin = 1
)
// ---- Dispatch Method IDs与核心仓 internal/meta/meta.go 同步) ----
const (
CoreRegisterTool = 1
CoreRegisterStage = 2
CoreRegisterOutputCh = 3
CoreRegisterPluginAPI = 4
CoreInjectText = 5
CoreInjectInterruptText = 6
CoreInjectTextNoMemory = 7
CoreSetAutoRestart = 8
CoreMemoryRecall = 9
CoreMemoryCommit = 10
CoreMemoryIntrospect = 11
CoreMemoryMerge = 12
CoreMemoryPurge = 13
CoreDocQuery = 14
CoreKnowledgeSearch = 15
CoreSettingsGet = 16
CoreSettingsSet = 17
CoreSettingsRegisterDef = 18
CoreLLMListSources = 19
CoreLLMSetSource = 20
CoreSocialGetPerson = 21
CoreSocialGetNetwork = 22
CoreSubscribe = 23
CoreUnsubscribe = 24
CoreFreeString = 25
CoreSettingsGetCore = 26
CoreSettingsSetCore = 27
CoreSettingsListCore = 28
CoreSettingsGetPlugin = 29
CoreSettingsSetPlugin = 30
CoreSettingsListPlugin = 31
CoreDocInsert = 32
CoreDocRemove = 33
CoreDocStats = 34
CoreKnowledgeAdd = 35
CoreKnowledgeList = 36
CoreLLMCurrentSource = 37
CoreSocialGetTrait = 38
CoreSocialGetRelations = 39
CoreSocialListPersons = 40
CoreTextMemoryAppend = 41
CoreSettingsList = 42
CoreSettingsDefs = 43
CoreSettingsDump = 44
CoreSettingsPlugins = 45
)

View File

@ -0,0 +1,14 @@
package sdk
// KnowledgeAPI provides access to the knowledge store.
type KnowledgeAPI interface {
Search(query string, topK int) ([]*Knowledge, error)
Add(name, content string) error
List() ([]string, error)
}
// Knowledge represents a knowledge entry.
type Knowledge struct {
Name string `json:"name"`
Content string `json:"content"`
}

8
third_party/homeagent-sdk/sdk/llm.go vendored Normal file
View File

@ -0,0 +1,8 @@
package sdk
// LLMAPI provides access to the LLM provider manager.
type LLMAPI interface {
ListSources() []string
SetSource(name string) error
CurrentSource() string
}

87
third_party/homeagent-sdk/sdk/memory.go vendored Normal file
View File

@ -0,0 +1,87 @@
package sdk
// MemoryAPI provides access to the graph memory (entity-relation store).
type MemoryAPI interface {
Recall(query []string, depth int) ([]Entity, []Relation, error)
Commit(triples []Triple) error
Introspect() (map[string]interface{}, error)
MergeEntities(source, target string) (int, error)
Purge(criteria map[string]string, mode string) (int, error)
}
// Entity represents a named entity in the knowledge graph.
type Entity struct {
Name string `json:"name"`
Type string `json:"type"`
MentionCount int `json:"mention_count"`
}
// Relation represents a relationship between two entities.
type Relation struct {
SourceName string `json:"source_name"`
TargetName string `json:"target_name"`
RelationType string `json:"relation_type"`
Confidence float64 `json:"confidence,omitempty"`
}
// Triple represents a subject-relation-object triple for the knowledge graph.
type Triple struct {
Subject string `json:"subject"`
Relation string `json:"relation"`
Object string `json:"object"`
Confidence float64 `json:"confidence,omitempty"`
SubjectType string `json:"subject_type,omitempty"`
ObjectType string `json:"object_type,omitempty"`
}
// TextMemoryAPI provides access to chronological text event storage.
type TextMemoryAPI interface {
Append(evt TextEvent) error
}
// TextEvent represents a single text memory event.
type TextEvent struct {
Role string `json:"role"`
Content string `json:"content"`
Timestamp int64 `json:"timestamp"`
Channel string `json:"channel,omitempty"`
}
// DocMemoryAPI provides access to the document vector store.
type DocMemoryAPI interface {
Query(text string, topK int) []*Doc
Insert(doc *Doc) error
Remove(id string)
Stats() map[string]interface{}
}
// Doc represents a document in the document store.
type Doc struct {
ID string `json:"id"`
Title string `json:"title"`
Content string `json:"content"`
Score float64 `json:"score,omitempty"`
}
// SocialAPI provides read-only access to the social graph (person profiles and relationships).
// External plugins can query person traits and social networks but cannot modify them.
type SocialAPI interface {
GetPerson(name string) (*PersonProfile, error)
GetTrait(name, trait string) (string, bool)
GetRelations(name string) ([]SocialRelation, error)
GetNetwork(name string, depth int) ([]*PersonProfile, error)
ListPersons() ([]string, error)
}
// PersonProfile represents a person's complete profile (traits + social relations).
type PersonProfile struct {
Name string `json:"name"`
Traits map[string]string `json:"traits,omitempty"`
Relations []SocialRelation `json:"relations,omitempty"`
}
// SocialRelation represents a social relationship between two persons.
type SocialRelation struct {
Person string `json:"person"`
Relation string `json:"relation"`
}

369
third_party/homeagent-sdk/sdk/plugin.go vendored Normal file
View File

@ -0,0 +1,369 @@
package sdk
import (
"sync"
"gitcode.com/JianFeeeee/homeagent-sdk/meta"
)
// SDKVersion 是对外暴露的 SDK 版本号。
var SDKVersion = meta.Version
// Plugin is the interface every plugin must implement.
type Plugin interface {
Name() string
Start(sdk *PluginSDK) error
Stop() error
}
// ToolHandler is a function that handles a tool call.
type ToolHandler func(args map[string]interface{}) (interface{}, error)
// StageHandler is a function that handles a pipeline stage event.
type StageHandler func(ctx *StageContext) error
// Stage represents a point in the message processing pipeline.
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"
)
// ChannelDef 描述通道在记忆计算层的行为,与 ToolDef.NoMemory/Cleaner 语义一致。
// NoMemory: 此通道输入/输出不参与记忆计算(向量化/关键词提取/蒸馏),但原文保留在上下文中
// Cleaner: 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏/存档提取关键词时调用
type ChannelDef struct {
NoMemory bool
Cleaner func(string) string
}
// StageContext provides context for stage handlers.
type StageContext struct {
mu sync.RWMutex
RawMessage string
UserID string
GroupID string
ContextMsgs []map[string]interface{}
LLMText string
ReasoningContent string
TokenUsage map[string]int
ToolCalls []ToolCall
ToolResults []ToolResult
FinalText string
Response *string
Phase Stage
Memory []MemItem
NoMemory bool
Extra map[string]interface{}
Errors []string // 阶段处理过程中的错误信息
}
func (c *StageContext) RLock() { c.mu.RLock() }
func (c *StageContext) RUnlock() { c.mu.RUnlock() }
func (c *StageContext) Lock() { c.mu.Lock() }
func (c *StageContext) Unlock() { c.mu.Unlock() }
func (c *StageContext) IsResponded() bool { c.mu.RLock(); defer c.mu.RUnlock(); return c.Response != nil }
// MemItem represents a memory item in stage context.
type MemItem struct {
Role string `json:"role"`
Content string `json:"content"`
Score float64 `json:"score"`
}
// ToolCall represents a model's request to call a tool.
type ToolCall struct {
ID string `json:"id"`
Name string `json:"name"`
Plugin string `json:"plugin,omitempty"`
Arguments map[string]interface{} `json:"arguments"`
}
// ToolResult represents the result of a tool call.
type ToolResult struct {
CallID string `json:"call_id"`
Name string `json:"name"`
Plugin string `json:"plugin,omitempty"`
Success bool `json:"success"`
Result interface{} `json:"result"`
}
// ToolDef describes a tool that the plugin exposes.
type ToolDef struct {
Name string `json:"name"`
Plugin string `json:"plugin,omitempty"`
Description string `json:"description"`
Parameters map[string]interface{} `json:"parameters"`
NoMemory bool `json:"no_memory,omitempty"` // 此工具输出不参与记忆计算,但原文保留
Cleaner func(string) string `json:"-"` // 计算层过滤函数,不改原文;仅在向量化/jieba/蒸馏时调用
}
// IOInjector provides methods for injecting input and interrupts into the agent pipeline.
// All methods accept (source, channel) where channel is the target output channel
// for routing the agent's response.
type IOInjector interface {
InjectInterruptText(source, channel, text string)
InjectText(source, channel, text string)
InjectTextNoMemory(source, channel, text string)
}
// EventType identifies the kind of system event.
type EventType string
const (
EventRawInput EventType = "raw_input"
EventAgentOutput EventType = "agent_output"
EventAgentLLMChain EventType = "agent_llm_chain"
EventToolCall EventType = "tool_call"
EventReasoning EventType = "reasoning"
EventStage EventType = "stage"
EventSystem EventType = "system"
)
// Event represents a system event published by the kernel.
type Event struct {
Type EventType `json:"type"`
Source string `json:"source"`
Payload map[string]interface{} `json:"payload"`
Timestamp int64 `json:"timestamp"`
}
// EventHandler processes a system event.
type EventHandler func(evt *Event)
// EventSubscriber allows plugins to subscribe to kernel events.
// This is a restricted interface: plugins can subscribe but the kernel
// controls which events are delivered.
type EventSubscriber interface {
Subscribe(eventType EventType, handler EventHandler) func()
}
// StageScope controls which events a stage handler receives.
type StageScope int
const (
// StageScopeGlobal receives all stage events (default).
StageScopeGlobal StageScope = 0
// StageScopeOwnTools only receives events for this plugin's own tool calls
// (before_toolcall / after_toolcall only). Other stages degrade to global.
StageScopeOwnTools StageScope = 1
)
// ToolRegistrar registers a tool dynamically.
type ToolRegistrar func(name string, def ToolDef, handler ToolHandler) error
// StageRegistrar registers a stage handler.
type StageRegistrar func(stage Stage, handler StageHandler)
// APIRegistrar registers a plugin API for external access.
type APIRegistrar func(name string) error
// InputChannelRegistrar registers an input channel with its memory behavior.
type InputChannelRegistrar func(name string, def ChannelDef) error
// OutputChannelRegistrar registers an output channel that the output_send tool can use.
type OutputChannelRegistrar func(name string, caps int, desc string, def ChannelDef, handler ToolHandler) error
// Output capability flags
const (
CapText = 1
CapFile = 2
CapImage = 4
CapAudio = 8
CapStructured = 16
)
// PluginSDK is the main API surface provided to plugins at runtime.
// It wraps tool registration, settings, memory, knowledge, LLM, and IO injection.
type PluginSDK struct {
name string
regTool ToolRegistrar
regStage StageRegistrar
regAPI APIRegistrar
regOutput OutputChannelRegistrar
regInput InputChannelRegistrar
io IOInjector
mem MemoryAPI
textMem TextMemoryAPI
docMem DocMemoryAPI
know KnowledgeAPI
llm LLMAPI
sett SettingsAPI
social SocialAPI
events EventSubscriber
autoRestart bool
}
// New creates a PluginSDK with the given dependencies.
func New(name string, sett SettingsAPI, regTool ToolRegistrar, regStage StageRegistrar, regAPI APIRegistrar, regOutput OutputChannelRegistrar) *PluginSDK {
return &PluginSDK{
name: name,
sett: sett,
regTool: regTool,
regStage: regStage,
regAPI: regAPI,
regOutput: regOutput,
autoRestart: true,
}
}
// PluginName returns the name of the plugin.
func (s *PluginSDK) PluginName() string { return s.name }
// Settings returns the settings API for reading/writing plugin configuration.
func (s *PluginSDK) Settings() SettingsAPI { return s.sett }
// Memory returns the graph memory API (may be nil if not available).
func (s *PluginSDK) Memory() MemoryAPI { return s.mem }
// TextMemory returns the text memory API (may be nil if not available).
func (s *PluginSDK) TextMemory() TextMemoryAPI { return s.textMem }
// DocMemory returns the document memory API (may be nil if not available).
func (s *PluginSDK) DocMemory() DocMemoryAPI { return s.docMem }
// Knowledge returns the knowledge store API (may be nil if not available).
func (s *PluginSDK) Knowledge() KnowledgeAPI { return s.know }
// LLM returns the LLM provider API (may be nil if not available).
func (s *PluginSDK) LLM() LLMAPI { return s.llm }
// Social returns the social graph API (may be nil if not available).
func (s *PluginSDK) Social() SocialAPI { return s.social }
// Events returns the event subscriber for listening to kernel events (may be nil if not available).
func (s *PluginSDK) Events() EventSubscriber { return s.events }
// RegisterTool registers a tool that the LLM can call.
func (s *PluginSDK) RegisterTool(name string, def ToolDef, handler ToolHandler) error {
if def.Plugin == "" {
def.Plugin = s.name
}
if s.regTool != nil {
return s.regTool(name, def, handler)
}
return nil
}
// RegisterStage registers a handler for a pipeline stage.
// scope: StageScopeGlobal (default) — receives all stage events.
// StageScopeOwnTools — only before_toolcall/after_toolcall for this plugin's tools.
func (s *PluginSDK) RegisterStage(stage Stage, handler StageHandler, scope ...StageScope) {
if s.regStage == nil {
return
}
sc := StageScopeGlobal
if len(scope) > 0 {
sc = scope[0]
}
if sc == StageScopeGlobal {
s.regStage(stage, handler)
return
}
// OwnTools scope — only for before_toolcall / after_toolcall
if stage != StageBeforeToolcall && stage != StageAfterToolcall {
s.regStage(stage, handler)
return
}
s.regStage(stage, func(ctx *StageContext) error {
ctx.RLock()
match := false
switch stage {
case StageBeforeToolcall:
match = len(ctx.ToolCalls) > 0 && ctx.ToolCalls[0].Plugin == s.name
case StageAfterToolcall:
match = len(ctx.ToolResults) > 0 && ctx.ToolResults[0].Plugin == s.name
}
ctx.RUnlock()
if !match {
return nil
}
return handler(ctx)
})
}
// RegisterPluginAPI registers this plugin's API for access by other plugins.
func (s *PluginSDK) RegisterPluginAPI(name string) error {
if s.regAPI != nil {
return s.regAPI(name)
}
return nil
}
// RegisterOutputChannel registers an output channel that the output_send tool can route to.
// name: channel name (e.g. "qq", "webui")
// caps: bitmask of supported output capabilities (CapText, CapFile, etc.)
// desc: description of the channel, expected meta format, and type enum
// def: 通道在记忆计算层的行为NoMemory/Cleaner
// handler: receives args map with keys: payload (string), type (string), meta (string|optional)
func (s *PluginSDK) RegisterOutputChannel(name string, caps int, desc string, def ChannelDef, handler ToolHandler) error {
if s.regOutput != nil {
return s.regOutput(name, caps, desc, def, handler)
}
return nil
}
// RegisterInputChannel registers an input channel with its memory behavior.
// def.NoMemory: 此通道输入不参与记忆计算
// def.Cleaner: 计算层对输入文本清洗后(不改原文)再向量化/提关键词
func (s *PluginSDK) RegisterInputChannel(name string, def ChannelDef) error {
if s.regInput != nil {
return s.regInput(name, def)
}
return nil
}
// SetOutputChannelRegistrar sets the output channel registrar (called by the core at startup).
func (s *PluginSDK) SetOutputChannelRegistrar(r OutputChannelRegistrar) { s.regOutput = r }
// SetInputChannelRegistrar sets the input channel registrar (called by the core at startup).
func (s *PluginSDK) SetInputChannelRegistrar(r InputChannelRegistrar) { s.regInput = r }
// SetIOInjector sets the IO injector (called by the core at startup).
func (s *PluginSDK) SetIOInjector(io IOInjector) { s.io = io }
// SetMemoryAPI sets the memory API (called by the core at startup).
func (s *PluginSDK) SetMemoryAPI(mem MemoryAPI) { s.mem = mem }
func (s *PluginSDK) SetTextMemoryAPI(tm TextMemoryAPI) { s.textMem = tm }
func (s *PluginSDK) SetDocMemoryAPI(dm DocMemoryAPI) { s.docMem = dm }
func (s *PluginSDK) SetKnowledgeAPI(kn KnowledgeAPI) { s.know = kn }
func (s *PluginSDK) SetLLMAPI(llm LLMAPI) { s.llm = llm }
func (s *PluginSDK) SetSocialAPI(social SocialAPI) { s.social = social }
func (s *PluginSDK) SetEventSubscriber(es EventSubscriber) { s.events = es }
// ---- IO Convenience Methods ----
// InjectInterruptText injects a text interrupt that can preempt current LLM processing.
func (s *PluginSDK) InjectInterruptText(source, channel, text string) {
if s.io != nil {
s.io.InjectInterruptText(source, channel, text)
}
}
// InjectText injects a text message into the agent pipeline.
func (s *PluginSDK) InjectText(source, channel, text string) {
if s.io != nil {
s.io.InjectText(source, channel, text)
}
}
// InjectTextNoMemory injects a text message without generating memory.
func (s *PluginSDK) InjectTextNoMemory(source, channel, text string) {
if s.io != nil {
s.io.InjectTextNoMemory(source, channel, text)
}
}
// SetAutoRestart 设置插件是否允许内核自动重启(崩溃后自动重载)。
// 默认 true。如果插件有无法恢复的状态如外部连接应设为 false。
func (s *PluginSDK) SetAutoRestart(enabled bool) { s.autoRestart = enabled }
// AutoRestart 返回插件是否允许自动重启。
func (s *PluginSDK) AutoRestart() bool { return s.autoRestart }

View File

@ -0,0 +1,253 @@
package sdk
import (
"testing"
)
func TestRegisterStageGlobalDefault(t *testing.T) {
called := false
regStage := func(stage Stage, handler StageHandler) {
called = true
}
s := &PluginSDK{regStage: regStage, name: "test"}
s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error { return nil })
if !called {
t.Error("global scope: handler not registered")
}
}
func TestRegisterStageGlobalExplicit(t *testing.T) {
called := false
regStage := func(stage Stage, handler StageHandler) {
called = true
}
s := &PluginSDK{regStage: regStage, name: "test"}
s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error { return nil }, StageScopeGlobal)
if !called {
t.Error("global scope: handler not registered")
}
}
func TestRegisterStageOwnToolsMatch(t *testing.T) {
var registered StageHandler
regStage := func(stage Stage, handler StageHandler) {
registered = handler
}
s := &PluginSDK{regStage: regStage, name: "myplugin"}
s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error { return nil }, StageScopeOwnTools)
if registered == nil {
t.Fatal("handler not registered")
}
ctx := &StageContext{}
ctx.ToolCalls = []ToolCall{{Plugin: "myplugin", Name: "my_tool"}}
ctx.ToolResults = nil
err := registered(ctx)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
}
func TestRegisterStageOwnToolsSkipOtherPlugin(t *testing.T) {
var registered StageHandler
regStage := func(stage Stage, handler StageHandler) {
registered = handler
}
s := &PluginSDK{regStage: regStage, name: "myplugin"}
callCount := 0
s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error {
callCount++
return nil
}, StageScopeOwnTools)
if registered == nil {
t.Fatal("handler not registered")
}
ctx := &StageContext{}
ctx.ToolCalls = []ToolCall{{Plugin: "other", Name: "other_tool"}}
err := registered(ctx)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if callCount != 0 {
t.Error("handler should not be called for other plugin's tool")
}
}
func TestRegisterStageOwnToolsNonToolcallDegrades(t *testing.T) {
regStage := func(stage Stage, handler StageHandler) {
if stage != StagePreAction {
t.Errorf("expected StagePreAction, got %s", stage)
}
}
s := &PluginSDK{regStage: regStage, name: "test"}
s.RegisterStage(StagePreAction, func(ctx *StageContext) error { return nil }, StageScopeOwnTools)
}
func TestRegisterStageOwnToolsStageBeforeToolcallNoToolCalls(t *testing.T) {
var registered StageHandler
regStage := func(stage Stage, handler StageHandler) {
registered = handler
}
s := &PluginSDK{regStage: regStage, name: "myplugin"}
callCount := 0
s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error {
callCount++
return nil
}, StageScopeOwnTools)
if registered == nil {
t.Fatal("handler not registered")
}
ctx := &StageContext{}
err := registered(ctx)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if callCount != 0 {
t.Error("handler should not be called when ToolCalls is empty")
}
}
func TestRegisterStageOwnToolsStageAfterToolcallMatch(t *testing.T) {
var registered StageHandler
regStage := func(stage Stage, handler StageHandler) {
registered = handler
}
s := &PluginSDK{regStage: regStage, name: "myplugin"}
callCount := 0
s.RegisterStage(StageAfterToolcall, func(ctx *StageContext) error {
callCount++
return nil
}, StageScopeOwnTools)
if registered == nil {
t.Fatal("handler not registered")
}
ctx := &StageContext{}
ctx.ToolResults = []ToolResult{{Plugin: "myplugin", Name: "my_tool"}}
err := registered(ctx)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if callCount != 1 {
t.Error("handler should be called for own plugin's tool result")
}
}
func TestRegisterStageOwnToolsStageAfterToolcallSkip(t *testing.T) {
var registered StageHandler
regStage := func(stage Stage, handler StageHandler) {
registered = handler
}
s := &PluginSDK{regStage: regStage, name: "myplugin"}
callCount := 0
s.RegisterStage(StageAfterToolcall, func(ctx *StageContext) error {
callCount++
return nil
}, StageScopeOwnTools)
ctx := &StageContext{}
ctx.ToolResults = []ToolResult{{Plugin: "other", Name: "other_tool"}}
err := registered(ctx)
if err != nil {
t.Errorf("expected nil, got %v", err)
}
if callCount != 0 {
t.Error("handler should not be called for other plugin's tool result")
}
}
func TestRegisterStageOwnToolsNilRegStage(t *testing.T) {
s := &PluginSDK{name: "test"}
s.RegisterStage(StageBeforeToolcall, func(ctx *StageContext) error { return nil }, StageScopeOwnTools)
}
func TestToolDefCleaner(t *testing.T) {
called := false
def := ToolDef{
Name: "test_clean",
Description: "A test tool with cleaner",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
Cleaner: func(output string) string {
called = true
return "cleaned:" + output
},
}
if def.Cleaner == nil {
t.Fatal("Cleaner should not be nil")
}
result := def.Cleaner("raw output")
if !called {
t.Error("Cleaner was not called")
}
if result != "cleaned:raw output" {
t.Errorf("expected 'cleaned:raw output', got '%s'", result)
}
}
func TestToolDefNoMemory(t *testing.T) {
def := ToolDef{
Name: "test_nomem",
Description: "A test tool with NoMemory",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
NoMemory: true,
}
if !def.NoMemory {
t.Error("NoMemory should be true")
}
if def.Cleaner != nil {
t.Error("Cleaner should be nil when not set")
}
}
func TestToolDefNoMemoryDefaultFalse(t *testing.T) {
def := ToolDef{
Name: "test_default",
Description: "A test tool with defaults",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
}
if def.NoMemory {
t.Error("NoMemory should default to false")
}
}
func TestToolDefRegisterPreservesNoMemory(t *testing.T) {
var capturedDef ToolDef
regTool := func(name string, def ToolDef, handler ToolHandler) error {
capturedDef = def
return nil
}
s := &PluginSDK{regTool: regTool, name: "test"}
def := ToolDef{
Name: "test_tool",
Description: "test desc",
Parameters: map[string]interface{}{"type": "object", "properties": map[string]interface{}{}},
NoMemory: true,
Cleaner: func(s string) string { return s },
}
s.RegisterTool("test_tool", def, func(args map[string]interface{}) (interface{}, error) {
return nil, nil
})
if !capturedDef.NoMemory {
t.Error("NoMemory should be preserved through RegisterTool")
}
if capturedDef.Cleaner == nil {
t.Error("Cleaner should be preserved through RegisterTool")
}
}

View File

@ -0,0 +1,58 @@
package sdk
type SettingsAPI interface {
// Get reads the plugin's own config value (config_<name> table).
Get(key string) (interface{}, error)
// Set writes a config value to the plugin's own config table.
Set(key string, value interface{}) error
// List returns all keys matching the given prefix.
List(prefix string) ([]string, error)
// GetCore reads the core config table.
GetCore(key string) (interface{}, error)
// SetCore writes to the core config table.
SetCore(key string, value interface{}) error
// ListCore lists core config keys matching the prefix.
ListCore(prefix string) ([]string, error)
// GetPlugin reads another plugin's config table.
GetPlugin(plugin, key string) (interface{}, error)
// SetPlugin writes to another plugin's config table.
SetPlugin(plugin, key string, value interface{}) error
// ListPlugin lists another plugin's config keys matching the prefix.
ListPlugin(plugin, prefix string) ([]string, error)
// RegisterDef registers a config definition for UI display.
RegisterDef(def ConfigDef)
// Defs returns config definitions matching the prefix.
Defs(prefix string) []*ConfigDef
// Dump returns all config values.
Dump() map[string]interface{}
// Plugins returns a list of all plugin config namespaces.
Plugins() []string
}
// ConfigDef describes a configuration field for the WebUI.
type ConfigDef struct {
Key string `json:"key"`
Default interface{} `json:"default,omitempty"`
Type string `json:"type"`
DisplayName string `json:"display_name"`
Description string `json:"description,omitempty"`
Category string `json:"category,omitempty"`
Options []string `json:"options,omitempty"`
Min float64 `json:"min,omitempty"`
Max float64 `json:"max,omitempty"`
Step float64 `json:"step,omitempty"`
Required bool `json:"required,omitempty"`
Secret bool `json:"secret,omitempty"`
}