Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
46b89577eb | ||
|
|
a6ddbfaaf2 | ||
|
|
362a1bd993 | ||
|
|
735222de0d |
@@ -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})
|
||||
|
||||
@@ -860,6 +860,17 @@ 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
|
||||
needsCognition = false
|
||||
}
|
||||
}
|
||||
if planningReply == "" && needsCognition && cognitiveAttempted && !cognitiveAvailable {
|
||||
planningReply = humanCognitiveDegradedReply(utterance.Content)
|
||||
needsCognition = false
|
||||
|
||||
@@ -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