package route
import (
"context"
"fmt"
"log"
"regexp"
"strings"
"time"
"github.com/LoveLosita/smartflow/backend/agent/quicknote"
"github.com/cloudwego/eino-ext/components/model/ark"
einoModel "github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/schema"
"github.com/google/uuid"
arkModel "github.com/volcengine/volcengine-go-sdk/service/arkruntime/model"
)
const (
// ControlTimeout 是“模型控制码分流”步骤的额外子超时。
// 设为 0 表示完全跟随父请求上下文,不额外截断。
ControlTimeout = 0 * time.Second
)
var (
// 控制头格式:
//
routeHeaderRegex = regexp.MustCompile(`(?is)<\s*smartflow_route\b[^>]*\bnonce\s*=\s*["']?([a-zA-Z0-9\-]+)["']?[^>]*\baction\s*=\s*["']?(quick_note|chat)["']?[^>]*>`)
// 可选理由块:
// ...
routeReasonRegex = regexp.MustCompile(`(?is)<\s*smartflow_reason\s*>(.*?)<\s*/\s*smartflow_reason\s*>`)
)
// Action 表示控制码路由动作。
type Action string
const (
ActionChat Action = "chat"
ActionQuickNote Action = "quick_note"
)
// ControlDecision 是“控制码解析结果”。
type ControlDecision struct {
Action Action
Reason string
Raw string
}
// RoutingDecision 是服务层最终使用的路由结果。
type RoutingDecision struct {
EnterQuickNote bool
TrustRoute bool
Detail string
}
// DecideQuickNoteRouting 通过“模型控制码”决定本次请求走向。
// 返回语义:
// 1) EnterQuickNote=true:进入 quick_note graph;
// 2) TrustRoute=true:表示可跳过 graph 二次意图判定。
func DecideQuickNoteRouting(ctx context.Context, selectedModel *ark.ChatModel, userMessage string) RoutingDecision {
decision, err := routeByModelControlTag(ctx, selectedModel, userMessage)
if err != nil {
if deadline, ok := ctx.Deadline(); ok {
log.Printf("quick note 路由控制码失败,进入 graph 兜底: err=%v parent_deadline_in_ms=%d route_timeout_ms=%d",
err, time.Until(deadline).Milliseconds(), ControlTimeout.Milliseconds())
} else {
log.Printf("quick note 路由控制码失败,进入 graph 兜底: err=%v parent_deadline=none route_timeout_ms=%d",
err, ControlTimeout.Milliseconds())
}
return RoutingDecision{
EnterQuickNote: true,
TrustRoute: false,
Detail: "路由判定暂不可用,已进入任务识别兜底流程。",
}
}
switch decision.Action {
case ActionQuickNote:
reason := strings.TrimSpace(decision.Reason)
if reason == "" {
reason = "模型识别到任务安排请求,准备执行随口记。"
}
return RoutingDecision{
EnterQuickNote: true,
TrustRoute: true,
Detail: reason,
}
case ActionChat:
return RoutingDecision{
EnterQuickNote: false,
TrustRoute: false,
Detail: "",
}
default:
log.Printf("quick note 未知路由动作,进入 graph 兜底: action=%s raw=%s", decision.Action, decision.Raw)
return RoutingDecision{
EnterQuickNote: true,
TrustRoute: false,
Detail: "路由结果异常,已进入任务识别兜底流程。",
}
}
}
func routeByModelControlTag(ctx context.Context, selectedModel *ark.ChatModel, userMessage string) (*ControlDecision, error) {
if selectedModel == nil {
return nil, fmt.Errorf("model is nil")
}
nonce := strings.ToLower(strings.ReplaceAll(uuid.NewString(), "-", ""))
routeCtx, cancel := deriveRouteControlContext(ctx, ControlTimeout)
defer cancel()
nowText := time.Now().In(time.Local).Format("2006-01-02 15:04")
userPrompt := fmt.Sprintf("nonce=%s\n当前时间=%s\n用户输入=%s", nonce, nowText, strings.TrimSpace(userMessage))
resp, err := selectedModel.Generate(routeCtx, []*schema.Message{
schema.SystemMessage(quicknote.QuickNoteRouteControlPrompt),
schema.UserMessage(userPrompt),
},
ark.WithThinking(&arkModel.Thinking{Type: arkModel.ThinkingTypeDisabled}),
einoModel.WithTemperature(0),
einoModel.WithMaxTokens(80),
)
if err != nil {
return nil, err
}
if resp == nil {
return nil, fmt.Errorf("empty route response")
}
raw := strings.TrimSpace(resp.Content)
if raw == "" {
return nil, fmt.Errorf("empty route content")
}
return ParseQuickNoteRouteControlTag(raw, nonce)
}
// deriveRouteControlContext 为“控制码路由”创建子上下文。
// 设计要点:
// 1) timeout<=0 时不加额外 deadline,仅继承父上下文;
// 2) 父 ctx deadline 更紧时,沿用父上下文,避免过早超时误判。
func deriveRouteControlContext(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
if timeout <= 0 {
return context.WithCancel(parent)
}
if deadline, ok := parent.Deadline(); ok {
if time.Until(deadline) <= timeout {
return context.WithCancel(parent)
}
}
return context.WithTimeout(parent, timeout)
}
// ParseQuickNoteRouteControlTag 解析控制码返回。
// 容错策略:
// 1) 允许大小写、属性顺序、额外属性差异;
// 2) nonce 必须精确匹配;
// 3) action 仅允许 quick_note/chat。
func ParseQuickNoteRouteControlTag(raw, expectedNonce string) (*ControlDecision, error) {
text := strings.TrimSpace(raw)
if text == "" {
return nil, fmt.Errorf("route content is empty")
}
header := routeHeaderRegex.FindStringSubmatch(text)
if len(header) < 3 {
return nil, fmt.Errorf("route header not found: %s", text)
}
nonce := strings.ToLower(strings.TrimSpace(header[1]))
if nonce != strings.ToLower(strings.TrimSpace(expectedNonce)) {
return nil, fmt.Errorf("route nonce mismatch")
}
actionText := strings.ToLower(strings.TrimSpace(header[2]))
action := Action(actionText)
if action != ActionQuickNote && action != ActionChat {
return nil, fmt.Errorf("invalid route action: %s", actionText)
}
reason := ""
reasonMatch := routeReasonRegex.FindStringSubmatch(text)
if len(reasonMatch) >= 2 {
reason = strings.TrimSpace(reasonMatch[1])
}
return &ControlDecision{
Action: action,
Reason: reason,
Raw: text,
}, nil
}