mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 09:58:06 +00:00
- P0-1: ProviderError type + ReportStatus for precise 401/403 detection - P0-2: Remove -config flag from deploy/homeagent.service - P2-1: 5s debounce on context.go Save() - P2-2→C1: Delete output_set_channel entirely - P2-3: Extract mediaDataURL/mediaChat helpers - P2-4: Dedup defaultSources var - P3: Delete dead packages (embed/tokenizer/container/snapshot) - P3: Delete dead functions (messagesToMap, RunStageAll) - CL: Update .gitignore, docs, Makefile, gojieba removal - Config: Delete config/config.yaml, update docs - Arch: Remove EmitOutputTo from emitResponse - CL-1: go.work 1.19→1.21 - Docs: Add Mermaid architecture diagrams to README - Docs: Add kernel-rebuild requires plugin-rebuild note to PLUGIN_DEV.md
274 lines
6.4 KiB
Go
274 lines
6.4 KiB
Go
package core
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
||
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
|
||
)
|
||
|
||
// ContextEvent — 单条上下文事件
|
||
type ContextEvent struct {
|
||
Timestamp time.Time `json:"timestamp"`
|
||
Source string `json:"source"`
|
||
Input string `json:"input"`
|
||
Response string `json:"response,omitempty"`
|
||
ToolsUsed []string `json:"tools_used,omitempty"`
|
||
Vector vector.Vector `json:"-"` // 缓存向量,避免重复计算
|
||
}
|
||
|
||
const contextFlushInterval = 5 * time.Second
|
||
|
||
// RelevanceContext — 基于相关性的上下文管理,非固定阈值
|
||
type RelevanceContext struct {
|
||
mu sync.Mutex
|
||
events []*ContextEvent
|
||
veczer *vector.TFIDFVectorizer
|
||
trained bool
|
||
savePath string // 持久化路径,空则不持久化
|
||
saveTimer *time.Timer
|
||
dirty bool
|
||
}
|
||
|
||
func NewRelevanceContext(savePath string) *RelevanceContext {
|
||
rc := &RelevanceContext{
|
||
veczer: vector.NewTFIDFVectorizer(2),
|
||
savePath: savePath,
|
||
}
|
||
if savePath != "" {
|
||
rc.load()
|
||
}
|
||
return rc
|
||
}
|
||
|
||
// load 从文件恢复上下文事件
|
||
func (c *RelevanceContext) load() {
|
||
data, err := os.ReadFile(c.savePath)
|
||
if err != nil {
|
||
return
|
||
}
|
||
var events []*ContextEvent
|
||
if err := json.Unmarshal(data, &events); err != nil {
|
||
return
|
||
}
|
||
for _, evt := range events {
|
||
evt.Vector = c.veczer.Vectorize(evt.Input + " " + evt.Response)
|
||
}
|
||
c.events = events
|
||
}
|
||
|
||
// Save 持久化上下文事件到文件
|
||
func (c *RelevanceContext) Save() error {
|
||
if c.savePath == "" {
|
||
return nil
|
||
}
|
||
if err := os.MkdirAll(filepath.Dir(c.savePath), 0755); err != nil {
|
||
return err
|
||
}
|
||
data, err := json.Marshal(c.events)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return os.WriteFile(c.savePath, data, 0644)
|
||
}
|
||
|
||
func (c *RelevanceContext) Append(evt ContextEvent) {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
evt.Vector = c.veczer.Vectorize(evt.Input + " " + evt.Response)
|
||
c.events = append(c.events, &evt)
|
||
|
||
// 增量训练向量化器
|
||
c.trained = false
|
||
|
||
c.save()
|
||
}
|
||
|
||
// save 无锁版本,Append/Prune 内部持有锁时调用。带 debounce,每 5s 写一次盘。
|
||
func (c *RelevanceContext) save() error {
|
||
if c.savePath == "" {
|
||
return nil
|
||
}
|
||
if !c.dirty {
|
||
c.dirty = true
|
||
if c.saveTimer == nil {
|
||
c.saveTimer = time.AfterFunc(contextFlushInterval, c.flush)
|
||
} else {
|
||
c.saveTimer.Reset(contextFlushInterval)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c *RelevanceContext) flush() {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
if !c.dirty {
|
||
return
|
||
}
|
||
data, err := json.Marshal(c.events)
|
||
if err != nil {
|
||
return
|
||
}
|
||
if err := os.WriteFile(c.savePath, data, 0644); err != nil {
|
||
return
|
||
}
|
||
c.dirty = false
|
||
}
|
||
|
||
// Prune — 基于当前输入计算每条上下文的相关性,归档最不相关的
|
||
// 返回被归档的事件(转为文档),保留 topK 个最相关的在活跃上下文中
|
||
func (c *RelevanceContext) Prune(currentInput string, topK int, docStore *document.Store) int {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
if len(c.events) <= topK {
|
||
return 0
|
||
}
|
||
|
||
// 保护最近 10 条记录不被淘汰,从更早的记录中选择淘汰对象
|
||
protectCount := 10
|
||
if protectCount > len(c.events) {
|
||
protectCount = len(c.events)
|
||
}
|
||
protected := c.events[len(c.events)-protectCount:]
|
||
candidates := c.events[:len(c.events)-protectCount]
|
||
|
||
if len(candidates) == 0 {
|
||
return 0
|
||
}
|
||
|
||
// 确保向量化器已训练
|
||
c.ensureTrained()
|
||
|
||
queryVec := c.veczer.Vectorize(currentInput)
|
||
|
||
// 计算每条候选上下文与当前输入的相关性
|
||
type scored struct {
|
||
event *ContextEvent
|
||
score float64
|
||
idx int
|
||
}
|
||
scoredEvents := make([]scored, len(candidates))
|
||
for i, evt := range candidates {
|
||
score := vector.CosineSimilarity(queryVec, evt.Vector)
|
||
scoredEvents[i] = scored{event: evt, score: score, idx: i}
|
||
}
|
||
|
||
// 按相关性从高到低排序
|
||
sort.Slice(scoredEvents, func(i, j int) bool {
|
||
return scoredEvents[i].score > scoredEvents[j].score
|
||
})
|
||
|
||
// 从候选中选 topK 最相关的保留,其余淘汰
|
||
keepCount := topK - len(protected)
|
||
if keepCount < 0 {
|
||
keepCount = 0
|
||
}
|
||
keep := scoredEvents
|
||
if len(keep) > keepCount {
|
||
keep = keep[:keepCount]
|
||
}
|
||
archive := scoredEvents[keepCount:]
|
||
|
||
// 重建 events 为保留的候选 + 受保护的最新记录
|
||
c.events = make([]*ContextEvent, 0, len(keep)+len(protected))
|
||
for _, s := range keep {
|
||
c.events = append(c.events, s.event)
|
||
}
|
||
c.events = append(c.events, protected...)
|
||
|
||
// 按时间重新排序
|
||
sort.Slice(c.events, func(i, j int) bool {
|
||
return c.events[i].Timestamp.Before(c.events[j].Timestamp)
|
||
})
|
||
|
||
// 归档到文档记忆
|
||
archived := 0
|
||
if docStore != nil && len(archive) > 0 {
|
||
entries := make([]document.ContextEntry, len(archive))
|
||
for i, s := range archive {
|
||
entries[i] = document.ContextEntry{
|
||
Timestamp: s.event.Timestamp,
|
||
Source: s.event.Source,
|
||
Content: s.event.Input,
|
||
Response: s.event.Response,
|
||
}
|
||
}
|
||
doc, err := docStore.ContextToDoc("context_archived", entries)
|
||
if err == nil && doc != nil {
|
||
archived = len(archive)
|
||
}
|
||
}
|
||
|
||
c.save()
|
||
|
||
return archived
|
||
}
|
||
|
||
// Format — 输出活跃上下文的文本,用于注入 prompt
|
||
func (c *RelevanceContext) Format() string {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
if len(c.events) == 0 {
|
||
return ""
|
||
}
|
||
|
||
var sb strings.Builder
|
||
sb.WriteString("【近期事件】\n")
|
||
for _, e := range c.events {
|
||
sb.WriteString(fmt.Sprintf("[%s] %s: %s", e.Timestamp.Format("15:04:05"), e.Source, e.Input))
|
||
if e.Response != "" {
|
||
sb.WriteString(fmt.Sprintf(" → %s", truncateStr(e.Response, 80)))
|
||
}
|
||
sb.WriteString("\n")
|
||
}
|
||
return sb.String()
|
||
}
|
||
|
||
// Recent — 返回最近 n 条
|
||
func (c *RelevanceContext) Recent(n int) []ContextEvent {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
|
||
if n <= 0 || n > len(c.events) {
|
||
n = len(c.events)
|
||
}
|
||
result := make([]ContextEvent, n)
|
||
for i, evt := range c.events[len(c.events)-n:] {
|
||
result[i] = *evt
|
||
}
|
||
return result
|
||
}
|
||
|
||
// Len — 当前上下文事件数
|
||
func (c *RelevanceContext) Len() int {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
return len(c.events)
|
||
}
|
||
|
||
func (c *RelevanceContext) ensureTrained() {
|
||
if !c.trained && len(c.events) > 0 {
|
||
texts := make([]string, len(c.events))
|
||
for i, evt := range c.events {
|
||
texts[i] = evt.Input + " " + evt.Response
|
||
}
|
||
c.veczer.Train(texts)
|
||
// 重算所有事件向量,与新的向量化器特征空间对齐
|
||
for _, evt := range c.events {
|
||
evt.Vector = c.veczer.Vectorize(evt.Input + " " + evt.Response)
|
||
}
|
||
c.trained = true
|
||
}
|
||
}
|