Files
Assistant/cmd/core-runtime/cognitive_interpretation.go
T
m ec86c983e3
CI / test (push) Successful in 1m20s
CI / test (pull_request) Successful in 1m16s
Route safe text directives through cognition
2026-08-26 10:08:42 +03:00

341 lines
17 KiB
Go

package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"hash/fnv"
"io"
"mime/multipart"
"net/http"
"net/url"
"strings"
"time"
"git.cin.su/m/Assistant/contracts/authority"
"git.cin.su/m/Assistant/contracts/capability"
"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"
"git.cin.su/m/Assistant/platform/ids"
)
type cognitiveInterpretationClient struct {
baseURL *url.URL
client *http.Client
media *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, personalMemory string, meaning cognitive.TaskMeaning) (string, error) {
requestID, err := ids.New()
if err != nil {
return "", err
}
prompt := "Ты редактор финального ответа Домового. Проверь черновик по смыслу и подготовь естественный ответ человеку. Учитывай весь запрос, включая недостающие параметры для конкретного результата. Если данных не хватает, задай один короткий полезный уточняющий вопрос, например откуда и куда ехать. Не выдавай поисковые сниппеты как выполненное действие и не показывай служебные URL поисковой выдачи. Сохрани проверяемые факты, названия источников, прямые ссылки и ограничения. Не выдумывай факты и не утверждай невыполненные действия. Сначала внутренне проверь, что ответ начинается с содержания, решающего вопрос. Не повторяй вопрос пользователя в качестве заголовка или вступления, если это не нужно для смысла. Представление, имя помощника, приветствие или предложение дополнительной помощи допустимы только если пользователь сам спросил об этом; иначе убери их. Верни только готовый ответ без разбора проверки, служебных слов и упоминания моделей. Если черновик уже адекватен, верни его без лишних добавлений.\n\nСообщение пользователя:\n" + userMessage + "\n\nЧерновик ответа:\n" + draft
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})
}
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,
Context: context, 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
}
// AnswerStableKnowledge asks the local cognitive route for a short answer to a
// non-current question only after evidence retrieval did not produce a usable
// result. The model decides whether its knowledge is stable enough; it must
// explicitly decline changing or source-dependent facts instead of guessing.
func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context, session identity.CreatorSession, userMessage, personalMemory string, meaning cognitive.TaskMeaning) (string, error) {
requestID, err := ids.New()
if err != nil {
return "", err
}
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})
}
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, Context: context, Locale: "ru", MaxOutputBytes: 1024, DataPolicy: capability.DataPolicyLocalOnly,
Meaning: cognitive.TaskMeaning{SpeechAct: "STABLE_KNOWLEDGE_ANSWER", 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 stable knowledge 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 stable knowledge 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 == "" {
return cognitiveInterpretationClient{}, fmt.Errorf("invalid Cognitive Runtime URL")
}
if client == nil {
client = http.DefaultClient
}
return cognitiveInterpretationClient{baseURL: base, client: client, media: &http.Client{Timeout: 15 * time.Minute}}, nil
}
func (c cognitiveInterpretationClient) Interpret(ctx context.Context, request cognitive.GatewayInterpretationRequest) (cognitive.GatewayInterpretationResponse, error) {
target := *c.baseURL
target.Path = "/v1/interpretations"
payload, err := json.Marshal(request)
if err != nil {
return cognitive.GatewayInterpretationResponse{}, err
}
upstream, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
if err != nil {
return cognitive.GatewayInterpretationResponse{}, err
}
upstream.Header.Set("Content-Type", "application/json")
response, err := c.client.Do(upstream)
if err != nil {
return cognitive.GatewayInterpretationResponse{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return cognitive.GatewayInterpretationResponse{}, fmt.Errorf("cognitive interpretation status %d", response.StatusCode)
}
var result cognitive.GatewayInterpretationResponse
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
return cognitive.GatewayInterpretationResponse{}, err
}
return result, nil
}
func (c cognitiveInterpretationClient) GenerateImage(ctx context.Context, prompt, size string) (capability.MultimodalResponse, error) {
var body bytes.Buffer
writer := multipart.NewWriter(&body)
if err := writer.WriteField("prompt", prompt); err != nil {
return capability.MultimodalResponse{}, err
}
if strings.TrimSpace(size) != "" {
if err := writer.WriteField("size", size); err != nil {
return capability.MultimodalResponse{}, err
}
}
if err := writer.Close(); err != nil {
return capability.MultimodalResponse{}, err
}
target := *c.baseURL
target.Path = "/v1/multimodal/generate-image"
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), &body)
if err != nil {
return capability.MultimodalResponse{}, err
}
request.Header.Set("Content-Type", writer.FormDataContentType())
client := c.media
if client == nil {
client = c.client
}
response, err := client.Do(request)
if err != nil {
return capability.MultimodalResponse{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return capability.MultimodalResponse{}, fmt.Errorf("image generation status %d", response.StatusCode)
}
var result capability.MultimodalResponse
if err := json.NewDecoder(io.LimitReader(response.Body, 32<<20)).Decode(&result); err != nil {
return capability.MultimodalResponse{}, err
}
if strings.TrimSpace(result.DataBase64) == "" {
return capability.MultimodalResponse{}, fmt.Errorf("image generation returned empty payload")
}
return result, nil
}
func shouldRequestBroaderInterpretation(result interactiondomain.SemanticResult) bool {
return shouldRequestSemanticInterpretation(result)
}
// shouldRequestSemanticInterpretation keeps the deterministic layer as a
// fast reflex, not as the meaning authority. Questions, requests, directives,
// mixed signals and capability-bearing messages go through cognitive
// interpretation; only stable low-risk conversational/assertive reflexes may
// answer without it.
func shouldRequestSemanticInterpretation(result interactiondomain.SemanticResult) bool {
if result.Interpretation.Clarification == interaction.ClarificationRequired {
return false
}
// Structured, read-only information intents already have a semantic owner
// in core-runtime. Sending them through the slower advisory interpretation
// path only adds latency and can make a healthy runtime look unavailable.
if result.InformationRequestKind != "" {
return false
}
if result.NeedsCognition || len(result.CapabilityHints) > 0 {
return true
}
if result.Interpretation.Applicability != interaction.SemanticApplicable {
return true
}
switch result.Interpretation.SpeechAct {
case interaction.SpeechActConversational, interaction.SpeechActConfirmation, interaction.SpeechActRejection:
return false
case interaction.SpeechActPreference:
// Explicit user preferences are durable interaction context. A sampled
// advisory audit must not reinterpret or erase them.
return false
case interaction.SpeechActAssertion:
return result.Interpretation.ConfidenceMillis < 850
default:
return true
}
}
const (
deterministicReflexAuditInterval = 10 * time.Minute
deterministicReflexAuditSample = uint32(16)
)
// shouldAuditDeterministicReflex samples stable reflexes in a time bucket.
// This is observability, not a correctness gate: a failed audit leaves the
// reflex usable, while a successful disagreement can replace it with the
// cognitive candidate and becomes durable evidence through the normal
// candidate store.
func shouldAuditDeterministicReflex(result interactiondomain.SemanticResult, utteranceID string, now time.Time) bool {
if shouldRequestSemanticInterpretation(result) || utteranceID == "" {
return false
}
bucket := now.Unix() / int64(deterministicReflexAuditInterval/time.Second)
hash := fnv.New32a()
_, _ = hash.Write([]byte(utteranceID))
_, _ = hash.Write([]byte(fmt.Sprintf(":%d", bucket)))
return hash.Sum32()%deterministicReflexAuditSample == 0
}
func isSimpleLocalQuestion(result interactiondomain.SemanticResult) bool {
if result.Interpretation.SpeechAct != interaction.SpeechActQuestion || result.Interpretation.Clarification == interaction.ClarificationRequired {
return false
}
for _, hint := range result.CapabilityHints {
switch hint {
case "MODIFY_CODE", "RUN_TESTS", "SEND_EMAIL", "SCHEDULE_REMINDER", "READ_DATABASE", "QUERY_LOGS":
return false
}
}
return true
}
func buildGatewayInterpretationRequest(session identity.CreatorSession, utterance interaction.HumanUtterance, semantic interactiondomain.SemanticResult, projectNames []string) cognitive.GatewayInterpretationRequest {
requestID, _ := ids.New()
return cognitive.GatewayInterpretationRequest{
Version: cognitive.CreatorAdvisoryV1,
RequestID: cognitive.RequestID(requestID),
ActorID: session.ActorID,
PrincipalID: session.PrincipalID,
ScopeID: authority.ScopeID("domovoy-global-scope"),
Message: utterance.Content,
Locale: "ru",
MaxOutputBytes: 4096,
DataPolicy: capability.DataPolicyLocalOnly,
DeterministicSpeechAct: string(semantic.Interpretation.SpeechAct),
DeterministicSecondary: string(semantic.Interpretation.SecondarySpeechAct),
DeterministicConfidence: semantic.Interpretation.ConfidenceMillis,
DeterministicApplicability: cognitive.InterpretationApplicability(semantic.Interpretation.Applicability),
RequestedOutcome: semantic.Interpretation.RequestedOutcome,
ProjectNames: projectNames,
}
}
func interactionCandidateFromGateway(utteranceID interaction.HumanUtteranceID, candidate cognitive.GatewayInterpretationCandidate, now interface{ UnixMilli() int64 }) (interaction.SemanticCandidate, error) {
id, err := ids.New()
if err != nil {
return interaction.SemanticCandidate{}, err
}
return interaction.SemanticCandidate{
ID: interaction.SemanticCandidateID(id),
UtteranceID: utteranceID,
Source: interaction.CandidateSourceCognitive,
SelectionState: interaction.CandidateSelected,
PrimarySpeechAct: interaction.SpeechAct(candidate.PrimarySpeechAct),
SecondarySpeechAct: interaction.SpeechAct(candidate.SecondarySpeechAct),
ConfidenceMillis: candidate.ConfidenceMillis,
Applicability: interaction.SemanticApplicability(candidate.Applicability),
RequestedOutcome: candidate.RequestedOutcome,
Clarification: interaction.ClarificationState(candidate.Clarification),
NeedsCognition: candidate.NeedsCognition,
SelectionReason: candidate.SelectionReason,
CapabilityHints: capabilityHintStrings(candidate.CapabilityHints),
RecordedAtUnixMillis: now.UnixMilli(),
}, nil
}
func capabilityHintStrings(values []capability.CapabilityKind) []string {
result := make([]string, 0, len(values))
for _, value := range values {
if value == "" {
continue
}
result = append(result, string(value))
}
return result
}