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