Add typed jevdecide HTTP client (circuit, metrics) and coordinator DecideAssist wiring behind JEV_DECIDE_MODE (default off). Offline eval and synthetic soak evidence gate realtime/profile before assist rollout.
125 lines
5.8 KiB
Go
125 lines
5.8 KiB
Go
package coordinator
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
|
||
"chat-agent-system/internal/intent"
|
||
"chat-agent-system/internal/maturity"
|
||
)
|
||
|
||
// handleAmbiguousConsult 处理用户犹豫或征询建议 (分支 3 模糊分忧)
|
||
func (e *CoordinatorEngine) handleAmbiguousConsult(ctx context.Context, cand *intent.CandidateIntent, userCtx *UserProjectContext, rawInput string) (*CoordinationResult, error) {
|
||
currentStage := userCtx.Project.CurrentStage
|
||
projName := userCtx.Project.Name
|
||
if projName == "" {
|
||
projName = "当前项目"
|
||
}
|
||
|
||
// 只在低置信度的模糊咨询里,才用关键词改走测试分析。高置信度保持原意图。
|
||
if cand.Confidence < 0.6 && (strings.Contains(rawInput, "测试") || strings.Contains(rawInput, "验证") || strings.Contains(rawInput, "型检") ||
|
||
strings.Contains(rawInput, "难点") || strings.Contains(rawInput, "注意") || strings.Contains(rawInput, "比较难") ||
|
||
strings.Contains(rawInput, "方案")) {
|
||
if cand.FilledSlots == nil {
|
||
cand.FilledSlots = make(map[string]string)
|
||
}
|
||
cand.FilledSlots[intent.SlotQueryTopic] = "testing_analysis"
|
||
return e.handleQueryProgress(ctx, cand, userCtx, rawInput)
|
||
}
|
||
|
||
// 通用非业务问句不得吃阶段推销模板;改道闲聊处理器(LLM 优先,失败回退问候模板)。
|
||
if intent.IsSmallTalkOrGeneralQuestion(rawInput) {
|
||
return e.handleCasualChat(ctx, userCtx, rawInput)
|
||
}
|
||
|
||
var sb strings.Builder
|
||
sb.WriteString(fmt.Sprintf("完全理解您想再掂量一下。针对「%s」目前的节点,我帮您快速对照一下:\n\n", projName))
|
||
|
||
if currentStage == maturity.StageV01_Triage {
|
||
sb.WriteString("• 若推进 V0.5(方案成型与法规对齐):\n")
|
||
sb.WriteString(" 我可帮您梳理初步架构与注册分类路径,并预估型检成本,便于与客户深入沟通;\n")
|
||
sb.WriteString("• 若先放一放 / 多做调研:\n")
|
||
sb.WriteString(" 可先补齐预算、目标市场或使用场景等信息,降低后续返工风险。\n\n")
|
||
sb.WriteString("💡 如果您接下来更希望先有一份可审阅的材料,我可以先整理一版 V0.5 方案框架与法规路径;节奏仍由您决定。\n\n")
|
||
sb.WriteString("您更倾向于:\n")
|
||
sb.WriteString("[1] 先做 V0.5 方案框架(可直接回复“好”或“1”)\n")
|
||
sb.WriteString("[2] 先说明您当前最关心的细节,我做针对性分析")
|
||
} else {
|
||
sb.WriteString("当前各项工作进展平稳。您可以把零散想法随时告诉我,我来帮您沉淀;时机成熟时,再一起推进下一阶段也完全可以。")
|
||
}
|
||
|
||
reply := sb.String()
|
||
source := ReplySourceTemplate
|
||
if llmReply, llmSource := e.generateDialogueReply(ctx, rawInput, userCtx); llmReply != "" {
|
||
reply = llmReply
|
||
source = llmSource
|
||
}
|
||
|
||
nextExp := BuildNextExpectation(currentStage, projName)
|
||
// 路由层:复杂自由任务画像命中时,先给即时初答,再附升级建议(复用挂起期望机制,含每会话冷却)
|
||
reply, nextExp = maybeAttachUpgradeHintWithEngine(e, ctx, rawInput, userCtx, reply, nextExp)
|
||
userCtx.applyPending(nextExp)
|
||
|
||
return &CoordinationResult{
|
||
Status: StatusAccepted,
|
||
Intent: cand.Intent,
|
||
Branch: intent.BranchAmbiguousFallback,
|
||
NaturalReply: reply,
|
||
AdvisorBrief: userCtx.LatestAdvisorBrief,
|
||
UpdatedContext: userCtx,
|
||
NextExpectation: nextExp,
|
||
ReplySource: source,
|
||
}, nil
|
||
}
|
||
|
||
func (e *CoordinatorEngine) handleCasualChat(ctx context.Context, userCtx *UserProjectContext, rawInput string) (*CoordinationResult, error) {
|
||
projName := userCtx.Project.Name
|
||
if projName == "" {
|
||
projName = "当前医疗器械研发项目"
|
||
}
|
||
|
||
nextExp := BuildNextExpectation(userCtx.Project.CurrentStage, userCtx.Project.Name)
|
||
userCtx.applyPending(nextExp)
|
||
|
||
nameSuffix := ""
|
||
if userCtx.User.Name != "" {
|
||
nameSuffix = "," + userCtx.User.Name
|
||
}
|
||
reply := fmt.Sprintf("您好%s!我是您的项目助手,很高兴协助您推进医疗器械立项与研发协同。\n\n针对「%s」,我可以帮您:\n• 查询进展、成熟度与风险;\n• 沉淀客户需求与技术指标;\n• 起草沟通话术与方案要点;\n• 推进方案论证与报价核算。\n\n如果您愿意,我们可以先从您当前最关心的一块开始——您想先看进展、补事实,还是直接聊方案?",
|
||
nameSuffix, projName)
|
||
source := ReplySourceTemplate
|
||
if llmReply, llmSource := e.generateDialogueReply(ctx, rawInput, userCtx); llmReply != "" {
|
||
reply = llmReply
|
||
source = llmSource
|
||
}
|
||
|
||
// 路由层:复杂自由任务画像命中时,先给即时初答,再附升级建议(复用挂起期望机制,含每会话冷却)
|
||
reply, nextExp = maybeAttachUpgradeHintWithEngine(e, ctx, rawInput, userCtx, reply, nextExp)
|
||
userCtx.applyPending(nextExp)
|
||
|
||
return &CoordinationResult{
|
||
Status: StatusAccepted,
|
||
Intent: intent.IntentCasualChat,
|
||
Branch: intent.BranchNewIntentPreempt,
|
||
NaturalReply: reply,
|
||
AdvisorBrief: userCtx.LatestAdvisorBrief,
|
||
UpdatedContext: userCtx,
|
||
NextExpectation: nextExp,
|
||
ReplySource: source,
|
||
}, nil
|
||
}
|
||
|
||
// handleSecurityForbidden 处理安全红线与合规拦截 (ISO 13485 / 21 CFR Part 11)
|
||
func (e *CoordinatorEngine) handleSecurityForbidden(userCtx *UserProjectContext) (*CoordinationResult, error) {
|
||
reply := "⚠️ **合规权限拦截**:根据医疗器械质量管理规范(ISO 13485 / 21 CFR Part 11),企业人员的法定身份(姓名、工号、职位、部门)与系统控制台属于基线受控资产,不支持通过对话动态修改。您可通过‘查询我的权限’查看您在当前各文档中的法定职责。"
|
||
return &CoordinationResult{
|
||
Status: StatusRejected,
|
||
Intent: intent.IntentSecurityForbidden,
|
||
Branch: intent.BranchNewIntentPreempt,
|
||
RejectReason: "SECURITY_FORBIDDEN_BASELINE_ASSET",
|
||
NaturalReply: reply,
|
||
UpdatedContext: userCtx,
|
||
}, nil
|
||
}
|