- 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
156 lines
3.6 KiB
Go
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:]
|
|
}
|