97 lines
4.9 KiB
Go
97 lines
4.9 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.cin.su/m/Assistant/contracts/capability"
|
|
"git.cin.su/m/Assistant/contracts/cognitive"
|
|
)
|
|
|
|
func (g cognitiveGateway) taskEvaluationHandler(w http.ResponseWriter, r *http.Request) {
|
|
var request capability.TaskEvaluationExecutionRequest
|
|
if !decodeJSON(w, r, 256<<10, &request) {
|
|
return
|
|
}
|
|
if err := request.Profile.Validate(); err != nil || request.CapabilityID == "" || request.CapabilityID != request.Profile.CapabilityID || request.BindingID == "" || request.BindingRevision < 1 || request.ActorID == "" || request.PrincipalID == "" || request.ScopeID == "" || request.MaxOutputBytes < 1 || len(request.Cases) != len(request.Profile.RequiredCaseIDs) {
|
|
http.Error(w, "invalid task evaluation execution request", http.StatusBadRequest)
|
|
return
|
|
}
|
|
resolver, ok := g.resolver.(candidateBindingResolver)
|
|
if !ok {
|
|
http.Error(w, "exact candidate resolver unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
selected, err := resolver.ResolveCandidate(r.Context(), request.BindingID, request.BindingRevision)
|
|
if err != nil || selected.Binding.ID != request.BindingID || selected.Binding.Revision != request.BindingRevision || selected.Binding.CapabilityID != request.CapabilityID || selected.Binding.OwnerPrincipalID != request.PrincipalID {
|
|
http.Error(w, "cognitive binding snapshot unavailable", http.StatusConflict)
|
|
return
|
|
}
|
|
if selected.Binding.DataPolicy != capability.DataPolicyLocalOnly && !request.EgressAcknowledged {
|
|
http.Error(w, "external egress acknowledgement required", http.StatusBadRequest)
|
|
return
|
|
}
|
|
provider := g.provider(selected)
|
|
if provider == nil {
|
|
http.Error(w, "cognitive provider unavailable", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
caseSet := make(map[string]struct{}, len(request.Profile.RequiredCaseIDs))
|
|
for _, id := range request.Profile.RequiredCaseIDs {
|
|
caseSet[id] = struct{}{}
|
|
}
|
|
observations := make([]capability.TaskCaseObservation, len(request.Cases))
|
|
responses := make([]capability.TaskCaseResponse, len(request.Cases))
|
|
var wg sync.WaitGroup
|
|
seenCases := make(map[string]struct{}, len(request.Cases))
|
|
for i, item := range request.Cases {
|
|
if item.CaseID == "" || item.Prompt == "" || item.EvidenceArtifactRef == "" {
|
|
http.Error(w, "invalid task evaluation case", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if _, exists := caseSet[item.CaseID]; !exists {
|
|
http.Error(w, "task evaluation case is not in profile", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if _, exists := seenCases[item.CaseID]; exists {
|
|
http.Error(w, "duplicate task evaluation case", http.StatusBadRequest)
|
|
return
|
|
}
|
|
seenCases[item.CaseID] = struct{}{}
|
|
wg.Add(1)
|
|
go func(index int, item capability.TaskEvaluationCase) {
|
|
defer wg.Done()
|
|
started := time.Now()
|
|
invocation := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("task-evaluation:%s:%s", request.Profile.ID, item.CaseID)), ActorID: request.ActorID, PrincipalID: request.PrincipalID, ScopeID: request.ScopeID, Message: item.Prompt, MaxOutputBytes: int(request.MaxOutputBytes), ProviderBindingID: selected.Binding.ID, ProviderBindingRevision: selected.Binding.Revision, ProviderInstanceID: selected.Provider.ID, ModelDescriptorID: selected.Model.ID, ProviderModelID: selected.Model.ProviderModelID}
|
|
response, invokeErr := g.invokeAdmitted(r.Context(), string(invocation.RequestID), provider, invocation, int(request.MaxOutputBytes))
|
|
observation := capability.TaskCaseObservation{CaseID: item.CaseID, LatencyMicros: time.Since(started).Microseconds(), EvidenceArtifactRef: item.EvidenceArtifactRef}
|
|
responses[index] = capability.TaskCaseResponse{CaseID: item.CaseID, ResponseText: response.AdvisoryText, LatencyMicros: observation.LatencyMicros, EvidenceArtifactRef: item.EvidenceArtifactRef}
|
|
if invokeErr == nil && response.FinishReason == cognitive.FinishCompleted && response.AdvisoryText != "" {
|
|
// Invocation success proves reachability only. Without an
|
|
// independent verifier and answer evidence, semantic support and
|
|
// quality must remain UNKNOWN rather than becoming false evidence.
|
|
observation.Result = capability.EvaluationUnknown
|
|
observation.QualityScoreMillis = 0
|
|
} else {
|
|
observation.Result = capability.EvaluationUnknown
|
|
observation.CriticalFailure = invokeErr != nil
|
|
}
|
|
observations[index] = observation
|
|
}(i, item)
|
|
}
|
|
if len(seenCases) != len(caseSet) {
|
|
http.Error(w, "task evaluation case set is incomplete", http.StatusBadRequest)
|
|
return
|
|
}
|
|
wg.Wait()
|
|
writeTaskEvaluationJSON(w, capability.TaskEvaluationExecutionResponse{ProfileID: request.Profile.ID, BindingID: selected.Binding.ID, BindingRevision: selected.Binding.Revision, Observations: observations, Responses: responses, ObservedAtUnixMillis: time.Now().UnixMilli()})
|
|
}
|
|
|
|
func writeTaskEvaluationJSON(w http.ResponseWriter, value any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(value)
|
|
}
|