- 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
164 lines
4.8 KiB
Go
164 lines
4.8 KiB
Go
// 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
|
||
}
|