From 34c0e7a0f46ffca7480934e45eb26d5d8c587c77 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 25 Aug 2026 03:49:12 +0300 Subject: [PATCH] Review chat replies through cognitive gateway --- cmd/cognitive-worker/gateway.go | 20 +++++--- cmd/cognitive-worker/gateway_registry_test.go | 2 +- cmd/core-runtime/cognitive_interpretation.go | 51 +++++++++++++++++++ .../cognitive_interpretation_test.go | 32 ++++++++++++ cmd/core-runtime/main.go | 16 ++++++ modules/interaction/domain/chat.go | 4 +- modules/interaction/domain/chat_test.go | 3 ++ 7 files changed, 119 insertions(+), 9 deletions(-) diff --git a/cmd/cognitive-worker/gateway.go b/cmd/cognitive-worker/gateway.go index 5ae0eb4..ddf409b 100644 --- a/cmd/cognitive-worker/gateway.go +++ b/cmd/cognitive-worker/gateway.go @@ -186,7 +186,15 @@ func selectionObjectiveForMeaning(meaning cognitive.TaskMeaning, message string) } func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.GatewayAdvisoryResponse { - text := "Проверю запрос доступным маршрутом и сообщу результат." + if request.Meaning.SpeechAct == "RESPONSE_REVIEW" { + const marker = "Черновик ответа:" + if index := strings.LastIndex(request.Message, marker); index >= 0 { + if draft := strings.TrimSpace(request.Message[index+len(marker):]); draft != "" { + return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: draft, FinishReason: cognitive.FinishCompleted} + } + } + } + text := "Понял." identityText := request.Meaning.RequestedOutcome if identityText == "" { identityText = request.Message @@ -196,20 +204,20 @@ func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.Gateway } switch request.Meaning.InformationRequestKind { case interaction.InformationRequestCognitiveModelInventory: - text = "Проверю подключённые модели. Ключи и пароли не показываю." + text = "Сейчас не смог проверить подключённые модели." case interaction.InformationRequestCapabilityInventory: - text = "Проверю доступные возможности и отдельно отмечу недоступные." + text = "Сейчас не смог проверить доступные возможности." case interaction.InformationRequestCognitiveChannelStatus: text = "Локальный канал Домового доступен; подробности состояния дам по запросу." case interaction.InformationRequestCognitiveRecovery: - text = "Проверю встроенный маршрут и предложу рабочий шаг." + text = "Встроенный канал сейчас недоступен." default: switch request.Meaning.SpeechAct { case "REQUEST_TO_INVESTIGATE": - text = "Понял задачу. Проверю доступный путь и сообщу результат." + text = "Понял задачу, но пока не смог получить проверяемый результат." case "QUESTION": if !isIdentityInquiry(identityText) { - text = "Проверю вопрос доступным источником и отвечу по существу." + text = "Пока не смог получить проверяемый ответ." } case "CONVERSATIONAL": text = "Я — Домовой, ваш помощник." diff --git a/cmd/cognitive-worker/gateway_registry_test.go b/cmd/cognitive-worker/gateway_registry_test.go index 0b39f08..911883e 100644 --- a/cmd/cognitive-worker/gateway_registry_test.go +++ b/cmd/cognitive-worker/gateway_registry_test.go @@ -185,7 +185,7 @@ func TestNoActiveBindingUsesBuiltInProviderWithoutInvocation(t *testing.T) { func TestBuiltInAdvisoryUsesTrustedInformationIntentInsteadOfMessageKeywords(t *testing.T) { request := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", Message: "arbitrary text", Meaning: cognitive.TaskMeaning{InformationRequestKind: interaction.InformationRequestCognitiveRecovery}} response := builtinAdvisory(request) - if !strings.Contains(response.AdvisoryText, "встроенный маршрут") || !strings.Contains(response.AdvisoryText, "рабочий шаг") { + if response.AdvisoryText != "Встроенный канал сейчас недоступен." { t.Fatalf("unexpected recovery response: %q", response.AdvisoryText) } } diff --git a/cmd/core-runtime/cognitive_interpretation.go b/cmd/core-runtime/cognitive_interpretation.go index 3ab05d4..e382362 100644 --- a/cmd/core-runtime/cognitive_interpretation.go +++ b/cmd/core-runtime/cognitive_interpretation.go @@ -8,6 +8,7 @@ import ( "hash/fnv" "net/http" "net/url" + "strings" "time" "git.cin.su/m/Assistant/contracts/authority" @@ -24,6 +25,56 @@ type cognitiveInterpretationClient struct { client *http.Client } +// ReviewResponse asks the cognitive gateway to turn a locally assembled draft +// into a natural answer. The draft remains the safe fallback when cognition is +// unavailable; the model does not mutate canonical state or execute actions. +func (c cognitiveInterpretationClient) ReviewResponse(ctx context.Context, session identity.CreatorSession, userMessage, draft string, meaning cognitive.TaskMeaning) (string, error) { + requestID, err := ids.New() + if err != nil { + return "", err + } + prompt := "Ты редактор ответа Домового. Проверь, отвечает ли черновик на сообщение пользователя по смыслу. Верни только готовый ответ пользователю, без разбора проверки, служебных слов и упоминания моделей. Не выдумывай факты, не утверждай невыполненные действия, сохрани важные имена, числа, источники и ограничения. Если черновик уже адекватен, верни его без лишних добавлений.\n\nСообщение пользователя:\n" + userMessage + "\n\nЧерновик ответа:\n" + draft + target := *c.baseURL + target.Path = "/v1/advisories" + payload, err := json.Marshal(cognitive.GatewayAdvisoryRequest{ + Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(requestID), + ActorID: session.ActorID, PrincipalID: session.PrincipalID, + ScopeID: authority.ScopeID("domovoy-global-scope"), Message: prompt, + Locale: "ru", MaxOutputBytes: 2048, DataPolicy: capability.DataPolicyLocalOnly, + Meaning: cognitive.TaskMeaning{ + SpeechAct: "RESPONSE_REVIEW", + CapabilityHints: meaning.CapabilityHints, + RequestedOutcome: meaning.RequestedOutcome, + InformationRequestKind: meaning.InformationRequestKind, + ConfidenceMillis: meaning.ConfidenceMillis, + }, + }) + if err != nil { + return "", err + } + request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload)) + if err != nil { + return "", err + } + request.Header.Set("Content-Type", "application/json") + response, err := c.client.Do(request) + if err != nil { + return "", err + } + defer response.Body.Close() + if response.StatusCode != http.StatusOK { + return "", fmt.Errorf("cognitive response review status %d", response.StatusCode) + } + var result cognitive.GatewayAdvisoryResponse + if err := json.NewDecoder(response.Body).Decode(&result); err != nil { + return "", err + } + if result.FinishReason != cognitive.FinishCompleted || strings.TrimSpace(result.AdvisoryText) == "" { + return "", fmt.Errorf("cognitive response review incomplete: %s", result.FinishReason) + } + return strings.TrimSpace(result.AdvisoryText), nil +} + func newCognitiveInterpretationClient(rawURL string, client *http.Client) (cognitiveInterpretationClient, error) { base, err := url.Parse(rawURL) if err != nil || base.Scheme == "" || base.Host == "" { diff --git a/cmd/core-runtime/cognitive_interpretation_test.go b/cmd/core-runtime/cognitive_interpretation_test.go index cf8c4cf..6d46dbd 100644 --- a/cmd/core-runtime/cognitive_interpretation_test.go +++ b/cmd/core-runtime/cognitive_interpretation_test.go @@ -1,13 +1,45 @@ package main import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" "testing" "time" + "git.cin.su/m/Assistant/contracts/cognitive" + "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/interaction" interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain" ) +func TestReviewResponseUsesCognitiveGatewayAndReturnsOnlyDraftAnswer(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var request cognitive.GatewayAdvisoryRequest + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + t.Fatalf("decode review request: %v", err) + } + if request.Message == "" || request.Meaning.SpeechAct == "" { + t.Fatalf("review request did not carry semantic context: %+v", request) + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Меня зовут Дед. Я Домовой.", FinishReason: cognitive.FinishCompleted}) + })) + defer server.Close() + client, err := newCognitiveInterpretationClient(server.URL, server.Client()) + if err != nil { + t.Fatal(err) + } + reviewed, err := client.ReviewResponse(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "кто ты?", "Я — Домовой.", cognitive.TaskMeaning{SpeechAct: "QUESTION"}) + if err != nil { + t.Fatal(err) + } + if reviewed != "Меня зовут Дед. Я Домовой." { + t.Fatalf("unexpected reviewed response %q", reviewed) + } +} + func TestSimpleQuestionUsesStrictInterpretationForSemanticRouting(t *testing.T) { result := interactiondomain.SemanticResult{ Interpretation: interaction.UtteranceInterpretation{ diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index b956063..f6faae9 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -792,6 +792,22 @@ func main() { planningReply = identityReply needsCognition = false } + if strings.TrimSpace(planningReply) != "" { + reviewContext, cancelReview := context.WithTimeout(r.Context(), 12*time.Second) + reviewedReply, reviewErr := cognitiveClient.ReviewResponse(reviewContext, session, utterance.Content, planningReply, cognitive.TaskMeaning{ + SpeechAct: string(semantic.Interpretation.SpeechAct), + CapabilityHints: toCapabilityKinds(semantic.CapabilityHints), + RequestedOutcome: semantic.Interpretation.RequestedOutcome, + InformationRequestKind: semantic.InformationRequestKind, + ConfidenceMillis: semantic.Interpretation.ConfidenceMillis, + }) + cancelReview() + if reviewErr == nil { + planningReply = reviewedReply + } else { + log.Printf("cognitive response review unavailable: %v", reviewErr) + } + } response := interaction.ChatMessageResponse{ ConversationID: conversation.ID, UtteranceID: utterance.ID, diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index ae67072..cea0cd9 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -129,7 +129,7 @@ func FallbackText(act interaction.SpeechAct) string { func PreferredAssistantName(message string) string { tokens := semanticTokens(message) for index, token := range tokens { - if !containsStem(token, "называ", "зов", "имен") { + if !containsStem(token, "называ", "назов", "зов", "имен") { continue } for _, candidate := range tokens[index+1:] { @@ -349,7 +349,7 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo func preferredNameFromTokens(tokens []string) bool { for index, token := range tokens { - if !containsStem(token, "называ", "зов", "имен") { + if !containsStem(token, "называ", "назов", "зов", "имен") { continue } for _, candidate := range tokens[index+1:] { diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index 697fd58..d22ce6e 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -195,6 +195,9 @@ func TestNamingPreferenceIsSemanticAndProducesNaturalAcceptance(t *testing.T) { if name := PreferredAssistantName("я буду называть тебя Дед, запомни ты дед"); name != "Дед" { t.Fatalf("expected extracted assistant name, got %q", name) } + if name := PreferredAssistantName("я назову тебя дед, запомни"); name != "Дед" { + t.Fatalf("expected conversational name form to be extracted, got %q", name) + } if !IsIdentityInquiry("ты кто?") || !IsIdentityInquiry("как тебя зовут") { t.Fatal("identity variants should be recognized") } -- 2.54.0