216 lines
11 KiB
Go
216 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"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),
|
|
Objective: selectionObjectiveForInterpretation(request),
|
|
}
|
|
selected, err := g.resolver.Resolve(r.Context(), requirement)
|
|
if err != nil {
|
|
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
|
|
return
|
|
}
|
|
provider := g.provider(selected)
|
|
if provider == nil {
|
|
writeInterpretationResponse(w, g.interpretWithBuiltin(r.Context(), request, maxOutputBytes))
|
|
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 (g cognitiveGateway) interpretWithBuiltin(ctx context.Context, request cognitive.GatewayInterpretationRequest, maxOutputBytes int) cognitive.GatewayInterpretationResponse {
|
|
if g.builtinLocal == nil || g.builtinLocal.Provider == nil {
|
|
return deterministicInterpretationResponse(request)
|
|
}
|
|
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: "builtin-local-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: "builtin-local-lifeboat-v1", ModelDescriptorID: "builtin-local-model-v1", ProviderModelID: "domovoy-lifeboat"}
|
|
started := g.now()
|
|
response, err := g.builtinLocal.Provider.Advise(ctx, invocation)
|
|
if response.RequestID == "" {
|
|
response.RequestID = request.RequestID
|
|
}
|
|
if response.Version == "" {
|
|
response.Version = request.Version
|
|
}
|
|
if err != nil {
|
|
response = cognitive.AdvisoryResponse{Version: request.Version, RequestID: request.RequestID, ProviderBindingID: "builtin-local-binding-v1", ProviderBindingRevision: 1, FinishReason: cognitive.FinishInvalidProviderResponse}
|
|
}
|
|
g.observeBuiltin(ctx, g.builtinLocal, response, started)
|
|
if err != nil || response.FinishReason != cognitive.FinishCompleted {
|
|
return deterministicInterpretationResponse(request)
|
|
}
|
|
candidate, ok := parseInterpretationCandidate(response.AdvisoryText)
|
|
if !ok {
|
|
return deterministicInterpretationResponse(request)
|
|
}
|
|
return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted, Usage: response.Usage}
|
|
}
|
|
|
|
// deterministicInterpretationResponse keeps the semantic route alive when the
|
|
// local model cannot produce strict JSON in time. It does not invent meaning:
|
|
// it carries forward the already-recorded deterministic interpretation and
|
|
// leaves the advisory answer to the built-in gateway path.
|
|
func deterministicInterpretationResponse(request cognitive.GatewayInterpretationRequest) cognitive.GatewayInterpretationResponse {
|
|
speechAct := request.DeterministicSpeechAct
|
|
if !validSpeechAct(speechAct) {
|
|
speechAct = "UNKNOWN"
|
|
}
|
|
applicability := request.DeterministicApplicability
|
|
switch applicability {
|
|
case cognitive.InterpretationApplicable, cognitive.InterpretationLowConfidence, cognitive.InterpretationMixedIntent, cognitive.InterpretationNeedsBroader, cognitive.InterpretationClarificationRequired:
|
|
default:
|
|
applicability = cognitive.InterpretationLowConfidence
|
|
}
|
|
confidence := request.DeterministicConfidence
|
|
if confidence < 0 {
|
|
confidence = 0
|
|
}
|
|
if confidence > 1000 {
|
|
confidence = 1000
|
|
}
|
|
candidate := cognitive.GatewayInterpretationCandidate{
|
|
PrimarySpeechAct: speechAct,
|
|
SecondarySpeechAct: request.DeterministicSecondary,
|
|
ConfidenceMillis: confidence,
|
|
Applicability: applicability,
|
|
RequestedOutcome: request.RequestedOutcome,
|
|
NeedsCognition: true,
|
|
Clarification: "NOT_REQUIRED",
|
|
SelectionReason: "deterministic interpretation retained; built-in structured interpretation was unavailable",
|
|
}
|
|
if applicability == cognitive.InterpretationClarificationRequired {
|
|
candidate.NeedsCognition = false
|
|
candidate.Clarification = "REQUIRED"
|
|
}
|
|
return cognitive.GatewayInterpretationResponse{Version: request.Version, RequestID: request.RequestID, Candidate: candidate, FinishReason: cognitive.FinishCompleted}
|
|
}
|
|
|
|
func selectionObjectiveForInterpretation(request cognitive.GatewayInterpretationRequest) capability.SelectionObjective {
|
|
if request.RequestedOutcome != "" || request.DeterministicApplicability == cognitive.InterpretationNeedsBroader || request.DeterministicApplicability == cognitive.InterpretationMixedIntent || request.DeterministicApplicability == cognitive.InterpretationLowConfidence {
|
|
return capability.SelectionQuality
|
|
}
|
|
if request.DeterministicConfidence >= 800 && request.DeterministicSpeechAct != "QUESTION" {
|
|
return capability.SelectionSpeed
|
|
}
|
|
return capability.SelectionBalanced
|
|
}
|
|
|
|
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)
|
|
}
|