Keep travel requests in semantic dialogue #312

Merged
m merged 2 commits from codex/chat-semantic-inventory into main 2026-08-25 01:55:33 +00:00
3 changed files with 103 additions and 2 deletions
+9 -2
View File
@@ -508,7 +508,7 @@ func main() {
http.Error(w, "project focus unavailable", http.StatusServiceUnavailable)
return
}
var effectiveTaskMessage = utterance.Content
var effectiveTaskMessage = interactiondomain.StripAssistantVocative(utterance.Content)
var previousTask *interaction.HumanUtterance
var conversationHistory []interaction.HumanUtterance
if request.ConversationID != "" {
@@ -536,7 +536,7 @@ func main() {
}
candidate := conversationHistory[index]
previousTask = &candidate
effectiveTaskMessage = candidate.Content
effectiveTaskMessage = interactiondomain.StripAssistantVocative(candidate.Content)
break
}
}
@@ -601,6 +601,13 @@ func main() {
cognitiveAvailable = false
}
}
if travelReply := interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory); travelReply != "" {
semantic.Interpretation.Clarification = interaction.ClarificationRequired
semantic.Interpretation.Applicability = interaction.SemanticCandidateClarify
semantic.Reply = travelReply
semantic.NeedsCognition = false
semantic.Interpretation.NeedsCognition = false
}
if err := interactionRepository.AppendUtterance(r.Context(), conversation, utterance, intent, relations); err != nil {
http.Error(w, "save chat message", http.StatusInternalServerError)
return
@@ -60,6 +60,28 @@ func TestStableConversationalReplySurvivesCognitiveRewrite(t *testing.T) {
}
}
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 {
+72
View File
@@ -0,0 +1,72 @@
package domain
import (
"strings"
"git.cin.su/m/Assistant/contracts/interaction"
)
// StripAssistantVocative removes a leading assistant name used as an address.
// The original utterance remains the historical source; this value is only for
// interpretation, cognitive prompts and external queries.
func StripAssistantVocative(message string) string {
trimmed := strings.TrimSpace(message)
lower := strings.ToLower(trimmed)
for _, name := range []string{"дед", "домовой"} {
if !strings.HasPrefix(lower, name) {
continue
}
rest := trimmed[len(name):]
if rest == "" || !strings.ContainsRune(" \t,;:!?—-", []rune(rest)[0]) {
continue
}
return strings.TrimSpace(strings.TrimLeft(rest, " \t,;:!?—-"))
}
return trimmed
}
// TravelSearchClarification keeps an incomplete travel request in dialogue
// instead of reducing it to an unrelated generic web search.
func TravelSearchClarification(message string, history []interaction.HumanUtterance) string {
current := semanticTokens(StripAssistantVocative(message))
travel := travelCue(current)
if !travel {
for index := len(history) - 1; index >= 0; index-- {
if travelCue(semanticTokens(history[index].Content)) {
travel = true
break
}
}
}
if !travel {
return ""
}
origin := containsStemInTokens(current, "из", "откуда", "от")
destination := containsStemInTokens(current, "в", "куда", "до")
date := containsStemInTokens(current, "сегодня", "завтра", "послезавтр", "январ", "феврал", "март", "апрел", "мая", "июн", "июл", "август", "сентябр", "октябр", "ноябр", "декабр")
if !date {
for _, token := range current {
if hasDigit(token) {
date = true
break
}
}
}
if origin && destination && date {
return ""
}
return "Уточните маршрут и дату: откуда, куда и на какой день нужны билеты?"
}
func travelCue(tokens []string) bool {
return containsStemInTokens(tokens, "билет", "рейс", "поезд", "автобус", "перелет", "перелёт")
}
func hasDigit(value string) bool {
for _, runeValue := range value {
if runeValue >= '0' && runeValue <= '9' {
return true
}
}
return false
}