mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 01:18:08 +00:00
重构: 插件自注册 + .so 动态加载 + 中断打断机制
- 所有内置插件 init() 自注册 (plugin.RegisterFactory), 移除 main.go 硬编码 - 新增 .so 动态加载器 (internal/plugin/dynamic.go), 插件可编译为 plugin.so - 新增 plugin.json 元数据 (internal/plugin/manifest.go) - 新增 interceptLoop 独立 goroutine: (a) cancelLLM() 取消进行中的 HTTP 请求 (b) interceptCh → drainInterrupt() 注入 [打断消息] 到 LLM 上下文 (c) InjectInput 空闲时触发新处理循环 - 新增 internal/plugins/all.go 空白导入触发所有内置插件 init() - internal/sdk/ 作为 PluginSDK 正式 Go API - internal/api/ → internal/plugins/webui/ 迁移 - 删除旧 cmd/cli/, 使用 cmd/waiter/ 替代 - 更新 PLAN.md / ARCHITECTURE.md / README.md 文档
This commit is contained in:
@ -131,6 +131,30 @@ func (s *Store) ContextToDoc(source string, entries []ContextEntry) (*Doc, error
|
||||
return doc, nil
|
||||
}
|
||||
|
||||
// Consume — 向量相似度查询并移除文档(召回后即从冷存储删除,避免重复记忆)
|
||||
func (s *Store) Consume(text string, topK int) []*Doc {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if topK <= 0 {
|
||||
topK = 5
|
||||
}
|
||||
|
||||
vec := s.veczer.Vectorize(text)
|
||||
results := s.vec.Search(vec, topK)
|
||||
|
||||
var docs []*Doc
|
||||
for _, r := range results {
|
||||
if d, ok := s.docs[r.ID]; ok {
|
||||
delete(s.docs, r.ID)
|
||||
s.vec.Remove(r.ID)
|
||||
s.dirty = true
|
||||
docs = append(docs, d)
|
||||
}
|
||||
}
|
||||
return docs
|
||||
}
|
||||
|
||||
// Query — 向量相似度查询文档
|
||||
func (s *Store) Query(text string, topK int) []*Doc {
|
||||
s.mu.RLock()
|
||||
|
||||
@ -14,14 +14,25 @@ type Indexer struct {
|
||||
vec *vector.Store
|
||||
veczer *vector.TFIDFVectorizer
|
||||
mu sync.RWMutex
|
||||
trained bool
|
||||
trained bool
|
||||
recalled map[string]bool // 已通过工具调用显式召回的实体名,自动注入时跳过
|
||||
}
|
||||
|
||||
func NewIndexer(db *GraphDB) *Indexer {
|
||||
return &Indexer{
|
||||
db: db,
|
||||
vec: vector.NewStore(),
|
||||
veczer: vector.NewTFIDFVectorizer(2),
|
||||
db: db,
|
||||
vec: vector.NewStore(),
|
||||
veczer: vector.NewTFIDFVectorizer(2),
|
||||
recalled: make(map[string]bool),
|
||||
}
|
||||
}
|
||||
|
||||
// MarkRecalled 标记实体名已被工具调用显式召回,后续自动注入时跳过
|
||||
func (idx *Indexer) MarkRecalled(names ...string) {
|
||||
idx.mu.Lock()
|
||||
defer idx.mu.Unlock()
|
||||
for _, name := range names {
|
||||
idx.recalled[name] = true
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,15 +111,25 @@ func (idx *Indexer) BuildContext(userInput string) *InjectedContext {
|
||||
return &InjectedContext{Summary: ""}
|
||||
}
|
||||
|
||||
// 过滤已被工具调用显式召回的实体,避免重复注入
|
||||
idx.mu.RLock()
|
||||
filtered := result.Entities[:0]
|
||||
for _, e := range result.Entities {
|
||||
if !idx.recalled[e.Name] {
|
||||
filtered = append(filtered, e)
|
||||
}
|
||||
}
|
||||
idx.mu.RUnlock()
|
||||
|
||||
ctx := &InjectedContext{
|
||||
Entities: result.Entities,
|
||||
Entities: filtered,
|
||||
Relations: nil,
|
||||
}
|
||||
|
||||
if len(result.Entities) > 0 {
|
||||
summary := buildIndexSummary(result.Entities)
|
||||
if len(filtered) > 0 {
|
||||
summary := buildIndexSummary(filtered)
|
||||
ctx.Summary = summary
|
||||
ctx.TokenEstimate = estimateTokens(summary) + len(result.Entities)*8
|
||||
ctx.TokenEstimate = estimateTokens(summary) + len(filtered)*8
|
||||
} else {
|
||||
ctx.Summary = ""
|
||||
}
|
||||
|
||||
298
internal/memory/social/social.go
Normal file
298
internal/memory/social/social.go
Normal file
@ -0,0 +1,298 @@
|
||||
package social
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
||||
)
|
||||
|
||||
const (
|
||||
entityTypePerson = "person"
|
||||
entityTypeTrait = "trait_value"
|
||||
traitPrefix = "trait:"
|
||||
)
|
||||
|
||||
type PersonProfile struct {
|
||||
Name string `json:"name"`
|
||||
Traits map[string]string `json:"traits,omitempty"`
|
||||
Relations []SocialRelation `json:"relations,omitempty"`
|
||||
}
|
||||
|
||||
type SocialRelation struct {
|
||||
Person string `json:"person"`
|
||||
Relation string `json:"relation"` // 关系类型:朋友/家人/同事/邻居/...
|
||||
}
|
||||
|
||||
type SocialStore struct {
|
||||
db *memory.GraphDB
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func New(db *memory.GraphDB) *SocialStore {
|
||||
return &SocialStore{db: db}
|
||||
}
|
||||
|
||||
// GetPerson 获取人物完整档案(特质 + 社交关系)
|
||||
func (s *SocialStore) GetPerson(name string) (*PersonProfile, error) {
|
||||
if s.db == nil {
|
||||
return nil, fmt.Errorf("social store not available")
|
||||
}
|
||||
|
||||
result, err := s.db.Recall([]string{name}, nil, 2, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
profile := &PersonProfile{
|
||||
Name: name,
|
||||
Traits: make(map[string]string),
|
||||
}
|
||||
|
||||
// 查找指定 person 的 ID
|
||||
var personID int64
|
||||
for _, e := range result.Entities {
|
||||
if e.Name == name {
|
||||
personID = e.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
if personID == 0 {
|
||||
return nil, fmt.Errorf("person '%s' not found", name)
|
||||
}
|
||||
|
||||
// 区分 trait 关系和社交关系
|
||||
for _, r := range result.Relations {
|
||||
if strings.HasPrefix(r.RelationType, traitPrefix) {
|
||||
// 特质:trait:<特质名>
|
||||
traitName := strings.TrimPrefix(r.RelationType, traitPrefix)
|
||||
if r.SourceID == personID {
|
||||
profile.Traits[traitName] = r.TargetName
|
||||
} else {
|
||||
profile.Traits[traitName] = r.SourceName
|
||||
}
|
||||
} else if r.SourceID == personID {
|
||||
profile.Relations = append(profile.Relations, SocialRelation{
|
||||
Person: r.TargetName,
|
||||
Relation: r.RelationType,
|
||||
})
|
||||
} else if r.TargetID == personID {
|
||||
profile.Relations = append(profile.Relations, SocialRelation{
|
||||
Person: r.SourceName,
|
||||
Relation: r.RelationType,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// SetTrait 设置/更新人物特质。如果同名特质已存在则覆盖
|
||||
func (s *SocialStore) SetTrait(name, trait, value string) error {
|
||||
if s.db == nil {
|
||||
return fmt.Errorf("social store not available")
|
||||
}
|
||||
|
||||
// 先清除旧特质值
|
||||
oldVal, found := s.GetTrait(name, trait)
|
||||
if found && oldVal != "" {
|
||||
s.db.Purge(map[string]string{
|
||||
"subject_contains": name,
|
||||
"relation_type": traitPrefix + trait,
|
||||
}, "soft")
|
||||
}
|
||||
|
||||
triples := []memory.Triple{
|
||||
{
|
||||
Subject: name,
|
||||
SubjectType: entityTypePerson,
|
||||
Relation: traitPrefix + trait,
|
||||
Object: value,
|
||||
ObjectType: entityTypeTrait,
|
||||
Confidence: 1.0,
|
||||
},
|
||||
}
|
||||
_, _, err := s.db.Commit(triples, "social_trait", 0)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetTrait 获取指定人物的指定特质值
|
||||
func (s *SocialStore) GetTrait(name, trait string) (string, bool) {
|
||||
if s.db == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
result, err := s.db.Recall([]string{name}, nil, 1, "")
|
||||
if err != nil || result == nil {
|
||||
return "", false
|
||||
}
|
||||
|
||||
var personID int64
|
||||
for _, e := range result.Entities {
|
||||
if e.Name == name {
|
||||
personID = e.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
if personID == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
for _, r := range result.Relations {
|
||||
if r.RelationType == traitPrefix+trait {
|
||||
if r.SourceID == personID {
|
||||
return r.TargetName, true
|
||||
}
|
||||
return r.SourceName, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// AddRelation 建立两人之间的社交关系
|
||||
func (s *SocialStore) AddRelation(personA, relation, personB string) error {
|
||||
if s.db == nil {
|
||||
return fmt.Errorf("social store not available")
|
||||
}
|
||||
|
||||
triples := []memory.Triple{
|
||||
{
|
||||
Subject: personA,
|
||||
SubjectType: entityTypePerson,
|
||||
Relation: relation,
|
||||
Object: personB,
|
||||
ObjectType: entityTypePerson,
|
||||
Confidence: 1.0,
|
||||
},
|
||||
}
|
||||
_, _, err := s.db.Commit(triples, "social_relation", 0)
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveRelation 删除两人之间的社交关系
|
||||
func (s *SocialStore) RemoveRelation(personA, relation, personB string) error {
|
||||
if s.db == nil {
|
||||
return fmt.Errorf("social store not available")
|
||||
}
|
||||
|
||||
_, err := s.db.Purge(map[string]string{
|
||||
"subject_contains": personA,
|
||||
"target_contains": personB,
|
||||
"relation_type": relation,
|
||||
}, "soft")
|
||||
return err
|
||||
}
|
||||
|
||||
// GetRelations 获取指定人物的所有社交关系
|
||||
func (s *SocialStore) GetRelations(name string) ([]SocialRelation, error) {
|
||||
if s.db == nil {
|
||||
return nil, fmt.Errorf("social store not available")
|
||||
}
|
||||
|
||||
result, err := s.db.Recall([]string{name}, nil, 1, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var personID int64
|
||||
for _, e := range result.Entities {
|
||||
if e.Name == name {
|
||||
personID = e.ID
|
||||
break
|
||||
}
|
||||
}
|
||||
if personID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var relations []SocialRelation
|
||||
for _, r := range result.Relations {
|
||||
if strings.HasPrefix(r.RelationType, traitPrefix) {
|
||||
continue
|
||||
}
|
||||
if r.SourceID == personID {
|
||||
relations = append(relations, SocialRelation{Person: r.TargetName, Relation: r.RelationType})
|
||||
} else if r.TargetID == personID {
|
||||
relations = append(relations, SocialRelation{Person: r.SourceName, Relation: r.RelationType})
|
||||
}
|
||||
}
|
||||
return relations, nil
|
||||
}
|
||||
|
||||
// GetNetwork 获取指定人物周围 depth 度的社交网络
|
||||
func (s *SocialStore) GetNetwork(name string, depth int) ([]*PersonProfile, error) {
|
||||
if s.db == nil {
|
||||
return nil, fmt.Errorf("social store not available")
|
||||
}
|
||||
|
||||
// 用 Recall 的 BFS 遍历获取多度关联
|
||||
result, err := s.db.Recall([]string{name}, nil, depth, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
personMap := make(map[int64]*PersonProfile)
|
||||
for _, e := range result.Entities {
|
||||
p := &PersonProfile{
|
||||
Name: e.Name,
|
||||
Traits: make(map[string]string),
|
||||
}
|
||||
personMap[e.ID] = p
|
||||
}
|
||||
|
||||
for _, r := range result.Relations {
|
||||
if strings.HasPrefix(r.RelationType, traitPrefix) {
|
||||
traitName := strings.TrimPrefix(r.RelationType, traitPrefix)
|
||||
if p, ok := personMap[r.SourceID]; ok {
|
||||
p.Traits[traitName] = r.TargetName
|
||||
}
|
||||
if p, ok := personMap[r.TargetID]; ok {
|
||||
p.Traits[traitName] = r.SourceName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 收集关系
|
||||
for _, r := range result.Relations {
|
||||
if strings.HasPrefix(r.RelationType, traitPrefix) {
|
||||
continue
|
||||
}
|
||||
sr := SocialRelation{Relation: r.RelationType}
|
||||
if p, ok := personMap[r.SourceID]; ok {
|
||||
sr.Person = r.TargetName
|
||||
p.Relations = append(p.Relations, sr)
|
||||
}
|
||||
sr = SocialRelation{Relation: r.RelationType}
|
||||
if p, ok := personMap[r.TargetID]; ok {
|
||||
sr.Person = r.SourceName
|
||||
p.Relations = append(p.Relations, sr)
|
||||
}
|
||||
}
|
||||
|
||||
var profiles []*PersonProfile
|
||||
for _, p := range personMap {
|
||||
profiles = append(profiles, p)
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
// ListPersons 列出所有已知人物(entity.type = person)
|
||||
func (s *SocialStore) ListPersons() ([]string, error) {
|
||||
if s.db == nil {
|
||||
return nil, fmt.Errorf("social store not available")
|
||||
}
|
||||
|
||||
result, err := s.db.Recall(nil, nil, 1, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var names []string
|
||||
for _, e := range result.Entities {
|
||||
if e.Type == entityTypePerson || e.Type == "Person" {
|
||||
names = append(names, e.Name)
|
||||
}
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
@ -227,6 +227,148 @@ func (m *Memory) FileCount() int {
|
||||
return len(files)
|
||||
}
|
||||
|
||||
// PurgeByFilter 删除所有满足 filter 函数的事件(重写所有 JSONL 文件)
|
||||
func (m *Memory) PurgeByFilter(filter func(Event) bool) (int, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// 关闭当前文件,准备重建
|
||||
if m.current != nil {
|
||||
m.current.Close()
|
||||
m.current = nil
|
||||
}
|
||||
|
||||
files, err := m.listFiles()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
totalRemoved := 0
|
||||
for _, fpath := range files {
|
||||
kept, removed, err := m.purgeFile(fpath, filter)
|
||||
if err != nil {
|
||||
log.Printf("[text memory] purge file %s: %v", fpath, err)
|
||||
continue
|
||||
}
|
||||
totalRemoved += removed
|
||||
|
||||
if len(kept) == 0 {
|
||||
os.Remove(fpath)
|
||||
} else if removed > 0 {
|
||||
m.rewriteFile(fpath, kept)
|
||||
}
|
||||
}
|
||||
|
||||
// 重新打开当前文件
|
||||
m.openCurrent()
|
||||
return totalRemoved, nil
|
||||
}
|
||||
|
||||
// ReplaceByFilter 替换所有满足 filter 的事件(通过 replace 函数修改),重写文件
|
||||
func (m *Memory) ReplaceByFilter(filter func(Event) bool, replace func(Event) Event) (int, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.current != nil {
|
||||
m.current.Close()
|
||||
m.current = nil
|
||||
}
|
||||
|
||||
files, err := m.listFiles()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
totalReplaced := 0
|
||||
for _, fpath := range files {
|
||||
events, replaced, err := m.replaceFile(fpath, filter, replace)
|
||||
if err != nil {
|
||||
log.Printf("[text memory] replace file %s: %v", fpath, err)
|
||||
continue
|
||||
}
|
||||
totalReplaced += replaced
|
||||
|
||||
if len(events) == 0 {
|
||||
os.Remove(fpath)
|
||||
} else if replaced > 0 {
|
||||
m.rewriteFile(fpath, events)
|
||||
}
|
||||
}
|
||||
|
||||
m.openCurrent()
|
||||
return totalReplaced, nil
|
||||
}
|
||||
|
||||
// ——— internal helpers ———
|
||||
|
||||
func (m *Memory) purgeFile(path string, filter func(Event) bool) (kept []Event, removed int, err error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var evt Event
|
||||
if err := json.Unmarshal([]byte(line), &evt); err != nil {
|
||||
continue
|
||||
}
|
||||
if filter(evt) {
|
||||
removed++
|
||||
} else {
|
||||
kept = append(kept, evt)
|
||||
}
|
||||
}
|
||||
return kept, removed, scanner.Err()
|
||||
}
|
||||
|
||||
func (m *Memory) replaceFile(path string, filter func(Event) bool, replace func(Event) Event) (events []Event, replaced int, err error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
scanner := bufio.NewScanner(f)
|
||||
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var evt Event
|
||||
if err := json.Unmarshal([]byte(line), &evt); err != nil {
|
||||
continue
|
||||
}
|
||||
if filter(evt) {
|
||||
evt = replace(evt)
|
||||
replaced++
|
||||
}
|
||||
events = append(events, evt)
|
||||
}
|
||||
return events, replaced, scanner.Err()
|
||||
}
|
||||
|
||||
func (m *Memory) rewriteFile(path string, events []Event) {
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
log.Printf("[text memory] rewrite %s: %v", path, err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
enc := json.NewEncoder(f)
|
||||
for _, evt := range events {
|
||||
enc.Encode(evt)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Memory) Stats() map[string]interface{} {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user