87214b9441
Phase 1 (基础设施): - ThinkChain 思考链连续性 + 差异化思考提示词 (persistent) - AutonomousToolPolicy 工具安全策略 (safe/unsafe/conditional) - MessageScheduler 自适应消息节奏 (Idle/Available/Busy) - SessionEnrichmentStore 渐进式上下文丰富 (5层) - ConversationBus 事件总线 + ResponseCache (dedup) - pkg/logger 统一日志 + 所有 handler 替换 fmt.Printf - NPE 守卫/链路优化/数据库表修复/Go workspace Phase 2 (人格交互): - EmotionState/EmotionTracker 情感状态机 (5种心情, 情绪衰减) - ProactiveGuard 主动消息多维决策 (静默时段/紧急度/频率/校验) - Gateway↔ai-core 在线状态感知链路 (presence notification) - 离线思考频率控制 + 重连问候 + 离线消息排队 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
133 lines
3.1 KiB
Go
133 lines
3.1 KiB
Go
// Package cache provides a response cache for skipping redundant LLM calls
|
|
// on semantically similar inputs (greetings and common IoT commands).
|
|
package cache
|
|
|
|
import (
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Entry is a cached LLM response.
|
|
type Entry struct {
|
|
FullContent string
|
|
CachedAt time.Time
|
|
AccessCount int
|
|
}
|
|
|
|
// ResponseCache caches LLM responses keyed by normalized user input.
|
|
// It uses separate TTLs for greetings (longer) and other queries (shorter).
|
|
type ResponseCache struct {
|
|
mu sync.RWMutex
|
|
entries map[string]*Entry
|
|
maxEntries int
|
|
greetingTTL time.Duration
|
|
defaultTTL time.Duration
|
|
}
|
|
|
|
// New creates a new ResponseCache with sensible defaults.
|
|
func New() *ResponseCache {
|
|
return &ResponseCache{
|
|
entries: make(map[string]*Entry),
|
|
maxEntries: 200,
|
|
greetingTTL: 10 * time.Minute,
|
|
defaultTTL: 30 * time.Second,
|
|
}
|
|
}
|
|
|
|
// Get returns a cached response for the given input if it exists and hasn't expired.
|
|
func (c *ResponseCache) Get(input string) (string, bool) {
|
|
key := normalize(input)
|
|
c.mu.RLock()
|
|
entry, ok := c.entries[key]
|
|
c.mu.RUnlock()
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
ttl := c.defaultTTL
|
|
if isGreeting(input) {
|
|
ttl = c.greetingTTL
|
|
}
|
|
if time.Since(entry.CachedAt) > ttl {
|
|
c.mu.Lock()
|
|
delete(c.entries, key)
|
|
c.mu.Unlock()
|
|
return "", false
|
|
}
|
|
c.mu.Lock()
|
|
entry.AccessCount++
|
|
c.mu.Unlock()
|
|
return entry.FullContent, true
|
|
}
|
|
|
|
// Set stores a response in the cache.
|
|
func (c *ResponseCache) Set(input, response string) {
|
|
key := normalize(input)
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
// Evict oldest entries if at capacity
|
|
if len(c.entries) >= c.maxEntries {
|
|
var oldestKey string
|
|
var oldestTime time.Time
|
|
for k, v := range c.entries {
|
|
if oldestKey == "" || v.CachedAt.Before(oldestTime) {
|
|
oldestKey = k
|
|
oldestTime = v.CachedAt
|
|
}
|
|
}
|
|
if oldestKey != "" {
|
|
delete(c.entries, oldestKey)
|
|
}
|
|
}
|
|
|
|
c.entries[key] = &Entry{
|
|
FullContent: response,
|
|
CachedAt: time.Now(),
|
|
AccessCount: 0,
|
|
}
|
|
}
|
|
|
|
// Invalidate clears all cached entries.
|
|
func (c *ResponseCache) Invalidate() {
|
|
c.mu.Lock()
|
|
c.entries = make(map[string]*Entry)
|
|
c.mu.Unlock()
|
|
}
|
|
|
|
// Size returns the current number of cached entries.
|
|
func (c *ResponseCache) Size() int {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return len(c.entries)
|
|
}
|
|
|
|
// normalize produces a cache key from user input.
|
|
func normalize(input string) string {
|
|
s := strings.TrimSpace(strings.ToLower(input))
|
|
// Collapse multiple spaces
|
|
parts := strings.Fields(s)
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
// isGreeting returns true if the input looks like a simple greeting/small-talk
|
|
// that can be cached with a longer TTL.
|
|
func isGreeting(input string) bool {
|
|
normalized := normalize(input)
|
|
greetings := []string{
|
|
"你好", "嗨", "嘿", "哈喽", "hello", "hi", "hey",
|
|
"早上好", "下午好", "晚上好", "晚安", "早安", "午安",
|
|
"在吗", "在不在", "在么",
|
|
"谢谢", "多谢", "感谢", "thanks", "thank you",
|
|
"好的", "ok", "okay", "行", "可以",
|
|
"再见", "拜拜", "bye", "byebye",
|
|
"嗯", "哦", "噢",
|
|
}
|
|
for _, g := range greetings {
|
|
if normalized == g {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|