Files
smartmate/backend/agent/state.go
Losita fb87ceaff5 Version: 0.5.2.dev.260312
feat(agent):  在 Agent 聊天接口中新增 AI 随口记功能

* 无相关意图时保持正常聊天,若识别到相关意图则自动切换为随口记模式
* 支持阶段状态反馈与话题化回复,提升交互体验

- 引入请求级当前时间基准,支持相对时间解析(如“明天”、“下周一”等)
- 增加非法日期拦截机制,防止用户输入格式错误的时间并返回修正提示
- 优化随口记图谱,补充阶段打点与详细中文注释,失败/重试分支处理更清晰
- 推送 `reasoning_content` 阶段状态,涵盖 `request.accepted`、`intent`、`deadline`、`priority`、`persisting`、`persisted`、`reply.polishing` 等状态
- 最终文案改为“事实句 + AI 生成的贴题轻松跟进句”,避免硬编码调侃内容
- 完善时间解析相关测试,确保功能正确性,测试通过 `go test ./...`

---

improvements: 🛠️ 开发心路历程与优化

* 修复随口记链路中 `assistant` 消息未写入 Redis 的问题,确保消息持久化
* 去除“分段正文伪流式”处理,改为最终正文一次性输出,简化内容流转
2026-03-12 22:17:20 +08:00

150 lines
5.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package agent
import "time"
const (
// QuickNoteDatetimeMinuteLayout 是“随口记”链路内部统一的分钟级时间格式。
// 说明:
// 1) 用于把“当前时间基准”传给模型,避免模型在相对时间推断时出现秒级抖动。
// 2) 用于日志和调试,读起来比 RFC3339 更直观。
QuickNoteDatetimeMinuteLayout = "2006-01-02 15:04"
// quickNoteTimezoneName 是随口记链路默认业务时区。
// 这里固定为东八区,避免容器运行在 UTC 时把“明天/今晚”解释偏移到错误日期。
quickNoteTimezoneName = "Asia/Shanghai"
// QuickNotePriorityImportantUrgent 对应四象限里的“重要且紧急”。
// 在当前 tasks 表中映射为 priority=1数值越小优先级越高
QuickNotePriorityImportantUrgent = 1
// QuickNotePriorityImportantNotUrgent 对应“重要不紧急”。
QuickNotePriorityImportantNotUrgent = 2
// QuickNotePrioritySimpleNotImportant 对应“简单不重要”。
QuickNotePrioritySimpleNotImportant = 3
// QuickNotePriorityComplexNotImportant 对应“不简单不重要”。
QuickNotePriorityComplexNotImportant = 4
)
// IsValidTaskPriority 判断优先级是否合法。
// 目前后端任务模型限定为 1~4。
func IsValidTaskPriority(priority int) bool {
return priority >= QuickNotePriorityImportantUrgent && priority <= QuickNotePriorityComplexNotImportant
}
// PriorityLabelCN 把优先级数值转换为中文标签,便于拼接给用户的自然语言回复。
func PriorityLabelCN(priority int) string {
switch priority {
case QuickNotePriorityImportantUrgent:
return "重要且紧急"
case QuickNotePriorityImportantNotUrgent:
return "重要不紧急"
case QuickNotePrioritySimpleNotImportant:
return "简单不重要"
case QuickNotePriorityComplexNotImportant:
return "不简单不重要"
default:
return "未知优先级"
}
}
// QuickNoteState 是“AI随口记”链路在 graph 节点间传递的统一状态容器。
// 设计目标:
// 1) 把本次请求的上下文收拢到一个结构里,降低节点函数参数散落;
// 2) 让“识别、评估、写库、重试、回复”每一步都可追踪;
// 3) 便于后续扩展打点和可观测字段(例如时间解析失败原因)。
type QuickNoteState struct {
// 基础上下文:用于日志关联与用户隔离。
TraceID string
UserID int
ConversationID string
// RequestNow 记录“请求进入随口记链路时”的时间基准(分钟级)。
// 所有相对时间(明天/后天/下周一)都必须基于这个时间计算,
// 这样同一次请求内不会因为时间流逝产生口径漂移。
RequestNow time.Time
// RequestNowText 是 RequestNow 的字符串形式,主要用于 prompt 注入。
RequestNowText string
// 用户原始输入(例如:提醒我下周日之前完成大作业)。
UserInput string
// 意图判定结果。
IsQuickNoteIntent bool
IntentJudgeReason string
// 结构化抽取结果:由“意图识别/信息抽取”节点写入。
ExtractedTitle string
ExtractedDeadline *time.Time
ExtractedDeadlineText string
ExtractedPriority int
// ExtractedPriorityReason 记录优先级评估理由,便于后续排查模型判断是否符合预期。
ExtractedPriorityReason string
// DeadlineValidationError 记录时间校验失败原因。
// 只要该字段非空,就说明用户提供了无法解析的时间表达,本次请求不应落库。
DeadlineValidationError string
// 工具调用过程状态:用于重试与故障回溯。
ToolAttemptCount int
MaxToolRetry int
LastToolError string
// 最终持久化结果:由“写库工具”节点回填。
PersistedTaskID int
Persisted bool
// AssistantReply 是 graph 最终给用户的回复文案。
AssistantReply string
}
// NewQuickNoteState 创建随口记状态对象并初始化默认重试次数。
func NewQuickNoteState(traceID string, userID int, conversationID, userInput string) *QuickNoteState {
requestNow := quickNoteNowToMinute()
return &QuickNoteState{
TraceID: traceID,
UserID: userID,
ConversationID: conversationID,
RequestNow: requestNow,
RequestNowText: formatQuickNoteTimeToMinute(requestNow),
UserInput: userInput,
MaxToolRetry: 3,
}
}
// CanRetryTool 判断当前是否还能继续重试工具调用。
func (s *QuickNoteState) CanRetryTool() bool {
return s.ToolAttemptCount < s.MaxToolRetry
}
// RecordToolError 记录一次工具调用失败。
func (s *QuickNoteState) RecordToolError(errMsg string) {
s.ToolAttemptCount++
s.LastToolError = errMsg
}
// RecordToolSuccess 记录一次工具调用成功。
func (s *QuickNoteState) RecordToolSuccess(taskID int) {
s.ToolAttemptCount++
s.PersistedTaskID = taskID
s.Persisted = true
s.LastToolError = ""
}
// quickNoteLocation 返回随口记链路使用的业务时区。
func quickNoteLocation() *time.Location {
loc, err := time.LoadLocation(quickNoteTimezoneName)
if err != nil {
return time.Local
}
return loc
}
// quickNoteNowToMinute 返回当前时间并截断到分钟级。
func quickNoteNowToMinute() time.Time {
return time.Now().In(quickNoteLocation()).Truncate(time.Minute)
}
// formatQuickNoteTimeToMinute 将时间格式化为分钟级字符串。
func formatQuickNoteTimeToMinute(t time.Time) string {
return t.In(quickNoteLocation()).Format(QuickNoteDatetimeMinuteLayout)
}