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Если именно для ответа не хватает ограниченного контекста, вместо ответа верни единственную строку {\"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 }