Review chat replies through cognitive gateway #305

Merged
m merged 2 commits from codex/chat-semantic-inventory into main 2026-08-25 00:54:06 +00:00
7 changed files with 119 additions and 9 deletions
+14 -6
View File
@@ -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 = "Я — Домовой, ваш помощник."
@@ -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)
}
}
@@ -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 == "" {
@@ -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{
+16
View File
@@ -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,
+2 -2
View File
@@ -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:] {
+3
View File
@@ -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")
}