mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-21 17:38:10 +00:00
260 lines
5.5 KiB
Go
260 lines
5.5 KiB
Go
package core
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"gitcode.com/JianFeeeee/HomeAgent/internal/memory"
|
|
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/document"
|
|
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
|
|
)
|
|
|
|
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
|
|
|
|
type RelevanceContext struct {
|
|
mu sync.Mutex
|
|
events []*ContextEvent
|
|
embedder *memory.StaticEmbedder
|
|
savePath string
|
|
saveTimer *time.Timer
|
|
dirty bool
|
|
}
|
|
|
|
func NewRelevanceContext(savePath string, embedder *memory.StaticEmbedder) *RelevanceContext {
|
|
rc := &RelevanceContext{
|
|
embedder: embedder,
|
|
savePath: savePath,
|
|
}
|
|
if savePath != "" {
|
|
rc.load()
|
|
}
|
|
return rc
|
|
}
|
|
|
|
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.Input = memory.CleanTemplateText(evt.Input)
|
|
evt.Vector = c.computeVector(evt)
|
|
}
|
|
c.events = events
|
|
}
|
|
|
|
func textForVector(evt *ContextEvent) string {
|
|
switch {
|
|
case evt.Source == "agent" && evt.Response != "":
|
|
return memory.CleanTemplateText(evt.Response)
|
|
case evt.Source == "cold_storage":
|
|
return memory.CleanTemplateText(evt.Input + " " + evt.Response)
|
|
default:
|
|
return memory.CleanTemplateText(evt.Input)
|
|
}
|
|
}
|
|
|
|
func (c *RelevanceContext) computeVector(evt *ContextEvent) vector.Vector {
|
|
return c.embedder.Vectorize(textForVector(evt))
|
|
}
|
|
|
|
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.Input = memory.CleanTemplateText(evt.Input)
|
|
evt.Vector = c.computeVector(&evt)
|
|
c.events = append(c.events, &evt)
|
|
|
|
c.save()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
pCount := 10
|
|
if pCount > len(c.events) {
|
|
pCount = len(c.events)
|
|
}
|
|
protected := c.events[len(c.events)-pCount:]
|
|
candidates := c.events[:len(c.events)-pCount]
|
|
|
|
if len(candidates) == 0 {
|
|
return 0
|
|
}
|
|
|
|
queryVec := c.embedder.VectorizeClean(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
|
|
})
|
|
|
|
keepCount := topK - len(protected)
|
|
if keepCount < 0 {
|
|
keepCount = 0
|
|
}
|
|
keep := scoredEvents
|
|
if len(keep) > keepCount {
|
|
keep = keep[:keepCount]
|
|
}
|
|
archive := scoredEvents[keepCount:]
|
|
|
|
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 {
|
|
var filtered []scored
|
|
for _, s := range archive {
|
|
if s.event.Source == "agentcli" || s.event.Source == "terminal" {
|
|
continue
|
|
}
|
|
filtered = append(filtered, s)
|
|
}
|
|
entries := make([]document.ContextEntry, len(filtered))
|
|
for i, s := range filtered {
|
|
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, c.embedder)
|
|
if err == nil && doc != nil {
|
|
archived = len(filtered)
|
|
}
|
|
}
|
|
|
|
c.save()
|
|
|
|
return archived
|
|
}
|
|
|
|
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()
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (c *RelevanceContext) Len() int {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return len(c.events)
|
|
}
|