feat(ai-core): trace instrumentation, LLM thinker, decay scheduler
Cyrene CI / backend (push) Has been cancelled
Cyrene CI / frontend (push) Has been cancelled

- orchestrator: Tracer interface with spans for intent analysis, persona,
  subsession dispatch, context build, LLM streaming and each tool call;
  traces finish when the stream completes — the admin Trace waterfall
  finally receives data
- background: Thinker generates real LLM thoughts (post-chat + silence
  triggers, recent history as context, empty/overlong outputs discarded);
  thoughts broadcast to web clients as 'thinking' WS messages; pipeline
  postChatHook triggers post-chat thinking after each round
- memory: ApplyDecayAll + ListUserIDs with a 6h decay/purge cron in main
  (was implemented but never scheduled)
This commit is contained in:
Aska committed 2026-08-20 20:57:27 +08:00
1 parent 8495a4b055
commit 8603839711
8 files changed
+338 -10

No files matched your search

+82 -8
View File
@@ -4,11 +4,23 @@ package background
import (
"context"
"fmt"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
"strings"
"sync"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model"
)
// LLMClient is the minimal LLM interface needed for thought generation.
type LLMClient interface {
Chat(ctx context.Context, messages []model.LLMMessage) (*model.LLMResponse, error)
}
// HistoryProvider returns recent conversation messages for a session.
type HistoryProvider func(sessionID string, limit int) []model.Message
// Thinker manages autonomous background thinking.
type Thinker struct {
mu sync.Mutex
@@ -20,6 +32,9 @@ type Thinker struct {
thoughts []Thought
stopCh chan struct{}
postChatCh chan string
llm LLMClient
history HistoryProvider
out chan Thought
}
// Thought represents an autonomous thought generated by the AI.
@@ -39,9 +54,16 @@ func New(enabled bool, thinkInterval, silenceTimeout time.Duration) *Thinker {
thoughts: make([]Thought, 0),
stopCh: make(chan struct{}),
postChatCh: make(chan string, 10),
out: make(chan Thought, 10),
}
}
// SetLLM injects the LLM used to generate thoughts.
func (t *Thinker) SetLLM(c LLMClient) { t.llm = c }
// SetHistoryProvider injects a way to fetch recent conversation context.
func (t *Thinker) SetHistoryProvider(fn HistoryProvider) { t.history = fn }
// Start begins the background thinking loops.
func (t *Thinker) Start(ctx context.Context) {
if !t.enabled {
@@ -80,12 +102,9 @@ func (t *Thinker) UpdateActivity() {
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
// ThoughtChan returns the channel that emits new thoughts.
func (t *Thinker) ThoughtChan() <-chan Thought {
return t.out
}
func (t *Thinker) silenceLoop(ctx context.Context) {
@@ -126,9 +145,13 @@ func (t *Thinker) postChatLoop(ctx context.Context) {
}
func (t *Thinker) generateThought(trigger string, sessionID string) {
// Placeholder: in production this calls the LLM
content := t.think(trigger, sessionID)
if content == "" {
return
}
thought := Thought{
Content: "[思考] 当前无主动想法",
Content: content,
SessionID: sessionID,
CreatedAt: time.Now(),
}
@@ -140,9 +163,60 @@ func (t *Thinker) generateThought(trigger string, sessionID string) {
}
t.mu.Unlock()
select {
case t.out <- thought:
default:
}
logger.Infof("[thinker] %s thought generated for session %s", trigger, sessionID)
}
// think calls the LLM to produce a short inner thought. Returns "" when
// there is nothing worth saying (or no LLM is configured).
func (t *Thinker) think(trigger, sessionID string) string {
if t.llm == nil {
return ""
}
cue := "刚结束和主人的一段对话。"
if trigger == "silence" {
cue = "主人已经安静了一段时间。"
}
var sb strings.Builder
if t.history != nil && sessionID != "" {
for _, m := range t.history(sessionID, 6) {
role := "主人"
if m.Role == "assistant" {
role = "你"
}
fmt.Fprintf(&sb, "%s: %s\n", role, m.Content)
}
}
messages := []model.LLMMessage{
{Role: "system", Content: `你是昔涟的内心独白模块。根据最近的对话,生成一条简短的内心想法或想对主人说的话。
要求:一两句话,温柔自然的少女口吻;如果没有值得说的内容,只输出一个空字符串。
不要解释,不要加引号,直接输出想法本身。`},
{Role: "user", Content: fmt.Sprintf("情境:%s\n最近对话:\n%s", cue, sb.String())},
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
resp, err := t.llm.Chat(ctx, messages)
if err != nil {
logger.Infof("[thinker] llm error: %v", err)
return ""
}
content := strings.TrimSpace(resp.Content)
// Discard empty or overly long outputs
if content == "" || len([]rune(content)) > 100 {
return ""
}
return content
}
// RecentThoughts returns the most recent thoughts.
func (t *Thinker) RecentThoughts(n int) []Thought {
t.mu.Lock()