Files
Assistant/modules/interaction/domain/semantics_test.go
T
m 9020100313
CI / test (push) Successful in 1m7s
CI / test (pull_request) Successful in 1m1s
Keep travel requests in semantic dialogue
2026-08-25 04:51:52 +03:00

267 lines
13 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 domain
import (
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
"git.cin.su/m/Assistant/contracts/situation"
)
func TestInterpretUtteranceRecordsExplicitProjectFocus(t *testing.T) {
result := interpretForTest(t, "Сейчас работаем с УПО.", nil, "")
if result.ActiveFocus == nil || result.ActiveFocus.ProjectID != "upo" {
t.Fatalf("expected explicit UPO focus, got %#v", result.ActiveFocus)
}
if result.Interpretation.SpeechAct != interaction.SpeechActAssertion {
t.Fatalf("expected assertion, got %s", result.Interpretation.SpeechAct)
}
if result.Projects[0].Provenance != situation.ProjectResolutionExplicit {
t.Fatalf("expected explicit provenance, got %#v", result.Projects)
}
}
func TestInterpretUtteranceAssertionIsNotVerifiedFact(t *testing.T) {
result := interpretForTest(t, "На сервере сейчас свободно 300 ГБ.", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActAssertion {
t.Fatalf("expected assertion, got %s", result.Interpretation.SpeechAct)
}
if result.Interpretation.AssertionVerification != "UNVERIFIED" || !strings.Contains(result.Reply, "не проверенный факт") {
t.Fatalf("expected unverified human assertion, got %#v reply=%q", result.Interpretation, result.Reply)
}
}
func TestInterpretUtteranceNamingPreferenceIsAccepted(t *testing.T) {
result := interpretForTest(t, "я буду называть тебя Дед, запомни ты дед", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActPreference {
t.Fatalf("expected preference, got %s", result.Interpretation.SpeechAct)
}
if result.Reply != "Хорошо, зови меня Дед." {
t.Fatalf("unexpected preference reply: %q", result.Reply)
}
}
func TestInterpretUtteranceThanksHasNaturalConversationalReply(t *testing.T) {
result := interpretForTest(t, "спасибо", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActConversational {
t.Fatalf("expected conversational act, got %s", result.Interpretation.SpeechAct)
}
if result.Reply != "Пожалуйста." {
t.Fatalf("unexpected thanks reply: %q", result.Reply)
}
}
func TestStableConversationalReplySurvivesCognitiveRewrite(t *testing.T) {
reply, stable := StableConversationalReply("спасибо", interaction.SpeechActConversational)
if !stable || reply != "Пожалуйста." {
t.Fatalf("unexpected stable conversational reply: stable=%t reply=%q", stable, reply)
}
}
func TestStripAssistantVocative(t *testing.T) {
if got := StripAssistantVocative("Дед, какая погода в Сочи?"); got != "какая погода в Сочи?" {
t.Fatalf("unexpected cleaned query: %q", got)
}
if got := StripAssistantVocative("домовой: найди ресторан"); got != "найди ресторан" {
t.Fatalf("unexpected cleaned query: %q", got)
}
}
func TestTravelSearchClarification(t *testing.T) {
if got := TravelSearchClarification("Найди билеты", nil); got == "" {
t.Fatal("underspecified travel request must ask for missing route and date")
}
if got := TravelSearchClarification("Найди билеты из Москвы в Сочи на 12 сентября", nil); got != "" {
t.Fatalf("complete travel request must proceed, got %q", got)
}
history := []interaction.HumanUtterance{{Content: "Найди билеты"}}
if got := TravelSearchClarification("а куда летим?", history); got == "" {
t.Fatal("follow-up must remain attached to the travel goal")
}
}
func TestInterpretUtteranceDirectiveRecordsOutcomeWithoutExecution(t *testing.T) {
result := interpretForTest(t, "Поменяй базу на УПО1.", activeFocus("upo", "УПО"), "")
if result.Interpretation.SpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive, got %s", result.Interpretation.SpeechAct)
}
if result.Interpretation.RequestedOutcome == "" || !strings.Contains(result.Reply, "Ничего не выполнено") {
t.Fatalf("expected non-executed requested outcome, got %#v reply=%q", result.Interpretation, result.Reply)
}
}
func TestInterpretUtteranceCorrectionIsAppendOnlySupersedes(t *testing.T) {
result := interpretForTest(t, "Нет, используй УПО2.", activeFocus("upo", "УПО"), "utterance-previous")
if result.Interpretation.SpeechAct != interaction.SpeechActCorrection {
t.Fatalf("expected correction, got %s", result.Interpretation.SpeechAct)
}
if len(result.Relations) != 1 || result.Relations[0].Kind != interaction.RelationSupersedes || result.Relations[0].ToUtteranceID != "utterance-previous" {
t.Fatalf("expected supersedes relation, got %#v", result.Relations)
}
}
func TestInterpretUtteranceQuestionAboutCurrentWorkUsesFocus(t *testing.T) {
result := interpretForTest(t, "Что мы сейчас делаем?", activeFocus("assistant", "Assistant"), "")
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion {
t.Fatalf("expected question, got %s", result.Interpretation.SpeechAct)
}
if !strings.Contains(result.Reply, "Assistant") {
t.Fatalf("expected focus-aware reply, got %q", result.Reply)
}
if result.NeedsCognition {
t.Fatalf("focus question should be answered from canonical focus without cognition")
}
}
func TestInterpretUtteranceInheritedFocusForThere(t *testing.T) {
result := interpretForTest(t, "Добавь туда новую настройку.", activeFocus("assistant", "Assistant"), "")
if len(result.Projects) != 1 || result.Projects[0].ProjectID != "assistant" || result.Projects[0].Provenance != situation.ProjectResolutionInheritedFocus {
t.Fatalf("expected inherited Assistant focus, got %#v", result.Projects)
}
}
func TestProjectFocusIsContextualEvidenceNotGlobalMode(t *testing.T) {
explicit := interpretForTest(t, "Сейчас работаем с УПО.", nil, "")
if explicit.ActiveFocus == nil || explicit.ActiveFocus.ProjectID != "upo" {
t.Fatalf("expected explicit UPO focus, got %#v", explicit.ActiveFocus)
}
focusedQuestion := interpretForTest(t, "Почему тормозит база?", activeFocus("upo", "УПО"), "")
if len(focusedQuestion.Projects) != 1 || focusedQuestion.Projects[0].ProjectID != "upo" || focusedQuestion.Projects[0].Provenance != situation.ProjectResolutionInheritedFocus {
t.Fatalf("database follow-up can use previous project as contextual evidence, got %#v", focusedQuestion.Projects)
}
unrelated := interpretForTest(t, "Помоги ответить на письмо бухгалтеру.", activeFocus("upo", "УПО"), "")
if len(unrelated.Projects) != 0 {
t.Fatalf("unrelated mail intent must not inherit UPO focus, got %#v", unrelated.Projects)
}
returned := interpretForTest(t, "Вернемся к УПО.", activeFocus("assistant", "Assistant"), "")
if returned.ActiveFocus == nil || returned.ActiveFocus.ProjectID != "upo" {
t.Fatalf("expected explicit return to UPO, got %#v", returned.ActiveFocus)
}
}
func TestInterpretUtteranceMultiProjectReferenceRoles(t *testing.T) {
result := interpretForTest(t, "Посмотри, как это сделано в Assistant, и сделай аналогично в УПО.", nil, "")
if len(result.Projects) != 2 {
t.Fatalf("expected two project references, got %#v", result.Projects)
}
roles := map[situation.ProjectID]interaction.ProjectReferenceRole{}
for _, ref := range result.Projects {
roles[ref.ProjectID] = ref.Role
}
if roles["assistant"] != interaction.ProjectRoleReferenceSource || roles["upo"] != interaction.ProjectRolePrimary {
t.Fatalf("unexpected roles: %#v", roles)
}
}
func TestInterpretUtteranceAmbiguousDestructiveRequiresClarification(t *testing.T) {
result := interpretForTest(t, "Удали туда старую базу.", activeFocus("assistant", "Assistant"), "")
if result.Interpretation.Clarification != interaction.ClarificationRequired {
t.Fatalf("expected clarification, got %#v", result.Interpretation)
}
if result.NeedsCognition {
t.Fatalf("ambiguous destructive directive must not require cognition")
}
}
func TestInterpretUtterancePunctuationFreeRussianQuestionStillRequestsBroaderInterpretation(t *testing.T) {
result := interpretForTest(t, "ты кто", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion {
t.Fatalf("expected question, got %#v", result.Interpretation)
}
if !result.NeedsCognition {
t.Fatalf("expected cognition route for punctuation-free identity question, got %#v", result)
}
}
func TestInterpretUtterancePunctuationFreeArithmeticStillRequestsCognition(t *testing.T) {
result := interpretForTest(t, "сколько будет 2 + 2", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion {
t.Fatalf("expected question, got %#v", result.Interpretation)
}
if !result.NeedsCognition {
t.Fatalf("expected cognition route for punctuation-free arithmetic, got %#v", result)
}
}
func TestCarryForwardPreviousTaskPreservesRoutingForShortRetry(t *testing.T) {
current := SemanticResult{
Interpretation: interaction.UtteranceInterpretation{UtteranceID: "retry", SpeechAct: interaction.SpeechActAssertion},
Candidates: []interaction.SemanticCandidate{{ID: "current", UtteranceID: "retry"}},
}
previous := SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
UtteranceID: "previous",
SpeechAct: interaction.SpeechActRequestToInvestigate,
RequestedOutcome: "узнать текущую погоду",
NeedsCognition: false,
Applicability: interaction.SemanticApplicable,
Clarification: interaction.ClarificationNotRequired,
RecordedAtUnixMillis: 100,
},
Candidates: []interaction.SemanticCandidate{{ID: "previous-candidate", PrimarySpeechAct: interaction.SpeechActRequestToInvestigate, CapabilityHints: []string{"WEB_RESEARCH"}}},
CapabilityHints: []string{"WEB_RESEARCH"},
}
result := CarryForwardPreviousTask(current, previous)
if result.Interpretation.SpeechAct != interaction.SpeechActRequestToInvestigate || result.CapabilityHints[0] != "WEB_RESEARCH" || result.Candidates[0].UtteranceID != "retry" {
t.Fatalf("previous routing was not carried forward: %#v", result)
}
}
func TestInterpretUtteranceModalExplanationRequestDefersToCognition(t *testing.T) {
for _, message := range []string{"можешь расказать об утре", "расскажи об утре", "про утро расскажешь"} {
result := interpretForTest(t, message, nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion && result.Interpretation.SpeechAct != interaction.SpeechActRequestToInvestigate {
t.Fatalf("expected cognition-worthy interpretation for %q, got %#v", message, result.Interpretation)
}
if !result.NeedsCognition {
t.Fatalf("expected cognition route for %q, got %#v", message, result)
}
}
}
func TestInterpretUtteranceNamedModelRequestIsDirectiveNeedingCognition(t *testing.T) {
result := interpretForTest(t, "обратись к модели gemma4 и спроси сколько звезд на небе", nil, "")
if result.Interpretation.SpeechAct != interaction.SpeechActDirective {
t.Fatalf("expected directive, got %#v", result.Interpretation)
}
if !result.NeedsCognition {
t.Fatalf("expected cognition route for named-model request, got %#v", result)
}
}
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}
utterance, _, _, err := NewHumanUtterance(conversation, interaction.ChatMessageRequest{Message: message, CorrectsUtteranceID: corrects}, time.Unix(100, 0))
if err != nil {
t.Fatal(err)
}
result, err := InterpretUtterance(utterance, projectRegistryForTest(), previous, time.Unix(100, 0))
if err != nil {
t.Fatal(err)
}
return result
}
func projectRegistryForTest() []situation.ProjectDescriptor {
return []situation.ProjectDescriptor{
{ID: "assistant", DisplayName: "Assistant", Aliases: []situation.ProjectAlias{"assistant", "ассистент", "домовой"}},
{ID: "upo", DisplayName: "УПО", Aliases: []situation.ProjectAlias{"упо", "upo"}},
}
}
func activeFocus(projectID situation.ProjectID, displayName string) *situation.ActiveProjectFocus {
return &situation.ActiveProjectFocus{
ActorID: identity.ActorID("actor-1"),
PrincipalID: identity.PrincipalID("principal-1"),
ProjectID: projectID,
ProjectDisplayName: displayName,
Provenance: situation.ProjectResolutionExplicit,
ConfidenceMillis: 980,
SourceUtteranceID: "utterance-focus",
UpdatedAtUnixMillis: time.Unix(90, 0).UnixMilli(),
}
}