Files
m d0ede46352
CI / test (push) Successful in 1m14s
CI / test (pull_request) Successful in 1m15s
feat(capability): persist model qualification observations
2026-08-23 19:21:04 +03:00

168 lines
7.0 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"slices"
"sync"
"time"
"git.cin.su/m/Assistant/adapters/models"
"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/identity"
"git.cin.su/m/Assistant/platform/ids"
)
type qualificationObserver interface {
ObserveQualification(context.Context, capability.ModelQualificationObservation) error
}
type httpQualificationObserver struct{ resolver *httpBindingResolver }
func (o httpQualificationObserver) ObserveQualification(ctx context.Context, value capability.ModelQualificationObservation) error {
if o.resolver == nil {
return fmt.Errorf("qualification observer resolver is nil")
}
return o.resolver.post(ctx, "/v1/cognitive-registry/model-qualification-observations", value, nil)
}
type modelQualificationRequest struct {
Prompt string `json:"prompt"`
MaxOutputBytes int `json:"max_output_bytes"`
ModelIDs []capability.ProviderModelID `json:"model_ids,omitempty"`
}
type modelQualificationResult struct {
ProviderModelID capability.ProviderModelID `json:"provider_model_id"`
Status string `json:"status"`
FinishReason cognitive.FinishReason `json:"finish_reason,omitempty"`
LatencyMillis int64 `json:"latency_millis"`
OutputObserved bool `json:"output_observed"`
QualityAssessed bool `json:"quality_assessed"`
Category capability.ModelRatingCategory `json:"category"`
Error string `json:"error,omitempty"`
}
type modelQualificationResponse struct {
Provider capability.ProviderInstanceID `json:"provider"`
ObservedAtUnixMillis int64 `json:"observed_at_unix_millis"`
Results []modelQualificationResult `json:"results"`
}
func modelQualificationHandler(providers map[string]providerRegistration, owner string, observers ...qualificationObserver) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
name := r.PathValue("provider")
registration, ok := providers[name]
if !ok || registration.Discovery == nil || registration.Provider == nil {
http.Error(w, "provider qualification not found", http.StatusNotFound)
return
}
var request modelQualificationRequest
if !decodeJSON(w, r, 16<<10, &request) || request.Prompt == "" {
return
}
if request.MaxOutputBytes < 1 {
request.MaxOutputBytes = 512
}
if request.MaxOutputBytes > 4096 {
http.Error(w, "max_output_bytes exceeds qualification bound", http.StatusBadRequest)
return
}
modelsFound, probe := registration.Discovery.Discover(r.Context())
if probe.Health != capability.HealthAvailable || len(modelsFound) == 0 {
http.Error(w, "provider unavailable for qualification", http.StatusServiceUnavailable)
return
}
if len(request.ModelIDs) > 0 {
modelsFound = slices.DeleteFunc(modelsFound, func(model models.DiscoveredModel) bool {
return !slices.Contains(request.ModelIDs, model.ProviderModelID)
})
}
if len(modelsFound) == 0 {
http.Error(w, "requested model not discovered", http.StatusNotFound)
return
}
if len(modelsFound) > 16 {
modelsFound = modelsFound[:16]
}
results := qualifyModels(r.Context(), registration, owner, request.Prompt, request.MaxOutputBytes, modelsFound)
if len(observers) > 0 && observers[0] != nil {
if err := persistQualificationResults(r.Context(), observers[0], registration, results); err != nil {
http.Error(w, "persist model qualification observation", http.StatusBadGateway)
return
}
}
w.Header().Set("Content-Type", "application/json")
_ = jsonEncode(w, modelQualificationResponse{Provider: registration.ProviderInstanceID, ObservedAtUnixMillis: time.Now().UnixMilli(), Results: results})
})
}
func qualifyModels(ctx context.Context, registration providerRegistration, owner, prompt string, maxOutputBytes int, discovered []models.DiscoveredModel) []modelQualificationResult {
results := make([]modelQualificationResult, len(discovered))
var wg sync.WaitGroup
for index, model := range discovered {
wg.Add(1)
go func(index int, model models.DiscoveredModel) {
defer wg.Done()
started := time.Now()
request := cognitive.AdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitive.RequestID(fmt.Sprintf("qualification:%s:%d", model.ProviderModelID, started.UnixNano())), ActorID: identity.ActorID("qualification-runtime"), PrincipalID: identity.PrincipalID(owner), ScopeID: authority.ScopeID("qualification-scope"), Message: prompt, Locale: "en-US", MaxOutputBytes: maxOutputBytes, ProviderBindingID: "qualification-binding-v1", ProviderBindingRevision: 1, ProviderInstanceID: registration.ProviderInstanceID, ModelDescriptorID: capability.ModelDescriptorID("qualification-" + string(model.ProviderModelID)), ProviderModelID: model.ProviderModelID}
response, err := registration.Provider.Advise(ctx, request)
result := modelQualificationResult{ProviderModelID: model.ProviderModelID, LatencyMillis: time.Since(started).Milliseconds(), QualityAssessed: false, Category: capability.ModelRatingUnrated}
if err != nil {
result.Status = "UNAVAILABLE"
result.FinishReason = cognitive.FinishProviderUnavailable
result.Error = err.Error()
results[index] = result
return
}
result.FinishReason = response.FinishReason
result.OutputObserved = response.AdvisoryText != ""
if response.FinishReason == cognitive.FinishCompleted && result.OutputObserved {
result.Status = "REACHABLE"
} else {
result.Status = "RESPONDED_WITHOUT_QUALIFICATION"
}
results[index] = result
}(index, model)
}
wg.Wait()
return results
}
func persistQualificationResults(ctx context.Context, observer qualificationObserver, registration providerRegistration, results []modelQualificationResult) error {
for _, result := range results {
observationID, err := ids.New()
if err != nil {
return err
}
status := capability.ModelQualificationRespondedWithoutRating
switch result.Status {
case "REACHABLE":
status = capability.ModelQualificationReachable
case "UNAVAILABLE":
status = capability.ModelQualificationUnavailable
}
observation := capability.ModelQualificationObservation{
ID: capability.ModelQualificationObservationID(observationID), ProviderInstanceID: registration.ProviderInstanceID, ProviderModelID: result.ProviderModelID,
Status: status, FinishReason: string(result.FinishReason), LatencyMillis: result.LatencyMillis,
OutputObserved: result.OutputObserved, QualityAssessed: false, Category: capability.ModelRatingUnrated,
ObservedAtUnixMillis: time.Now().UnixMilli(),
}
if err := observation.Validate(); err != nil {
return err
}
if err := observer.ObserveQualification(ctx, observation); err != nil {
return err
}
}
return nil
}
func jsonEncode(w http.ResponseWriter, value any) error {
return json.NewEncoder(w).Encode(value)
}