Files
HomeAgent/internal/knowledge/knowledge.go
root bc26850b50 feat: complete HomeAgent architecture v2
- IO abstraction layer with OutputChannel routing and capability validation
- Three-layer memory (Context-Document-Graph) with TF-IDF relevance pruning
- OneBot V11 QQ protocol plugin with Reverse WebSocket client
- Plugin system with hot-reload (SKILL.md + native factories)
- Knowledge system with TF-IDF vector indexing
- Personality system (personal.md)
- Text memory (JSONL with rotation)
- Change tracker (overlayfs) with rollback
- Lua adapter VM
- Design document (DESIGN.md)

Module: gitcode.com/JianFeeeee/HomeAgent
2026-07-02 12:04:36 +08:00

275 lines
6.0 KiB
Go

package knowledge
import (
"fmt"
"log"
"os"
"path/filepath"
"sort"
"strings"
"sync"
"time"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
)
// Knowledge — 单条知识
type Knowledge struct {
Name string `json:"name"`
Content string `json:"content"`
Path string `json:"path"`
Tags []string `json:"tags"`
UpdatedAt time.Time `json:"updated_at"`
Meta map[string]string `json:"meta,omitempty"`
}
// Store — 知识库,文件系统 + 向量索引
type Store struct {
root string
vec *vector.Store
veczer *vector.TFIDFVectorizer
mu sync.RWMutex
items map[string]*Knowledge
summaries []string
}
func NewStore(root string) *Store {
return &Store{
root: root,
vec: vector.NewStore(),
veczer: vector.NewTFIDFVectorizer(3),
items: make(map[string]*Knowledge),
}
}
func (s *Store) Start() error {
if err := os.MkdirAll(s.root, 0755); err != nil {
return fmt.Errorf("knowledge root: %w", err)
}
if err := s.scanAll(); err != nil {
log.Printf("[knowledge] scan error: %v", err)
}
log.Printf("[knowledge] started with %d items, %d vectors", len(s.items), s.vec.Size())
return nil
}
func (s *Store) Stop() {}
// Search — 向量查询知识
func (s *Store) Search(query string, topK int) []*Knowledge {
s.mu.RLock()
defer s.mu.RUnlock()
if topK <= 0 {
topK = 5
}
vec := s.veczer.Vectorize(query)
results := s.vec.Search(vec, topK)
var out []*Knowledge
for _, r := range results {
if k, ok := s.items[r.ID]; ok {
out = append(out, k)
}
}
return out
}
// Add — 添加或更新知识
func (s *Store) Add(name, content string) error {
s.mu.Lock()
defer s.mu.Unlock()
// 创建知识目录
dir := filepath.Join(s.root, sanitize(name))
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("create knowledge dir: %w", err)
}
// 写入知识文件
path := filepath.Join(dir, "content.md")
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
return fmt.Errorf("write knowledge: %w", err)
}
now := time.Now()
k := &Knowledge{
Name: name,
Content: content,
Path: path,
Tags: extractKeywords(name + " " + content),
UpdatedAt: now,
}
// 生成 ID = 目录名
id := sanitize(name)
s.items[id] = k
vec := s.veczer.Vectorize(name + " " + content)
s.vec.Insert(id, name+": "+content, vec, map[string]string{
"name": name, "path": path,
})
s.summaries = append(s.summaries, name+" "+content)
log.Printf("[knowledge] added: %s (%d bytes)", name, len(content))
return nil
}
// SearchCategories — 返回所有知识类别
func (s *Store) SearchCategories(query string, topK int) []string {
s.mu.RLock()
defer s.mu.RUnlock()
if query == "" {
var names []string
for _, k := range s.items {
names = append(names, k.Name)
}
sort.Strings(names)
if len(names) > topK {
names = names[:topK]
}
return names
}
vec := s.veczer.Vectorize(query)
results := s.vec.Search(vec, topK)
var names []string
for _, r := range results {
if k, ok := s.items[r.ID]; ok {
names = append(names, k.Name)
}
}
return names
}
func (s *Store) Remove(name string) error {
s.mu.Lock()
defer s.mu.Unlock()
id := sanitize(name)
dir := filepath.Join(s.root, id)
if err := os.RemoveAll(dir); err != nil {
return err
}
delete(s.items, id)
s.vec.Remove(id)
return nil
}
func (s *Store) Stats() map[string]interface{} {
s.mu.RLock()
defer s.mu.RUnlock()
return map[string]interface{}{
"knowledge_count": len(s.items),
"vector_count": s.vec.Size(),
"root": s.root,
}
}
func (s *Store) List() []string {
s.mu.RLock()
defer s.mu.RUnlock()
var names []string
for _, k := range s.items {
names = append(names, k.Name)
}
sort.Strings(names)
return names
}
// ——— internal ———
func (s *Store) scanAll() error {
entries, err := os.ReadDir(s.root)
if err != nil {
return err
}
for _, entry := range entries {
if !entry.IsDir() {
continue
}
dir := filepath.Join(s.root, entry.Name())
contentPath := filepath.Join(dir, "content.md")
data, err := os.ReadFile(contentPath)
if err != nil {
continue
}
name := entry.Name()
content := string(data)
now := time.Now()
k := &Knowledge{
Name: name,
Content: content,
Path: contentPath,
Tags: extractKeywords(name + " " + content),
UpdatedAt: now,
}
s.items[name] = k
s.summaries = append(s.summaries, name+" "+content)
}
// 训练向量化器
if len(s.summaries) > 0 {
s.veczer.Train(s.summaries)
}
// 构建向量索引
for _, k := range s.items {
vec := s.veczer.Vectorize(k.Name + " " + k.Content)
s.vec.Insert(k.Name, k.Name+": "+k.Content, vec, map[string]string{
"name": k.Name, "path": k.Path,
})
}
return nil
}
func sanitize(name string) string {
name = strings.ToLower(name)
name = strings.TrimSpace(name)
name = strings.ReplaceAll(name, " ", "_")
name = strings.ReplaceAll(name, "/", "_")
name = strings.ReplaceAll(name, "\\", "_")
return name
}
func extractKeywords(text string) []string {
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,
}
var keywords []string
runes := []rune(text)
seen := make(map[string]bool)
// bi-gram
for i := 0; i < len(runes)-1; i++ {
word := string(runes[i : i+2])
if !stopWords[word] && len(strings.TrimSpace(word)) == len(word) && !seen[word] {
seen[word] = true
keywords = append(keywords, word)
}
}
if len(keywords) > 10 {
keywords = keywords[:10]
}
return keywords
}