mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
- 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
275 lines
6.0 KiB
Go
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
|
|
}
|