From ec86c983e37453b0630ae2a99c6cc9db05d2d39c Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 26 Aug 2026 10:08:42 +0300 Subject: [PATCH] Route safe text directives through cognition --- cmd/core-runtime/cognitive_interpretation.go | 2 +- cmd/core-runtime/main.go | 13 ++++++++++++- cmd/core-runtime/main_test.go | 14 ++++++++++++++ modules/interaction/domain/chat.go | 13 ++++++++++--- modules/interaction/domain/chat_test.go | 10 ++++++++++ 5 files changed, 47 insertions(+), 5 deletions(-) diff --git a/cmd/core-runtime/cognitive_interpretation.go b/cmd/core-runtime/cognitive_interpretation.go index abf2373..dc7bc48 100644 --- a/cmd/core-runtime/cognitive_interpretation.go +++ b/cmd/core-runtime/cognitive_interpretation.go @@ -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}) diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index 8861df6..9c627a6 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -866,7 +866,8 @@ func main() { needsCognition = false } } - if planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion && !hasChatRoute(routePlan, chatRouteResearch) && !creativeRequest { + canGenerateTextReply := shouldGenerateNonMutatingTextDirective(semantic, routePlan, helpPath, creativeRequest) && planningReply == deterministicReply + if (planningReply == "" && needsCognition && semantic.Interpretation.SpeechAct == interaction.SpeechActQuestion || canGenerateTextReply) && !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, @@ -2024,6 +2025,16 @@ func stableKnowledgeAnswer(answer string) bool { return strings.TrimSpace(answer) != "" && strings.TrimSpace(answer) != "NEEDS_VERIFIED_RESEARCH" } +// shouldGenerateNonMutatingTextDirective identifies a request that can be +// completed entirely in the reply. It excludes specialized routes and durable +// work, so cognition can produce content but cannot stand in for an executor. +func shouldGenerateNonMutatingTextDirective(semantic interactiondomain.SemanticResult, routes []chatRoute, helpPath work.HelpPath, creativeRequest bool) bool { + if semantic.Interpretation.SpeechAct != interaction.SpeechActDirective || creativeRequest || helpPath.Kind != "" || helpPath.NeedsDurableWork { + return false + } + return !hasChatRoute(routes, chatRouteResearch) && !hasChatRoute(routes, chatRouteMedia) && !hasChatRoute(routes, chatRouteAgent) +} + func reviewPreservesResearchEvidence(reviewed, draft string) bool { if strings.Contains(draft, "http://") || strings.Contains(draft, "https://") { return strings.Contains(reviewed, "http://") || strings.Contains(reviewed, "https://") diff --git a/cmd/core-runtime/main_test.go b/cmd/core-runtime/main_test.go index 4a718e4..4bbce78 100644 --- a/cmd/core-runtime/main_test.go +++ b/cmd/core-runtime/main_test.go @@ -13,6 +13,7 @@ import ( "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/epistemics" "git.cin.su/m/Assistant/contracts/interaction" + "git.cin.su/m/Assistant/contracts/work" interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain" ) @@ -145,6 +146,19 @@ func TestResponseReviewPreservesAttemptedResearchOutcome(t *testing.T) { } } +func TestNonMutatingTextDirectiveCanUseCognitiveReply(t *testing.T) { + semantic := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: interaction.SpeechActDirective}} + if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{}, false) { + t.Fatal("ordinary formatting directive was not eligible for a cognitive reply") + } + if shouldGenerateNonMutatingTextDirective(semantic, []chatRoute{{Kind: chatRouteMedia}}, work.HelpPath{}, false) { + t.Fatal("media route was incorrectly replaced by a textual reply") + } + if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{NeedsDurableWork: true}, false) { + t.Fatal("durable work was incorrectly replaced by a textual reply") + } +} + func TestBearerTokenRequiresBearerScheme(t *testing.T) { request := httptest.NewRequest("GET", "/", nil) request.Header.Set("Authorization", "Basic value") diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index 6e33f44..3200196 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -196,7 +196,7 @@ func BuildDeterministicCandidate(message string, corrects interaction.HumanUtter } if primary == interaction.SpeechActQuestion && !hasQuestionMark && !containsMeaningfulInterrogative(tokens) && !startsWithModalRequest(tokens) && !startsWithExplanatoryRequest(tokens, "расска", "раска", "объяс", "опиш", "покаж", "подскаж") { - if len(tokens) > 0 && containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ") { + if len(tokens) > 0 && containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ", "вывед") { primary, secondary = interaction.SpeechActDirective, interaction.SpeechActUnknown } else { primary, secondary = interaction.SpeechActAssertion, interaction.SpeechActUnknown @@ -279,7 +279,7 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo questionWords := map[string]struct{}{"кто": {}, "что": {}, "как": {}, "почему": {}, "зачем": {}, "где": {}, "когда": {}, "сколько": {}, "какой": {}, "какая": {}, "какие": {}, "чей": {}} conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй"} investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "уточн", "подум", "понят", "посмотри", "попроб", "повтор", "снов"} - directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "найд", "посчитай", "рассчитай", "ответь"} + directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "найд", "посчитай", "рассчитай", "ответь", "вывед"} explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж", "сообщ"} preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"} hypothesisStems := []string{"думаю", "кажется", "гипотез", "похоже"} @@ -494,6 +494,13 @@ func needsCognitionForCandidate(primary, secondary interaction.SpeechAct, tokens if primary == interaction.SpeechActDirective && (containsInterrogative(tokens) || containsModelReference(tokens) || containsStemInTokens(tokens, "спрос", "обрати", "посчитай", "рассчитай")) { return true } + // A harmless imperative can ask for a textual or structural result even + // without a question mark. Treat it as a cognition-worthy request instead + // of silently recording it as an assertion. Consequential instructions are + // kept on their explicit confirmation/capability paths. + if primary == interaction.SpeechActDirective && !likelyConsequential(tokens) { + return true + } return false } @@ -904,7 +911,7 @@ func followsNibud(tokens []string) bool { } func startsWithDirectiveWhatNibud(tokens []string) bool { - if len(tokens) < 2 || !containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ") { + if len(tokens) < 2 || !containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ", "вывед") { return false } for index, token := range tokens { diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index 05b5b82..ee4cc74 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -112,6 +112,16 @@ func TestDeterministicCandidateTreatsModelRequestAsDirectiveNeedingCognition(t * } } +func TestDeterministicCandidateTreatsFormattingImperativeAsCognitionWorthyDirective(t *testing.T) { + candidate := BuildDeterministicCandidate("выведи пустую таблицу из трех колонок и пяти строк", "", time.Unix(10, 0).UnixMilli()) + if candidate.PrimarySpeechAct != interaction.SpeechActDirective { + t.Fatalf("formatting request was not recognized as a directive: %+v", candidate) + } + if !candidate.NeedsCognition { + t.Fatalf("formatting request must reach cognitive reasoning: %+v", candidate) + } +} + func TestAWordModelDoesNotTriggerModelInventoryOrVerification(t *testing.T) { candidate := BuildDeterministicCandidate("модель должна понимать смысл вопроса", "", time.Unix(10, 0).UnixMilli()) for _, hint := range candidate.CapabilityHints { -- 2.54.0