Files
smartmate/backend/services/agent/router/chat_route.go
Losita d7184b776b Version: 0.9.75.dev.260505
后端:
1.收口阶段 6 agent 结构迁移,将 newAgent 内核与 agentsvc 编排层迁入 services/agent
- 切换 Agent 启动装配与 HTTP handler 直连 agent sv,移除旧 service agent bridge
- 补齐 Agent 对 memory、task、task-class、schedule 的 RPC 适配与契约字段
- 扩展 schedule、task、task-class RPC/contract 支撑 Agent 查询、写入与 provider 切流
- 更新迁移文档、README 与相关注释,明确 agent 当前切流点和剩余 memory 迁移面
2026-05-05 16:00:57 +08:00

165 lines
5.5 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 agentrouter
import (
"fmt"
"regexp"
"strings"
agentmodel "github.com/LoveLosita/smartflow/backend/services/agent/model"
)
var (
// chatRouteHeaderRegex 从模型流式输出中解析 SMARTFLOW_ROUTE 控制码头部。
//
// 格式示例:
// <SMARTFLOW_ROUTE nonce="abc" route="execute" rough_build="true" refine="false" reorder="false" thinking="true"/>
//
// 属性说明:
// 1. nonce防注入校验必须与调用方传入的 nonce 精确匹配;
// 2. route路由目标direct_reply / execute / deep_answer / plan
// 3. rough_build可选仅 route=execute 时有效,默认 false
// 4. refine可选仅 rough_build=true 时有效,默认 false
// 5. reorder可选仅 route=execute 时有效,默认 false
// 6. thinking可选仅 route=execute 时有效,默认 false。
chatRouteHeaderRegex = regexp.MustCompile(
`(?is)<\s*SMARTFLOW_ROUTE\b` +
`[^>]*\bnonce\s*=\s*["']?([a-zA-Z0-9\-]+)["']?` +
`[^>]*\broute\s*=\s*["']?(direct_reply|execute|deep_answer|plan|quick_task)["']?` +
`(?:[^>]*\brough_build\s*=\s*["']?(true|false)["']?)?` +
`(?:[^>]*\brefine\s*=\s*["']?(true|false)["']?)?` +
`(?:[^>]*\breorder\s*=\s*["']?(true|false)["']?)?` +
`(?:[^>]*\bthinking\s*=\s*["']?(true|false)["']?)?` +
`[^>]*/\s*>`)
)
// StreamRouteParser 从 LLM 流式输出中增量提取路由决策。
//
// 协议约定:模型输出以 SMARTFLOW_ROUTE 控制码标签开头,标签结束后是用户可见内容。
// 例如:<SMARTFLOW_ROUTE nonce="abc" route="direct_reply"/>你好!很高兴见到你...
//
// 职责边界:
// 1. 只负责从流式 chunk 中提取控制码并解析为 ChatRoutingDecision
// 2. 不负责推送 SSE chunk不负责决定后续走哪条链路
// 3. 控制码解析失败时标记 fallback由上层决定降级策略。
type StreamRouteParser struct {
buf strings.Builder
nonce string
routeFound bool
decision *agentmodel.ChatRoutingDecision
}
// NewStreamRouteParser 创建流式路由解析器。
func NewStreamRouteParser(nonce string) *StreamRouteParser {
return &StreamRouteParser{
nonce: strings.ToLower(strings.TrimSpace(nonce)),
}
}
// Feed 写入一段 chunk content。
//
// 返回值:
// - visible控制码标签之后的内容用户可见文本
// - routeReady路由决策是否已确定
// - err解析错误。
//
// 调用方应在 routeReady=true 后调用 Decision() 获取路由决策,
// 并根据 route 进入对应分支处理 visible 及后续 chunk。
func (p *StreamRouteParser) Feed(content string) (visible string, routeReady bool, err error) {
if p.routeFound {
// 路由已解析,后续 chunk 直接透传。
return content, true, nil
}
p.buf.WriteString(content)
text := p.buf.String()
match := chatRouteHeaderRegex.FindStringSubmatchIndex(text)
if match == nil {
// 控制码尚未完整,检查是否应该 fallback。
if len(text) > 500 {
// 超过 500 字符仍未匹配到控制码 -> fallback 到 plan。
p.routeFound = true
p.decision = &agentmodel.ChatRoutingDecision{
Route: agentmodel.ChatRoutePlan,
Raw: text,
}
return text, true, fmt.Errorf("控制码解析超时fallback 到 plan")
}
return "", false, nil
}
// 提取匹配到的子组。
groups := chatRouteHeaderRegex.FindStringSubmatch(text)
if len(groups) < 3 {
return "", false, fmt.Errorf("控制码正则子组不足: %d", len(groups))
}
// nonce 校验。
parsedNonce := strings.ToLower(strings.TrimSpace(groups[1]))
if parsedNonce != p.nonce {
return "", false, fmt.Errorf("nonce 不匹配: got=%s expected=%s", parsedNonce, p.nonce)
}
// 解析 route。
route := agentmodel.ChatRoute(strings.TrimSpace(groups[2]))
// 解析可选布尔属性(默认 false
roughBuild := parseOptionalBool(groups, 3)
refine := parseOptionalBool(groups, 4)
reorder := parseOptionalBool(groups, 5)
thinking := parseOptionalBool(groups, 6)
p.decision = &agentmodel.ChatRoutingDecision{
Route: route,
NeedsRoughBuild: roughBuild,
NeedsRefineAfterRoughBuild: refine,
AllowReorder: reorder,
Thinking: thinking,
Raw: groups[0],
}
// 归一化与校验。
if validateErr := p.decision.Validate(); validateErr != nil {
// 校验失败 -> fallback 到 plan。
p.decision.Route = agentmodel.ChatRoutePlan
p.decision.NeedsRoughBuild = false
p.decision.NeedsRefineAfterRoughBuild = false
p.decision.AllowReorder = false
p.decision.Thinking = false
}
p.routeFound = true
// 控制码标签之后的文本作为 visible 返回。
fullMatch := groups[0]
tagEndIdx := strings.Index(text, fullMatch)
if tagEndIdx >= 0 {
afterTag := text[tagEndIdx+len(fullMatch):]
// 去掉标签后紧跟的换行符(如果有)。
afterTag = strings.TrimPrefix(afterTag, "\r\n")
afterTag = strings.TrimPrefix(afterTag, "\n")
return afterTag, true, nil
}
return "", true, nil
}
// RouteReady 返回路由决策是否已确定。
func (p *StreamRouteParser) RouteReady() bool {
return p.routeFound
}
// Decision 返回已解析的路由决策RouteReady=true 后可用)。
func (p *StreamRouteParser) Decision() *agentmodel.ChatRoutingDecision {
return p.decision
}
// parseOptionalBool 从正则子组中解析可选布尔值。
// 如果子组不存在或为空,返回 false。
func parseOptionalBool(groups []string, index int) bool {
if index >= len(groups) {
return false
}
return strings.TrimSpace(groups[index]) == "true"
}