528 lines
27 KiB
Go
528 lines
27 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/epistemics"
|
|
"git.cin.su/m/Assistant/contracts/identity"
|
|
"git.cin.su/m/Assistant/contracts/interaction"
|
|
epistemicsdomain "git.cin.su/m/Assistant/modules/epistemics/domain"
|
|
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
|
|
}
|
|
|
|
// compileInteractionContext is the Core-side handoff from the existing
|
|
// Interaction retrieval projection to the existing Context Compiler. The
|
|
// compiler receives only already-authorized, typed fragments and applies its
|
|
// scope and byte-budget guards before the cognitive gateway sees them.
|
|
func compileInteractionContext(projection interactiondomain.ContextProjection, focusProjectID string) (string, error) {
|
|
return compileInteractionContextWithExperience(projection, focusProjectID, nil, "")
|
|
}
|
|
|
|
func compileInteractionContextWithExperience(projection interactiondomain.ContextProjection, focusProjectID string, experiences []epistemics.ExperienceRecord, query string) (string, error) {
|
|
if len(projection.Fragments) == 0 && len(experiences) == 0 {
|
|
return projection.Text, nil
|
|
}
|
|
candidates := interactionContextCandidates(projection, focusProjectID, experiences, query)
|
|
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{Candidates: candidates, DataPolicy: capability.DataPolicyLocalOnly, MaxBytes: 2048, ContextScopeRef: focusProjectID, FreshnessRequirement: "conversation-current", RiskClass: "low"})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
parts := make([]string, 0, len(bundle.Excerpts))
|
|
for _, excerpt := range bundle.Excerpts {
|
|
parts = append(parts, excerpt.Text)
|
|
}
|
|
return strings.Join(parts, "\n"), nil
|
|
}
|
|
|
|
func interactionContextCandidates(projection interactiondomain.ContextProjection, focusProjectID string, experiences []epistemics.ExperienceRecord, query string) []cognitive.ContextCandidate {
|
|
candidates := make([]cognitive.ContextCandidate, 0, len(projection.Fragments)+len(experiences))
|
|
for _, fragment := range projection.Fragments {
|
|
candidates = append(candidates, cognitive.ContextCandidate{
|
|
Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("interaction:" + string(fragment.UtteranceID)), SourceKind: "interaction-gen2-retrieval", SourceID: "interaction:utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: fragment.Text},
|
|
Relevance: fragment.Relevance,
|
|
Lane: "current-fact",
|
|
ContextScopeRef: fragment.ScopeRef,
|
|
FreshnessState: "CURRENT",
|
|
})
|
|
}
|
|
if needsHistoricalAnalogy(query) {
|
|
for _, fragment := range projection.Historical {
|
|
candidates = append(candidates, cognitive.ContextCandidate{
|
|
Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("interaction:historical:" + string(fragment.UtteranceID)), SourceKind: "interaction-gen2-retrieval", SourceID: "interaction:utterances", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: fragment.Text},
|
|
Relevance: fragment.Relevance - 1, Lane: "historical-analogy", ContextScopeRef: fragment.ScopeRef, FreshnessState: "HISTORICAL_ONLY",
|
|
})
|
|
}
|
|
}
|
|
retrieved := epistemicsdomain.RetrieveExperience(experiences, "", focusProjectID, query, 4)
|
|
for _, record := range retrieved.Positive {
|
|
candidates = append(candidates, experienceContextCandidate(record, "positive-experience", false))
|
|
}
|
|
for _, record := range retrieved.Negative {
|
|
candidates = append(candidates, experienceContextCandidate(record, "negative-experience", true))
|
|
}
|
|
for _, record := range retrieved.Contradictions {
|
|
candidates = append(candidates, experienceContextCandidate(record, "contradiction", true))
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
func needsHistoricalAnalogy(query string) bool {
|
|
for _, marker := range []string{"истор", "раньше", "предыдущ", "старое", "аналог"} {
|
|
if strings.Contains(strings.ToLower(query), marker) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func experienceContextCandidate(record epistemics.ExperienceRecord, lane string, negative bool) cognitive.ContextCandidate {
|
|
text := strings.TrimSpace(record.ProblemStatement + ": " + record.OutcomeDetail)
|
|
if detail := strings.TrimSpace(record.WhySucceededOrFailed); detail != "" {
|
|
text += " (" + detail + ")"
|
|
}
|
|
if len(record.ApplicabilityConditions) > 0 {
|
|
text += " [conditions: " + strings.Join(record.ApplicabilityConditions, ", ") + "]"
|
|
}
|
|
return cognitive.ContextCandidate{Excerpt: cognitive.ContextExcerpt{ID: cognitive.ContextExcerptID("experience:" + string(record.ID)), SourceKind: "epistemics-experience", SourceID: "epistemics:experience-records", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: text}, Relevance: 1, Lane: lane, ContextScopeRef: record.ContextScopeRef, FreshnessState: "CURRENT", Negative: negative}
|
|
}
|
|
|
|
const (
|
|
iterativeRetrievalMaxRounds = 1
|
|
iterativeRetrievalMaxFragments = 2
|
|
iterativeRetrievalMaxAddedBytes = 1024
|
|
iterativeRetrievalMaxLatency = 1500 * time.Millisecond
|
|
)
|
|
|
|
func newIterativeContextRetriever(history []interaction.HumanUtterance, references []interaction.ProjectReference, principalID, focusProjectID string, experiences []epistemics.ExperienceRecord) iterativeContextRetriever {
|
|
seen := map[string]struct{}{}
|
|
return func(ctx context.Context, request cognitive.IterativeRetrievalRequest) cognitive.IterativeRetrievalResponse {
|
|
started := time.Now()
|
|
if request.Pass < 0 || request.Pass >= iterativeRetrievalMaxRounds || request.RemainingBytes <= 0 || request.RemainingBytes > iterativeRetrievalMaxAddedBytes {
|
|
return cognitive.IterativeRetrievalResponse{Exhausted: true, Reason: "budget"}
|
|
}
|
|
projection := interactiondomain.BuildScopedGen2ContextForQuery(history, references, principalID, focusProjectID, request.Query)
|
|
candidates := interactionContextCandidates(projection, focusProjectID, experiences, request.Query)
|
|
filtered := make([]cognitive.ContextCandidate, 0, len(candidates))
|
|
for _, candidate := range candidates {
|
|
if !iterativeLaneMatches(request.Lane, candidate.Lane) {
|
|
continue
|
|
}
|
|
key := string(request.Lane) + ":" + string(candidate.Excerpt.ID)
|
|
if _, duplicate := seen[key]; duplicate {
|
|
continue
|
|
}
|
|
filtered = append(filtered, candidate)
|
|
}
|
|
bundle, err := cognitive.CompileContext(cognitive.ContextCompileInput{Candidates: filtered, DataPolicy: capability.DataPolicyLocalOnly, MaxBytes: request.RemainingBytes, ContextScopeRef: focusProjectID, TaskRef: "chat", FocusFrameRef: focusProjectID, FreshnessRequirement: "conversation-current"})
|
|
if err != nil || len(bundle.Excerpts) == 0 {
|
|
return cognitive.IterativeRetrievalResponse{Exhausted: true, Reason: "no_additional_evidence"}
|
|
}
|
|
out := make([]cognitive.ContextCandidate, 0, iterativeRetrievalMaxFragments)
|
|
for _, excerpt := range bundle.Excerpts {
|
|
if len(out) == iterativeRetrievalMaxFragments || time.Since(started) > iterativeRetrievalMaxLatency {
|
|
break
|
|
}
|
|
seen[string(request.Lane)+":"+string(excerpt.ID)] = struct{}{}
|
|
for _, candidate := range filtered {
|
|
if candidate.Excerpt.ID == excerpt.ID {
|
|
out = append(out, candidate)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(out) == 0 {
|
|
return cognitive.IterativeRetrievalResponse{Exhausted: true, Reason: "latency_budget"}
|
|
}
|
|
addedBytes := 0
|
|
for _, candidate := range out {
|
|
addedBytes += len([]byte(candidate.Excerpt.Text))
|
|
}
|
|
return cognitive.IterativeRetrievalResponse{Candidates: out, Exhausted: true, Reason: "completed", AddedBytes: addedBytes}
|
|
}
|
|
}
|
|
|
|
func iterativeLaneMatches(request cognitive.IterativeRetrievalLane, actual string) bool {
|
|
return (request == cognitive.IterativeRetrievalCurrentFact && actual == "current-fact") ||
|
|
(request == cognitive.IterativeRetrievalPositiveExperience && actual == "positive-experience") ||
|
|
(request == cognitive.IterativeRetrievalNegativeExperience && actual == "negative-experience") ||
|
|
(request == cognitive.IterativeRetrievalContradiction && actual == "contradiction") ||
|
|
(request == cognitive.IterativeRetrievalHistoricalAnalogy && actual == "historical-analogy")
|
|
}
|
|
|
|
const (
|
|
// Household interpretation and response review are intentionally compact
|
|
// calls. Keeping their output bounded prevents a loaded local model from
|
|
// spending the whole request budget generating unnecessary prose.
|
|
responseReviewMaxOutputBytes = 512
|
|
stableKnowledgeMaxOutputBytes = 512
|
|
semanticInterpretMaxOutputBytes = 1024
|
|
)
|
|
|
|
// 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: responseReviewMaxOutputBytes, 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.
|
|
type iterativeContextRetriever func(context.Context, cognitive.IterativeRetrievalRequest) cognitive.IterativeRetrievalResponse
|
|
|
|
func (c cognitiveInterpretationClient) AnswerStableKnowledge(ctx context.Context, session identity.CreatorSession, userMessage, personalMemory string, meaning cognitive.TaskMeaning) (string, error) {
|
|
return c.AnswerStableKnowledgeWithContinuation(ctx, session, userMessage, personalMemory, meaning, nil)
|
|
}
|
|
|
|
// AnswerStableKnowledgeWithContinuation permits one model-requested, Core-run
|
|
// retrieval round. The callback receives only a typed intent and cannot alter
|
|
// identity, scope or provider selection.
|
|
func (c cognitiveInterpretationClient) AnswerStableKnowledgeWithContinuation(ctx context.Context, session identity.CreatorSession, userMessage, personalMemory string, meaning cognitive.TaskMeaning, retrieve iterativeContextRetriever) (string, error) {
|
|
requestID, err := ids.New()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
prompt := "Ты отвечаешь как Домовой на обычное сообщение пользователя. Сначала пойми цель: на вопрос дай короткий естественный ответ по устойчивому общеизвестному знанию, а безопасную просьбу создать текстовый или структурный результат выполни прямо в ответе. Начинай сразу с содержания и не представляйся, если пользователь не спросил об имени. Не называй модель или внутреннюю систему. Если истинность ответа зависит от текущего наблюдения, внешнего источника или инструмента, верни ровно NEEDS_VERIFIED_RESEARCH. Не придумывай факты и не утверждай выполнение внешнего действия.\n\nСообщение пользователя:\n" + userMessage
|
|
if retrieve != nil {
|
|
prompt += "\n\nЕсли именно для ответа не хватает ограниченного контекста, вместо ответа верни единственную строку <domovoy-retrieval>{\"Query\":\"...\",\"SemanticSubject\":\"...\",\"Lane\":\"CURRENT_FACT|POSITIVE_EXPERIENCE|NEGATIVE_EXPERIENCE|CONTRADICTION|HISTORICAL_ANALOGY\",\"Reason\":\"...\"}. Не запрашивай широкий дамп памяти."
|
|
}
|
|
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})
|
|
}
|
|
requestValue := 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: stableKnowledgeMaxOutputBytes, DataPolicy: capability.DataPolicyLocalOnly,
|
|
AllowIterativeRetrieval: retrieve != nil,
|
|
Meaning: cognitive.TaskMeaning{SpeechAct: "STABLE_KNOWLEDGE_ANSWER", CapabilityHints: meaning.CapabilityHints, RequestedOutcome: meaning.RequestedOutcome, InformationRequestKind: meaning.InformationRequestKind, ConfidenceMillis: meaning.ConfidenceMillis},
|
|
}
|
|
result, err := c.requestAdvisory(ctx, requestValue)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if result.IterativeRetrieval != nil && retrieve != nil {
|
|
additional := retrieve(ctx, *result.IterativeRetrieval)
|
|
for _, candidate := range additional.Candidates {
|
|
context = append(context, candidate.Excerpt)
|
|
}
|
|
if len(additional.Candidates) == 0 {
|
|
context = append(context, cognitive.ContextExcerpt{ID: "continuation:no-additional-evidence", SourceKind: "iterative-retrieval", SourceID: "core", Revision: 1, Sensitivity: cognitive.SensitivityPrivateAllowed, Text: "No additional evidence was found; answer with uncertainty rather than inventing facts."})
|
|
}
|
|
requestValue.Context = context
|
|
requestValue.AllowIterativeRetrieval = false
|
|
result, err = c.requestAdvisory(ctx, requestValue)
|
|
if 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 (c cognitiveInterpretationClient) requestAdvisory(ctx context.Context, value cognitive.GatewayAdvisoryRequest) (cognitive.GatewayAdvisoryResponse, error) {
|
|
target := *c.baseURL
|
|
target.Path = "/v1/advisories"
|
|
payload, err := json.Marshal(value)
|
|
if err != nil {
|
|
return cognitive.GatewayAdvisoryResponse{}, err
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, target.String(), bytes.NewReader(payload))
|
|
if err != nil {
|
|
return cognitive.GatewayAdvisoryResponse{}, err
|
|
}
|
|
request.Header.Set("Content-Type", "application/json")
|
|
response, err := c.client.Do(request)
|
|
if err != nil {
|
|
return cognitive.GatewayAdvisoryResponse{}, err
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode != http.StatusOK {
|
|
return cognitive.GatewayAdvisoryResponse{}, fmt.Errorf("cognitive stable knowledge status %d", response.StatusCode)
|
|
}
|
|
var result cognitive.GatewayAdvisoryResponse
|
|
if err := json.NewDecoder(response.Body).Decode(&result); err != nil {
|
|
return cognitive.GatewayAdvisoryResponse{}, err
|
|
}
|
|
return result, 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: semanticInterpretMaxOutputBytes,
|
|
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
|
|
}
|