feat(ai-core): add LLM orchestration engine
- model: shared data types (Message, Intent, StreamToken, LLMMessage) - persona: YAML/Raw loading + system prompt injection - llm: OpenAI-compatible adapter with streaming + semantic cache - context: sliding window context store with token budgeting - subsession: parallel provider dispatch + review message parser - orchestrator: intent analysis pipeline (IoT/greeting/chat/memory) - background: autonomous thinking engine - 20+ tests, build + vet clean, ~130 total tests
This commit is contained in:
1 parent
99806fefad
commit
3b3e5a0697
9 files changed
+1529
No files matched your search
@@ -0,0 +1,155 @@
|
||||
// Package background provides the autonomous thinking engine for Cyrene.
|
||||
// It generates thoughts after conversations and during idle periods.
|
||||
package background
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"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 {
|
||||
log.Println("[thinker] background thinking disabled")
|
||||
return
|
||||
}
|
||||
|
||||
go t.silenceLoop(ctx)
|
||||
go t.postChatLoop(ctx)
|
||||
log.Printf("[thinker] started (interval=%v, silence=%v)", t.thinkInterval, t.silenceTimeout)
|
||||
}
|
||||
|
||||
// Stop shuts down the thinker.
|
||||
func (t *Thinker) Stop() {
|
||||
close(t.stopCh)
|
||||
log.Println("[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()
|
||||
|
||||
log.Printf("[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:]
|
||||
}
|
||||
Reference in new issue
Block a user