Files
Assistant/cmd/cognitive-worker/interpretation.go
m ce666bbc43
CI / test (push) Successful in 18s
CI / test (pull_request) Successful in 17s
DEV-000072 semantic interpretation deferral boundary
2026-08-17 23:02:00 +03:00

138 lines
6.9 KiB
Go

package main
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
)
func (g cognitiveGateway) interpretationHandler(w http.ResponseWriter, r *http.Request) {
var request cognitive.GatewayInterpretationRequest
if !decodeJSON(w, r, 80<<10, &request) {
return
}
if request.Version != cognitive.CreatorAdvisoryV1 || request.RequestID == "" || request.ActorID == "" || request.PrincipalID == "" || request.ScopeID == "" || strings.TrimSpace(request.Message) == "" || request.MaxOutputBytes < 1 {
http.Error(w, "invalid interpretation request", http.StatusBadRequest)
return
}
maxOutputBytes := request.MaxOutputBytes
if g.allowCanary && g.canaryMaxOutputBytes > 0 && maxOutputBytes > g.canaryMaxOutputBytes {
maxOutputBytes = g.canaryMaxOutputBytes
}
requirement := capability.CognitiveRequirement{
CapabilityID: creatorAdvisoryCapabilityID,
OwnerPrincipalID: request.PrincipalID,
DataPolicy: request.DataPolicy,
RequiredModalities: []capability.ModelModality{capability.ModalityText},
RequiredCapabilities: []capability.ModelCapability{capability.CapabilityAdvisory},
AllowCanary: g.allowCanary,
InputBytes: int64(len(request.Message)),
MaxOutputBytes: int64(maxOutputBytes),
}
selected, err := g.resolver.Resolve(r.Context(), requirement)
if err != nil {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
return
}
provider := g.provider(selected)
if provider == nil {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishProviderUnavailable})
return
}
invocation := cognitive.AdvisoryRequest{
Version: request.Version,
RequestID: request.RequestID,
ActorID: request.ActorID,
PrincipalID: request.PrincipalID,
ScopeID: request.ScopeID,
Message: interpretationPrompt(request),
Locale: request.Locale,
MaxOutputBytes: maxOutputBytes,
ProviderBindingID: selected.Binding.ID,
ProviderBindingRevision: selected.Binding.Revision,
ProviderInstanceID: selected.Provider.ID,
ModelDescriptorID: selected.Model.ID,
ProviderModelID: selected.Model.ProviderModelID,
}
started := g.now()
response, invokeErr := provider.Advise(r.Context(), invocation)
if invokeErr != nil {
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, FinishReason: cognitive.FinishInvalidProviderResponse}
}
g.observe(r.Context(), selected, response, started)
if response.FinishReason != cognitive.FinishCompleted {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: response.FinishReason, Usage: response.Usage})
return
}
candidate, ok := parseInterpretationCandidate(response.AdvisoryText)
if !ok {
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishInvalidStructuredOutput, Usage: response.Usage})
return
}
writeInterpretationResponse(w, cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage})
}
func interpretationPrompt(request cognitive.GatewayInterpretationRequest) string {
projects := "none"
if len(request.ProjectNames) > 0 {
projects = strings.Join(request.ProjectNames, ", ")
}
return fmt.Sprintf(`Return only strict JSON with keys primary_speech_act, secondary_speech_act, confidence_millis, applicability, requested_outcome, needs_cognition, clarification, selection_reason, capability_hints.
Allowed speech acts: DIRECTIVE, ASSERTION, DECISION, PREFERENCE, QUESTION, HYPOTHESIS, CORRECTION, REQUEST_TO_INVESTIGATE, CONFIRMATION, REJECTION, CANCELLATION, CONVERSATIONAL, UNKNOWN.
Allowed applicability: APPLICABLE, LOW_CONFIDENCE, MIXED_INTENT, NEEDS_BROADER_INTERPRETATION, CLARIFICATION_REQUIRED.
Allowed clarification: NOT_REQUIRED or REQUIRED.
Capability hints may only contain known capability kinds like COGNITIVE_REASONING, MODIFY_CODE, RUN_TESTS, SEND_EMAIL, SCHEDULE_REMINDER, VERIFY_FACT, INSPECT_PROJECT, READ_DATABASE, QUERY_LOGS.
Interpret the human utterance semantically. Do not claim action execution. Do not treat punctuation alone as decisive. If uncertain, set applicability accordingly.
Message: %q
Deterministic primary: %s
Deterministic secondary: %s
Deterministic confidence: %d
Deterministic applicability: %s
Deterministic requested outcome: %q
Relevant project names: %s`, request.Message, request.DeterministicSpeechAct, request.DeterministicSecondary, request.DeterministicConfidence, request.DeterministicApplicability, request.RequestedOutcome, projects)
}
func parseInterpretationCandidate(text string) (cognitive.GatewayInterpretationCandidate, bool) {
start := strings.Index(text, "{")
end := strings.LastIndex(text, "}")
if start < 0 || end <= start {
return cognitive.GatewayInterpretationCandidate{}, false
}
var candidate cognitive.GatewayInterpretationCandidate
if err := json.Unmarshal([]byte(text[start:end+1]), &candidate); err != nil {
return cognitive.GatewayInterpretationCandidate{}, false
}
if !validSpeechAct(candidate.PrimarySpeechAct) || candidate.ConfidenceMillis < 0 || candidate.ConfidenceMillis > 1000 {
return cognitive.GatewayInterpretationCandidate{}, false
}
if candidate.SecondarySpeechAct != "" && !validSpeechAct(candidate.SecondarySpeechAct) {
return cognitive.GatewayInterpretationCandidate{}, false
}
switch candidate.Applicability {
case cognitive.InterpretationApplicable, cognitive.InterpretationLowConfidence, cognitive.InterpretationMixedIntent, cognitive.InterpretationNeedsBroader, cognitive.InterpretationClarificationRequired:
default:
return cognitive.GatewayInterpretationCandidate{}, false
}
if candidate.Clarification != "NOT_REQUIRED" && candidate.Clarification != "REQUIRED" {
return cognitive.GatewayInterpretationCandidate{}, false
}
return candidate, true
}
func validSpeechAct(value string) bool {
switch value {
case "DIRECTIVE", "ASSERTION", "DECISION", "PREFERENCE", "QUESTION", "HYPOTHESIS", "CORRECTION", "REQUEST_TO_INVESTIGATE", "CONFIRMATION", "REJECTION", "CANCELLATION", "CONVERSATIONAL", "UNKNOWN":
return true
}
return false
}
func writeInterpretationResponse(w http.ResponseWriter, response cognitive.GatewayInterpretationResponse) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(response)
}