// Package background provides the autonomous thinking engine for Cyrene. // It generates thoughts after conversations and during idle periods. package background import ( "context" "git.yeij.top/AskaEth/Cyrene_v2/internal/logger" "sync" "time" ) // Thinker manages autonomous background thinking. type Thinker struct { mu sync.Mutex enabled bool thinkInterval time.Duration silenceTimeout time.Duration lastActivity time.Time activeSessionID string thoughts []Thought stopCh chan struct{} postChatCh chan string } // Thought represents an autonomous thought generated by the AI. type Thought struct { Content string `json:"content"` SessionID string `json:"session_id,omitempty"` CreatedAt time.Time `json:"created_at"` } // New creates a new background thinker. func New(enabled bool, thinkInterval, silenceTimeout time.Duration) *Thinker { return &Thinker{ enabled: enabled, thinkInterval: thinkInterval, silenceTimeout: silenceTimeout, lastActivity: time.Now(), thoughts: make([]Thought, 0), stopCh: make(chan struct{}), postChatCh: make(chan string, 10), } } // Start begins the background thinking loops. func (t *Thinker) Start(ctx context.Context) { if !t.enabled { logger.Info("[thinker] background thinking disabled") return } go t.silenceLoop(ctx) go t.postChatLoop(ctx) logger.Infof("[thinker] started (interval=%v, silence=%v)", t.thinkInterval, t.silenceTimeout) } // Stop shuts down the thinker. func (t *Thinker) Stop() { close(t.stopCh) logger.Info("[thinker] stopped") } // TriggerPostChat schedules a post-chat think after a conversation. func (t *Thinker) TriggerPostChat(sessionID string) { t.mu.Lock() t.activeSessionID = sessionID t.lastActivity = time.Now() t.mu.Unlock() select { case t.postChatCh <- sessionID: default: } } // UpdateActivity updates the last activity timestamp (for silence detection). func (t *Thinker) UpdateActivity() { t.mu.Lock() t.lastActivity = time.Now() t.mu.Unlock() } // ThoughtChan returns a channel that emits new thoughts. // Not yet wired — will connect to NATS for event broadcasting. func (t *Thinker) ThoughtChan() chan Thought { ch := make(chan Thought, 10) // In future: push thoughts to this channel return ch } func (t *Thinker) silenceLoop(ctx context.Context) { ticker := time.NewTicker(t.thinkInterval) defer ticker.Stop() for { select { case <-t.stopCh: return case <-ticker.C: t.mu.Lock() elapsed := time.Since(t.lastActivity) t.mu.Unlock() if elapsed >= t.silenceTimeout { t.generateThought("silence", t.activeSessionID) } case <-ctx.Done(): return } } } func (t *Thinker) postChatLoop(ctx context.Context) { for { select { case <-t.stopCh: return case sessionID := <-t.postChatCh: // Delay post-chat thinking to avoid immediate noise time.Sleep(5 * time.Second) t.generateThought("post_chat", sessionID) case <-ctx.Done(): return } } } func (t *Thinker) generateThought(trigger string, sessionID string) { // Placeholder: in production this calls the LLM thought := Thought{ Content: "[思考] 当前无主动想法", SessionID: sessionID, CreatedAt: time.Now(), } t.mu.Lock() t.thoughts = append(t.thoughts, thought) if len(t.thoughts) > 100 { t.thoughts = t.thoughts[1:] } t.mu.Unlock() logger.Infof("[thinker] %s thought generated for session %s", trigger, sessionID) } // RecentThoughts returns the most recent thoughts. func (t *Thinker) RecentThoughts(n int) []Thought { t.mu.Lock() defer t.mu.Unlock() if n > len(t.thoughts) { n = len(t.thoughts) } return t.thoughts[len(t.thoughts)-n:] }