Version: 0.5.9.dev.260315
✨ 为原有流式聊天链路补充“聊天结束后异步调用 LLM 生成对话标题并落库”的机制,相关测试已通过 📄 新增“获取对话元信息”接口,便于前端统一获取对话的各类信息,包括上述异步生成的标题
This commit is contained in:
@@ -1,16 +1,20 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/LoveLosita/smartflow/backend/model"
|
"github.com/LoveLosita/smartflow/backend/model"
|
||||||
"github.com/LoveLosita/smartflow/backend/respond"
|
"github.com/LoveLosita/smartflow/backend/respond"
|
||||||
"github.com/LoveLosita/smartflow/backend/service"
|
"github.com/LoveLosita/smartflow/backend/service"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AgentHandler struct {
|
type AgentHandler struct {
|
||||||
@@ -82,3 +86,39 @@ func (api *AgentHandler) ChatAgent(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetConversationMeta 返回单个会话的元信息(标题、消息数、最近消息时间等)。
|
||||||
|
// 设计说明:
|
||||||
|
// 1) 该接口用于配合 SSE 聊天链路:标题异步生成后,前端可通过 conversation_id 拉取;
|
||||||
|
// 2) 不依赖 SSE header 动态更新,避免“header 必须首包前写入”的协议限制;
|
||||||
|
// 3) 会话不存在时返回 400,避免前端把无效会话当成系统错误。
|
||||||
|
func (api *AgentHandler) GetConversationMeta(c *gin.Context) {
|
||||||
|
// 1. 读取 query 参数并做基础校验。
|
||||||
|
conversationID := strings.TrimSpace(c.Query("conversation_id"))
|
||||||
|
if conversationID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, respond.MissingParam)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 统一透传 user_id,避免越权读取他人会话。
|
||||||
|
userID := c.GetInt("user_id")
|
||||||
|
|
||||||
|
// 3. 设置短超时,避免该查询接口被慢查询长时间占用。
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 1*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 4. 调 service 查询会话元信息。
|
||||||
|
meta, err := api.svc.GetConversationMeta(ctx, userID, conversationID)
|
||||||
|
if err != nil {
|
||||||
|
// 会话不存在按参数错误处理,返回 400 给前端更直观。
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
c.JSON(http.StatusBadRequest, respond.WrongParamType)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
respond.DealWithError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 返回统一响应结构。
|
||||||
|
c.JSON(http.StatusOK, respond.RespWithData(respond.Ok, meta))
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package dao
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/LoveLosita/smartflow/backend/model"
|
"github.com/LoveLosita/smartflow/backend/model"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -70,3 +71,57 @@ func (a *AgentDAO) IfChatExists(ctx context.Context, userID int, chatID string)
|
|||||||
}
|
}
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetConversationMeta 查询单个会话的元信息。
|
||||||
|
// 用途:
|
||||||
|
// 1) 给前端提供“当前会话标题/消息数/最近消息时间”等展示字段;
|
||||||
|
// 2) 与流式聊天接口解耦,避免在 SSE 头部里塞动态标题。
|
||||||
|
func (a *AgentDAO) GetConversationMeta(ctx context.Context, userID int, chatID string) (*model.AgentChat, error) {
|
||||||
|
var chat model.AgentChat
|
||||||
|
err := a.db.WithContext(ctx).
|
||||||
|
Select("chat_id", "title", "message_count", "last_message_at", "status").
|
||||||
|
Where("user_id = ? AND chat_id = ?", userID, chatID).
|
||||||
|
First(&chat).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &chat, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetConversationTitle 读取当前会话标题。
|
||||||
|
// 返回值说明:
|
||||||
|
// 1) title:标题内容(若为空表示尚未生成);
|
||||||
|
// 2) exists:会话是否存在;
|
||||||
|
// 3) err:数据库错误。
|
||||||
|
func (a *AgentDAO) GetConversationTitle(ctx context.Context, userID int, chatID string) (title string, exists bool, err error) {
|
||||||
|
var chat model.AgentChat
|
||||||
|
queryErr := a.db.WithContext(ctx).
|
||||||
|
Select("title").
|
||||||
|
Where("user_id = ? AND chat_id = ?", userID, chatID).
|
||||||
|
First(&chat).Error
|
||||||
|
if queryErr != nil {
|
||||||
|
if errors.Is(queryErr, gorm.ErrRecordNotFound) {
|
||||||
|
return "", false, nil
|
||||||
|
}
|
||||||
|
return "", false, queryErr
|
||||||
|
}
|
||||||
|
if chat.Title == nil {
|
||||||
|
return "", true, nil
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(*chat.Title), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateConversationTitleIfEmpty 仅在标题为空时写入会话标题。
|
||||||
|
// 设计目的:
|
||||||
|
// 1) 避免每轮对话都覆盖已有标题;
|
||||||
|
// 2) 并发下保持幂等:多个 goroutine 同时尝试写标题,最终只会成功一次。
|
||||||
|
func (a *AgentDAO) UpdateConversationTitleIfEmpty(ctx context.Context, userID int, chatID, title string) error {
|
||||||
|
normalized := strings.TrimSpace(title)
|
||||||
|
if normalized == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return a.db.WithContext(ctx).
|
||||||
|
Model(&model.AgentChat{}).
|
||||||
|
Where("user_id = ? AND chat_id = ? AND (title IS NULL OR title = '')", userID, chatID).
|
||||||
|
Update("title", normalized).Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,20 @@ type UserSendMessageRequest struct {
|
|||||||
Thinking bool `json:"thinking,omitempty"`
|
Thinking bool `json:"thinking,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetConversationMetaResponse 是会话元信息查询接口的返回结构。
|
||||||
|
// 说明:
|
||||||
|
// 1) title 可能为空字符串(表示标题尚未生成);
|
||||||
|
// 2) has_title 便于前端快速判断是否需要展示默认占位文案;
|
||||||
|
// 3) 保留 message_count/last_message_at,方便前端后续扩展会话列表排序或角标。
|
||||||
|
type GetConversationMetaResponse struct {
|
||||||
|
ConversationID string `json:"conversation_id"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
HasTitle bool `json:"has_title"`
|
||||||
|
MessageCount int `json:"message_count"`
|
||||||
|
LastMessageAt *time.Time `json:"last_message_at,omitempty"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
type SSEResponse struct {
|
type SSEResponse struct {
|
||||||
Event string `json:"event"`
|
Event string `json:"event"`
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty"`
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ func RegisterRouters(handlers *api.ApiHandlers, cache *dao.CacheDAO, limiter *pk
|
|||||||
{
|
{
|
||||||
agentGroup.Use(middleware.JWTTokenAuth(cache), middleware.RateLimitMiddleware(limiter, 20, 1))
|
agentGroup.Use(middleware.JWTTokenAuth(cache), middleware.RateLimitMiddleware(limiter, 20, 1))
|
||||||
agentGroup.POST("/chat", handlers.AgentHandler.ChatAgent)
|
agentGroup.POST("/chat", handlers.AgentHandler.ChatAgent)
|
||||||
|
agentGroup.GET("/conversation-meta", handlers.AgentHandler.GetConversationMeta)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 初始化Gin引擎
|
// 初始化Gin引擎
|
||||||
|
|||||||
@@ -194,6 +194,10 @@ func (s *AgentService) runNormalChatFlow(
|
|||||||
}); saveErr != nil {
|
}); saveErr != nil {
|
||||||
pushErrNonBlocking(errChan, saveErr)
|
pushErrNonBlocking(errChan, saveErr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 9. 在主回复完成后异步尝试生成会话标题(仅首次、仅标题为空时生效)。
|
||||||
|
// 该步骤不影响当前请求返回时延,也不影响聊天主链路成功与否。
|
||||||
|
s.ensureConversationTitleAsync(userID, chatID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AgentService) AgentChat(ctx context.Context, userMessage string, ifThinking bool, modelName string, userID int, chatID string) (<-chan string, <-chan error) {
|
func (s *AgentService) AgentChat(ctx context.Context, userMessage string, ifThinking bool, modelName string, userID int, chatID string) (<-chan string, <-chan error) {
|
||||||
@@ -289,10 +293,12 @@ func (s *AgentService) AgentChat(ctx context.Context, userMessage string, ifThin
|
|||||||
|
|
||||||
// 3.6 对随口记回复执行统一后置持久化(Redis + outbox/DB)。
|
// 3.6 对随口记回复执行统一后置持久化(Redis + outbox/DB)。
|
||||||
s.persistChatAfterReply(ctx, userID, chatID, userMessage, quickReply, errChan)
|
s.persistChatAfterReply(ctx, userID, chatID, userMessage, quickReply, errChan)
|
||||||
|
// 3.7 随口记链路同样异步生成会话标题(仅首次写入)。
|
||||||
|
s.ensureConversationTitleAsync(userID, chatID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3.7 路由误判或 graph 判定非随口记时,回落普通聊天,保证“能聊”。
|
// 3.8 路由误判或 graph 判定非随口记时,回落普通聊天,保证“能聊”。
|
||||||
progress.Emit("quick_note.fallback", "当前输入不是随口记请求,切换到普通对话。")
|
progress.Emit("quick_note.fallback", "当前输入不是随口记请求,切换到普通对话。")
|
||||||
s.runNormalChatFlow(ctx, selectedModel, resolvedModelName, userMessage, ifThinking, userID, chatID, traceID, requestStart, outChan, errChan)
|
s.runNormalChatFlow(ctx, selectedModel, resolvedModelName, userMessage, ifThinking, userID, chatID, traceID, requestStart, outChan, errChan)
|
||||||
}()
|
}()
|
||||||
|
|||||||
224
backend/service/agentsvc/agent_meta.go
Normal file
224
backend/service/agentsvc/agent_meta.go
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
package agentsvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/LoveLosita/smartflow/backend/model"
|
||||||
|
"github.com/cloudwego/eino-ext/components/model/ark"
|
||||||
|
einoModel "github.com/cloudwego/eino/components/model"
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
arkModel "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// conversationTitleTimeout 是异步标题生成的超时时间。
|
||||||
|
// 该过程不在主请求链路里,但仍要设置上限,避免后台协程长时间阻塞。
|
||||||
|
conversationTitleTimeout = 4 * time.Second
|
||||||
|
// conversationTitleHistoryLimit 限制参与“生成标题”的最近消息条数。
|
||||||
|
// 只取最近几轮可减少 token 成本,同时足够概括当前会话主题。
|
||||||
|
conversationTitleHistoryLimit = 8
|
||||||
|
// conversationTitleMaxChars 是标题最大字符数(按 rune 计)。
|
||||||
|
// 控制标题长度,避免前端展示溢出。
|
||||||
|
conversationTitleMaxChars = 24
|
||||||
|
)
|
||||||
|
|
||||||
|
const conversationTitlePrompt = `你是 SmartFlow 的会话标题生成器。
|
||||||
|
请基于给定对话内容,生成一个简短中文标题。
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1) 只输出标题文本,不要解释,不要加引号,不要 markdown。
|
||||||
|
2) 标题长度控制在 8~20 个中文字符,尽量自然、口语化。
|
||||||
|
3) 不要出现“用户/助手/对话/聊天记录”等泛化词。
|
||||||
|
4) 如果内容是任务提醒类,标题应体现核心事项。`
|
||||||
|
|
||||||
|
// GetConversationMeta 返回单个会话的元信息(供前端轮询/主动拉取)。
|
||||||
|
// 说明:
|
||||||
|
// 1) 该接口和 SSE 流解耦,不依赖流式 header;
|
||||||
|
// 2) title 允许为空,前端可根据 has_title 决定是否展示占位文案。
|
||||||
|
func (s *AgentService) GetConversationMeta(ctx context.Context, userID int, chatID string) (*model.GetConversationMetaResponse, error) {
|
||||||
|
chat, err := s.repo.GetConversationMeta(ctx, userID, strings.TrimSpace(chatID))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
title := ""
|
||||||
|
if chat.Title != nil {
|
||||||
|
title = strings.TrimSpace(*chat.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &model.GetConversationMetaResponse{
|
||||||
|
ConversationID: chat.ChatID,
|
||||||
|
Title: title,
|
||||||
|
HasTitle: title != "",
|
||||||
|
MessageCount: chat.MessageCount,
|
||||||
|
LastMessageAt: chat.LastMessageAt,
|
||||||
|
Status: chat.Status,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureConversationTitleAsync 在后台异步生成并写入会话标题。
|
||||||
|
// 设计约束:
|
||||||
|
// 1) 仅在“标题为空”时尝试生成,避免覆盖用户已确认/已存在标题;
|
||||||
|
// 2) 失败只记日志,不影响当前聊天链路;
|
||||||
|
// 3) 标题素材优先来自 Redis 历史(命中快、与当前上下文一致)。
|
||||||
|
func (s *AgentService) ensureConversationTitleAsync(userID int, chatID string) {
|
||||||
|
if s == nil || s.repo == nil || s.agentCache == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(chatID) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// 1. 后台任务使用独立超时上下文,避免受请求 ctx 取消影响。
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), conversationTitleTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// 2. 先查当前标题;若已存在则直接返回,不做多余模型调用。
|
||||||
|
title, exists, err := s.repo.GetConversationTitle(ctx, userID, chatID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("异步生成会话标题失败(读取标题失败) chat=%s err=%v", chatID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !exists || strings.TrimSpace(title) != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 从 Redis 读取当前会话历史,作为标题生成素材。
|
||||||
|
history, err := s.agentCache.GetHistory(ctx, chatID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("异步生成会话标题失败(读取历史失败) chat=%s err=%v", chatID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(history) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 调用模型生成标题,并做格式清洗。
|
||||||
|
generated, err := s.generateConversationTitle(ctx, history)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("异步生成会话标题失败(模型生成失败) chat=%s err=%v", chatID, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(generated) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 只在标题仍为空时写入,保证并发幂等。
|
||||||
|
if err = s.repo.UpdateConversationTitleIfEmpty(ctx, userID, chatID, generated); err != nil {
|
||||||
|
log.Printf("异步生成会话标题失败(写库失败) chat=%s err=%v", chatID, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateConversationTitle 使用聊天模型从近期历史生成标题。
|
||||||
|
func (s *AgentService) generateConversationTitle(ctx context.Context, history []*schema.Message) (string, error) {
|
||||||
|
modelInst := s.pickTitleModel()
|
||||||
|
if modelInst == nil {
|
||||||
|
return "", fmt.Errorf("标题生成模型未初始化")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 只取最近 N 条,降低 token 并聚焦当前会话主题。
|
||||||
|
trimmed := tailMessages(history, conversationTitleHistoryLimit)
|
||||||
|
prompt := buildConversationTitleUserPrompt(trimmed)
|
||||||
|
if strings.TrimSpace(prompt) == "" {
|
||||||
|
return "", fmt.Errorf("缺少可用历史内容")
|
||||||
|
}
|
||||||
|
|
||||||
|
messages := []*schema.Message{
|
||||||
|
schema.SystemMessage(conversationTitlePrompt),
|
||||||
|
schema.UserMessage(prompt),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 标题生成属于结构化短输出,关闭 thinking 并限制 tokens,降低延迟与发散。
|
||||||
|
resp, err := modelInst.Generate(ctx, messages,
|
||||||
|
ark.WithThinking(&arkModel.Thinking{Type: arkModel.ThinkingTypeDisabled}),
|
||||||
|
einoModel.WithTemperature(0.2),
|
||||||
|
einoModel.WithMaxTokens(40),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
return "", fmt.Errorf("标题生成模型返回为空")
|
||||||
|
}
|
||||||
|
return normalizeConversationTitle(resp.Content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// pickTitleModel 选择用于标题生成的模型。
|
||||||
|
// 优先 worker(成本低、速度快);worker 不可用时回退 strategist。
|
||||||
|
func (s *AgentService) pickTitleModel() *ark.ChatModel {
|
||||||
|
if s.AIHub == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if s.AIHub.Worker != nil {
|
||||||
|
return s.AIHub.Worker
|
||||||
|
}
|
||||||
|
return s.AIHub.Strategist
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildConversationTitleUserPrompt 把消息历史拼成可读文本供模型总结。
|
||||||
|
func buildConversationTitleUserPrompt(messages []*schema.Message) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString("请根据以下对话内容生成标题:\n")
|
||||||
|
for _, msg := range messages {
|
||||||
|
if msg == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
content := strings.TrimSpace(msg.Content)
|
||||||
|
if content == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// 单条消息做长度裁剪,避免超长回复把标题主题“冲淡”。
|
||||||
|
content = trimRunes(content, 80)
|
||||||
|
role := "助手"
|
||||||
|
if strings.EqualFold(strings.TrimSpace(string(msg.Role)), string(schema.User)) {
|
||||||
|
role = "用户"
|
||||||
|
}
|
||||||
|
builder.WriteString(role)
|
||||||
|
builder.WriteString(":")
|
||||||
|
builder.WriteString(content)
|
||||||
|
builder.WriteString("\n")
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(builder.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func tailMessages(messages []*schema.Message, limit int) []*schema.Message {
|
||||||
|
if limit <= 0 || len(messages) <= limit {
|
||||||
|
return messages
|
||||||
|
}
|
||||||
|
return messages[len(messages)-limit:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeConversationTitle 清洗模型输出,确保可直接展示/存库。
|
||||||
|
func normalizeConversationTitle(raw string) string {
|
||||||
|
text := strings.TrimSpace(raw)
|
||||||
|
if text == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if idx := strings.Index(text, "\n"); idx >= 0 {
|
||||||
|
text = strings.TrimSpace(text[:idx])
|
||||||
|
}
|
||||||
|
text = strings.Trim(text, "\"'“”‘’《》[]【】")
|
||||||
|
text = strings.TrimPrefix(text, "标题:")
|
||||||
|
text = strings.TrimPrefix(text, "标题:")
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
text = trimRunes(text, conversationTitleMaxChars)
|
||||||
|
return strings.TrimSpace(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimRunes(text string, limit int) string {
|
||||||
|
if limit <= 0 || text == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if utf8.RuneCountInString(text) <= limit {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
runes := []rune(text)
|
||||||
|
return string(runes[:limit])
|
||||||
|
}
|
||||||
40
backend/service/agentsvc/agent_meta_test.go
Normal file
40
backend/service/agentsvc/agent_meta_test.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package agentsvc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/cloudwego/eino/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestNormalizeConversationTitle
|
||||||
|
// 目的:确保标题清洗逻辑能去掉引号/前缀并裁剪到上限长度。
|
||||||
|
func TestNormalizeConversationTitle(t *testing.T) {
|
||||||
|
raw := "标题:\"明天上午去机场接人并顺路取快递,记得提前出门\""
|
||||||
|
got := normalizeConversationTitle(raw)
|
||||||
|
if strings.HasPrefix(got, "标题") {
|
||||||
|
t.Fatalf("标题前缀未清洗,got=%s", got)
|
||||||
|
}
|
||||||
|
if len([]rune(got)) > conversationTitleMaxChars {
|
||||||
|
t.Fatalf("标题长度超限,got=%s", got)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(got) == "" {
|
||||||
|
t.Fatalf("清洗后标题不应为空")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBuildConversationTitleUserPrompt
|
||||||
|
// 目的:确保 prompt 构造时能正确标注用户/助手角色并包含有效内容。
|
||||||
|
func TestBuildConversationTitleUserPrompt(t *testing.T) {
|
||||||
|
msgs := []*schema.Message{
|
||||||
|
{Role: schema.User, Content: "明天早上九点去机场接人"},
|
||||||
|
{Role: schema.Assistant, Content: "收到,我帮你记下了。"},
|
||||||
|
}
|
||||||
|
prompt := buildConversationTitleUserPrompt(msgs)
|
||||||
|
if !strings.Contains(prompt, "用户:明天早上九点去机场接人") {
|
||||||
|
t.Fatalf("prompt 未包含用户内容,prompt=%s", prompt)
|
||||||
|
}
|
||||||
|
if !strings.Contains(prompt, "助手:收到,我帮你记下了。") {
|
||||||
|
t.Fatalf("prompt 未包含助手内容,prompt=%s", prompt)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user