Files
Cyrene_v2/backend/services/ai-core/background/thinker.go
T
Aska 6a4887e52a feat(llm): add LLM call recording with admin panel integration
- llm.Adapter: SetCallLogger hook for recording every API call
- llm.CallLogger: ring-buffer with Recent+Stats for admin API
- main.go: wire DeepSeek adapter to CallLogger
- Fix: single CallLogger instance for both recording and display
- Metrics: model name, duration, success/fail, tokens
- All 16 packages pass
2026-08-12 19:10:51 +08:00

156 lines
3.6 KiB
Go

// 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:]
}