Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35fc2438dc | ||
|
|
70335a78e2 | ||
|
|
41067a0b16 | ||
|
|
fba05d5ed4 | ||
|
|
6e14ba1727 | ||
|
|
9c6e2097b1 | ||
|
|
96559f6ab8 | ||
|
|
46b89577eb | ||
|
|
a6ddbfaaf2 | ||
|
|
362a1bd993 | ||
|
|
735222de0d |
@@ -67,12 +67,18 @@ func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.R
|
||||
}
|
||||
g.observe(r.Context(), selected, response, started)
|
||||
if response.FinishReason != cognitive.FinishCompleted {
|
||||
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: response.FinishReason, Usage: response.Usage})
|
||||
// A selected provider can become unavailable after discovery. Semantic
|
||||
// routing must still retain the deterministic interpretation and use the
|
||||
// local lifeboat rather than turn an ordinary conversation into a
|
||||
// degraded reply solely because one provider failed.
|
||||
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
candidate, ok := parseInterpretationCandidate(response.AdvisoryText)
|
||||
if !ok {
|
||||
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishInvalidStructuredOutput, Usage: response.Usage})
|
||||
// Structured interpretation is a transport contract. A malformed model
|
||||
// answer is not a reason to discard the available local fallback.
|
||||
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
|
||||
return
|
||||
}
|
||||
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage})
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.cin.su/m/Assistant/contracts/capability"
|
||||
"git.cin.su/m/Assistant/contracts/cognitive"
|
||||
)
|
||||
|
||||
@@ -24,3 +30,32 @@ func TestBuiltinInterpretationTimeoutRetainsDeterministicCandidate(t *testing.T)
|
||||
t.Fatalf("candidate did not retain deterministic meaning: %+v", response.Candidate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInterpretationProviderFailureUsesLocalLifeboat(t *testing.T) {
|
||||
selected := gatewaySelection()
|
||||
selected.Binding.DataPolicy = capability.DataPolicyLocalOnly
|
||||
primary := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{FinishReason: cognitive.FinishProviderUnavailable}}
|
||||
lifeboat := &stubAdvisoryProvider{response: cognitive.AdvisoryResponse{AdvisoryText: `{"primary_speech_act":"QUESTION","secondary_speech_act":"","confidence_millis":900,"applicability":"APPLICABLE","requested_outcome":"","needs_cognition":true,"clarification":"NOT_REQUIRED","selection_reason":"local lifeboat","capability_hints":["COGNITIVE_REASONING"]}`, FinishReason: cognitive.FinishCompleted}}
|
||||
gateway := cognitiveGateway{
|
||||
resolver: ®istryStub{selected: selected},
|
||||
providers: map[capability.ProviderKind][]providerRegistration{
|
||||
selected.Provider.Kind: {{ProviderInstanceID: selected.Provider.ID, Kind: selected.Provider.Kind, Provider: primary}},
|
||||
},
|
||||
builtinLocal: &providerRegistration{Provider: lifeboat},
|
||||
now: time.Now,
|
||||
}
|
||||
request := cognitive.GatewayInterpretationRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", ActorID: "actor-1", PrincipalID: "principal-1", ScopeID: "scope-1", Message: "сколько фигур в шахматах?", Locale: "ru", MaxOutputBytes: 4096, DataPolicy: capability.DataPolicyLocalOnly, DeterministicSpeechAct: "QUESTION", DeterministicConfidence: 800, DeterministicApplicability: cognitive.InterpretationApplicable}
|
||||
payload, _ := json.Marshal(request)
|
||||
recorder := httptest.NewRecorder()
|
||||
gateway.interpretationHandler(recorder, httptest.NewRequest(http.MethodPost, "/v1/interpretations", bytes.NewReader(payload)))
|
||||
if recorder.Code != http.StatusOK || !primary.called || !lifeboat.called {
|
||||
t.Fatalf("status=%d primary=%t lifeboat=%t body=%s", recorder.Code, primary.called, lifeboat.called, recorder.Body.String())
|
||||
}
|
||||
var response cognitive.GatewayInterpretationResponse
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if response.FinishReason != cognitive.FinishCompleted || response.Candidate.SelectionReason != "local lifeboat" {
|
||||
t.Fatalf("response=%+v", response)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ func (c cognitiveInterpretationClient) ReviewResponse(ctx context.Context, sessi
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
prompt := "Ты редактор ответа Домового. Проверь черновик по смыслу и подготовь естественный ответ человеку. Учитывай весь запрос, включая недостающие параметры для конкретного результата. Если данных не хватает, задай один короткий полезный уточняющий вопрос, например откуда и куда ехать. Не выдавай поисковые сниппеты как выполненное действие и не показывай служебные URL поисковой выдачи. Сохрани проверяемые факты, названия источников, прямые ссылки и ограничения. Не выдумывай факты и не утверждай невыполненные действия. Верни только готовый ответ без разбора проверки, служебных слов и упоминания моделей. Если черновик уже адекватен, верни его без лишних добавлений.\n\nСообщение пользователя:\n" + userMessage + "\n\nЧерновик ответа:\n" + draft
|
||||
prompt := "Ты редактор финального ответа Домового. Проверь черновик по смыслу и подготовь естественный ответ человеку. Учитывай весь запрос, включая недостающие параметры для конкретного результата. Если данных не хватает, задай один короткий полезный уточняющий вопрос, например откуда и куда ехать. Не выдавай поисковые сниппеты как выполненное действие и не показывай служебные URL поисковой выдачи. Сохрани проверяемые факты, названия источников, прямые ссылки и ограничения. Не выдумывай факты и не утверждай невыполненные действия. Сначала внутренне проверь, что ответ начинается с содержания, решающего вопрос. Не повторяй вопрос пользователя в качестве заголовка или вступления, если это не нужно для смысла. Представление, имя помощника, приветствие или предложение дополнительной помощи допустимы только если пользователь сам спросил об этом; иначе убери их. Верни только готовый ответ без разбора проверки, служебных слов и упоминания моделей. Если черновик уже адекватен, верни его без лишних добавлений.\n\nСообщение пользователя:\n" + userMessage + "\n\nЧерновик ответа:\n" + draft
|
||||
context := []cognitive.ContextExcerpt{}
|
||||
if strings.TrimSpace(personalMemory) != "" {
|
||||
context = append(context, cognitive.ContextExcerpt{ID: "personal-memory", SourceKind: "derived-personal-memory", SourceID: "interaction:human-utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: personalMemory})
|
||||
@@ -91,7 +91,7 @@ func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
prompt := "Ты отвечаешь как Домовой на обычный вопрос пользователя. Сначала пойми цель вопроса и верни короткий естественный ответ по устойчивому общеизвестному знанию. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты.\n\nВопрос пользователя:\n" + userMessage
|
||||
prompt := "Ты отвечаешь как Домовой на обычный вопрос пользователя. Сначала пойми цель вопроса и верни короткий естественный ответ по устойчивому общеизвестному знанию. Начинай сразу с ответа и не представляйся, если пользователь не спросил об имени. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты.\n\nВопрос пользователя:\n" + userMessage
|
||||
context := []cognitive.ContextExcerpt{}
|
||||
if strings.TrimSpace(personalMemory) != "" {
|
||||
context = append(context, cognitive.ContextExcerpt{ID: "personal-memory", SourceKind: "derived-personal-memory", SourceID: "interaction:human-utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: personalMemory})
|
||||
|
||||
@@ -25,6 +25,9 @@ func TestReviewResponseUsesCognitiveGatewayAndReturnsOnlyDraftAnswer(t *testing.
|
||||
if request.Message == "" || request.Meaning.SpeechAct == "" {
|
||||
t.Fatalf("review request did not carry semantic context: %+v", request)
|
||||
}
|
||||
if !strings.Contains(request.Message, "ответ начинается с содержания") || !strings.Contains(request.Message, "Не повторяй вопрос пользователя") || !strings.Contains(request.Message, "только если пользователь сам спросил") {
|
||||
t.Fatalf("review request lacks the conversational quality contract: %q", request.Message)
|
||||
}
|
||||
if len(request.Context) != 1 || !strings.Contains(request.Context[0].Text, "красный цвет") {
|
||||
t.Fatalf("review request did not carry compact personal memory: %+v", request.Context)
|
||||
}
|
||||
|
||||
@@ -799,6 +799,7 @@ func main() {
|
||||
}
|
||||
researchBacked := false
|
||||
researchAttempted := false
|
||||
stableKnowledgeReply := false
|
||||
if hasChatRoute(routePlan, chatRouteResearch) && semantic.Interpretation.Clarification != interaction.ClarificationRequired && shouldRunGeneralResearch(semantic.InformationRequestKind) {
|
||||
researchAttempted = true
|
||||
if planID, idErr := ids.New(); idErr == nil {
|
||||
@@ -830,6 +831,7 @@ func main() {
|
||||
cancelAnswer()
|
||||
if answerErr == nil && stableKnowledgeAnswer(answer) {
|
||||
reply, backed = answer, true
|
||||
stableKnowledgeReply = true
|
||||
}
|
||||
}
|
||||
cancelResearch()
|
||||
@@ -860,6 +862,18 @@ func main() {
|
||||
needsCognition = false
|
||||
}
|
||||
}
|
||||
if planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion && !hasChatRoute(routePlan, chatRouteResearch) && !creativeRequest {
|
||||
answerCtx, cancelAnswer := context.WithTimeout(r.Context(), 12*time.Second)
|
||||
answer, answerErr := cognitiveClient.AnswerStableKnowledge(answerCtx, session, effectiveTaskMessage, personalMemory.ContextText(), cognitive.TaskMeaning{
|
||||
SpeechAct: string(semantic.Interpretation.SpeechAct), CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), RequestedOutcome: semantic.Interpretation.RequestedOutcome, InformationRequestKind: semantic.InformationRequestKind, ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
|
||||
})
|
||||
cancelAnswer()
|
||||
if answerErr == nil && stableKnowledgeAnswer(answer) {
|
||||
planningReply = answer
|
||||
stableKnowledgeReply = true
|
||||
needsCognition = false
|
||||
}
|
||||
}
|
||||
if planningReply == "" && needsCognition && cognitiveAttempted && !cognitiveAvailable {
|
||||
planningReply = humanCognitiveDegradedReply(utterance.Content)
|
||||
needsCognition = false
|
||||
@@ -984,7 +998,7 @@ func main() {
|
||||
stableSemanticReply := mediaAttempted || identityReply != "" || memoryInstructionReply != "" || creativeReply != "" || (creativeFallbackReply != "" && !cognitiveAvailable) || semantic.InformationRequestKind != "" || locationReply != "" || deterministicSpeechAct == interaction.SpeechActConversational || deterministicSpeechAct == interaction.SpeechActConfirmation || deterministicSpeechAct == interaction.SpeechActRejection
|
||||
if reviewErr != nil {
|
||||
log.Printf("cognitive response review unavailable: %v", reviewErr)
|
||||
} else if reviewMayReplacePlanningReply(stableSemanticReply, researchAttempted, researchBacked, reviewedReply, planningReply) {
|
||||
} else if reviewMayReplacePlanningReply(stableSemanticReply, stableKnowledgeReply, researchAttempted, researchBacked, reviewedReply, planningReply) {
|
||||
planningReply = reviewedReply
|
||||
} else if reviewedReply != planningReply {
|
||||
log.Printf("cognitive response review proposed stable semantic rewrite; preserving semantic reply")
|
||||
@@ -2001,7 +2015,14 @@ func reviewPreservesResearchEvidence(reviewed, draft string) bool {
|
||||
// reviewMayReplacePlanningReply keeps ownership of a concrete capability
|
||||
// result with that capability. A review may improve source-backed research,
|
||||
// but must not turn an honest no-result into an invented clarification.
|
||||
func reviewMayReplacePlanningReply(stableSemanticReply, researchAttempted, researchBacked bool, reviewed, draft string) bool {
|
||||
func reviewMayReplacePlanningReply(stableSemanticReply, stableKnowledgeReply, researchAttempted, researchBacked bool, reviewed, draft string) bool {
|
||||
// A stable-knowledge answer is a model-produced draft rather than an owned
|
||||
// capability result. It therefore goes through a second cognitive pass for
|
||||
// conversational quality, while identity, memory, media and action outcomes
|
||||
// remain immutable at this boundary.
|
||||
if stableKnowledgeReply {
|
||||
return true
|
||||
}
|
||||
if stableSemanticReply {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -102,18 +102,21 @@ func TestResearchIdentifiersExcludeSearchDate(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestResponseReviewPreservesAttemptedResearchOutcome(t *testing.T) {
|
||||
if reviewMayReplacePlanningReply(false, true, false, "Уточните аэропорт.", "Пока не смог получить проверяемый ответ.") {
|
||||
if reviewMayReplacePlanningReply(false, false, true, false, "Уточните аэропорт.", "Пока не смог получить проверяемый ответ.") {
|
||||
t.Fatal("review replaced an honest unbacked research outcome")
|
||||
}
|
||||
if !reviewMayReplacePlanningReply(false, true, true, "Расписание. Источник: https://example.test", "Расписание. Источник: https://example.test") {
|
||||
if !reviewMayReplacePlanningReply(false, false, true, true, "Расписание. Источник: https://example.test", "Расписание. Источник: https://example.test") {
|
||||
t.Fatal("review should be able to improve a source-backed research outcome")
|
||||
}
|
||||
if reviewMayReplacePlanningReply(false, true, true, "Расписание без ссылки", "Расписание. Источник: https://example.test") {
|
||||
if reviewMayReplacePlanningReply(false, false, true, true, "Расписание без ссылки", "Расписание. Источник: https://example.test") {
|
||||
t.Fatal("review dropped the research evidence")
|
||||
}
|
||||
if !reviewMayReplacePlanningReply(false, false, false, "Нормальный ответ", "Черновик") {
|
||||
if !reviewMayReplacePlanningReply(false, false, false, false, "Нормальный ответ", "Черновик") {
|
||||
t.Fatal("ordinary conversation review was unexpectedly blocked")
|
||||
}
|
||||
if !reviewMayReplacePlanningReply(true, true, false, false, "Санкт-Петербург — город в России.", "Меня зовут Домовой. Санкт-Петербург — город в России.") {
|
||||
t.Fatal("stable-knowledge draft must be eligible for semantic quality review")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBearerTokenRequiresBearerScheme(t *testing.T) {
|
||||
|
||||
@@ -251,6 +251,38 @@ func TestChatMessageUsesLocalAdvisoryForOrdinaryConversationalQuestion(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatMessagePreservesCompletedCoreAnswerInsteadOfSecondAdvisory(t *testing.T) {
|
||||
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current":
|
||||
_ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"})
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages":
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
_ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, NeedsCognition: true, FallbackText: "fallback", PlanningReply: "Готовый ответ ядра."})
|
||||
default:
|
||||
t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer core.Close()
|
||||
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("completed core answer must not be replaced by a second advisory")
|
||||
}))
|
||||
defer cognitiveRuntime.Close()
|
||||
handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), cognitiveRuntime.URL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Обычный вопрос"}`))
|
||||
request.Header.Set("Authorization", "Bearer session-token")
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
handler.ServeHTTP(recorder, request)
|
||||
var response browserChatResponse
|
||||
if recorder.Code != http.StatusOK || json.NewDecoder(recorder.Body).Decode(&response) != nil || response.ReplyText != "Готовый ответ ядра." {
|
||||
t.Fatalf("unexpected browser response: status=%d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestChatMessageReturnsHumanCapabilityMetadata(t *testing.T) {
|
||||
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
|
||||
@@ -480,7 +480,10 @@ func (g *gateway) handleChatMessage(w http.ResponseWriter, r *http.Request) {
|
||||
saved.NeedsCognition = false
|
||||
}
|
||||
}
|
||||
if saved.NeedsCognition && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
|
||||
// Core owns the completed planning answer. The browser may ask for a local
|
||||
// advisory only when core explicitly left the turn unanswered; otherwise a
|
||||
// second model pass can replace a grounded result with a weaker one.
|
||||
if saved.NeedsCognition && saved.PlanningReply == "" && !saved.WorkPlanned && saved.Clarification != interaction.ClarificationRequired {
|
||||
cognition = "unavailable"
|
||||
requestID, idErr := ids.New()
|
||||
if idErr == nil {
|
||||
|
||||
@@ -612,7 +612,7 @@ func sourceDependentQuestion(primary, secondary interaction.SpeechAct, tokens []
|
||||
currentCue := containsStemInTokens(tokens, "сейчас", "сегодня", "текущ", "актуаль", "последн", "прямо")
|
||||
observedStateCue := containsStemInTokens(tokens, "погод", "температур", "осад", "ветер", "курс", "цен", "стоим", "новост", "распис", "событ", "статус", "доступен", "работает", "запущен", "срок") || approximateToken(tokens, "погод", 2)
|
||||
placeSearchCue := containsStemInTokens(tokens, "ресторан", "кафе", "отел", "магаз", "мест", "ближ", "адрес", "организац", "бренд", "товар", "артикул", "продавец", "маркетплейс", "wildberries", "wb")
|
||||
travelSearchCue := containsStemInTokens(tokens, "билет", "рейс", "самолет", "самолёт", "вылет", "поезд", "автобус", "перелет", "перелёт", "отправлен", "прибыт", "расстоян", "километр", "добрат", "маршрут")
|
||||
travelSearchCue := travelCue(tokens) || containsStemInTokens(tokens, "отправлен", "прибыт", "расстоян", "километр", "добрат", "маршрут")
|
||||
timeDifferenceCue := containsStemInTokens(tokens, "разниц") && containsStemInTokens(tokens, "времен", "час")
|
||||
return currentCue || observedStateCue || placeSearchCue || travelSearchCue || timeDifferenceCue
|
||||
}
|
||||
@@ -622,13 +622,33 @@ func sourceDependentQuestion(primary, secondary interaction.SpeechAct, tokens []
|
||||
// source-dependent concepts such as weather.
|
||||
func approximateToken(tokens []string, target string, maxDistance int) bool {
|
||||
for _, token := range tokens {
|
||||
if editDistance([]rune(token), []rune(target)) <= maxDistance {
|
||||
if nearToken(token, target, maxDistance) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func nearToken(token, target string, maxDistance int) bool {
|
||||
if editDistance([]rune(token), []rune(target)) <= maxDistance {
|
||||
return true
|
||||
}
|
||||
if maxDistance < 1 {
|
||||
return false
|
||||
}
|
||||
left, right := []rune(token), []rune(target)
|
||||
if len(left) != len(right) || len(left) < 2 {
|
||||
return false
|
||||
}
|
||||
for index := range left {
|
||||
if left[index] == right[index] {
|
||||
continue
|
||||
}
|
||||
return index+1 < len(left) && left[index] == right[index+1] && left[index+1] == right[index] && string(left[index+2:]) == string(right[index+2:])
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func editDistance(left, right []rune) int {
|
||||
previous := make([]int, len(right)+1)
|
||||
for index := range previous {
|
||||
|
||||
@@ -50,9 +50,11 @@ func TestNewHumanUtteranceClassifiesReminderAndCapabilityConnectionAsDirective(t
|
||||
}
|
||||
|
||||
func TestPlaneRequestReceivesTravelResearchCapability(t *testing.T) {
|
||||
candidate := BuildDeterministicCandidate("найди ближайший самолет до Казани", "", 100)
|
||||
if !containsToken(candidate.CapabilityHints, "WEB_RESEARCH") {
|
||||
t.Fatalf("plane request did not select web research: %+v", candidate)
|
||||
for _, message := range []string{"найди ближайший самолет до Казани", "найди сомалет до Казани"} {
|
||||
candidate := BuildDeterministicCandidate(message, "", 100)
|
||||
if !containsToken(candidate.CapabilityHints, "WEB_RESEARCH") {
|
||||
t.Fatalf("plane request did not select web research for %q: %+v", message, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -236,6 +236,14 @@ func TestTravelResearchQueryUsesKnownOriginForTrainSchedule(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTravelResearchQueryNormalizesNearUnambiguousTravelTypo(t *testing.T) {
|
||||
history := []interaction.HumanUtterance{{Content: "я сейчас в Москве"}}
|
||||
got := TravelResearchQuery("найди сомалет до Казани", history, time.Time{})
|
||||
if !strings.Contains(got, "самолёт") || !strings.Contains(got, "пункт отправления: Москве") {
|
||||
t.Fatalf("travel typo was not normalized with remembered route context: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndependentRequestPartsKeepsCoordinatedEntitiesTogether(t *testing.T) {
|
||||
if got := IndependentRequestParts("какая разница во времени между Москвой и Шанхаем?"); len(got) != 1 {
|
||||
t.Fatalf("coordinated entities became independent requests: %#v", got)
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
// TravelSearchQuery carries the implicit current-day meaning of a nearest
|
||||
// flight request into the research adapter.
|
||||
func TravelSearchQuery(message string, now time.Time) string {
|
||||
cleaned := strings.TrimSpace(StripAssistantVocative(message))
|
||||
cleaned := normalizedTravelSearchText(message)
|
||||
tokens := semanticTokens(cleaned)
|
||||
if !travelCue(tokens) || !containsStemInTokens(tokens, "ближ") || isTravelDateSpecified(tokens) {
|
||||
return cleaned
|
||||
@@ -21,6 +21,25 @@ func TravelSearchQuery(message string, now time.Time) string {
|
||||
return cleaned + " сегодня " + now.Format("02.01.2006")
|
||||
}
|
||||
|
||||
// normalizedTravelSearchText corrects only a near-unambiguous typo in a
|
||||
// capability word before an external query. The original utterance is kept by
|
||||
// Conversation; this is a replaceable search-skill normalization, not a fact
|
||||
// about a user, city or provider.
|
||||
func normalizedTravelSearchText(message string) string {
|
||||
tokens := semanticTokens(StripAssistantVocative(message))
|
||||
changed := false
|
||||
for index, token := range tokens {
|
||||
if nearToken(token, "самолет", 2) || nearToken(token, "самолёт", 2) {
|
||||
tokens[index] = "самолёт"
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return strings.TrimSpace(StripAssistantVocative(message))
|
||||
}
|
||||
return strings.Join(tokens, " ")
|
||||
}
|
||||
|
||||
// TravelResearchQuery keeps route and distance questions tied to a known
|
||||
// origin before they reach a generic web search. The history is semantic
|
||||
// context, not a list of example-specific answers.
|
||||
@@ -419,7 +438,7 @@ func capitalizeWord(value string) string {
|
||||
}
|
||||
|
||||
func travelCue(tokens []string) bool {
|
||||
return containsStemInTokens(tokens, "билет", "рейс", "самолет", "самолёт", "вылет", "поезд", "автобус", "перелет", "перелёт")
|
||||
return containsStemInTokens(tokens, "билет", "рейс", "самолет", "самолёт", "вылет", "поезд", "автобус", "перелет", "перелёт") || approximateToken(tokens, "самолет", 2) || approximateToken(tokens, "самолёт", 2)
|
||||
}
|
||||
|
||||
func trainScheduleRequest(tokens []string) bool {
|
||||
|
||||
Reference in New Issue
Block a user