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) }