fix: preserve selected semantic candidate provenance

This commit is contained in:
m
2026-08-21 01:15:48 +03:00
parent 1389d6b24c
commit 8448e95257
2 changed files with 55 additions and 7 deletions
+8 -3
View File
@@ -29,6 +29,7 @@ func InterpretUtterance(utterance interaction.HumanUtterance, projects []situati
millis := now.UnixMilli()
deterministicCandidate := BuildDeterministicCandidate(utterance.Content, utterance.CorrectsUtteranceID, millis)
deterministicCandidate.UtteranceID = utterance.ID
deterministicCandidate.SelectionState = interaction.CandidateSelected
selectedCandidate := deterministicCandidate
result := SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
@@ -103,14 +104,18 @@ func ApplyCognitiveCandidate(result SemanticResult, message string, candidate in
matched := false
for index := range result.Candidates {
if result.Candidates[index].SelectionState == interaction.CandidateSelected {
result.Candidates[index].SelectionState = interaction.CandidateObserved
}
if result.Candidates[index].ID == candidate.ID {
result.Candidates[index] = candidate
matched = true
}
}
if selected {
for index := range result.Candidates {
if result.Candidates[index].ID != candidate.ID && result.Candidates[index].SelectionState == interaction.CandidateSelected {
result.Candidates[index].SelectionState = interaction.CandidateObserved
}
}
}
if !matched {
result.Candidates = append(result.Candidates, candidate)
}
+47 -4
View File
@@ -119,10 +119,10 @@ func TestInterpretUtteranceAmbiguousDestructiveRequiresClarification(t *testing.
func TestInterpretUtteranceNaturalRussianHelpflowUsesContextAwareCognitionInput(t *testing.T) {
cases := []struct {
message string
wantSpeech interaction.SpeechAct
wantCognition bool
allowNoMatch bool
message string
wantSpeech interaction.SpeechAct
wantCognition bool
allowNoMatch bool
}{
{message: "расскажи как это работает", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "Расскажи, как это работает?", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
@@ -325,6 +325,49 @@ func TestInterpretUtteranceModalExplanationRequestStillRequestsCognition(t *test
}
}
func TestInterpretUtteranceSelectsDeterministicCandidateByDefault(t *testing.T) {
result := interpretForTest(t, "основная база называется UPO", nil, "")
if len(result.Candidates) != 1 {
t.Fatalf("expected one deterministic candidate, got %d", len(result.Candidates))
}
if result.Candidates[0].SelectionState != interaction.CandidateSelected {
t.Fatalf("deterministic candidate must be selected by default, got %s", result.Candidates[0].SelectionState)
}
}
func TestRejectedCognitiveCandidatePreservesSelectedDeterministicCandidate(t *testing.T) {
result := interpretForTest(t, "основная база называется UPO", nil, "")
candidateID, err := ids.New()
if err != nil {
t.Fatal(err)
}
rejected := interaction.SemanticCandidate{
ID: interaction.SemanticCandidateID(candidateID),
UtteranceID: result.Interpretation.UtteranceID,
Source: interaction.CandidateSourceCognitive,
PrimarySpeechAct: interaction.SpeechActQuestion,
SecondarySpeechAct: interaction.SpeechActUnknown,
ConfidenceMillis: 700,
Applicability: interaction.SemanticApplicable,
Clarification: interaction.ClarificationNotRequired,
NeedsCognition: false,
SelectionReason: "must not replace a safe assertion with an unrelated candidate",
}
result = ApplyCognitiveCandidate(result, "основная база называется UPO", rejected, nil)
selected := 0
for _, candidate := range result.Candidates {
if candidate.SelectionState == interaction.CandidateSelected {
selected++
if candidate.Source != interaction.CandidateSourceDeterministic {
t.Fatalf("rejected cognitive candidate replaced deterministic selection: %#v", candidate)
}
}
}
if selected != 1 {
t.Fatalf("expected exactly one selected candidate after rejection, got %d", selected)
}
}
func interpretForTest(t *testing.T, message string, previous *situation.ActiveProjectFocus, corrects interaction.HumanUtteranceID) SemanticResult {
t.Helper()
conversation := interaction.Conversation{ID: "conversation-1", ActorID: "actor-1", PrincipalID: "principal-1", Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen}