Files
HomeAgent/internal/memory/embedder.go
root 7f28b997e6 feat: output channel redesign - per-channel output gates, LLM chain events, SDKConfig
- Output channels generate per-channel tools: output_send__{name} (type=output) + output_send__{name}_help
- content is JSON string transparently passed to plugin handler for routing
- EventAgentLLMChain: full LLM response forwarded after each turn for webui/logs
- sdk.New refactored to SDKConfig struct (no more 13 positional args)
- RegisterOutputChannel adds desc param for JSON format documentation
- channelDevice simplified (no Tools method), desc field added
- Child agent permission updated for output_send__ prefix
- System prompt: output gates, multi-call, long messages split
- WebUI: subscribes to EventAgentLLMChain in SSE, no output channel
- Tests updated for new naming convention
2026-07-16 12:11:16 +08:00

222 lines
4.1 KiB
Go

package memory
import (
"math"
"sort"
"strings"
"sync"
"github.com/yanyiwu/gojieba"
"gitcode.com/JianFeeeee/HomeAgent/internal/memory/vector"
)
type LocalWordEmbedder struct {
mu sync.RWMutex
jieba *gojieba.Jieba
stopWords map[string]bool
docFreq map[string]float64
totalDocs int
coOccur map[string]map[string]float64
vocab map[string]bool
trained bool
}
func NewLocalWordEmbedder() *LocalWordEmbedder {
sw := make(map[string]bool)
for k, v := range stopWords {
sw[k] = v
}
return &LocalWordEmbedder{
jieba: GetJieba(),
stopWords: sw,
docFreq: make(map[string]float64),
coOccur: make(map[string]map[string]float64),
vocab: make(map[string]bool),
}
}
func (e *LocalWordEmbedder) tokenize(text string) []string {
if e.jieba == nil {
return nil
}
words := e.jieba.Cut(text, true)
var result []string
seen := make(map[string]bool)
for _, w := range words {
w = strings.TrimSpace(w)
if w == "" || e.stopWords[w] || seen[w] {
continue
}
runes := []rune(w)
if len(runes) < 2 {
continue
}
seen[w] = true
result = append(result, w)
}
return result
}
func (e *LocalWordEmbedder) Train(docs []string) {
if e.jieba == nil {
return
}
e.mu.Lock()
defer e.mu.Unlock()
e.docFreq = make(map[string]float64)
e.coOccur = make(map[string]map[string]float64)
e.vocab = make(map[string]bool)
tokenized := make([][]string, len(docs))
for i, doc := range docs {
tokens := e.tokenize(doc)
tokenized[i] = tokens
seen := make(map[string]bool)
for _, t := range tokens {
e.vocab[t] = true
if !seen[t] {
e.docFreq[t]++
seen[t] = true
}
}
}
e.totalDocs = len(docs)
windowSize := 5
for _, tokens := range tokenized {
for i, word := range tokens {
start := i - windowSize
if start < 0 {
start = 0
}
end := i + windowSize + 1
if end > len(tokens) {
end = len(tokens)
}
for j := start; j < end; j++ {
if i == j {
continue
}
ctx := tokens[j]
if e.coOccur[word] == nil {
e.coOccur[word] = make(map[string]float64)
}
e.coOccur[word][ctx]++
}
}
}
for word, ctxs := range e.coOccur {
totalPairs := 0.0
for _, count := range ctxs {
totalPairs += count
}
pWord := e.docFreq[word] / float64(e.totalDocs)
for ctx, count := range ctxs {
pCtx := e.docFreq[ctx] / float64(e.totalDocs)
pJoint := count / totalPairs
pmi := math.Log2(pJoint / (pWord * pCtx))
if pmi <= 0 {
delete(ctxs, ctx)
} else {
ctxs[ctx] = pmi
}
}
e.coOccur[word] = pruneTopK(ctxs, 50)
}
e.trained = true
}
func pruneTopK(m map[string]float64, k int) map[string]float64 {
if len(m) <= k {
return m
}
type kv struct {
k string
v float64
}
var sorted []kv
for key, val := range m {
sorted = append(sorted, kv{key, val})
}
sort.Slice(sorted, func(i, j int) bool {
return sorted[i].v > sorted[j].v
})
result := make(map[string]float64, k)
for i := 0; i < k; i++ {
result[sorted[i].k] = sorted[i].v
}
return result
}
func (e *LocalWordEmbedder) Vectorize(text string) vector.Vector {
e.mu.RLock()
useEmbedding := e.trained
e.mu.RUnlock()
tokens := e.tokenize(text)
if len(tokens) == 0 {
return vector.Vector{}
}
tf := make(map[string]float64)
for _, t := range tokens {
tf[t]++
}
maxTF := 0.0
for _, count := range tf {
if count > maxTF {
maxTF = count
}
}
vec := make(vector.Vector)
if useEmbedding {
e.mu.RLock()
for word, count := range tf {
tfidf := (count / maxTF) * idf(e.docFreq[word], e.totalDocs)
if ctxs, ok := e.coOccur[word]; ok {
for ctx, pmi := range ctxs {
vec[ctx] += tfidf * pmi
}
}
vec["__w__"+word] += tfidf
}
e.mu.RUnlock()
} else {
for word, count := range tf {
tfNorm := count / maxTF
var df float64
e.mu.RLock()
df = e.docFreq[word]
e.mu.RUnlock()
vec[word] = tfNorm * idf(df, e.totalDocs)
}
}
return vec
}
func idf(df float64, total int) float64 {
if df <= 0 || total <= 0 {
return 1.0
}
return math.Log(float64(total+1)/(df+1)+1) + 1
}
func (e *LocalWordEmbedder) Trained() bool {
e.mu.RLock()
defer e.mu.RUnlock()
return e.trained
}