Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8f20326c07 | ||
|
|
a82592a130 | ||
|
|
feea909d69 | ||
|
|
b78a026ee1 |
@@ -430,6 +430,14 @@ func (r *CapabilityRepository) SaveCognitiveBinding(ctx context.Context, value c
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID) ([]capability.ResolvedCognitiveBinding, error) {
|
func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID) ([]capability.ResolvedCognitiveBinding, error) {
|
||||||
|
return r.loadCognitiveCandidates(ctx, capabilityID, owner, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CapabilityRepository) LoadCognitiveCandidatesForProfile(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID, profileID capability.TaskEvaluationProfileID) ([]capability.ResolvedCognitiveBinding, error) {
|
||||||
|
return r.loadCognitiveCandidates(ctx, capabilityID, owner, profileID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *CapabilityRepository) loadCognitiveCandidates(ctx context.Context, capabilityID capability.CapabilityID, owner identity.PrincipalID, profileID capability.TaskEvaluationProfileID) ([]capability.ResolvedCognitiveBinding, error) {
|
||||||
rows, err := r.pool.Query(ctx, `SELECT
|
rows, err := r.pool.Query(ctx, `SELECT
|
||||||
b.binding_id,b.revision,b.capability_id,b.owner_principal_id,b.provider_instance_id,b.provider_revision,b.model_descriptor_id,b.model_revision,b.data_policy,b.qualification,b.activation,b.health,b.limit_policy_ref,
|
b.binding_id,b.revision,b.capability_id,b.owner_principal_id,b.provider_instance_id,b.provider_revision,b.model_descriptor_id,b.model_revision,b.data_policy,b.qualification,b.activation,b.health,b.limit_policy_ref,
|
||||||
p.provider_instance_id,p.revision,p.provider_kind,p.endpoint_ref,COALESCE(p.credential_ref,''),p.enabled,p.qualification,COALESCE(ph.health,p.health),GREATEST(p.observed_at_unix_millis,COALESCE(ph.observed_at_unix_millis,0)),p.billing_mode,p.auth_mode,p.input_cost_micros,p.output_cost_micros,p.request_cost_micros,
|
p.provider_instance_id,p.revision,p.provider_kind,p.endpoint_ref,COALESCE(p.credential_ref,''),p.enabled,p.qualification,COALESCE(ph.health,p.health),GREATEST(p.observed_at_unix_millis,COALESCE(ph.observed_at_unix_millis,0)),p.billing_mode,p.auth_mode,p.input_cost_micros,p.output_cost_micros,p.request_cost_micros,
|
||||||
@@ -454,7 +462,7 @@ func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err = r.loadCognitiveEvidence(ctx, &value); err != nil {
|
if err = r.loadCognitiveEvidence(ctx, &value, profileID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
result = append(result, value)
|
result = append(result, value)
|
||||||
@@ -462,7 +470,7 @@ func (r *CapabilityRepository) LoadCognitiveCandidates(ctx context.Context, capa
|
|||||||
return result, rows.Err()
|
return result, rows.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *CapabilityRepository) loadCognitiveEvidence(ctx context.Context, value *capability.ResolvedCognitiveBinding) error {
|
func (r *CapabilityRepository) loadCognitiveEvidence(ctx context.Context, value *capability.ResolvedCognitiveBinding, profileID capability.TaskEvaluationProfileID) error {
|
||||||
var evaluation capability.EvaluatedCapability
|
var evaluation capability.EvaluatedCapability
|
||||||
err := r.pool.QueryRow(ctx, `SELECT evaluation_id,binding_id,binding_revision,capability_id,result,quality_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis FROM capability.cognitive_evaluated_capabilities WHERE binding_id=$1 AND binding_revision=$2 ORDER BY observed_at_unix_millis DESC LIMIT 1`, value.Binding.ID, value.Binding.Revision).Scan(&evaluation.ID, &evaluation.BindingID, &evaluation.BindingRevision, &evaluation.CapabilityID, &evaluation.Result, &evaluation.QualityScoreMillis, &evaluation.CriticalFailureMillis, &evaluation.BenchmarkVersion, &evaluation.ArtifactID, &evaluation.ObservedAtUnixMillis)
|
err := r.pool.QueryRow(ctx, `SELECT evaluation_id,binding_id,binding_revision,capability_id,result,quality_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis FROM capability.cognitive_evaluated_capabilities WHERE binding_id=$1 AND binding_revision=$2 ORDER BY observed_at_unix_millis DESC LIMIT 1`, value.Binding.ID, value.Binding.Revision).Scan(&evaluation.ID, &evaluation.BindingID, &evaluation.BindingRevision, &evaluation.CapabilityID, &evaluation.Result, &evaluation.QualityScoreMillis, &evaluation.CriticalFailureMillis, &evaluation.BenchmarkVersion, &evaluation.ArtifactID, &evaluation.ObservedAtUnixMillis)
|
||||||
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||||
@@ -479,6 +487,16 @@ func (r *CapabilityRepository) loadCognitiveEvidence(ctx context.Context, value
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
value.Performance = &performance
|
value.Performance = &performance
|
||||||
}
|
}
|
||||||
|
if profileID != "" {
|
||||||
|
var taskEvaluation capability.TaskEvaluation
|
||||||
|
err = r.pool.QueryRow(ctx, `SELECT evaluation_id,task_profile_id,binding_id,binding_revision,capability_id,result,quality_score_millis,reliability_score_millis,latency_score_millis,overall_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis FROM capability.cognitive_task_evaluations WHERE task_profile_id=$1 AND binding_id=$2 AND binding_revision=$3 ORDER BY observed_at_unix_millis DESC LIMIT 1`, profileID, value.Binding.ID, value.Binding.Revision).Scan(&taskEvaluation.ID, &taskEvaluation.ProfileID, &taskEvaluation.BindingID, &taskEvaluation.BindingRevision, &taskEvaluation.CapabilityID, &taskEvaluation.Result, &taskEvaluation.QualityScoreMillis, &taskEvaluation.ReliabilityScoreMillis, &taskEvaluation.LatencyScoreMillis, &taskEvaluation.OverallScoreMillis, &taskEvaluation.CriticalFailureMillis, &taskEvaluation.BenchmarkVersion, &taskEvaluation.ArtifactID, &taskEvaluation.ObservedAtUnixMillis)
|
||||||
|
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
value.TaskEvaluation = &taskEvaluation
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -592,3 +610,8 @@ func (r *CapabilityRepository) SavePerformanceEvidence(ctx context.Context, valu
|
|||||||
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_performance_evidence (evidence_id,binding_id,binding_revision,model_descriptor_id,model_revision,hardware_profile,runtime,quantization,context_units,thinking_enabled,generation_tps_millis,prompt_tps_millis,ttft_micros,latency_micros,memory_bytes,reliability_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) ON CONFLICT DO NOTHING`, value.ID, value.BindingID, value.BindingRevision, value.ModelDescriptorID, value.ModelRevision, value.HardwareProfile, value.Runtime, value.Quantization, value.ContextUnits, value.ThinkingEnabled, value.GenerationTPSMillis, value.PromptTPSMillis, value.TTFTMicros, value.LatencyMicros, value.MemoryBytes, value.ReliabilityMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis)
|
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_performance_evidence (evidence_id,binding_id,binding_revision,model_descriptor_id,model_revision,hardware_profile,runtime,quantization,context_units,thinking_enabled,generation_tps_millis,prompt_tps_millis,ttft_micros,latency_micros,memory_bytes,reliability_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19) ON CONFLICT DO NOTHING`, value.ID, value.BindingID, value.BindingRevision, value.ModelDescriptorID, value.ModelRevision, value.HardwareProfile, value.Runtime, value.Quantization, value.ContextUnits, value.ThinkingEnabled, value.GenerationTPSMillis, value.PromptTPSMillis, value.TTFTMicros, value.LatencyMicros, value.MemoryBytes, value.ReliabilityMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *CapabilityRepository) SaveTaskEvaluation(ctx context.Context, value capability.TaskEvaluation) error {
|
||||||
|
_, err := r.pool.Exec(ctx, `INSERT INTO capability.cognitive_task_evaluations (evaluation_id,task_profile_id,binding_id,binding_revision,capability_id,result,quality_score_millis,reliability_score_millis,latency_score_millis,overall_score_millis,critical_failure_millis,benchmark_version,artifact_id,observed_at_unix_millis) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) ON CONFLICT DO NOTHING`, value.ID, value.ProfileID, value.BindingID, value.BindingRevision, value.CapabilityID, value.Result, value.QualityScoreMillis, value.ReliabilityScoreMillis, value.LatencyScoreMillis, value.OverallScoreMillis, value.CriticalFailureMillis, value.BenchmarkVersion, value.ArtifactID, value.ObservedAtUnixMillis)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -842,7 +842,13 @@ func main() {
|
|||||||
http.Error(w, "invalid cognitive requirement", http.StatusBadRequest)
|
http.Error(w, "invalid cognitive requirement", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
candidates, err := capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
|
var candidates []capability.ResolvedCognitiveBinding
|
||||||
|
var err error
|
||||||
|
if requirement.TaskProfileID != "" {
|
||||||
|
candidates, err = capabilityRepository.LoadCognitiveCandidatesForProfile(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID, requirement.TaskProfileID)
|
||||||
|
} else {
|
||||||
|
candidates, err = capabilityRepository.LoadCognitiveCandidates(r.Context(), requirement.CapabilityID, requirement.OwnerPrincipalID)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
|
http.Error(w, "cognitive registry unavailable", http.StatusServiceUnavailable)
|
||||||
return
|
return
|
||||||
@@ -891,6 +897,20 @@ func main() {
|
|||||||
}
|
}
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("POST /v1/cognitive-registry/task-evaluations", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var value capability.TaskEvaluation
|
||||||
|
decoder := json.NewDecoder(io.LimitReader(r.Body, 64<<10))
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
if err := decoder.Decode(&value); err != nil || capabilitydomain.ValidateTaskEvaluation(value) != nil {
|
||||||
|
http.Error(w, "invalid task evaluation", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := capabilityRepository.SaveTaskEvaluation(r.Context(), value); err != nil {
|
||||||
|
http.Error(w, "save task evaluation", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
})
|
||||||
mux.HandleFunc("POST /v1/creator/cognitive-canary", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("POST /v1/creator/cognitive-canary", func(w http.ResponseWriter, r *http.Request) {
|
||||||
session, ok := authenticateCreator(w, r)
|
session, ok := authenticateCreator(w, r)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -42,6 +42,19 @@ func main() {
|
|||||||
}
|
}
|
||||||
writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready})
|
writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready})
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("POST /v1/work-graphs/task-evaluations/schedule", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var request application.TaskEvaluationScheduleRequest
|
||||||
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4<<20)).Decode(&request); err != nil {
|
||||||
|
http.Error(w, "invalid task evaluation graph request", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
graph, ready, err := application.AdmitAndScheduleTaskEvaluation(r.Context(), repository, request)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusUnprocessableEntity)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, map[string]any{"graph": graph, "scheduled_nodes": ready})
|
||||||
|
})
|
||||||
mux.HandleFunc("POST /v1/work-graphs/complete", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("POST /v1/work-graphs/complete", func(w http.ResponseWriter, r *http.Request) {
|
||||||
var request application.GraphCompleteRequest
|
var request application.GraphCompleteRequest
|
||||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&request); err != nil {
|
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 64<<10)).Decode(&request); err != nil {
|
||||||
|
|||||||
@@ -160,28 +160,32 @@ type CognitiveRequirement struct {
|
|||||||
ExcludedBindingIDs []CapabilityBindingID
|
ExcludedBindingIDs []CapabilityBindingID
|
||||||
Objective SelectionObjective
|
Objective SelectionObjective
|
||||||
MaxCostMicros *int64
|
MaxCostMicros *int64
|
||||||
|
TaskProfileID TaskEvaluationProfileID
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolvedCognitiveBinding is a provider-neutral invocation snapshot selected
|
// ResolvedCognitiveBinding is a provider-neutral invocation snapshot selected
|
||||||
// by Capability. It contains references and declared metadata, never secrets.
|
// by Capability. It contains references and declared metadata, never secrets.
|
||||||
type ResolvedCognitiveBinding struct {
|
type ResolvedCognitiveBinding struct {
|
||||||
Binding CognitiveBinding
|
Binding CognitiveBinding
|
||||||
Provider ProviderInstance
|
Provider ProviderInstance
|
||||||
Model ModelDescriptor
|
Model ModelDescriptor
|
||||||
Evaluation *EvaluatedCapability
|
Evaluation *EvaluatedCapability
|
||||||
Performance *PerformanceEvidence
|
Performance *PerformanceEvidence
|
||||||
Rating *ModelRating
|
TaskEvaluation *TaskEvaluation
|
||||||
|
Rating *ModelRating
|
||||||
}
|
}
|
||||||
|
|
||||||
// ModelRating is a derived, provider-neutral explanation of why an eligible
|
// ModelRating is a derived, provider-neutral explanation of why an eligible
|
||||||
// model is routed. It never grants qualification or activation authority.
|
// model is routed. It never grants qualification or activation authority.
|
||||||
type ModelRating struct {
|
type ModelRating struct {
|
||||||
Category ModelRatingCategory `json:"category"`
|
TaskProfileID TaskEvaluationProfileID `json:"task_profile_id,omitempty"`
|
||||||
OverallScoreMillis int64 `json:"overall_score_millis"`
|
TaskScoreMillis int64 `json:"task_score_millis,omitempty"`
|
||||||
QualityScoreMillis int64 `json:"quality_score_millis"`
|
Category ModelRatingCategory `json:"category"`
|
||||||
ReliabilityScoreMillis int64 `json:"reliability_score_millis"`
|
OverallScoreMillis int64 `json:"overall_score_millis"`
|
||||||
LatencyScoreMillis int64 `json:"latency_score_millis"`
|
QualityScoreMillis int64 `json:"quality_score_millis"`
|
||||||
EvidenceScoreMillis int64 `json:"evidence_score_millis"`
|
ReliabilityScoreMillis int64 `json:"reliability_score_millis"`
|
||||||
|
LatencyScoreMillis int64 `json:"latency_score_millis"`
|
||||||
|
EvidenceScoreMillis int64 `json:"evidence_score_millis"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CognitiveResultClass string
|
type CognitiveResultClass string
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
package capability
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"git.cin.su/m/Assistant/contracts/artifact"
|
||||||
|
"git.cin.su/m/Assistant/platform/ids"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TaskEvaluationProfileID ids.ID
|
||||||
|
type TaskEvaluationID ids.ID
|
||||||
|
type TaskEvaluationProfileStage string
|
||||||
|
|
||||||
|
const (
|
||||||
|
TaskEvaluationGeneral TaskEvaluationProfileStage = "GENERAL"
|
||||||
|
TaskEvaluationAdditional TaskEvaluationProfileStage = "ADDITIONAL"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TaskEvaluationProfile is the extensible, provider-neutral description of a
|
||||||
|
// task-specific qualification run. Case payloads live in Artifact; the
|
||||||
|
// profile stores only stable case references and scoring policy.
|
||||||
|
type TaskEvaluationProfile struct {
|
||||||
|
ID TaskEvaluationProfileID
|
||||||
|
Revision int64
|
||||||
|
CapabilityID CapabilityID
|
||||||
|
Stage TaskEvaluationProfileStage
|
||||||
|
ParentProfileID TaskEvaluationProfileID
|
||||||
|
TriggerEvidenceRef string
|
||||||
|
BenchmarkVersion string
|
||||||
|
RequiredCaseIDs []string
|
||||||
|
QualityWeightMillis int64
|
||||||
|
ReliabilityWeightMillis int64
|
||||||
|
LatencyWeightMillis int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p TaskEvaluationProfile) Validate() error {
|
||||||
|
if p.ID == "" || p.Revision < 1 || p.CapabilityID == "" || (p.Stage != TaskEvaluationGeneral && p.Stage != TaskEvaluationAdditional) || p.BenchmarkVersion == "" || len(p.RequiredCaseIDs) == 0 {
|
||||||
|
return errors.New("task evaluation profile identity and cases are incomplete")
|
||||||
|
}
|
||||||
|
if p.Stage == TaskEvaluationGeneral && (p.ParentProfileID != "" || p.TriggerEvidenceRef != "") {
|
||||||
|
return errors.New("general task evaluation profile cannot have an expansion parent")
|
||||||
|
}
|
||||||
|
if p.Stage == TaskEvaluationAdditional && (p.ParentProfileID == "" || p.TriggerEvidenceRef == "") {
|
||||||
|
return errors.New("additional task evaluation profile requires parent and trigger evidence")
|
||||||
|
}
|
||||||
|
if p.QualityWeightMillis < 0 || p.ReliabilityWeightMillis < 0 || p.LatencyWeightMillis < 0 || p.QualityWeightMillis+p.ReliabilityWeightMillis+p.LatencyWeightMillis != 1000 {
|
||||||
|
return errors.New("task evaluation profile weights must be non-negative and sum to 1000")
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(p.RequiredCaseIDs))
|
||||||
|
for _, caseID := range p.RequiredCaseIDs {
|
||||||
|
if caseID == "" {
|
||||||
|
return errors.New("task evaluation profile contains an empty case")
|
||||||
|
}
|
||||||
|
if _, ok := seen[caseID]; ok {
|
||||||
|
return errors.New("task evaluation profile contains duplicate cases")
|
||||||
|
}
|
||||||
|
seen[caseID] = struct{}{}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskEvaluationExpansionRequest is emitted after a task remains unresolved
|
||||||
|
// with the current best eligible candidate. It creates only a bounded new
|
||||||
|
// profile; it never silently rewrites the general benchmark.
|
||||||
|
type TaskEvaluationExpansionRequest struct {
|
||||||
|
ParentProfileID TaskEvaluationProfileID
|
||||||
|
CapabilityID CapabilityID
|
||||||
|
TriggerEvidenceRef string
|
||||||
|
NewCaseIDs []string
|
||||||
|
BenchmarkVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskCaseObservation is produced by the bounded evaluator and verifier. A
|
||||||
|
// score without a case result and evidence reference is not accepted as
|
||||||
|
// qualification evidence.
|
||||||
|
type TaskCaseObservation struct {
|
||||||
|
CaseID string
|
||||||
|
Result EvaluationResult
|
||||||
|
QualityScoreMillis int64
|
||||||
|
LatencyMicros int64
|
||||||
|
CriticalFailure bool
|
||||||
|
EvidenceArtifactRef string
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskEvaluation is durable evidence for one model/binding revision under one
|
||||||
|
// task profile. It is not activation authority.
|
||||||
|
type TaskEvaluation struct {
|
||||||
|
ID TaskEvaluationID
|
||||||
|
ProfileID TaskEvaluationProfileID
|
||||||
|
BindingID CapabilityBindingID
|
||||||
|
BindingRevision int64
|
||||||
|
CapabilityID CapabilityID
|
||||||
|
Result EvaluationResult
|
||||||
|
QualityScoreMillis int64
|
||||||
|
ReliabilityScoreMillis int64
|
||||||
|
LatencyScoreMillis int64
|
||||||
|
OverallScoreMillis int64
|
||||||
|
CriticalFailureMillis int64
|
||||||
|
BenchmarkVersion string
|
||||||
|
ArtifactID artifact.ArtifactID
|
||||||
|
ObservedAtUnixMillis int64
|
||||||
|
}
|
||||||
+40
-11
@@ -1,11 +1,17 @@
|
|||||||
package work
|
package work
|
||||||
|
|
||||||
|
import "git.cin.su/m/Assistant/contracts/capability"
|
||||||
|
|
||||||
type GraphNodeID string
|
type GraphNodeID string
|
||||||
|
|
||||||
type GraphState string
|
type GraphState string
|
||||||
type GraphNodeState string
|
type GraphNodeState string
|
||||||
|
type GraphNodePurpose string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
GraphNodePurposeTaskEvaluation GraphNodePurpose = "TASK_EVALUATION_SPECIALIST"
|
||||||
|
GraphNodePurposeGeneric GraphNodePurpose = "GENERIC"
|
||||||
|
|
||||||
GraphDraft GraphState = "DRAFT"
|
GraphDraft GraphState = "DRAFT"
|
||||||
GraphAdmitted GraphState = "ADMITTED"
|
GraphAdmitted GraphState = "ADMITTED"
|
||||||
GraphRunning GraphState = "RUNNING"
|
GraphRunning GraphState = "RUNNING"
|
||||||
@@ -31,11 +37,15 @@ type GraphBudget struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GraphNode struct {
|
type GraphNode struct {
|
||||||
ID GraphNodeID `json:"id"`
|
ID GraphNodeID `json:"id"`
|
||||||
DispatchID string `json:"dispatch_id"`
|
DispatchID string `json:"dispatch_id"`
|
||||||
IdempotencyKey string `json:"idempotency_key"`
|
IdempotencyKey string `json:"idempotency_key"`
|
||||||
State GraphNodeState `json:"state"`
|
State GraphNodeState `json:"state"`
|
||||||
Attempts int `json:"attempts"`
|
Attempts int `json:"attempts"`
|
||||||
|
Purpose GraphNodePurpose `json:"purpose,omitempty"`
|
||||||
|
TaskProfileID capability.TaskEvaluationProfileID `json:"task_profile_id,omitempty"`
|
||||||
|
BindingID capability.CapabilityBindingID `json:"binding_id,omitempty"`
|
||||||
|
CaseIDs []string `json:"case_ids,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GraphEdge struct {
|
type GraphEdge struct {
|
||||||
@@ -44,12 +54,31 @@ type GraphEdge struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type WorkGraph struct {
|
type WorkGraph struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Revision int64 `json:"revision"`
|
Revision int64 `json:"revision"`
|
||||||
State GraphState `json:"state"`
|
State GraphState `json:"state"`
|
||||||
Nodes []GraphNode `json:"nodes"`
|
Nodes []GraphNode `json:"nodes"`
|
||||||
Edges []GraphEdge `json:"edges"`
|
Edges []GraphEdge `json:"edges"`
|
||||||
Budget GraphBudget `json:"budget"`
|
Budget GraphBudget `json:"budget"`
|
||||||
|
ParentGraphID string `json:"parent_graph_id,omitempty"`
|
||||||
|
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
|
||||||
|
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TaskEvaluationGraphRequest describes a durable fan-out. The graph contains
|
||||||
|
// one specialist node per binding; each node receives the profile's bounded
|
||||||
|
// cases and reports evidence back to Capability for fan-in aggregation.
|
||||||
|
type TaskEvaluationGraphRequest struct {
|
||||||
|
GraphID string `json:"graph_id"`
|
||||||
|
Profile capability.TaskEvaluationProfile `json:"profile"`
|
||||||
|
BindingIDs []capability.CapabilityBindingID `json:"binding_ids"`
|
||||||
|
DispatchIDs []string `json:"dispatch_ids"`
|
||||||
|
IdempotencyPrefix string `json:"idempotency_prefix"`
|
||||||
|
Budget GraphBudget `json:"budget"`
|
||||||
|
ParentGraphID string `json:"parent_graph_id,omitempty"`
|
||||||
|
ParentGraphRevision int64 `json:"parent_graph_revision,omitempty"`
|
||||||
|
ParentGraphState GraphState `json:"parent_graph_state,omitempty"`
|
||||||
|
TriggerEvidenceRef string `json:"trigger_evidence_ref,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResultCandidate struct {
|
type ResultCandidate struct {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
BEGIN;
|
||||||
|
SET ROLE capability_role;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS capability.cognitive_task_evaluations (
|
||||||
|
evaluation_id TEXT PRIMARY KEY,
|
||||||
|
task_profile_id TEXT NOT NULL,
|
||||||
|
binding_id TEXT NOT NULL,
|
||||||
|
binding_revision BIGINT NOT NULL,
|
||||||
|
capability_id TEXT NOT NULL,
|
||||||
|
result TEXT NOT NULL,
|
||||||
|
quality_score_millis BIGINT NOT NULL,
|
||||||
|
reliability_score_millis BIGINT NOT NULL,
|
||||||
|
latency_score_millis BIGINT NOT NULL,
|
||||||
|
overall_score_millis BIGINT NOT NULL,
|
||||||
|
critical_failure_millis BIGINT NOT NULL,
|
||||||
|
benchmark_version TEXT NOT NULL,
|
||||||
|
artifact_id TEXT NOT NULL,
|
||||||
|
observed_at_unix_millis BIGINT NOT NULL,
|
||||||
|
recorded_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CHECK (binding_revision > 0),
|
||||||
|
CHECK (result IN ('UNKNOWN','SUPPORTED','UNSUPPORTED')),
|
||||||
|
CHECK (quality_score_millis BETWEEN 0 AND 1000),
|
||||||
|
CHECK (reliability_score_millis BETWEEN 0 AND 1000),
|
||||||
|
CHECK (latency_score_millis BETWEEN 0 AND 1000),
|
||||||
|
CHECK (overall_score_millis BETWEEN 0 AND 1000),
|
||||||
|
CHECK (critical_failure_millis BETWEEN 0 AND 1000),
|
||||||
|
CHECK (observed_at_unix_millis > 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS cognitive_task_evaluations_lookup
|
||||||
|
ON capability.cognitive_task_evaluations(task_profile_id, binding_id, binding_revision, observed_at_unix_millis DESC);
|
||||||
|
|
||||||
|
GRANT SELECT, INSERT ON capability.cognitive_task_evaluations TO capability_role;
|
||||||
|
RESET ROLE;
|
||||||
|
COMMIT;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"task_file": "dev/tasks/DEV-000207-gpu-invocation-qualification.json",
|
"task_file": "dev/tasks/DEV-000209-task-evaluator-work-graph.json",
|
||||||
"task_id": "DEV-000207",
|
"task_id": "DEV-000209",
|
||||||
"task_revision": 1
|
"task_revision": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"task_id": "DEV-000208",
|
||||||
|
"task_revision": 1,
|
||||||
|
"architecture_revision": "ADR-0018",
|
||||||
|
"base_repo_revision": "b78a026ee169047feb8468f0799fa1b9f4e27562",
|
||||||
|
"risk_tier": "E",
|
||||||
|
"intent": "Make Domovoy's model qualification and routing task-aware through parallel durable specialist work: typed evaluation profiles/evidence, durable task scores, and explainable selection without hardware/provider heuristics.",
|
||||||
|
"requirement_ids": ["task-specific model evaluation", "parallel specialist evaluation", "no main execution thread", "model fleet/failover", "hardware-neutral routing", "explainable model rating"],
|
||||||
|
"allowed_change_scope": {"modules": ["capability", "core-runtime"], "paths": ["contracts/capability/**", "modules/capability/**", "adapters/postgres/capability_repository.go", "cmd/core-runtime/main.go", "database/migrations/039_capability_task_evaluations.sql", "tests/model_qualification/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000208-task-aware-model-evaluation.json"]},
|
||||||
|
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", ".gitea/**", "contracts/authority/**", "modules/identity/**"]},
|
||||||
|
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 1},
|
||||||
|
"contracts": {"consume": ["CognitiveRequirement", "ResolvedCognitiveBinding", "existing capability evidence", "existing capability owner repository", "Work Graph durable fan-out/fan-in"], "may_change": ["task profile/evaluation contracts", "task-aware selection request/response serialization"]},
|
||||||
|
"semantic_owner": "capability",
|
||||||
|
"canonical_state": ["capability task evaluation evidence keyed by task profile and binding revision"],
|
||||||
|
"state_transitions": ["GENERAL profile -> first bounded evaluation", "unresolved task evidence -> ADDITIONAL profile with parent and trigger evidence", "profile result UNKNOWN -> SUPPORTED/UNSUPPORTED; evidence is append-only by binding revision", "task-aware requirement -> eligible candidates with explainable task rating", "evaluation request -> durable fan-out -> specialist evidence -> fan-in aggregate"],
|
||||||
|
"side_effects_and_privileges": ["core capability database role writes only; no provider calls and no secret access in this task"],
|
||||||
|
"persistence_and_migrations": ["039 adds capability.cognitive_task_evaluations owned by capability"],
|
||||||
|
"failure_retry_restart": ["missing, stale or unknown task evidence excludes a task-aware candidate; no score defaults to success", "one specialist failure does not block unrelated tasks; stale leases are recoverable and fan-in preserves partial/unknown state"],
|
||||||
|
"assumptions": ["a later Work Graph evaluation runner will produce per-case evidence; this task establishes the durable contract and routing gate", "the chat/request path never waits synchronously for the full evaluation graph"],
|
||||||
|
"acceptance_scenarios": ["general profile runs before additional profiles", "an unresolved task creates only a bounded additional profile linked to failure evidence", "task-aware requirement rejects a model without current supported task evidence", "task scores are included in explainable rating", "provider kind, endpoint and hardware do not affect score", "unknown/failed task evidence never silently becomes supported", "existing requirements without a task profile retain current behavior"],
|
||||||
|
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "migration/schema checks", "CI", "production public health check"],
|
||||||
|
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"task_id": "DEV-000209",
|
||||||
|
"task_revision": 1,
|
||||||
|
"architecture_revision": "ADR-0018",
|
||||||
|
"base_repo_revision": "a82592a1300c06ed7a568c058faca6560e085ab3",
|
||||||
|
"risk_tier": "E",
|
||||||
|
"intent": "Create durable Work Graph plans for task-aware model evaluation: run the general profile first, fan out independent model specialists in parallel, and permit bounded additional profiles linked to unresolved evidence without blocking the chat path.",
|
||||||
|
"requirement_ids": ["general-first model evaluation", "parallel specialist evaluation", "bounded additional task tests", "fan-out/fan-in", "no synchronous chat blocking", "provider and hardware neutral"],
|
||||||
|
"allowed_change_scope": {"modules": ["work"], "paths": ["contracts/work/**", "modules/work/**", "cmd/work-graph-runtime/**", "tests/model_qualification/**", "dev/ACTIVE_TASK.json", "dev/tasks/DEV-000209-task-evaluator-work-graph.json"]},
|
||||||
|
"forbidden_change_scope": {"paths": ["architecture/**", "security/**", "deploy/**", "cmd/devctl/**", "modules/identity/**", "database/**"]},
|
||||||
|
"allowed_architecture_delta": {"new_cross_module_dependencies": 0, "new_canonical_owners": 0, "new_privileges": 0, "new_third_party_dependencies": 0, "new_database_owner_roles": 0},
|
||||||
|
"contracts": {"consume": ["capability.TaskEvaluationProfile", "capability.TaskEvaluationProfileStage", "capability.TaskEvaluationProfileID", "existing Work Graph schedule contract"], "may_change": ["typed Work Graph node purpose and task-evaluation planning contract"]},
|
||||||
|
"semantic_owner": "work",
|
||||||
|
"canonical_state": ["work-owned durable graph and node lifecycle; capability remains owner of model evaluation evidence"],
|
||||||
|
"state_transitions": ["GENERAL profile -> admitted evaluation graph", "eligible model bindings -> parallel specialist nodes", "all specialist nodes -> fan-in completion state", "ADDITIONAL profile -> a separate bounded graph linked by parent/trigger evidence"],
|
||||||
|
"side_effects_and_privileges": ["none; graph planning only, no provider calls and no secret access"],
|
||||||
|
"persistence_and_migrations": ["uses existing work graph persistence; no new schema"],
|
||||||
|
"failure_retry_restart": ["each specialist remains independently retryable through existing node attempts/leases; partial or unknown graph state is preserved", "invalid stage/parent linkage is rejected before scheduling"],
|
||||||
|
"assumptions": ["a later worker adapter executes each dispatched specialist and posts evidence to capability", "chat/request handlers enqueue the graph and return without waiting for all nodes"],
|
||||||
|
"acceptance_scenarios": ["additional profile cannot be scheduled as a replacement for general profile", "one node is created per binding and profile with independent dispatch identity", "parallel budget is retained for fan-out", "graph metadata is sufficient for a worker to identify profile, binding and specialist role", "provider kind, endpoint and hardware are absent from planning inputs"],
|
||||||
|
"required_tests": ["go test", "go vet", "preflight", "check-diff", "check-architecture", "independent", "git diff --check", "CI", "production public health check"],
|
||||||
|
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_MISSING_CAPABILITY"]
|
||||||
|
}
|
||||||
@@ -148,6 +148,9 @@ func SelectCognitiveBinding(requirement capability.CognitiveRequirement, candida
|
|||||||
if candidate.Evaluation != nil && candidate.Evaluation.Result == capability.EvaluationUnsupported {
|
if candidate.Evaluation != nil && candidate.Evaluation.Result == capability.EvaluationUnsupported {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if requirement.TaskProfileID != "" && (candidate.TaskEvaluation == nil || candidate.TaskEvaluation.ProfileID != requirement.TaskProfileID || candidate.TaskEvaluation.Result != capability.EvaluationSupported) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
rating := RateCognitiveBinding(candidate)
|
rating := RateCognitiveBinding(candidate)
|
||||||
candidate.Rating = &rating
|
candidate.Rating = &rating
|
||||||
if requirement.MaxCostMicros != nil {
|
if requirement.MaxCostMicros != nil {
|
||||||
@@ -171,6 +174,19 @@ func SelectCognitiveBinding(requirement capability.CognitiveRequirement, candida
|
|||||||
// evidence. Provider kind, endpoint name and hardware are intentionally not
|
// evidence. Provider kind, endpoint name and hardware are intentionally not
|
||||||
// inputs to the score.
|
// inputs to the score.
|
||||||
func RateCognitiveBinding(candidate capability.ResolvedCognitiveBinding) capability.ModelRating {
|
func RateCognitiveBinding(candidate capability.ResolvedCognitiveBinding) capability.ModelRating {
|
||||||
|
if candidate.TaskEvaluation != nil {
|
||||||
|
evaluation := candidate.TaskEvaluation
|
||||||
|
if evaluation.Result != capability.EvaluationSupported {
|
||||||
|
return capability.ModelRating{TaskProfileID: evaluation.ProfileID, Category: capability.ModelRatingUnrated}
|
||||||
|
}
|
||||||
|
category := capability.ModelRatingBalanced
|
||||||
|
if evaluation.OverallScoreMillis >= 850 && evaluation.QualityScoreMillis >= 800 {
|
||||||
|
category = capability.ModelRatingQuality
|
||||||
|
} else if evaluation.LatencyScoreMillis >= 800 && evaluation.OverallScoreMillis >= 700 {
|
||||||
|
category = capability.ModelRatingFast
|
||||||
|
}
|
||||||
|
return capability.ModelRating{TaskProfileID: evaluation.ProfileID, TaskScoreMillis: evaluation.OverallScoreMillis, Category: category, OverallScoreMillis: evaluation.OverallScoreMillis, QualityScoreMillis: evaluation.QualityScoreMillis, ReliabilityScoreMillis: evaluation.ReliabilityScoreMillis, LatencyScoreMillis: evaluation.LatencyScoreMillis, EvidenceScoreMillis: 1000}
|
||||||
|
}
|
||||||
if candidate.Evaluation == nil || candidate.Evaluation.Result != capability.EvaluationSupported || candidate.Evaluation.QualityScoreMillis == nil || candidate.Evaluation.CriticalFailureMillis == nil {
|
if candidate.Evaluation == nil || candidate.Evaluation.Result != capability.EvaluationSupported || candidate.Evaluation.QualityScoreMillis == nil || candidate.Evaluation.CriticalFailureMillis == nil {
|
||||||
return capability.ModelRating{Category: capability.ModelRatingUnrated}
|
return capability.ModelRating{Category: capability.ModelRatingUnrated}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"git.cin.su/m/Assistant/contracts/artifact"
|
||||||
|
"git.cin.su/m/Assistant/contracts/capability"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidTaskEvaluation = errors.New("invalid task evaluation")
|
||||||
|
|
||||||
|
func BuildAdditionalTaskEvaluationProfile(parent capability.TaskEvaluationProfile, request capability.TaskEvaluationExpansionRequest, profileID capability.TaskEvaluationProfileID, revision int64) (capability.TaskEvaluationProfile, error) {
|
||||||
|
if err := parent.Validate(); err != nil || parent.Stage != capability.TaskEvaluationGeneral || request.ParentProfileID != parent.ID || request.CapabilityID != parent.CapabilityID || request.TriggerEvidenceRef == "" || len(request.NewCaseIDs) == 0 || profileID == "" || revision < 1 || request.BenchmarkVersion == "" {
|
||||||
|
return capability.TaskEvaluationProfile{}, ErrInvalidTaskEvaluation
|
||||||
|
}
|
||||||
|
known := make(map[string]struct{}, len(parent.RequiredCaseIDs))
|
||||||
|
for _, caseID := range parent.RequiredCaseIDs {
|
||||||
|
known[caseID] = struct{}{}
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(request.NewCaseIDs))
|
||||||
|
for _, caseID := range request.NewCaseIDs {
|
||||||
|
if caseID == "" {
|
||||||
|
return capability.TaskEvaluationProfile{}, ErrInvalidTaskEvaluation
|
||||||
|
}
|
||||||
|
if _, exists := known[caseID]; exists {
|
||||||
|
return capability.TaskEvaluationProfile{}, ErrInvalidTaskEvaluation
|
||||||
|
}
|
||||||
|
if _, exists := seen[caseID]; exists {
|
||||||
|
return capability.TaskEvaluationProfile{}, ErrInvalidTaskEvaluation
|
||||||
|
}
|
||||||
|
seen[caseID] = struct{}{}
|
||||||
|
}
|
||||||
|
return capability.TaskEvaluationProfile{ID: profileID, Revision: revision, CapabilityID: parent.CapabilityID, Stage: capability.TaskEvaluationAdditional, ParentProfileID: parent.ID, TriggerEvidenceRef: request.TriggerEvidenceRef, BenchmarkVersion: request.BenchmarkVersion, RequiredCaseIDs: append([]string(nil), request.NewCaseIDs...), QualityWeightMillis: parent.QualityWeightMillis, ReliabilityWeightMillis: parent.ReliabilityWeightMillis, LatencyWeightMillis: parent.LatencyWeightMillis}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildTaskEvaluation aggregates independently observed case results. It is
|
||||||
|
// intentionally deterministic: no provider name, hardware label or caller
|
||||||
|
// supplied aggregate score can influence the result.
|
||||||
|
func BuildTaskEvaluation(profile capability.TaskEvaluationProfile, evaluationID capability.TaskEvaluationID, bindingID capability.CapabilityBindingID, bindingRevision int64, capabilityID capability.CapabilityID, observations []capability.TaskCaseObservation, artifactID artifact.ArtifactID, observedAtUnixMillis int64) (capability.TaskEvaluation, error) {
|
||||||
|
if err := profile.Validate(); err != nil || evaluationID == "" || bindingID == "" || bindingRevision < 1 || capabilityID == "" || capabilityID != profile.CapabilityID || artifactID == "" || observedAtUnixMillis < 1 || len(observations) != len(profile.RequiredCaseIDs) {
|
||||||
|
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
|
||||||
|
}
|
||||||
|
required := make(map[string]struct{}, len(profile.RequiredCaseIDs))
|
||||||
|
for _, caseID := range profile.RequiredCaseIDs {
|
||||||
|
required[caseID] = struct{}{}
|
||||||
|
}
|
||||||
|
seen := make(map[string]struct{}, len(observations))
|
||||||
|
qualityTotal, latencyTotal, passed, critical := int64(0), int64(0), int64(0), int64(0)
|
||||||
|
unknown := false
|
||||||
|
unsupported := false
|
||||||
|
for _, observation := range observations {
|
||||||
|
if _, ok := required[observation.CaseID]; !ok {
|
||||||
|
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
|
||||||
|
}
|
||||||
|
if _, ok := seen[observation.CaseID]; ok || observation.EvidenceArtifactRef == "" || observation.QualityScoreMillis < 0 || observation.QualityScoreMillis > 1000 || observation.LatencyMicros < 0 {
|
||||||
|
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
|
||||||
|
}
|
||||||
|
seen[observation.CaseID] = struct{}{}
|
||||||
|
qualityTotal += observation.QualityScoreMillis
|
||||||
|
latencyTotal += observation.LatencyMicros
|
||||||
|
if observation.CriticalFailure {
|
||||||
|
critical++
|
||||||
|
}
|
||||||
|
switch observation.Result {
|
||||||
|
case capability.EvaluationSupported:
|
||||||
|
passed++
|
||||||
|
case capability.EvaluationUnsupported:
|
||||||
|
unsupported = true
|
||||||
|
case capability.EvaluationUnknown:
|
||||||
|
unknown = true
|
||||||
|
default:
|
||||||
|
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(seen) != len(required) {
|
||||||
|
return capability.TaskEvaluation{}, ErrInvalidTaskEvaluation
|
||||||
|
}
|
||||||
|
count := int64(len(observations))
|
||||||
|
quality := qualityTotal / count
|
||||||
|
reliability := passed * 1000 / count
|
||||||
|
latency := latencyScore(latencyTotal / count)
|
||||||
|
criticalMillis := critical * 1000 / count
|
||||||
|
overall := (quality*profile.QualityWeightMillis + reliability*profile.ReliabilityWeightMillis + latency*profile.LatencyWeightMillis) / 1000
|
||||||
|
result := capability.EvaluationSupported
|
||||||
|
if unknown {
|
||||||
|
result = capability.EvaluationUnknown
|
||||||
|
} else if unsupported {
|
||||||
|
result = capability.EvaluationUnsupported
|
||||||
|
}
|
||||||
|
return capability.TaskEvaluation{ID: evaluationID, ProfileID: profile.ID, BindingID: bindingID, BindingRevision: bindingRevision, CapabilityID: capabilityID, Result: result, QualityScoreMillis: quality, ReliabilityScoreMillis: reliability, LatencyScoreMillis: latency, OverallScoreMillis: overall, CriticalFailureMillis: criticalMillis, BenchmarkVersion: profile.BenchmarkVersion, ArtifactID: artifactID, ObservedAtUnixMillis: observedAtUnixMillis}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ValidateTaskEvaluation(value capability.TaskEvaluation) error {
|
||||||
|
if value.ID == "" || value.ProfileID == "" || value.BindingID == "" || value.BindingRevision < 1 || value.CapabilityID == "" || value.BenchmarkVersion == "" || value.ArtifactID == "" || value.ObservedAtUnixMillis < 1 {
|
||||||
|
return ErrInvalidTaskEvaluation
|
||||||
|
}
|
||||||
|
if value.Result != capability.EvaluationUnknown && value.Result != capability.EvaluationSupported && value.Result != capability.EvaluationUnsupported {
|
||||||
|
return ErrInvalidTaskEvaluation
|
||||||
|
}
|
||||||
|
for _, score := range []int64{value.QualityScoreMillis, value.ReliabilityScoreMillis, value.LatencyScoreMillis, value.OverallScoreMillis, value.CriticalFailureMillis} {
|
||||||
|
if score < 0 || score > 1000 {
|
||||||
|
return ErrInvalidTaskEvaluation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.cin.su/m/Assistant/contracts/artifact"
|
||||||
|
"git.cin.su/m/Assistant/contracts/capability"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuildTaskEvaluationAggregatesOnlyProfileCases(t *testing.T) {
|
||||||
|
profile := capability.TaskEvaluationProfile{ID: "research-v1", Revision: 1, CapabilityID: "creator-advisory", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "research-v1", RequiredCaseIDs: []string{"claim", "uncertainty"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
|
||||||
|
value, err := BuildTaskEvaluation(profile, "evaluation-1", "binding-1", 2, "creator-advisory", []capability.TaskCaseObservation{
|
||||||
|
{CaseID: "claim", Result: capability.EvaluationSupported, QualityScoreMillis: 900, LatencyMicros: 400_000, EvidenceArtifactRef: "artifact-case-1"},
|
||||||
|
{CaseID: "uncertainty", Result: capability.EvaluationSupported, QualityScoreMillis: 800, LatencyMicros: 600_000, EvidenceArtifactRef: "artifact-case-2"},
|
||||||
|
}, artifact.ArtifactID("artifact-summary"), 10)
|
||||||
|
if err != nil || value.Result != capability.EvaluationSupported || value.OverallScoreMillis == 0 || value.ProfileID != profile.ID {
|
||||||
|
t.Fatalf("evaluation=%+v err=%v", value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildTaskEvaluationDoesNotPromoteUnknown(t *testing.T) {
|
||||||
|
profile := capability.TaskEvaluationProfile{ID: "coding-v1", Revision: 1, CapabilityID: "creator-advisory", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "coding-v1", RequiredCaseIDs: []string{"compile"}, QualityWeightMillis: 700, ReliabilityWeightMillis: 200, LatencyWeightMillis: 100}
|
||||||
|
value, err := BuildTaskEvaluation(profile, "evaluation-2", "binding-1", 2, "creator-advisory", []capability.TaskCaseObservation{{CaseID: "compile", Result: capability.EvaluationUnknown, QualityScoreMillis: 0, LatencyMicros: 0, EvidenceArtifactRef: "artifact-case"}}, artifact.ArtifactID("artifact-summary"), 11)
|
||||||
|
if err != nil || value.Result != capability.EvaluationUnknown {
|
||||||
|
t.Fatalf("evaluation=%+v err=%v", value, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildAdditionalProfileRequiresUnresolvedEvidenceAndPreservesHistory(t *testing.T) {
|
||||||
|
parent := capability.TaskEvaluationProfile{ID: "general-v1", Revision: 1, CapabilityID: "creator-advisory", Stage: capability.TaskEvaluationGeneral, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"general"}, QualityWeightMillis: 700, ReliabilityWeightMillis: 200, LatencyWeightMillis: 100}
|
||||||
|
profile, err := BuildAdditionalTaskEvaluationProfile(parent, capability.TaskEvaluationExpansionRequest{ParentProfileID: parent.ID, CapabilityID: parent.CapabilityID, TriggerEvidenceRef: "experience-unsolved-1", NewCaseIDs: []string{"edge-case"}, BenchmarkVersion: "additional-v1"}, "additional-v1", 1)
|
||||||
|
if err != nil || profile.Stage != capability.TaskEvaluationAdditional || profile.ParentProfileID != parent.ID || profile.RequiredCaseIDs[0] != "edge-case" {
|
||||||
|
t.Fatalf("profile=%+v err=%v", profile, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ type Repository interface {
|
|||||||
SaveModelDescriptor(context.Context, capability.ModelDescriptor) error
|
SaveModelDescriptor(context.Context, capability.ModelDescriptor) error
|
||||||
SaveCognitiveBinding(context.Context, capability.CognitiveBinding) error
|
SaveCognitiveBinding(context.Context, capability.CognitiveBinding) error
|
||||||
LoadCognitiveCandidates(context.Context, capability.CapabilityID, identity.PrincipalID) ([]capability.ResolvedCognitiveBinding, error)
|
LoadCognitiveCandidates(context.Context, capability.CapabilityID, identity.PrincipalID) ([]capability.ResolvedCognitiveBinding, error)
|
||||||
|
LoadCognitiveCandidatesForProfile(context.Context, capability.CapabilityID, identity.PrincipalID, capability.TaskEvaluationProfileID) ([]capability.ResolvedCognitiveBinding, error)
|
||||||
LoadCognitiveCandidate(context.Context, capability.CapabilityBindingID, int64) (capability.ResolvedCognitiveBinding, error)
|
LoadCognitiveCandidate(context.Context, capability.CapabilityBindingID, int64) (capability.ResolvedCognitiveBinding, error)
|
||||||
SaveCognitiveCanaryPromotion(context.Context, capability.CognitiveCanaryPromotion) error
|
SaveCognitiveCanaryPromotion(context.Context, capability.CognitiveCanaryPromotion) error
|
||||||
SaveCognitiveActivationPromotion(context.Context, capability.CognitiveActivationPromotion) error
|
SaveCognitiveActivationPromotion(context.Context, capability.CognitiveActivationPromotion) error
|
||||||
@@ -25,4 +26,5 @@ type Repository interface {
|
|||||||
SaveCognitiveDiscovery(context.Context, capability.CognitiveDiscoveryRegistration) error
|
SaveCognitiveDiscovery(context.Context, capability.CognitiveDiscoveryRegistration) error
|
||||||
SaveEvaluatedCapability(context.Context, capability.EvaluatedCapability) error
|
SaveEvaluatedCapability(context.Context, capability.EvaluatedCapability) error
|
||||||
SavePerformanceEvidence(context.Context, capability.PerformanceEvidence) error
|
SavePerformanceEvidence(context.Context, capability.PerformanceEvidence) error
|
||||||
|
SaveTaskEvaluation(context.Context, capability.TaskEvaluation) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,12 @@ type GraphScheduleRequest struct {
|
|||||||
Now int64 `json:"now_unix_millis"`
|
Now int64 `json:"now_unix_millis"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TaskEvaluationScheduleRequest struct {
|
||||||
|
Plan work.TaskEvaluationGraphRequest `json:"plan"`
|
||||||
|
Intents []work.AgentDispatchIntent `json:"intents"`
|
||||||
|
Now int64 `json:"now_unix_millis"`
|
||||||
|
}
|
||||||
|
|
||||||
type GraphCompleteRequest struct {
|
type GraphCompleteRequest struct {
|
||||||
GraphID string `json:"graph_id"`
|
GraphID string `json:"graph_id"`
|
||||||
Revision int64 `json:"revision"`
|
Revision int64 `json:"revision"`
|
||||||
@@ -105,6 +111,18 @@ func AdmitAndSchedule(ctx context.Context, store ports.GraphStore, request Graph
|
|||||||
return next, ready, nil
|
return next, ready, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdmitAndScheduleTaskEvaluation creates the durable general-first or
|
||||||
|
// bounded-expansion fan-out before handing it to the existing graph scheduler.
|
||||||
|
// The request path only persists/enqueues work; specialist execution happens
|
||||||
|
// independently through the dispatch workers.
|
||||||
|
func AdmitAndScheduleTaskEvaluation(ctx context.Context, store ports.GraphStore, request TaskEvaluationScheduleRequest) (work.WorkGraph, []work.GraphNodeID, error) {
|
||||||
|
graph, err := domain.BuildTaskEvaluationGraph(request.Plan)
|
||||||
|
if err != nil {
|
||||||
|
return work.WorkGraph{}, nil, err
|
||||||
|
}
|
||||||
|
return AdmitAndSchedule(ctx, store, GraphScheduleRequest{Graph: graph, Intents: request.Intents, Now: request.Now})
|
||||||
|
}
|
||||||
|
|
||||||
func CompleteGraphNode(ctx context.Context, store ports.GraphStore, graphID string, revision int64, nodeID work.GraphNodeID, success bool) (work.WorkGraph, error) {
|
func CompleteGraphNode(ctx context.Context, store ports.GraphStore, graphID string, revision int64, nodeID work.GraphNodeID, success bool) (work.WorkGraph, error) {
|
||||||
graph, err := store.LoadWorkGraph(ctx, graphID, revision)
|
graph, err := store.LoadWorkGraph(ctx, graphID, revision)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.cin.su/m/Assistant/contracts/capability"
|
||||||
|
"git.cin.su/m/Assistant/contracts/work"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidTaskEvaluationGraph = errors.New("invalid task evaluation graph request")
|
||||||
|
|
||||||
|
// BuildTaskEvaluationGraph turns one bounded profile into durable specialist
|
||||||
|
// nodes. It deliberately has no provider, endpoint or hardware inputs: those
|
||||||
|
// are resolved later by the cognitive worker for each binding.
|
||||||
|
func BuildTaskEvaluationGraph(request work.TaskEvaluationGraphRequest) (work.WorkGraph, error) {
|
||||||
|
if request.GraphID == "" || request.IdempotencyPrefix == "" || len(request.BindingIDs) == 0 || len(request.BindingIDs) != len(request.DispatchIDs) || request.Budget.MaxParallel < 1 || request.Budget.MaxAttempts < 1 {
|
||||||
|
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||||
|
}
|
||||||
|
if err := request.Profile.Validate(); err != nil {
|
||||||
|
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||||
|
}
|
||||||
|
if request.Profile.Stage == capability.TaskEvaluationGeneral {
|
||||||
|
if request.ParentGraphID != "" || request.ParentGraphRevision != 0 || request.ParentGraphState != "" || request.TriggerEvidenceRef != "" {
|
||||||
|
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if request.ParentGraphID == "" || request.ParentGraphRevision < 1 || request.TriggerEvidenceRef == "" || (request.ParentGraphState != work.GraphCompleted && request.ParentGraphState != work.GraphPartial) {
|
||||||
|
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||||
|
}
|
||||||
|
if request.Profile.ParentProfileID == "" {
|
||||||
|
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||||
|
}
|
||||||
|
}
|
||||||
|
seenBindings := make(map[capability.CapabilityBindingID]struct{}, len(request.BindingIDs))
|
||||||
|
seenDispatches := make(map[string]struct{}, len(request.DispatchIDs))
|
||||||
|
nodes := make([]work.GraphNode, 0, len(request.BindingIDs))
|
||||||
|
for i, bindingID := range request.BindingIDs {
|
||||||
|
if bindingID == "" || request.DispatchIDs[i] == "" {
|
||||||
|
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||||
|
}
|
||||||
|
if _, ok := seenBindings[bindingID]; ok {
|
||||||
|
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||||
|
}
|
||||||
|
if _, ok := seenDispatches[request.DispatchIDs[i]]; ok {
|
||||||
|
return work.WorkGraph{}, ErrInvalidTaskEvaluationGraph
|
||||||
|
}
|
||||||
|
seenBindings[bindingID] = struct{}{}
|
||||||
|
seenDispatches[request.DispatchIDs[i]] = struct{}{}
|
||||||
|
nodes = append(nodes, work.GraphNode{
|
||||||
|
ID: work.GraphNodeID(fmt.Sprintf("%s-node-%d", request.GraphID, i+1)),
|
||||||
|
DispatchID: request.DispatchIDs[i],
|
||||||
|
IdempotencyKey: fmt.Sprintf("%s:%s:%d", request.IdempotencyPrefix, bindingID, request.Profile.Revision),
|
||||||
|
State: work.GraphNodeBlocked,
|
||||||
|
Purpose: work.GraphNodePurposeTaskEvaluation,
|
||||||
|
TaskProfileID: request.Profile.ID,
|
||||||
|
BindingID: bindingID,
|
||||||
|
CaseIDs: append([]string(nil), request.Profile.RequiredCaseIDs...),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return work.WorkGraph{ID: request.GraphID, Revision: 1, State: work.GraphDraft, Nodes: nodes, Budget: request.Budget, ParentGraphID: request.ParentGraphID, ParentGraphRevision: request.ParentGraphRevision, TriggerEvidenceRef: request.TriggerEvidenceRef}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.cin.su/m/Assistant/contracts/capability"
|
||||||
|
"git.cin.su/m/Assistant/contracts/work"
|
||||||
|
)
|
||||||
|
|
||||||
|
func evaluationGraphRequest(stage capability.TaskEvaluationProfileStage) work.TaskEvaluationGraphRequest {
|
||||||
|
profile := capability.TaskEvaluationProfile{ID: "profile-1", Revision: 1, CapabilityID: "capability-1", Stage: stage, BenchmarkVersion: "general-v1", RequiredCaseIDs: []string{"case-a", "case-b"}, QualityWeightMillis: 600, ReliabilityWeightMillis: 300, LatencyWeightMillis: 100}
|
||||||
|
request := work.TaskEvaluationGraphRequest{GraphID: "graph-1", Profile: profile, BindingIDs: []capability.CapabilityBindingID{"binding-a", "binding-b"}, DispatchIDs: []string{"dispatch-a", "dispatch-b"}, IdempotencyPrefix: "task-eval", Budget: work.GraphBudget{MaxParallel: 2, MaxAttempts: 2}}
|
||||||
|
if stage == capability.TaskEvaluationAdditional {
|
||||||
|
request.Profile.ParentProfileID = "profile-general"
|
||||||
|
request.Profile.TriggerEvidenceRef = "artifact:unresolved-1"
|
||||||
|
request.Profile.Stage = capability.TaskEvaluationAdditional
|
||||||
|
request.ParentGraphID, request.ParentGraphRevision, request.ParentGraphState, request.TriggerEvidenceRef = "graph-general", 2, work.GraphPartial, "artifact:unresolved-1"
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildTaskEvaluationGraphFansOutOneNodePerBinding(t *testing.T) {
|
||||||
|
graph, err := BuildTaskEvaluationGraph(evaluationGraphRequest(capability.TaskEvaluationGeneral))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(graph.Nodes) != 2 || graph.Nodes[0].Purpose != work.GraphNodePurposeTaskEvaluation || graph.Nodes[0].TaskProfileID != "profile-1" || len(graph.Nodes[0].CaseIDs) != 2 {
|
||||||
|
t.Fatalf("unexpected task graph nodes: %+v", graph.Nodes)
|
||||||
|
}
|
||||||
|
if graph.ParentGraphID != "" || graph.TriggerEvidenceRef != "" {
|
||||||
|
t.Fatalf("general graph unexpectedly has expansion lineage: %+v", graph)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildTaskEvaluationGraphRequiresCompletedOrPartialGeneralParent(t *testing.T) {
|
||||||
|
request := evaluationGraphRequest(capability.TaskEvaluationAdditional)
|
||||||
|
request.ParentGraphState = work.GraphRunning
|
||||||
|
if _, err := BuildTaskEvaluationGraph(request); err == nil {
|
||||||
|
t.Fatal("expected additional graph to require a finished parent graph")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildTaskEvaluationGraphRejectsGeneralReplacement(t *testing.T) {
|
||||||
|
request := evaluationGraphRequest(capability.TaskEvaluationGeneral)
|
||||||
|
request.ParentGraphID = "old-graph"
|
||||||
|
request.ParentGraphRevision = 2
|
||||||
|
request.ParentGraphState = work.GraphPartial
|
||||||
|
request.TriggerEvidenceRef = "artifact:unresolved"
|
||||||
|
if _, err := BuildTaskEvaluationGraph(request); err == nil {
|
||||||
|
t.Fatal("expected general graph replacement to be rejected")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user