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,136 @@
|
||||
package subsession
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/model"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
|
||||
)
|
||||
|
||||
// GeneralProvider 通用对话子会话提供者
|
||||
// 职责:理解用户消息,构思回复思路,为最终回复提供思考框架
|
||||
type GeneralProvider struct {
|
||||
personaLoader *persona.Loader
|
||||
}
|
||||
|
||||
// NewGeneralProvider 创建通用对话子会话提供者
|
||||
func NewGeneralProvider(personaLoader *persona.Loader) *GeneralProvider {
|
||||
return &GeneralProvider{
|
||||
personaLoader: personaLoader,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *GeneralProvider) Type() model.SubSessionType {
|
||||
return model.SubSessionGeneral
|
||||
}
|
||||
|
||||
func (p *GeneralProvider) CanHandle(_ context.Context, _ *model.IntentResult, _ string) bool {
|
||||
// General 子会话总是需要(核心对话逻辑)
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *GeneralProvider) Priority() int {
|
||||
return 1 // 最高优先级
|
||||
}
|
||||
|
||||
func (p *GeneralProvider) Timeout() time.Duration {
|
||||
return 30 * time.Second
|
||||
}
|
||||
|
||||
func (p *GeneralProvider) CreateContext(ctx context.Context, params CreateContextParams) ([]model.LLMMessage, error) {
|
||||
messages := []model.LLMMessage{}
|
||||
|
||||
// 加载人格配置获取昔涟身份
|
||||
personaConfig, err := p.personaLoader.Get("cyrene")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载人格配置失败: %w", err)
|
||||
}
|
||||
|
||||
// 构建思维型系统提示词
|
||||
userName := params.Nickname
|
||||
if userName == "" {
|
||||
userName = params.UserID
|
||||
}
|
||||
|
||||
systemPrompt := fmt.Sprintf(`你是%s,正在和%s聊天。
|
||||
|
||||
## 你的回复风格
|
||||
- 像小女友一样自然、温柔、俏皮
|
||||
- 一句话简短些,不要长篇大论
|
||||
- 可以单次发送多条短消息
|
||||
- 句尾可以带 ♪ 符号,适当使用"呢"、"哦"、"呀"等语气词
|
||||
- 永远不说"再见"
|
||||
|
||||
## 你现在要做的是
|
||||
理解%s刚才说的话,想想怎么回复最自然、最温暖。
|
||||
不要急着给完整答案——先思考他想表达什么、他的情绪如何。
|
||||
把你的回答思路整理出来,主会话会综合所有信息后生成最终回复。
|
||||
|
||||
## 输入
|
||||
开拓者刚才说:%s
|
||||
|
||||
## 请按以下格式输出
|
||||
【情绪理解】
|
||||
(简要分析他的情绪状态)
|
||||
|
||||
【话题理解】
|
||||
(他在说什么、想聊什么)
|
||||
|
||||
【回复思路】
|
||||
(你打算怎么回复,1-3个方向即可)`,
|
||||
personaConfig.Identity.TrueName,
|
||||
userName,
|
||||
userName,
|
||||
params.UserMessage,
|
||||
)
|
||||
|
||||
messages = append(messages, model.LLMMessage{
|
||||
Role: model.RoleSystem,
|
||||
Content: systemPrompt,
|
||||
})
|
||||
|
||||
messages = append(messages, model.LLMMessage{
|
||||
Role: model.RoleUser,
|
||||
Content: params.UserMessage,
|
||||
})
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (p *GeneralProvider) Execute(ctx context.Context, subCtx []model.LLMMessage) (*model.SubSessionResult, error) {
|
||||
// General provider 不直接调用 LLM,而是依赖 Manager 注入的 LLMClient
|
||||
// 但我们在此处需要 LLM 调用能力。Provider 通过闭包/接口获取 LLM 客户端。
|
||||
// 由于 Manager 持有 LLMClient,Provider 需要能访问它。
|
||||
// 这里我们返回一个"占位"结果——实际 LLM 调用由 Manager 通过 llmClient 完成。
|
||||
|
||||
// 实际上,根据设计文档,子会话的 LLM 调用应该在 Manager 的 Dispatch 中完成,
|
||||
// 但为了灵活性,我们在 Provider 中也支持直接调用。
|
||||
// 这里我们返回一个空的思考结果(表示无需特殊处理),让 Manager 处理 LLM 调用。
|
||||
|
||||
// 因为 Manager.Dispatch 会先 CreateContext 再调用 Execute,而 Execute 应该
|
||||
// 通过 Manager 提供的 LLMClient 来实际调用 LLM。但当前设计是 Provider 自包含的。
|
||||
// 我们在 manager.go 中会调用 llmClient.Chat,所以这里的 Execute 我们将其简化——
|
||||
// 直接返回一个空结果(没有特殊处理需要),实际的 LLM 调用由 manager 通过 createContext 后的
|
||||
// 消息列表来调用 llmClient。
|
||||
|
||||
// 更好的设计是:Manager 调用 CreateContext 获取上下文,然后用自己的 llmClient 调用 LLM,
|
||||
// Execute 只做后处理。但为了统一接口,我们让 Execute 完成全部逻辑。
|
||||
|
||||
// 由于 GeneralProvider 暂时不需要工具调用等特殊逻辑,我们返回一个简单的摘要标记,
|
||||
// 实际的 LLM 调用将在 orchestrator 中完成(通过 Manager.Dispatch 后的 llmClient)。
|
||||
|
||||
log.Printf("[general-subsession] 通用对话子会话上下文已创建 (%d 条消息)", len(subCtx))
|
||||
return &model.SubSessionResult{
|
||||
Type: model.SubSessionGeneral,
|
||||
Summary: "思考完成,等待主会话综合",
|
||||
Confidence: 0.8,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Ensure llm, persona are used
|
||||
var _ = llm.NewAdapter
|
||||
var _ = persona.NewLoader
|
||||
@@ -0,0 +1,325 @@
|
||||
package subsession
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/tools"
|
||||
)
|
||||
|
||||
// IoTDeviceProvider IoT 设备查询接口
|
||||
type IoTDeviceProvider interface {
|
||||
GetAllDevices() ([]tools.IoTDevice, error)
|
||||
GetDevice(id string) (*tools.IoTDevice, error)
|
||||
ToggleDevice(id string) error
|
||||
SetDeviceProperty(id string, field string, value interface{}) error
|
||||
GetDevicesForContext() []tools.IoTDevice
|
||||
}
|
||||
|
||||
// IoTProvider IoT 控制子会话提供者
|
||||
// 职责:处理 IoT 设备查询和控制请求
|
||||
type IoTProvider struct {
|
||||
iotClient IoTDeviceProvider
|
||||
}
|
||||
|
||||
// NewIoTProvider 创建 IoT 控制子会话提供者
|
||||
func NewIoTProvider(iotClient IoTDeviceProvider) *IoTProvider {
|
||||
return &IoTProvider{
|
||||
iotClient: iotClient,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *IoTProvider) Type() model.SubSessionType {
|
||||
return model.SubSessionIoT
|
||||
}
|
||||
|
||||
func (p *IoTProvider) CanHandle(_ context.Context, intent *model.IntentResult, userMessage string) bool {
|
||||
// 意图分析明确需要 IoT
|
||||
if intent != nil && intent.NeedsIoT {
|
||||
return true
|
||||
}
|
||||
|
||||
// 关键词触发(作为意图分析的补充)
|
||||
iotKeywords := []string{
|
||||
"灯", "空调", "窗帘", "电视", "设备", "开关",
|
||||
"打开", "关闭", "调到", "设置", "温度", "亮度",
|
||||
"传感器", "门锁", "插座", "风扇", "加湿器",
|
||||
}
|
||||
msgLower := strings.ToLower(userMessage)
|
||||
for _, kw := range iotKeywords {
|
||||
if strings.Contains(msgLower, kw) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *IoTProvider) Priority() int {
|
||||
return 3 // 低于 General 和 Memory
|
||||
}
|
||||
|
||||
func (p *IoTProvider) Timeout() time.Duration {
|
||||
return 15 * time.Second
|
||||
}
|
||||
|
||||
func (p *IoTProvider) CreateContext(ctx context.Context, params CreateContextParams) ([]model.LLMMessage, error) {
|
||||
messages := []model.LLMMessage{}
|
||||
|
||||
// 获取当前设备状态
|
||||
var deviceStatusText string
|
||||
if p.iotClient != nil {
|
||||
devices := p.iotClient.GetDevicesForContext()
|
||||
if len(devices) > 0 {
|
||||
deviceStatusText = "当前设备状态:\n"
|
||||
for _, d := range devices {
|
||||
switch d.Type {
|
||||
case "light":
|
||||
if d.Status == "on" {
|
||||
deviceStatusText += fmt.Sprintf("- %s: 开启 (亮度%d%%, 颜色%s)\n", d.Name, d.Brightness, d.Color)
|
||||
} else {
|
||||
deviceStatusText += fmt.Sprintf("- %s: 关闭\n", d.Name)
|
||||
}
|
||||
case "ac":
|
||||
if d.Status == "on" {
|
||||
modeLabel := acModeLabel(d.Mode)
|
||||
deviceStatusText += fmt.Sprintf("- %s: 运行中 (%s %.0f°C)\n", d.Name, modeLabel, d.Temperature)
|
||||
} else {
|
||||
deviceStatusText += fmt.Sprintf("- %s: 关闭\n", d.Name)
|
||||
}
|
||||
case "curtain":
|
||||
if d.Status == "open" {
|
||||
deviceStatusText += fmt.Sprintf("- %s: 已打开\n", d.Name)
|
||||
} else {
|
||||
deviceStatusText += fmt.Sprintf("- %s: 已关闭\n", d.Name)
|
||||
}
|
||||
case "sensor":
|
||||
deviceStatusText += fmt.Sprintf("- %s: %.1f%s\n", d.Name, d.Value, d.Unit)
|
||||
default:
|
||||
deviceStatusText += fmt.Sprintf("- %s: %s\n", d.Name, d.Status)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
deviceStatusText = "(暂无设备状态信息)"
|
||||
}
|
||||
} else {
|
||||
deviceStatusText = "(IoT 客户端未配置)"
|
||||
}
|
||||
|
||||
// 加载人格配置
|
||||
loader, err := persona.NewLoader("")
|
||||
if err != nil {
|
||||
log.Printf("[iot-provider] 加载人格配置失败: %v", err)
|
||||
}
|
||||
personaConfig, _ := loader.Get("cyrene")
|
||||
trueName := "昔涟"
|
||||
if personaConfig != nil {
|
||||
trueName = personaConfig.Identity.TrueName
|
||||
}
|
||||
|
||||
userName := params.Nickname
|
||||
if userName == "" {
|
||||
userName = params.UserID
|
||||
}
|
||||
|
||||
systemPrompt := fmt.Sprintf(`你是%s,正在帮%s控制家里的智能设备。
|
||||
|
||||
## 你的能力
|
||||
你可以通过以下方式帮%s控制设备:
|
||||
- 查询设备当前状态
|
||||
- 开关设备(灯、空调、窗帘等)
|
||||
- 调节设备参数(亮度、温度、模式等)
|
||||
|
||||
## 回复风格
|
||||
- 用俏皮可爱的语气告诉%s操作结果
|
||||
- 简短自然,像小女友一样
|
||||
|
||||
## 当前设备状态
|
||||
%s
|
||||
|
||||
## 用户请求
|
||||
%s说:%s
|
||||
|
||||
## 你的任务
|
||||
分析%s的请求,判断需要:
|
||||
1. 只是查询设备状态?→ 直接基于上面的设备状态回答
|
||||
2. 需要控制设备?→ 说明需要执行什么操作(开关/调节),并生成一个可爱的操作确认消息
|
||||
3. 不需要IoT操作?→ 回复"无需IoT操作"
|
||||
|
||||
请用JSON格式输出:
|
||||
{
|
||||
"action": "query" | "control" | "none",
|
||||
"device_id": "设备ID (如果需要操作)",
|
||||
"device_name": "设备名称",
|
||||
"operation": "toggle" | "set" | "query",
|
||||
"field": "属性名 (如 brightness, temperature)",
|
||||
"value": "属性值",
|
||||
"summary": "给用户的简短操作结果"
|
||||
}`,
|
||||
trueName, userName,
|
||||
userName,
|
||||
userName,
|
||||
deviceStatusText,
|
||||
userName, params.UserMessage,
|
||||
userName,
|
||||
)
|
||||
|
||||
messages = append(messages, model.LLMMessage{
|
||||
Role: model.RoleSystem,
|
||||
Content: systemPrompt,
|
||||
})
|
||||
|
||||
messages = append(messages, model.LLMMessage{
|
||||
Role: model.RoleUser,
|
||||
Content: params.UserMessage,
|
||||
})
|
||||
|
||||
return messages, nil
|
||||
}
|
||||
|
||||
func (p *IoTProvider) Execute(ctx context.Context, subCtx []model.LLMMessage) (*model.SubSessionResult, error) {
|
||||
result := &model.SubSessionResult{
|
||||
Type: model.SubSessionIoT,
|
||||
Summary: "(未执行 IoT 操作)",
|
||||
}
|
||||
|
||||
// 提取用户消息
|
||||
userMessage := ""
|
||||
for i := len(subCtx) - 1; i >= 0; i-- {
|
||||
if subCtx[i].Role == model.RoleUser {
|
||||
userMessage = subCtx[i].Content
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if p.iotClient == nil {
|
||||
result.Summary = "(IoT 客户端未配置,无法控制设备)"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 简单的关键词匹配来执行设备操作(不依赖 LLM 解析)
|
||||
// 这是作为降级方案,当 LLM 不可用时仍然可以处理基本 IoT 命令
|
||||
msgLower := strings.ToLower(userMessage)
|
||||
|
||||
// 尝试获取设备列表进行匹配
|
||||
devices := p.iotClient.GetDevicesForContext()
|
||||
|
||||
for _, dev := range devices {
|
||||
devNameLower := strings.ToLower(dev.Name)
|
||||
|
||||
if !strings.Contains(msgLower, devNameLower) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 匹配到了设备名称
|
||||
if strings.Contains(msgLower, "打开") || strings.Contains(msgLower, "开") {
|
||||
if dev.Status != "on" && dev.Status != "open" {
|
||||
if dev.Type == "curtain" {
|
||||
// 窗帘使用 set 而非 toggle
|
||||
_ = p.iotClient.SetDeviceProperty(dev.ID, "status", "open")
|
||||
} else {
|
||||
_ = p.iotClient.ToggleDevice(dev.ID)
|
||||
}
|
||||
result.Summary = fmt.Sprintf("已帮%s打开%s♪", extractUserName(subCtx), dev.Name)
|
||||
result.Confidence = 0.9
|
||||
result.ToolCalls = []model.ToolCallRecord{{
|
||||
Name: "iot_control",
|
||||
Arguments: map[string]any{"device_id": dev.ID, "operation": "toggle"},
|
||||
Result: "success",
|
||||
}}
|
||||
log.Printf("[iot-subsession] 执行操作: 打开 %s (%s)", dev.Name, dev.ID)
|
||||
return result, nil
|
||||
} else {
|
||||
result.Summary = fmt.Sprintf("%s已经是打开状态啦~", dev.Name)
|
||||
result.Confidence = 0.9
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(msgLower, "关闭") || strings.Contains(msgLower, "关") {
|
||||
if dev.Status == "on" || dev.Status == "open" {
|
||||
if dev.Type == "curtain" {
|
||||
_ = p.iotClient.SetDeviceProperty(dev.ID, "status", "closed")
|
||||
} else {
|
||||
_ = p.iotClient.ToggleDevice(dev.ID)
|
||||
}
|
||||
result.Summary = fmt.Sprintf("已帮%s关闭%s~", extractUserName(subCtx), dev.Name)
|
||||
result.Confidence = 0.9
|
||||
result.ToolCalls = []model.ToolCallRecord{{
|
||||
Name: "iot_control",
|
||||
Arguments: map[string]any{"device_id": dev.ID, "operation": "toggle"},
|
||||
Result: "success",
|
||||
}}
|
||||
log.Printf("[iot-subsession] 执行操作: 关闭 %s (%s)", dev.Name, dev.ID)
|
||||
return result, nil
|
||||
} else {
|
||||
result.Summary = fmt.Sprintf("%s已经是关闭状态啦~", dev.Name)
|
||||
result.Confidence = 0.9
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 查询设备状态
|
||||
deviceStatus := fmt.Sprintf("%s当前状态: %s", dev.Name, dev.Status)
|
||||
if dev.Type == "light" && dev.Status == "on" {
|
||||
deviceStatus += fmt.Sprintf(" (亮度%d%%, 颜色%s)", dev.Brightness, dev.Color)
|
||||
} else if dev.Type == "ac" && dev.Status == "on" {
|
||||
deviceStatus += fmt.Sprintf(" (模式%s, 温度%.0f°C)", dev.Mode, dev.Temperature)
|
||||
}
|
||||
result.Summary = deviceStatus
|
||||
result.Confidence = 0.8
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 没有匹配到设备,可能只是查询所有设备状态
|
||||
if strings.Contains(msgLower, "设备") && (strings.Contains(msgLower, "状态") || strings.Contains(msgLower, "怎么样") || strings.Contains(msgLower, "看看")) {
|
||||
if len(devices) > 0 {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("家里设备状态:\n")
|
||||
for _, d := range devices {
|
||||
sb.WriteString(fmt.Sprintf("- %s: %s\n", d.Name, d.Status))
|
||||
}
|
||||
result.Summary = sb.String()
|
||||
result.Confidence = 0.7
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
|
||||
result.Summary = "(未匹配到 IoT 操作)"
|
||||
result.Confidence = 0.5
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// extractUserName 从上下文中提取用户名
|
||||
func extractUserName(subCtx []model.LLMMessage) string {
|
||||
for _, msg := range subCtx {
|
||||
if msg.Role == model.RoleSystem {
|
||||
// 尝试从系统提示词中提取称呼
|
||||
// 简单返回"你"
|
||||
break
|
||||
}
|
||||
}
|
||||
return "你"
|
||||
}
|
||||
|
||||
func acModeLabel(mode string) string {
|
||||
switch mode {
|
||||
case "cool":
|
||||
return "制冷"
|
||||
case "heat":
|
||||
return "制热"
|
||||
case "auto":
|
||||
return "自动"
|
||||
default:
|
||||
return mode
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure json is used
|
||||
var _ = json.Marshal
|
||||
@@ -0,0 +1,162 @@
|
||||
package subsession
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/model"
|
||||
)
|
||||
|
||||
// Manager 子会话管理器
|
||||
// 负责注册 Provider、分派任务、并行执行、超时控制、结果收集
|
||||
type Manager struct {
|
||||
mu sync.RWMutex
|
||||
providers map[model.SubSessionType]Provider
|
||||
llmClient LLMClient
|
||||
}
|
||||
|
||||
// NewManager 创建子会话管理器
|
||||
func NewManager(llmClient LLMClient) *Manager {
|
||||
return &Manager{
|
||||
providers: make(map[model.SubSessionType]Provider),
|
||||
llmClient: llmClient,
|
||||
}
|
||||
}
|
||||
|
||||
// Register 注册子会话提供者
|
||||
func (m *Manager) Register(provider Provider) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.providers[provider.Type()] = provider
|
||||
log.Printf("[subsession] 注册子会话提供者: %s (优先级=%d, 超时=%v)", provider.Type(), provider.Priority(), provider.Timeout())
|
||||
}
|
||||
|
||||
// RegisterWithOverride 注册或覆盖子会话提供者
|
||||
func (m *Manager) RegisterWithOverride(provider Provider) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.providers[provider.Type()] = provider
|
||||
log.Printf("[subsession] 注册(覆盖)子会话提供者: %s (优先级=%d, 超时=%v)", provider.Type(), provider.Priority(), provider.Timeout())
|
||||
}
|
||||
|
||||
// GetProvider 获取指定类型的 Provider
|
||||
func (m *Manager) GetProvider(t model.SubSessionType) (Provider, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
p, ok := m.providers[t]
|
||||
return p, ok
|
||||
}
|
||||
|
||||
// ListProviders 列出所有已注册的 Provider 类型
|
||||
func (m *Manager) ListProviders() []model.SubSessionType {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
types := make([]model.SubSessionType, 0, len(m.providers))
|
||||
for t := range m.providers {
|
||||
types = append(types, t)
|
||||
}
|
||||
return types
|
||||
}
|
||||
|
||||
// Dispatch 分派任务到子会话,并行执行,返回结果通道
|
||||
func (m *Manager) Dispatch(
|
||||
ctx context.Context,
|
||||
intent *model.IntentResult,
|
||||
userMessage string,
|
||||
params CreateContextParams,
|
||||
) <-chan model.SubSessionResult {
|
||||
|
||||
m.mu.RLock()
|
||||
providers := make([]Provider, 0, len(m.providers))
|
||||
for _, p := range m.providers {
|
||||
providers = append(providers, p)
|
||||
}
|
||||
m.mu.RUnlock()
|
||||
|
||||
resultCh := make(chan model.SubSessionResult, len(providers))
|
||||
var wg sync.WaitGroup
|
||||
|
||||
for _, provider := range providers {
|
||||
if !provider.CanHandle(ctx, intent, userMessage) {
|
||||
log.Printf("[subsession] 跳过子会话 %s: CanHandle 返回 false", provider.Type())
|
||||
continue
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go func(p Provider) {
|
||||
defer wg.Done()
|
||||
|
||||
result := model.SubSessionResult{Type: p.Type()}
|
||||
|
||||
// 创建带超时的 context
|
||||
subCtx, cancel := context.WithTimeout(ctx, p.Timeout())
|
||||
defer cancel()
|
||||
|
||||
// 构建 LLM 上下文
|
||||
llmMessages, err := p.CreateContext(subCtx, params)
|
||||
if err != nil {
|
||||
result.Error = fmt.Sprintf("创建上下文失败: %v", err)
|
||||
log.Printf("[subsession] %s 创建上下文失败: %v", p.Type(), err)
|
||||
resultCh <- result
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[subsession] %s 开始执行 (上下文 %d 条消息)", p.Type(), len(llmMessages))
|
||||
|
||||
// 执行子会话
|
||||
subResult, execErr := p.Execute(subCtx, llmMessages)
|
||||
if execErr != nil {
|
||||
result.Error = fmt.Sprintf("执行失败: %v", execErr)
|
||||
log.Printf("[subsession] %s 执行失败: %v", p.Type(), execErr)
|
||||
resultCh <- result
|
||||
return
|
||||
}
|
||||
|
||||
// 检查超时
|
||||
select {
|
||||
case <-subCtx.Done():
|
||||
result.Error = "子会话超时"
|
||||
log.Printf("[subsession] %s 超时 (limit=%v)", p.Type(), p.Timeout())
|
||||
default:
|
||||
if subResult != nil {
|
||||
result = *subResult
|
||||
result.Type = p.Type()
|
||||
log.Printf("[subsession] %s 完成: 摘要=%s", p.Type(), truncate(result.Summary, 50))
|
||||
}
|
||||
}
|
||||
|
||||
resultCh <- result
|
||||
}(provider)
|
||||
}
|
||||
|
||||
// 等待所有子会话完成,关闭通道
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultCh)
|
||||
}()
|
||||
|
||||
return resultCh
|
||||
}
|
||||
|
||||
// generateID 生成随机 ID
|
||||
func generateID() string {
|
||||
b := make([]byte, 12)
|
||||
rand.Read(b)
|
||||
return fmt.Sprintf("sub-%x", b)
|
||||
}
|
||||
|
||||
// truncate 截断字符串
|
||||
func truncate(s string, maxLen int) string {
|
||||
runes := []rune(s)
|
||||
if len(runes) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return string(runes[:maxLen]) + "..."
|
||||
}
|
||||
|
||||
// Ensure llm is used
|
||||
var _ = llm.NewAdapter
|
||||
@@ -0,0 +1,143 @@
|
||||
package subsession
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/model"
|
||||
)
|
||||
|
||||
// MemoryRetriever 记忆检索接口
|
||||
type MemoryRetriever interface {
|
||||
Retrieve(ctx context.Context, userID string, query string) ([]memory.MemoryEntry, error)
|
||||
}
|
||||
|
||||
// MemoryProvider 记忆检索子会话提供者
|
||||
// 职责:检索与当前对话相关的用户记忆,排序去重,返回结构化摘要
|
||||
type MemoryProvider struct {
|
||||
retriever MemoryRetriever
|
||||
}
|
||||
|
||||
// NewMemoryProvider 创建记忆检索子会话提供者
|
||||
func NewMemoryProvider(retriever MemoryRetriever) *MemoryProvider {
|
||||
return &MemoryProvider{
|
||||
retriever: retriever,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) Type() model.SubSessionType {
|
||||
return model.SubSessionMemory
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) CanHandle(_ context.Context, intent *model.IntentResult, _ string) bool {
|
||||
// 如果意图分析明确不需要记忆,则跳过
|
||||
if intent != nil && !intent.NeedsMemory {
|
||||
// 但为了对话质量,大多数情况下仍然需要记忆
|
||||
// 只有明确 negative 时才跳过
|
||||
if intent.Sentiment == "neutral" && intent.Primary == "chat" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 默认总是检索记忆
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) Priority() int {
|
||||
return 2 // 仅次于 General
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) Timeout() time.Duration {
|
||||
return 10 * time.Second
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) CreateContext(ctx context.Context, params CreateContextParams) ([]model.LLMMessage, error) {
|
||||
// Memory 子会话不依赖 LLM 上下文构建,直接在 Execute 中检索
|
||||
// 返回简单上下文供日志记录
|
||||
return []model.LLMMessage{
|
||||
{Role: model.RoleSystem, Content: "记忆检索子会话"},
|
||||
{Role: model.RoleUser, Content: params.UserMessage},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *MemoryProvider) Execute(ctx context.Context, subCtx []model.LLMMessage) (*model.SubSessionResult, error) {
|
||||
// 从 subCtx 中提取用户消息 (最后一条 user 消息)
|
||||
userMessage := ""
|
||||
for i := len(subCtx) - 1; i >= 0; i-- {
|
||||
if subCtx[i].Role == model.RoleUser {
|
||||
userMessage = subCtx[i].Content
|
||||
break
|
||||
}
|
||||
}
|
||||
if userMessage == "" {
|
||||
return nil, fmt.Errorf("无法从子会话上下文中提取用户消息")
|
||||
}
|
||||
|
||||
// 从 context 中提取 userID (通过 context value 传递)
|
||||
userID, _ := ctx.Value("userID").(string)
|
||||
if userID == "" {
|
||||
userID = "unknown"
|
||||
}
|
||||
|
||||
result := &model.SubSessionResult{
|
||||
Type: model.SubSessionMemory,
|
||||
Memories: []model.MemorySnippet{},
|
||||
Confidence: 0,
|
||||
}
|
||||
|
||||
if p.retriever == nil {
|
||||
log.Printf("[memory-subsession] 记忆检索器未初始化")
|
||||
result.Summary = "(记忆系统未就绪)"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
memories, err := p.retriever.Retrieve(ctx, userID, userMessage)
|
||||
if err != nil {
|
||||
log.Printf("[memory-subsession] 记忆检索失败: %v", err)
|
||||
result.Error = fmt.Sprintf("检索失败: %v", err)
|
||||
result.Summary = "(记忆检索失败,但不影响对话)"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 转换为 MemorySnippet
|
||||
snippets := make([]model.MemorySnippet, 0, len(memories))
|
||||
for _, m := range memories {
|
||||
snippets = append(snippets, model.MemorySnippet{
|
||||
ID: m.ID,
|
||||
Content: m.Content,
|
||||
Category: string(m.Category),
|
||||
Importance: m.Importance,
|
||||
Relevance: 0.5, // 默认相关度
|
||||
})
|
||||
}
|
||||
|
||||
// 生成摘要
|
||||
if len(snippets) == 0 {
|
||||
result.Summary = "(没有找到相关记忆)"
|
||||
} else {
|
||||
result.Summary = fmt.Sprintf("检索到 %d 条相关记忆", len(snippets))
|
||||
// 按重要性列出前几条
|
||||
topCount := len(snippets)
|
||||
if topCount > 3 {
|
||||
topCount = 3
|
||||
}
|
||||
details := ""
|
||||
for i := 0; i < topCount; i++ {
|
||||
s := snippets[i]
|
||||
content := s.Content
|
||||
runes := []rune(content)
|
||||
if len(runes) > 40 {
|
||||
content = string(runes[:40]) + "..."
|
||||
}
|
||||
details += fmt.Sprintf("- [%s] %s\n", s.Category, content)
|
||||
}
|
||||
result.Details = details
|
||||
result.Confidence = 0.7
|
||||
}
|
||||
|
||||
result.Memories = snippets
|
||||
log.Printf("[memory-subsession] 完成: %s", result.Summary)
|
||||
return result, nil
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package subsession
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/model"
|
||||
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
|
||||
)
|
||||
|
||||
// Provider 子会话提供者接口
|
||||
// 每种子会话类型实现此接口
|
||||
type Provider interface {
|
||||
// Type 返回子会话类型标识
|
||||
Type() model.SubSessionType
|
||||
|
||||
// CanHandle 判断是否需要为此消息创建子会话
|
||||
CanHandle(ctx context.Context, intent *model.IntentResult, userMessage string) bool
|
||||
|
||||
// Priority 返回优先级 (数字越小优先级越高)
|
||||
Priority() int
|
||||
|
||||
// CreateContext 创建子会话的 LLM 上下文
|
||||
// 不包含对话历史(历史由 Orchestrator 统一管理)
|
||||
CreateContext(ctx context.Context, params CreateContextParams) ([]model.LLMMessage, error)
|
||||
|
||||
// Timeout 返回此子会话的超时时间
|
||||
Timeout() time.Duration
|
||||
|
||||
// Execute 执行子会话逻辑,返回结果
|
||||
// 子会话可以调用 LLM、执行工具调用等
|
||||
Execute(ctx context.Context, subCtx []model.LLMMessage) (*model.SubSessionResult, error)
|
||||
}
|
||||
|
||||
// CreateContextParams 创建上下文参数
|
||||
type CreateContextParams struct {
|
||||
UserID string
|
||||
SessionID string
|
||||
UserMessage string
|
||||
PersonaConfig *persona.PersonaConfig
|
||||
DeviceContext string // IoT 设备状态文本
|
||||
Intent *model.IntentResult
|
||||
Nickname string // 用户昵称
|
||||
}
|
||||
|
||||
// LLMClient LLM 调用接口(避免循环依赖)
|
||||
type LLMClient interface {
|
||||
Chat(ctx context.Context, messages []model.LLMMessage) (*model.LLMResponse, error)
|
||||
ChatWithTools(ctx context.Context, messages []model.LLMMessage, tools []llm.OpenAITool) (*model.LLMResponse, error)
|
||||
}
|
||||
Reference in New Issue
Block a user