mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-23 02:18:06 +00:00
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
This commit is contained in:
274
internal/knowledge/knowledge.go
Normal file
274
internal/knowledge/knowledge.go
Normal file
@ -0,0 +1,274 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user