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:
Aska committed 2026-08-09 21:21:27 +08:00
1 parent 99806fefad
commit 3b3e5a0697
9 files changed
+1529

No files matched your search

@@ -0,0 +1,163 @@
// Package subsession manages parallel sub-session providers for dialogue enrichment.
package subsession
import (
"context"
"fmt"
"strings"
"sync"
"time"
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model"
)
// Provider executes a specific sub-session task in parallel with others.
type Provider interface {
Name() string
Execute(ctx context.Context, input ProviderInput) (*model.SubSessionResult, error)
}
// ProviderInput holds the context for sub-session execution.
type ProviderInput struct {
UserMessage string
History []model.Message
Intent *model.Intent
}
// Manager orchestrates parallel sub-session execution.
type Manager struct {
providers []Provider
}
// NewManager creates a sub-session manager.
func NewManager(providers ...Provider) *Manager {
return &Manager{providers: providers}
}
// Dispatch runs all providers in parallel and returns results within the timeout.
func (m *Manager) Dispatch(ctx context.Context, input ProviderInput, timeout time.Duration) []model.SubSessionResult {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
var wg sync.WaitGroup
results := make([]model.SubSessionResult, 0, len(m.providers))
var mu sync.Mutex
for _, p := range m.providers {
wg.Add(1)
go func(provider Provider) {
defer wg.Done()
result, err := provider.Execute(ctx, input)
mu.Lock()
defer mu.Unlock()
if err != nil {
results = append(results, model.SubSessionResult{
Provider: provider.Name(),
Error: err.Error(),
})
} else if result != nil {
results = append(results, *result)
}
}(p)
}
wg.Wait()
return results
}
// --- Built-in Providers ---
// GeneralProvider handles general conversation intent.
type GeneralProvider struct{}
func (p *GeneralProvider) Name() string { return "general" }
func (p *GeneralProvider) Execute(_ context.Context, input ProviderInput) (*model.SubSessionResult, error) {
return &model.SubSessionResult{
Provider: "general",
Content: fmt.Sprintf("Intent: %s (confidence: %.2f)", input.Intent.Type, input.Intent.Confidence),
}, nil
}
// MemoryProvider retrieves relevant memories (placeholder — wired to Memory Service later).
type MemoryProvider struct{}
func (p *MemoryProvider) Name() string { return "memory" }
func (p *MemoryProvider) Execute(_ context.Context, _ ProviderInput) (*model.SubSessionResult, error) {
return &model.SubSessionResult{Provider: "memory", Content: "no relevant memories"}, nil
}
// IoTProvider executes IoT device queries and commands.
type IoTProvider struct {
// GetDeviceState is injected by the main app to query IoT devices.
GetDeviceState func(ctx context.Context) (string, error)
// ControlDevice is injected to execute IoT commands.
ControlDevice func(ctx context.Context, device, action string) (string, error)
}
func (p *IoTProvider) Name() string { return "iot" }
func (p *IoTProvider) Execute(ctx context.Context, input ProviderInput) (*model.SubSessionResult, error) {
if input.Intent == nil || (input.Intent.Type != model.IntentIoTControl && input.Intent.Type != model.IntentIoTQuery) {
return &model.SubSessionResult{Provider: "iot", Content: ""}, nil
}
if p.GetDeviceState != nil {
state, err := p.GetDeviceState(ctx)
if err == nil {
return &model.SubSessionResult{Provider: "iot", Content: state}, nil
}
}
return &model.SubSessionResult{Provider: "iot", Content: "iot devices available"}, nil
}
// ReviewProvider parses LLM output into action/chat segments.
type ReviewProvider struct{}
func (p *ReviewProvider) Name() string { return "review" }
func (p *ReviewProvider) Execute(_ context.Context, _ ProviderInput) (*model.SubSessionResult, error) {
return &model.SubSessionResult{Provider: "review", Content: ""}, nil
}
// ParseReviewMessages splits a message with action markers into segments.
// Input: "(歪着头看) 客厅灯已经开啦♪"
// Output: [{type:"action", content:"歪着头看"}, {type:"chat", content:"客厅灯已经开啦♪"}]
func ParseReviewMessages(content string) []model.ReviewSegment {
var segments []model.ReviewSegment
var current strings.Builder
inAction := false
runes := []rune(content)
for i := 0; i < len(runes); i++ {
r := runes[i]
// Chinese bracket detection
if r == '(' || r == '(' {
if current.Len() > 0 && !inAction {
segments = append(segments, model.ReviewSegment{Type: "chat", Content: current.String()})
current.Reset()
}
inAction = true
continue
}
if r == ')' || r == ')' {
if inAction && current.Len() > 0 {
segments = append(segments, model.ReviewSegment{Type: "action", Content: current.String()})
current.Reset()
}
inAction = false
continue
}
current.WriteRune(r)
}
if current.Len() > 0 {
segType := "chat"
if inAction {
segType = "action"
}
segments = append(segments, model.ReviewSegment{Type: segType, Content: current.String()})
}
return segments
}
@@ -0,0 +1,79 @@
package subsession
import (
"testing"
"git.yeij.top/AskaEth/Cyrene_v2/services/ai-core/model"
)
func TestParseReviewMessages(t *testing.T) {
tests := []struct {
input string
expected []model.ReviewSegment
}{
{
"(歪着头看你) 叶酱,客厅灯早就开着啦♪",
[]model.ReviewSegment{
{Type: "action", Content: "歪着头看你"},
{Type: "chat", Content: " 叶酱,客厅灯早就开着啦♪"},
},
},
{
"你好呀~ 今天有什么需要帮忙的吗?",
[]model.ReviewSegment{
{Type: "chat", Content: "你好呀~ 今天有什么需要帮忙的吗?"},
},
},
{
"(思考中)嗯...这个问题比较复杂",
[]model.ReviewSegment{
{Type: "action", Content: "思考中"},
{Type: "chat", Content: "嗯...这个问题比较复杂"},
},
},
{
"",
nil,
},
{
"(only action)",
[]model.ReviewSegment{
{Type: "action", Content: "only action"},
},
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
got := ParseReviewMessages(tt.input)
if len(got) != len(tt.expected) {
t.Fatalf("expected %d segments, got %d: %+v", len(tt.expected), len(got), got)
}
for i, seg := range got {
if seg.Type != tt.expected[i].Type {
t.Errorf("segment[%d].Type = %q, want %q", i, seg.Type, tt.expected[i].Type)
}
if seg.Content != tt.expected[i].Content {
t.Errorf("segment[%d].Content = %q, want %q", i, seg.Content, tt.expected[i].Content)
}
}
})
}
}
func TestProviderNames(t *testing.T) {
p := &GeneralProvider{}
if p.Name() != "general" {
t.Errorf("got %s", p.Name())
}
m := &MemoryProvider{}
if m.Name() != "memory" {
t.Errorf("got %s", m.Name())
}
i := &IoTProvider{}
if i.Name() != "iot" {
t.Errorf("got %s", i.Name())
}
}