Files
Assistant/modules/interaction/domain/semantics_test.go
T
2026-08-18 16:50:19 +03:00

217 lines
12 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 TestInterpretUtteranceDirectiveRecordsOutcomeWithoutExecution(t *testing.T) {
result := interpretForTest(t, "Поменяй базу на УПО1.", activeFocus("upo", "УПО"), "")
if result.Interpretation.SpeechAct != interaction.SpeechActDirective && result.Interpretation.SpeechAct != interaction.SpeechActRequestToInvestigate {
t.Fatalf("expected directive-style intent, 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 TestInterpretUtteranceNaturalRussianHelpflowUsesContextAwareCognitionInput(t *testing.T) {
cases := []struct {
message string
wantSpeech interaction.SpeechAct
wantCognition bool
allowNoMatch bool
}{
{message: "расскажи как это работает", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "Расскажи, как это работает?", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "Расскажи как это работает", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "можешь объяснить почему база растёт", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "ПОЧЕМУ postgres использует так много памяти", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "разберись почему сервис упал", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "посмотри что произошло", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "исправь ошибку если найдёшь", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "разберись почему падает и исправь если причина очевидна", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "нет не UPO а UPO2", wantSpeech: interaction.SpeechActCorrection, wantCognition: false},
{message: "нет не UPO а UPO2 и обнови конфиг", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "postgres сейчас работает", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "сейчас домовой не видит ии", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "основная база называется UPO", wantSpeech: interaction.SpeechActAssertion, wantCognition: false},
{message: "привет", wantSpeech: interaction.SpeechActConversational, wantCognition: false},
{message: "спасибо", wantSpeech: interaction.SpeechActConversational, wantCognition: false},
{message: "ты кто", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "Ты кто?", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "как тебя зовут", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "кто ты вообще", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "что ты думаешь", wantSpeech: interaction.SpeechActQuestion, wantCognition: true},
{message: "расскажи подробнее", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "СЕЙЧАС ДОМОВОЙ НЕ ВИДИТ ИИ", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "не понимаю меня не понимает ии", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "домовой не вижит ии", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "домовой не отвечает мне", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "домовой не слушает меня", wantSpeech: "", wantCognition: true, allowNoMatch: true},
{message: "домовой не отвечает на команды", wantSpeech: "", wantCognition: true, allowNoMatch: true},
}
for _, tc := range cases {
result := interpretForTest(t, tc.message, nil, "")
if tc.wantSpeech != "" && result.Interpretation.SpeechAct != tc.wantSpeech {
t.Fatalf("message %q expected speech=%s got %s", tc.message, tc.wantSpeech, result.Interpretation.SpeechAct)
}
if tc.wantSpeech == "" && !tc.allowNoMatch && result.Interpretation.SpeechAct == "" {
t.Fatalf("message %q got empty speech interpretation", tc.message)
}
if result.NeedsCognition != tc.wantCognition {
t.Fatalf("message %q expected needs_cognition=%t got %t, applicability=%s interpretation=%#v", tc.message, tc.wantCognition, result.NeedsCognition, result.Interpretation.Applicability, result.Interpretation)
}
if tc.message != "" && result.Interpretation.Clarification == interaction.ClarificationRequired && tc.wantCognition {
t.Fatalf("message %q unexpectedly required clarification while asking for safe conversational understanding", tc.message)
}
}
}
func TestInterpretUtteranceModalExplanationRequestStillRequestsCognition(t *testing.T) {
for _, message := range []string{"можешь расказать об утре", "расскажи об утре", "про утро расскажешь"} {
result := interpretForTest(t, message, nil, "")
if result.Interpretation.SpeechAct == interaction.SpeechActAssertion && result.Interpretation.Applicability == interaction.SemanticApplicable {
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 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(),
}
}