diff --git a/modules/interaction/domain/semantics.go b/modules/interaction/domain/semantics.go index 84ec283..0531848 100644 --- a/modules/interaction/domain/semantics.go +++ b/modules/interaction/domain/semantics.go @@ -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) } diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index ade21f6..810ed62 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -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}