feat: 第四轮大版本更新 — 修复4个严重Bug、2个UI Bug,实现自主思考重构与主-子会话架构
## 🐛 Bug 修复 - 修复前端对话无响应:消除 ChatContainer 中的双重 WebSocket 连接,优化 sendMessage 失败提示 - 修复 Memory-Service 数据库迁移失败:ai-core 和 memory-service 均添加 ALTER TABLE ADD COLUMN IF NOT EXISTS 模式演化 - 修复语音/STT 不可用:添加 MediaRecorder API 降级方案,修复 whisper-cli 输出文件名错误 - 修复仪表盘数据库按钮失效:补充按钮 ID 属性,重写 controlDB() 控制逻辑 ## 🎨 UI 修复 - 修正用户消息头像位置:从 flex-row-reverse 改为 justify-end - 移除空聊天列表的 emoji 占位图标 ## ✨ 新功能 - devtools 新增 STT 处理日志面板(环形缓冲区 + WebSocket 广播 + 可视化表格) - 新增 ADMIN_NICKNAME 环境变量,支持自定义管理员昵称 ## 🔧 改进 - 注册流程增加昵称必填字段(前后端同步) ## 🏗️ 架构重构 - 重构自主思考逻辑:从定时器轮询改为事件驱动(对话后触发 + 静默检测),优化提示词使其更自然人性化 - 实现主-子会话架构:新增 4 种子会话类型(general/memory/iot/knowledge),意图分析 → 并行分发 → 结果合成流程 ## 📄 新增文档 - docs/architecture/main-session-sub-session-design.md — 子会话架构设计文档
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/model"
|
||||
)
|
||||
|
||||
// IntentAnalyzer 意图分析器
|
||||
// 使用轻量 LLM 调用判断用户消息的意图
|
||||
type IntentAnalyzer struct {
|
||||
llmAdapter *llm.Adapter
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// NewIntentAnalyzer 创建意图分析器
|
||||
func NewIntentAnalyzer(llmAdapter *llm.Adapter) *IntentAnalyzer {
|
||||
return &IntentAnalyzer{
|
||||
llmAdapter: llmAdapter,
|
||||
enabled: llmAdapter != nil,
|
||||
}
|
||||
}
|
||||
|
||||
// Analyze 分析用户消息意图
|
||||
// 优先使用 LLM,失败时使用关键词规则降级
|
||||
func (a *IntentAnalyzer) Analyze(ctx context.Context, userMessage string) (*model.IntentResult, error) {
|
||||
// 如果 LLM 不可用,直接使用关键词匹配
|
||||
if !a.enabled || a.llmAdapter == nil {
|
||||
log.Printf("[intent] LLM 不可用,使用关键词规则分析意图")
|
||||
return a.keywordAnalyze(userMessage), nil
|
||||
}
|
||||
|
||||
// 构建轻量意图分析提示词
|
||||
messages := []model.LLMMessage{
|
||||
{
|
||||
Role: model.RoleSystem,
|
||||
Content: intentAnalysisSystemPrompt,
|
||||
},
|
||||
{
|
||||
Role: model.RoleUser,
|
||||
Content: fmt.Sprintf("用户消息: %s", userMessage),
|
||||
},
|
||||
}
|
||||
|
||||
// 调用 LLM (同步)
|
||||
resp, err := a.llmAdapter.Chat(ctx, messages)
|
||||
if err != nil {
|
||||
log.Printf("[intent] LLM 意图分析失败: %v,降级使用关键词规则", err)
|
||||
return a.keywordAnalyze(userMessage), nil
|
||||
}
|
||||
|
||||
// 解析 JSON 响应
|
||||
intent, err := parseIntentResponse(resp.Content)
|
||||
if err != nil {
|
||||
log.Printf("[intent] 解析意图 JSON 失败: %v,降级使用关键词规则", err)
|
||||
return a.keywordAnalyze(userMessage), nil
|
||||
}
|
||||
|
||||
log.Printf("[intent] 意图分析完成: primary=%s, iot=%v, memory=%v, sentiment=%s",
|
||||
intent.Primary, intent.NeedsIoT, intent.NeedsMemory, intent.Sentiment)
|
||||
|
||||
return intent, nil
|
||||
}
|
||||
|
||||
// keywordAnalyze 基于关键词的意图分析(降级方案)
|
||||
func (a *IntentAnalyzer) keywordAnalyze(userMessage string) *model.IntentResult {
|
||||
result := &model.IntentResult{
|
||||
Primary: "chat",
|
||||
NeedsMemory: true, // 默认检索记忆
|
||||
Sentiment: "neutral",
|
||||
Urgency: "low",
|
||||
}
|
||||
|
||||
msgLower := strings.ToLower(userMessage)
|
||||
|
||||
// IoT 关键词检测
|
||||
iotKeywords := []string{
|
||||
"灯", "空调", "窗帘", "电视", "设备", "开关",
|
||||
"打开", "关闭", "调到", "设置", "温度", "亮度",
|
||||
"传感器", "门锁", "插座", "风扇", "加湿器",
|
||||
}
|
||||
for _, kw := range iotKeywords {
|
||||
if strings.Contains(msgLower, kw) {
|
||||
result.NeedsIoT = true
|
||||
result.Primary = "iot_control"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 情感检测
|
||||
positiveWords := []string{"开心", "高兴", "哈哈", "好棒", "喜欢", "爱", "谢谢", "棒", "赞", "太好了"}
|
||||
negativeWords := []string{"难过", "伤心", "生气", "烦", "累", "不开心", "讨厌", "恨", "糟糕", "烦死了"}
|
||||
|
||||
for _, w := range positiveWords {
|
||||
if strings.Contains(msgLower, w) {
|
||||
result.Sentiment = "positive"
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, w := range negativeWords {
|
||||
if strings.Contains(msgLower, w) {
|
||||
result.Sentiment = "negative"
|
||||
result.Primary = "emotional"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 问题检测
|
||||
questionWords := []string{"什么", "怎么", "为什么", "如何", "谁", "哪里", "哪个", "多少", "能不能", "可以"}
|
||||
for _, w := range questionWords {
|
||||
if strings.Contains(msgLower, w) {
|
||||
result.Primary = "question"
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// intentAnalysisSystemPrompt 意图分析系统提示词 (轻量,快速返回)
|
||||
const intentAnalysisSystemPrompt = `分析以下用户消息的意图。只需返回 JSON,不要其他内容。
|
||||
|
||||
返回格式:
|
||||
{
|
||||
"primary": "chat|iot_control|iot_query|question|emotional",
|
||||
"needs_iot": true/false,
|
||||
"needs_memory": true/false,
|
||||
"sentiment": "positive|neutral|negative",
|
||||
"urgency": "low|medium|high"
|
||||
}
|
||||
|
||||
规则:
|
||||
- primary: 用户的主要意图
|
||||
- chat: 日常闲聊
|
||||
- iot_control: 需要控制智能设备
|
||||
- iot_query: 查询设备状态
|
||||
- question: 提问
|
||||
- emotional: 情绪表达/倾诉
|
||||
- needs_iot: 是否需要调用 IoT 相关功能
|
||||
- needs_memory: 是否需要检索用户记忆(大部分情况为 true)
|
||||
- sentiment: 用户情绪
|
||||
- urgency: low=普通闲聊, medium=需要回应, high=紧急求助`
|
||||
|
||||
// parseIntentResponse 从 LLM 响应中解析意图 JSON
|
||||
func parseIntentResponse(content string) (*model.IntentResult, error) {
|
||||
// 尝试找到 JSON 块
|
||||
content = strings.TrimSpace(content)
|
||||
|
||||
// 如果被 markdown 代码块包裹,提取内容
|
||||
if strings.HasPrefix(content, "```") {
|
||||
// 找到第一行换行符
|
||||
idx := strings.Index(content, "\n")
|
||||
if idx >= 0 {
|
||||
content = content[idx+1:]
|
||||
}
|
||||
// 找到结尾的 ```
|
||||
lastIdx := strings.LastIndex(content, "```")
|
||||
if lastIdx >= 0 {
|
||||
content = content[:lastIdx]
|
||||
}
|
||||
content = strings.TrimSpace(content)
|
||||
}
|
||||
|
||||
// 尝试找到 JSON 对象
|
||||
startIdx := strings.Index(content, "{")
|
||||
endIdx := strings.LastIndex(content, "}")
|
||||
if startIdx >= 0 && endIdx > startIdx {
|
||||
content = content[startIdx : endIdx+1]
|
||||
}
|
||||
|
||||
var result model.IntentResult
|
||||
if err := json.Unmarshal([]byte(content), &result); err != nil {
|
||||
return nil, fmt.Errorf("JSON 解析失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if result.Primary == "" {
|
||||
result.Primary = "chat"
|
||||
}
|
||||
if result.Sentiment == "" {
|
||||
result.Sentiment = "neutral"
|
||||
}
|
||||
if result.Urgency == "" {
|
||||
result.Urgency = "low"
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
@@ -1,143 +1,297 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
|
||||
ctxt "github.com/yourname/cyrene-ai/ai-core/internal/context"
|
||||
ctxbuild "github.com/yourname/cyrene-ai/ai-core/internal/context"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/model"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/subsession"
|
||||
)
|
||||
|
||||
// Orchestrator 对话编排器 —— 核心组件
|
||||
// 当前MVP阶段由 main.go 内联处理,此结构体作为未来重构的基础
|
||||
// Orchestrator 对话编排器 v2.0
|
||||
// 负责:意图分析 → 子会话分派 → 结果汇总 → 综合生成回复
|
||||
type Orchestrator struct {
|
||||
personaLoader *persona.Loader
|
||||
contextBuilder *ctxt.Builder
|
||||
llmAdapter *llm.Adapter
|
||||
memoryExtractor *memory.Extractor
|
||||
memoryRetriever *memory.Retriever
|
||||
personaLoader *persona.Loader
|
||||
contextBuilder *ctxbuild.Builder
|
||||
llmAdapter *llm.Adapter
|
||||
subManager *subsession.Manager
|
||||
intentAnalyzer *IntentAnalyzer
|
||||
synthesizer *Synthesizer
|
||||
memoryRetriever *memory.Retriever
|
||||
memoryExtractor *memory.Extractor
|
||||
}
|
||||
|
||||
// ProcessInput 处理用户输入的主流程
|
||||
// NewOrchestrator 创建编排器
|
||||
func NewOrchestrator(
|
||||
personaLoader *persona.Loader,
|
||||
contextBuilder *ctxbuild.Builder,
|
||||
llmAdapter *llm.Adapter,
|
||||
subManager *subsession.Manager,
|
||||
memoryRetriever *memory.Retriever,
|
||||
memoryExtractor *memory.Extractor,
|
||||
) *Orchestrator {
|
||||
return &Orchestrator{
|
||||
personaLoader: personaLoader,
|
||||
contextBuilder: contextBuilder,
|
||||
llmAdapter: llmAdapter,
|
||||
subManager: subManager,
|
||||
intentAnalyzer: NewIntentAnalyzer(llmAdapter),
|
||||
synthesizer: NewSynthesizer(llmAdapter),
|
||||
memoryRetriever: memoryRetriever,
|
||||
memoryExtractor: memoryExtractor,
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessParams 处理参数
|
||||
type ProcessParams struct {
|
||||
UserID string
|
||||
SessionID string
|
||||
Message string
|
||||
Mode string // text / voice_msg / voice_assistant
|
||||
Nickname string
|
||||
}
|
||||
|
||||
// ProcessResult 处理结果
|
||||
type ProcessResult struct {
|
||||
FullContent string // 完整回复文本
|
||||
Mode string // 回复模式
|
||||
Segments []model.Segment // 断句片段
|
||||
Intent *model.IntentResult // 意图分析结果
|
||||
}
|
||||
|
||||
// ProcessInput 处理用户输入 — 新的主入口
|
||||
// 返回流式事件通道
|
||||
func (o *Orchestrator) ProcessInput(
|
||||
ctx context.Context,
|
||||
userID string,
|
||||
sessionID string,
|
||||
userMessage string,
|
||||
mode string, // text / voice_msg / voice_assistant
|
||||
) (*Response, error) {
|
||||
ctx context.Context,
|
||||
params ProcessParams,
|
||||
) (<-chan model.StreamEvent, error) {
|
||||
|
||||
// 步骤1: 检索相关记忆
|
||||
memories, err := o.memoryRetriever.Retrieve(ctx, userID, userMessage)
|
||||
if err != nil {
|
||||
// 记忆检索失败不阻断对话
|
||||
memories = nil
|
||||
}
|
||||
eventCh := make(chan model.StreamEvent, 100)
|
||||
|
||||
// 步骤2: 加载人格配置
|
||||
personaConfig, err := o.personaLoader.Get("cyrene")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载人格配置失败: %w", err)
|
||||
}
|
||||
if params.Mode == "" {
|
||||
params.Mode = "text"
|
||||
}
|
||||
|
||||
// 步骤3: 构建对话上下文
|
||||
llmMessages, err := o.contextBuilder.Build(ctx, ctxt.BuildParams{
|
||||
UserID: userID,
|
||||
SessionID: sessionID,
|
||||
UserMessage: userMessage,
|
||||
Persona: personaConfig,
|
||||
Memories: memories,
|
||||
HistoryLimit: 20,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("构建上下文失败: %w", err)
|
||||
}
|
||||
go func() {
|
||||
defer close(eventCh)
|
||||
|
||||
// 步骤4: 调用LLM生成回复
|
||||
llmResponse, err := o.llmAdapter.Chat(ctx, llmMessages)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LLM调用失败: %w", err)
|
||||
}
|
||||
// 1. 意图分析
|
||||
intent, err := o.intentAnalyzer.Analyze(ctx, params.Message)
|
||||
if err != nil || intent == nil {
|
||||
log.Printf("[orchestrator] 意图分析失败: %v,使用默认值", err)
|
||||
intent = &model.IntentResult{
|
||||
Primary: "chat",
|
||||
NeedsMemory: true,
|
||||
Sentiment: "neutral",
|
||||
Urgency: "low",
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤5: 提取并存储新的记忆
|
||||
go o.memoryExtractor.ExtractAndStore(
|
||||
context.Background(),
|
||||
userID, sessionID,
|
||||
userMessage, llmResponse.Content,
|
||||
)
|
||||
// 2. 加载人格配置
|
||||
personaConfig, err := o.personaLoader.Get("cyrene")
|
||||
if err != nil {
|
||||
eventCh <- model.StreamEvent{
|
||||
Type: model.StreamError,
|
||||
Error: fmt.Errorf("加载人格配置失败: %w", err),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 步骤6: 构建响应
|
||||
response := &Response{
|
||||
Text: llmResponse.Content,
|
||||
ResponseMode: mode,
|
||||
}
|
||||
// 确定用户名
|
||||
userName := params.Nickname
|
||||
if userName == "" {
|
||||
userName = params.UserID
|
||||
}
|
||||
|
||||
// 步骤7: 如果是语音助手模式,进行断句处理
|
||||
if mode == "voice_assistant" {
|
||||
response.Segments = splitIntoSegments(llmResponse.Content)
|
||||
}
|
||||
// 3. 分派子会话(并行执行)
|
||||
createParams := subsession.CreateContextParams{
|
||||
UserID: params.UserID,
|
||||
SessionID: params.SessionID,
|
||||
UserMessage: params.Message,
|
||||
PersonaConfig: personaConfig,
|
||||
Intent: intent,
|
||||
Nickname: userName,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
// 注入 userID 到 context 供 MemoryProvider 使用
|
||||
subCtx := context.WithValue(ctx, "userID", params.UserID)
|
||||
|
||||
resultCh := o.subManager.Dispatch(subCtx, intent, params.Message, createParams)
|
||||
|
||||
// 4. 收集子会话结果
|
||||
var results []model.SubSessionResult
|
||||
for result := range resultCh {
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
log.Printf("[orchestrator] 子会话全部完成: 收集到 %d 个结果", len(results))
|
||||
|
||||
// 5. 汇总子会话结果
|
||||
agg := AggregateResults(results)
|
||||
|
||||
// 6. 构建对话历史
|
||||
history := o.contextBuilder.GetHistory(params.SessionID, 20)
|
||||
|
||||
// 7. 构建完整人格提示词
|
||||
systemPrompt := personaConfig.BuildSystemPrompt(userName, 1)
|
||||
|
||||
// 8. 构建综合参数
|
||||
synthParams := SynthesizeParams{
|
||||
UserID: params.UserID,
|
||||
SessionID: params.SessionID,
|
||||
UserMessage: params.Message,
|
||||
Nickname: userName,
|
||||
PersonaPrompt: systemPrompt,
|
||||
DialogHistory: history,
|
||||
MemorySummary: agg.MemorySummary,
|
||||
ThoughtOutline: agg.ThoughtOutline,
|
||||
IoTSummary: agg.IoTSummary,
|
||||
Mode: params.Mode,
|
||||
}
|
||||
|
||||
// 9. 调用 Synthesizer 流式生成最终回复
|
||||
chunkCh, err := o.synthesizer.Synthesize(ctx, synthParams)
|
||||
if err != nil {
|
||||
log.Printf("[orchestrator] 综合器启动失败: %v", err)
|
||||
eventCh <- model.StreamEvent{
|
||||
Type: model.StreamError,
|
||||
Error: fmt.Errorf("生成回复失败: %w", err),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 10. 流式输出 delta
|
||||
var fullContent string
|
||||
segmenter := llm.NewSegmenter()
|
||||
var segments []model.Segment
|
||||
|
||||
for chunk := range chunkCh {
|
||||
if chunk.Error != nil {
|
||||
log.Printf("[orchestrator] 流式错误: %v", chunk.Error)
|
||||
eventCh <- model.StreamEvent{
|
||||
Type: model.StreamError,
|
||||
Error: chunk.Error,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if chunk.Done {
|
||||
if remaining := segmenter.Flush(); remaining != nil {
|
||||
segments = append(segments, model.Segment{
|
||||
Index: remaining.Index,
|
||||
Text: remaining.Text,
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if chunk.Content != "" {
|
||||
fullContent += chunk.Content
|
||||
|
||||
// 实时断句
|
||||
newSegs := segmenter.Feed(chunk.Content)
|
||||
for _, s := range newSegs {
|
||||
segments = append(segments, model.Segment{
|
||||
Index: s.Index,
|
||||
Text: s.Text,
|
||||
})
|
||||
}
|
||||
|
||||
eventCh <- model.StreamEvent{
|
||||
Type: model.StreamDelta,
|
||||
Delta: chunk.Content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 11. 发送断句信息
|
||||
if len(segments) > 0 {
|
||||
eventCh <- model.StreamEvent{
|
||||
Type: model.StreamSegments,
|
||||
Segments: segments,
|
||||
}
|
||||
}
|
||||
|
||||
// 12. 完成
|
||||
eventCh <- model.StreamEvent{
|
||||
Type: model.StreamDone,
|
||||
}
|
||||
|
||||
// 13. 后处理:缓存回复
|
||||
if fullContent != "" {
|
||||
o.contextBuilder.CacheMessage(params.SessionID, model.RoleAssistant, fullContent)
|
||||
}
|
||||
|
||||
// 14. 异步提取记忆
|
||||
if o.memoryExtractor != nil && fullContent != "" {
|
||||
go o.memoryExtractor.ExtractAndStore(
|
||||
context.Background(),
|
||||
params.UserID,
|
||||
params.SessionID,
|
||||
params.Message,
|
||||
fullContent,
|
||||
)
|
||||
}
|
||||
|
||||
log.Printf("[orchestrator] 处理完成: intent=%s, content_len=%d, sub_results=%d",
|
||||
intent.Primary, len(fullContent), len(results))
|
||||
}()
|
||||
|
||||
return eventCh, nil
|
||||
}
|
||||
|
||||
// Response 回复结构
|
||||
type Response struct {
|
||||
Text string
|
||||
Segments []Segment
|
||||
ResponseMode string
|
||||
ToolCalls []ToolCall
|
||||
// ProcessInputSync 同步处理用户输入(兼容旧接口)
|
||||
func (o *Orchestrator) ProcessInputSync(
|
||||
ctx context.Context,
|
||||
params ProcessParams,
|
||||
) (*ProcessResult, error) {
|
||||
|
||||
eventCh, err := o.ProcessInput(ctx, params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &ProcessResult{
|
||||
Mode: params.Mode,
|
||||
}
|
||||
|
||||
for event := range eventCh {
|
||||
switch event.Type {
|
||||
case model.StreamError:
|
||||
return nil, event.Error
|
||||
case model.StreamDelta:
|
||||
result.FullContent += event.Delta
|
||||
case model.StreamSegments:
|
||||
result.Segments = event.Segments
|
||||
case model.StreamDone:
|
||||
// 完成
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ToolCall 工具调用
|
||||
type ToolCall struct {
|
||||
Name string
|
||||
Arguments map[string]interface{}
|
||||
Result interface{}
|
||||
// GetHistory 获取会话历史(暴露给外部使用)
|
||||
func (o *Orchestrator) GetHistory(sessionID string, limit int) []model.LLMMessage {
|
||||
if o.contextBuilder == nil {
|
||||
return nil
|
||||
}
|
||||
return o.contextBuilder.GetHistory(sessionID, limit)
|
||||
}
|
||||
|
||||
// Segment 语音片段
|
||||
type Segment struct {
|
||||
Index int
|
||||
Text string
|
||||
}
|
||||
|
||||
// splitIntoSegments 按句号断句
|
||||
func splitIntoSegments(text string) []Segment {
|
||||
var segments []Segment
|
||||
runes := []rune(text)
|
||||
start := 0
|
||||
index := 0
|
||||
|
||||
for i, r := range runes {
|
||||
if isSentenceEnd(r) {
|
||||
segText := strings.TrimSpace(string(runes[start : i+1]))
|
||||
if segText != "" {
|
||||
index++
|
||||
segments = append(segments, Segment{Index: index, Text: segText})
|
||||
}
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
|
||||
if start < len(runes) {
|
||||
remaining := strings.TrimSpace(string(runes[start:]))
|
||||
if remaining != "" {
|
||||
index++
|
||||
segments = append(segments, Segment{Index: index, Text: remaining})
|
||||
}
|
||||
}
|
||||
|
||||
return segments
|
||||
}
|
||||
|
||||
func isSentenceEnd(r rune) bool {
|
||||
switch r {
|
||||
case '。', '!', '?', '.', '!', '?', '\n':
|
||||
return true
|
||||
}
|
||||
return false
|
||||
// CacheMessage 缓存消息
|
||||
func (o *Orchestrator) CacheMessage(sessionID string, role model.Role, content string) {
|
||||
if o.contextBuilder != nil {
|
||||
o.contextBuilder.CacheMessage(sessionID, role, content)
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure time, memory are used
|
||||
var _ = time.Now
|
||||
var _ = memory.NewRetriever
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package orchestrator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/model"
|
||||
)
|
||||
|
||||
// Synthesizer 主会话综合器
|
||||
// 汇总子会话结果,生成最终回复
|
||||
type Synthesizer struct {
|
||||
llmAdapter *llm.Adapter
|
||||
}
|
||||
|
||||
// NewSynthesizer 创建综合器
|
||||
func NewSynthesizer(llmAdapter *llm.Adapter) *Synthesizer {
|
||||
return &Synthesizer{
|
||||
llmAdapter: llmAdapter,
|
||||
}
|
||||
}
|
||||
|
||||
// SynthesizeParams 综合参数
|
||||
type SynthesizeParams struct {
|
||||
UserID string
|
||||
SessionID string
|
||||
UserMessage string
|
||||
Nickname string
|
||||
PersonaPrompt string // 完整人格提示词
|
||||
DialogHistory []model.LLMMessage // 对话历史
|
||||
MemorySummary string // 记忆检索摘要
|
||||
ThoughtOutline string // 通用对话思考
|
||||
IoTSummary string // IoT 操作摘要
|
||||
DeviceContext string // 设备状态上下文
|
||||
Mode string // text / voice_assistant
|
||||
}
|
||||
|
||||
// Synthesize 综合所有子会话结果,流式生成最终回复
|
||||
func (s *Synthesizer) Synthesize(ctx context.Context, params SynthesizeParams) (<-chan llm.StreamChunk, error) {
|
||||
messages := s.buildSynthesizeMessages(params)
|
||||
|
||||
log.Printf("[synthesizer] 开始综合 (上下文 %d 条消息)", len(messages))
|
||||
|
||||
// 流式调用 LLM
|
||||
return s.llmAdapter.ChatStream(ctx, messages)
|
||||
}
|
||||
|
||||
// buildSynthesizeMessages 构建综合用的 LLM 消息列表
|
||||
func (s *Synthesizer) buildSynthesizeMessages(params SynthesizeParams) []model.LLMMessage {
|
||||
var messages []model.LLMMessage
|
||||
|
||||
userName := params.Nickname
|
||||
if userName == "" {
|
||||
userName = params.UserID
|
||||
}
|
||||
|
||||
// 构建综合系统提示词
|
||||
systemPrompt := params.PersonaPrompt
|
||||
|
||||
// 注入设备上下文
|
||||
if params.DeviceContext != "" {
|
||||
systemPrompt += "\n\n" + params.DeviceContext
|
||||
}
|
||||
|
||||
messages = append(messages, model.LLMMessage{
|
||||
Role: model.RoleSystem,
|
||||
Content: systemPrompt,
|
||||
})
|
||||
|
||||
// 注入记忆摘要
|
||||
if params.MemorySummary != "" && !strings.Contains(params.MemorySummary, "没有找到") {
|
||||
messages = append(messages, model.LLMMessage{
|
||||
Role: model.RoleSystem,
|
||||
Content: fmt.Sprintf("【你回忆起的关于%s的事】\n%s", userName, params.MemorySummary),
|
||||
})
|
||||
}
|
||||
|
||||
// 注入通用对话思考
|
||||
if params.ThoughtOutline != "" && params.ThoughtOutline != "思考完成,等待主会话综合" {
|
||||
messages = append(messages, model.LLMMessage{
|
||||
Role: model.RoleSystem,
|
||||
Content: fmt.Sprintf("【你对%s这句话的理解】\n%s", userName, params.ThoughtOutline),
|
||||
})
|
||||
}
|
||||
|
||||
// 注入 IoT 操作摘要
|
||||
if params.IoTSummary != "" && !strings.Contains(params.IoTSummary, "未匹配") && !strings.Contains(params.IoTSummary, "未执行") {
|
||||
messages = append(messages, model.LLMMessage{
|
||||
Role: model.RoleSystem,
|
||||
Content: fmt.Sprintf("【IoT 设备操作结果】\n%s", params.IoTSummary),
|
||||
})
|
||||
}
|
||||
|
||||
// 注入对话历史
|
||||
if len(params.DialogHistory) > 0 {
|
||||
messages = append(messages, params.DialogHistory...)
|
||||
}
|
||||
|
||||
// 当前用户消息
|
||||
messages = append(messages, model.LLMMessage{
|
||||
Role: model.RoleUser,
|
||||
Content: params.UserMessage,
|
||||
})
|
||||
|
||||
return messages
|
||||
}
|
||||
|
||||
// AggregateResults 汇总子会话结果
|
||||
func AggregateResults(results []model.SubSessionResult) *AggregatedContext {
|
||||
agg := &AggregatedContext{
|
||||
MemorySummary: "",
|
||||
ThoughtOutline: "",
|
||||
IoTSummary: "",
|
||||
}
|
||||
|
||||
for _, r := range results {
|
||||
if r.Error != "" {
|
||||
log.Printf("[aggregate] 子会话 %s 出错: %s", r.Type, r.Error)
|
||||
continue
|
||||
}
|
||||
|
||||
switch r.Type {
|
||||
case model.SubSessionMemory:
|
||||
agg.MemorySummary = r.Summary
|
||||
if r.Details != "" {
|
||||
agg.MemorySummary += "\n" + r.Details
|
||||
}
|
||||
agg.MemorySnippets = r.Memories
|
||||
|
||||
case model.SubSessionGeneral:
|
||||
agg.ThoughtOutline = r.Summary
|
||||
if r.Details != "" {
|
||||
agg.ThoughtOutline += "\n" + r.Details
|
||||
}
|
||||
|
||||
case model.SubSessionIoT:
|
||||
agg.IoTSummary = r.Summary
|
||||
|
||||
case model.SubSessionKnowledge:
|
||||
agg.KnowledgeInfo = r.Summary
|
||||
}
|
||||
}
|
||||
|
||||
return agg
|
||||
}
|
||||
|
||||
// AggregatedContext 汇总后的上下文
|
||||
type AggregatedContext struct {
|
||||
MemorySummary string `json:"memory_summary"`
|
||||
ThoughtOutline string `json:"thought_outline"`
|
||||
IoTSummary string `json:"iot_summary"`
|
||||
KnowledgeInfo string `json:"knowledge_info"`
|
||||
MemorySnippets []model.MemorySnippet `json:"memory_snippets"`
|
||||
}
|
||||
Reference in New Issue
Block a user