Add RUN_STATUS_QUERY/RUN_RESUME/RUN_RESTART with matcher and coordinator handlers; write QUERY_PROGRESS/SUPPLEMENT_INFO subtypes to V2 frame; support safe query+query multi-intent plans via IntentPlan dual-write. Co-authored-by: Cursor <cursoragent@cursor.com>
173 lines
5.0 KiB
Go
173 lines
5.0 KiB
Go
package intent
|
|
|
|
import "strings"
|
|
|
|
// branchToRelation 将三分支映射为 ExpectationRelation
|
|
func mapQueryProgressSubtype(topic string) string {
|
|
switch topic {
|
|
case "risk":
|
|
return "risk"
|
|
case "testing_analysis", "technical_design":
|
|
return "technical"
|
|
case "talk_script":
|
|
return "communication"
|
|
case "progress":
|
|
return "status"
|
|
default:
|
|
return topic
|
|
}
|
|
}
|
|
|
|
func branchToRelation(branch ResolutionBranch, cand *CandidateIntent) ExpectationRelation {
|
|
switch branch {
|
|
case BranchExpectationHit:
|
|
if cand != nil && cand.IsDecline {
|
|
return RelationExpectationDecline
|
|
}
|
|
if cand != nil && len(cand.PatchSlots) > 0 {
|
|
return RelationExpectationPatch
|
|
}
|
|
return RelationExpectationHit
|
|
case BranchAmbiguousFallback:
|
|
return RelationUnresolvedReference
|
|
default:
|
|
return RelationNew
|
|
}
|
|
}
|
|
|
|
// BuildIntentFrameV2 从 Legacy 候选意图构建 V2 语义帧
|
|
func BuildIntentFrameV2(res *IntentRecognitionResult, registry *ActionRegistry) *IntentFrameV2 {
|
|
if res == nil || res.TopIntent == nil {
|
|
return nil
|
|
}
|
|
if registry == nil {
|
|
registry = DefaultRegistry()
|
|
}
|
|
top := res.TopIntent
|
|
spec, ok := registry.GetByLegacyIntent(top.Intent)
|
|
frame := &IntentFrameV2{
|
|
Version: IntentFrameV2Version,
|
|
LegacyIntent: top.Intent,
|
|
Relation: branchToRelation(res.Branch, top),
|
|
Score: top.Confidence,
|
|
Margin: res.ShadowMargin,
|
|
DialogueAct: detectDialogueAct(res.RawInput),
|
|
MissingSlots: append([]string(nil), top.MissingSlots...),
|
|
}
|
|
if len(res.Candidates) >= 2 {
|
|
frame.Margin = res.Candidates[0].Confidence - res.Candidates[1].Confidence
|
|
}
|
|
if ok {
|
|
frame.ActionKey = spec.Key
|
|
frame.Domain = spec.Domain
|
|
frame.Object = spec.Object
|
|
frame.Goal = spec.Goal
|
|
} else {
|
|
frame.ActionKey = "unknown." + strings.ToLower(string(top.Intent))
|
|
frame.Domain = "unknown"
|
|
frame.Object = "unknown"
|
|
frame.Goal = GoalConverse
|
|
}
|
|
if top.FilledSlots != nil {
|
|
frame.Slots = make(map[string]SlotValue, len(top.FilledSlots))
|
|
for k, v := range top.FilledSlots {
|
|
frame.Slots[k] = SlotValue{Value: v, Source: "rule"}
|
|
}
|
|
}
|
|
if top.FilledSlots != nil {
|
|
switch top.Intent {
|
|
case IntentQueryProgress:
|
|
if topic, ok := top.FilledSlots[SlotQueryTopic]; ok {
|
|
frame.Subtype = mapQueryProgressSubtype(topic)
|
|
}
|
|
case IntentSupplementInfo:
|
|
if action, ok := top.FilledSlots[SlotSupplementAction]; ok {
|
|
frame.Subtype = action
|
|
}
|
|
}
|
|
}
|
|
frame.Evidence = buildEvidenceFromCandidate(top, res)
|
|
return frame
|
|
}
|
|
|
|
func buildEvidenceFromCandidate(top *CandidateIntent, res *IntentRecognitionResult) []IntentEvidence {
|
|
var ev []IntentEvidence
|
|
if top.Reasoning != "" {
|
|
ev = append(ev, IntentEvidence{Kind: "rule_match", Detail: top.Reasoning, Weight: top.Confidence})
|
|
}
|
|
if res.ShadowDialogueAct != "" {
|
|
ev = append(ev, IntentEvidence{Kind: "dialogue_act", Detail: res.ShadowDialogueAct, Weight: 0.1})
|
|
}
|
|
if strings.Contains(top.Reasoning, "权限不符") {
|
|
ev = append(ev, IntentEvidence{Kind: "policy_hint", Detail: "permission_mismatch_l1", Weight: 0.0})
|
|
}
|
|
if len(top.MissingSlots) > 0 {
|
|
ev = append(ev, IntentEvidence{Kind: "missing_slots", Detail: strings.Join(top.MissingSlots, ","), Weight: 0.0})
|
|
}
|
|
return ev
|
|
}
|
|
|
|
// BuildRankedIntentSet 从识别结果构建排序候选帧集合
|
|
func BuildRankedIntentSet(res *IntentRecognitionResult, registry *ActionRegistry) *RankedIntentSet {
|
|
if res == nil || len(res.Candidates) == 0 {
|
|
return nil
|
|
}
|
|
if registry == nil {
|
|
registry = DefaultRegistry()
|
|
}
|
|
set := &RankedIntentSet{TopMargin: res.ShadowMargin}
|
|
if len(res.Candidates) >= 2 {
|
|
set.TopMargin = res.Candidates[0].Confidence - res.Candidates[1].Confidence
|
|
}
|
|
for i := range res.Candidates {
|
|
c := res.Candidates[i]
|
|
tmp := &IntentRecognitionResult{
|
|
RawInput: res.RawInput,
|
|
Branch: res.Branch,
|
|
TopIntent: &c,
|
|
Candidates: res.Candidates,
|
|
}
|
|
if fr := BuildIntentFrameV2(tmp, registry); fr != nil {
|
|
set.Candidates = append(set.Candidates, *fr)
|
|
}
|
|
}
|
|
if len(res.Candidates) >= 2 {
|
|
top := res.Candidates[0]
|
|
second := res.Candidates[1]
|
|
margin := top.Confidence - second.Confidence
|
|
if margin < 0.05 && ((isWriteIntent(top.Intent) && isQueryIntent(second.Intent)) ||
|
|
(isQueryIntent(top.Intent) && isWriteIntent(second.Intent))) {
|
|
set.Conflicts = append(set.Conflicts, CandidateConflict{
|
|
Kind: "write_vs_query",
|
|
TopAction: string(top.Intent),
|
|
SecondAction: string(second.Intent),
|
|
Margin: margin,
|
|
})
|
|
}
|
|
}
|
|
return set
|
|
}
|
|
|
|
// LegacyIntentFromFrame 从 V2 帧还原 Legacy TopIntent(契约测试与灰度回退)
|
|
func LegacyIntentFromFrame(frame *IntentFrameV2) IntentType {
|
|
if frame == nil {
|
|
return IntentCasualChat
|
|
}
|
|
if frame.LegacyIntent != "" {
|
|
return frame.LegacyIntent
|
|
}
|
|
if spec, ok := DefaultRegistry().Get(frame.ActionKey); ok {
|
|
return spec.LegacyIntent
|
|
}
|
|
return IntentCasualChat
|
|
}
|
|
|
|
// AttachIntentFrameV2 为识别结果附加 V2 双写字段(不改变 TopIntent 路由)
|
|
func AttachIntentFrameV2(res *IntentRecognitionResult) {
|
|
if res == nil {
|
|
return
|
|
}
|
|
res.IntentFrameV2 = BuildIntentFrameV2(res, DefaultRegistry())
|
|
res.RankedFrames = BuildRankedIntentSet(res, DefaultRegistry())
|
|
}
|