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>
165 lines
4.2 KiB
Go
165 lines
4.2 KiB
Go
package background
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// ThinkRecord is a single thinking session's result.
|
|
type ThinkRecord struct {
|
|
ID string `json:"id"`
|
|
Content string `json:"content"`
|
|
Conclusions []string `json:"conclusions"` // key takeaways
|
|
FollowUps []string `json:"follow_ups"` // questions to continue
|
|
ToolCalls int `json:"tool_calls"`
|
|
Trigger string `json:"trigger"` // post_chat, silence, periodic
|
|
Timestamp time.Time `json:"timestamp"`
|
|
}
|
|
|
|
// ThinkChain stores linked thinking records so each round
|
|
// can build on previous conclusions.
|
|
type ThinkChain struct {
|
|
mu sync.Mutex
|
|
records []ThinkRecord
|
|
maxSize int
|
|
}
|
|
|
|
// NewThinkChain creates a think chain with the given max size.
|
|
func NewThinkChain(maxSize int) *ThinkChain {
|
|
if maxSize <= 0 {
|
|
maxSize = 10
|
|
}
|
|
return &ThinkChain{
|
|
records: make([]ThinkRecord, 0, maxSize),
|
|
maxSize: maxSize,
|
|
}
|
|
}
|
|
|
|
// Add appends a new think record, evicting oldest if at capacity.
|
|
func (c *ThinkChain) Add(r ThinkRecord) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if len(c.records) >= c.maxSize {
|
|
c.records = c.records[1:]
|
|
}
|
|
c.records = append(c.records, r)
|
|
}
|
|
|
|
// LastConclusions returns conclusions from the most recent N records.
|
|
func (c *ThinkChain) LastConclusions(n int) []string {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
var result []string
|
|
start := len(c.records) - n
|
|
if start < 0 {
|
|
start = 0
|
|
}
|
|
for _, r := range c.records[start:] {
|
|
result = append(result, r.Conclusions...)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// LastFollowUps returns follow-up questions from the single most recent record.
|
|
func (c *ThinkChain) LastFollowUps() []string {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if len(c.records) == 0 {
|
|
return nil
|
|
}
|
|
return c.records[len(c.records)-1].FollowUps
|
|
}
|
|
|
|
// LastTopic attempts to infer a topic from recent conclusions.
|
|
func (c *ThinkChain) LastTopic() string {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if len(c.records) == 0 {
|
|
return ""
|
|
}
|
|
// Use first conclusion line of the most recent record as topic
|
|
for _, r := range c.records {
|
|
for _, c := range r.Conclusions {
|
|
if c != "" {
|
|
runes := []rune(c)
|
|
if len(runes) > 50 {
|
|
return string(runes[:50]) + "..."
|
|
}
|
|
return c
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// Size returns the current number of records in the chain.
|
|
func (c *ThinkChain) Size() int {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
return len(c.records)
|
|
}
|
|
|
|
// extractConclusions parses the LLM thinking output to find conclusions and follow-ups.
|
|
// Looks for "结论" / "后续" markers in the content.
|
|
func extractConclusions(content string) (conclusions []string, followUps []string) {
|
|
lines := strings.Split(content, "\n")
|
|
inConclusions := false
|
|
inFollowUps := false
|
|
|
|
for _, line := range lines {
|
|
line = strings.TrimSpace(line)
|
|
|
|
if strings.Contains(line, "结论") && (strings.Contains(line, "💭") || strings.Contains(line, "📝") || strings.HasPrefix(line, "-")) {
|
|
// Heuristic: this line starts a conclusions section
|
|
}
|
|
|
|
// Match bullet-point conclusions: lines starting with - or •
|
|
if (strings.HasPrefix(line, "- ") || strings.HasPrefix(line, "• ")) && !inFollowUps {
|
|
text := strings.TrimPrefix(line, "- ")
|
|
text = strings.TrimPrefix(text, "• ")
|
|
text = strings.TrimSpace(text)
|
|
if text != "" && len([]rune(text)) > 2 {
|
|
if inConclusions {
|
|
conclusions = append(conclusions, text)
|
|
} else {
|
|
// Without explicit marker, treat all bullets as conclusions
|
|
conclusions = append(conclusions, text)
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Detect section transitions
|
|
if strings.Contains(line, "后续") || strings.Contains(line, "继续思考") || strings.Contains(line, "下次") {
|
|
inFollowUps = true
|
|
inConclusions = false
|
|
continue
|
|
}
|
|
if strings.Contains(line, "结论") || strings.Contains(line, "观察") || strings.Contains(line, "记忆") {
|
|
inConclusions = true
|
|
inFollowUps = false
|
|
continue
|
|
}
|
|
}
|
|
|
|
// If no structured markers found, treat the whole content as a single conclusion
|
|
if len(conclusions) == 0 {
|
|
runes := []rune(content)
|
|
if len(runes) > 200 {
|
|
content = string(runes[:200]) + "..."
|
|
}
|
|
conclusions = []string{content}
|
|
}
|
|
|
|
return conclusions, followUps
|
|
}
|
|
|
|
// generateID generates a short random ID.
|
|
func generateID() string {
|
|
b := make([]byte, 6)
|
|
rand.Read(b)
|
|
return fmt.Sprintf("th-%x", b)
|
|
} |