217 lines
12 KiB
Go
217 lines
12 KiB
Go
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(),
|
||
}
|
||
}
|