feat: integrate AI candidate review and automatic publication
This commit is contained in:
@@ -22,12 +22,12 @@ func NewCandidateRepository(pool *pgxpool.Pool) *CandidateRepository {
|
||||
// All successors of an automatically generated candidate retain the deployment binding.
|
||||
const screeningEligibility = `NOT EXISTS(SELECT 1 FROM veyra_strategy_screening x JOIN veyra_candidate_revision original ON original.id=x.candidate_revision_id JOIN veyra_strategy_version sv ON sv.id=x.strategy_version_id JOIN veyra_strategy s ON s.id=sv.strategy_id WHERE original.candidate_id=c.id AND (s.status<>'ACTIVE' OR s.active_version_id IS DISTINCT FROM sv.id))`
|
||||
|
||||
const candidateColumns = `r.id,r.candidate_id,r.revision,r.version,r.status,r.data_mode,r.snapshot,r.author_id,r.observed_at,r.valid_until,r.published_at,r.created_at,r.updated_at,(r.revision=c.latest_revision),COALESCE(c.published_revision_id=r.id AND r.status='PUBLISHED' AND r.valid_until>now() AND ` + screeningEligibility + `,false),COALESCE((SELECT reason FROM veyra_candidate_review v WHERE v.revision_id=r.id AND v.decision='WITHDRAW' ORDER BY v.input_version DESC LIMIT 1),''),(SELECT jsonb_build_object('id',x.id,'strategy_version_id',x.strategy_version_id,'strategy_name',s.name,'strategy_revision',sv.revision,'symbol',x.symbol,'exchange',x.exchange,'bar_date',x.bar_date::text,'source_id',x.source_id,'source_version',x.source_version,'source_adapter',x.source_adapter,'observed_at',x.observed_at,'received_at',x.received_at,'input_hash',x.input_hash,'status',x.status,'candidate_revision_id',x.candidate_revision_id,'created_at',x.created_at) FROM veyra_strategy_screening x JOIN veyra_candidate_revision original ON original.id=x.candidate_revision_id JOIN veyra_strategy_version sv ON sv.id=x.strategy_version_id JOIN veyra_strategy s ON s.id=sv.strategy_id WHERE original.candidate_id=c.id LIMIT 1)`
|
||||
const candidateColumns = `r.id,r.candidate_id,r.revision,r.version,r.status,r.data_mode,r.snapshot,r.author_id,r.observed_at,r.valid_until,r.published_at,r.created_at,r.updated_at,(r.revision=c.latest_revision),COALESCE(c.published_revision_id=r.id AND r.status='PUBLISHED' AND r.valid_until>now() AND ` + screeningEligibility + `,false),COALESCE((SELECT reason FROM veyra_candidate_review v WHERE v.revision_id=r.id AND v.decision='WITHDRAW' ORDER BY v.input_version DESC LIMIT 1),''),(SELECT jsonb_build_object('id',x.id,'strategy_version_id',x.strategy_version_id,'strategy_name',s.name,'strategy_revision',sv.revision,'symbol',x.symbol,'exchange',x.exchange,'bar_date',x.bar_date::text,'source_id',x.source_id,'source_version',x.source_version,'source_adapter',x.source_adapter,'observed_at',x.observed_at,'received_at',x.received_at,'input_hash',x.input_hash,'status',x.status,'candidate_revision_id',x.candidate_revision_id,'created_at',x.created_at) FROM veyra_strategy_screening x JOIN veyra_candidate_revision original ON original.id=x.candidate_revision_id JOIN veyra_strategy_version sv ON sv.id=x.strategy_version_id JOIN veyra_strategy s ON s.id=sv.strategy_id WHERE original.candidate_id=c.id LIMIT 1),(SELECT jsonb_build_object('id',a.id,'candidate_revision_id',a.candidate_revision_id,'input_version',a.input_version,'policy_version',a.policy_version,'status',a.status,'primary_model',a.primary_model,'review_model',a.review_model,'primary_version',a.primary_version,'review_version',a.review_version,'analysis',a.analysis,'review',a.review,'message',a.message,'created_at',a.created_at,'updated_at',a.updated_at) FROM veyra_candidate_analysis a WHERE a.candidate_revision_id=r.id AND (a.input_version=r.version OR (r.status='PUBLISHED' AND a.input_version=r.version-1)) AND a.status IN ('APPROVED','PUBLISHED') ORDER BY a.updated_at DESC LIMIT 1)`
|
||||
|
||||
func scanCandidate(row pgx.Row) (model.CandidateRevision, error) {
|
||||
var r model.CandidateRevision
|
||||
var snapshot, screening []byte
|
||||
err := row.Scan(&r.ID, &r.CandidateID, &r.Revision, &r.Version, &r.Status, &r.DataMode, &snapshot, &r.AuthorID, &r.ObservedAt, &r.ValidUntil, &r.PublishedAt, &r.CreatedAt, &r.UpdatedAt, &r.IsLatest, &r.IsActivePublication, &r.WithdrawalReason, &screening)
|
||||
var snapshot, screening, analysis []byte
|
||||
err := row.Scan(&r.ID, &r.CandidateID, &r.Revision, &r.Version, &r.Status, &r.DataMode, &snapshot, &r.AuthorID, &r.ObservedAt, &r.ValidUntil, &r.PublishedAt, &r.CreatedAt, &r.UpdatedAt, &r.IsLatest, &r.IsActivePublication, &r.WithdrawalReason, &screening, &analysis)
|
||||
if err != nil {
|
||||
return r, mapError(err)
|
||||
}
|
||||
@@ -39,6 +39,11 @@ func scanCandidate(row pgx.Row) (model.CandidateRevision, error) {
|
||||
return r, apperror.ErrUnavailable
|
||||
}
|
||||
}
|
||||
if len(analysis) > 0 {
|
||||
if err := json.Unmarshal(analysis, &r.AIAnalysis); err != nil {
|
||||
return r, apperror.ErrUnavailable
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
@@ -178,6 +183,14 @@ func (r *CandidateRepository) Transition(ctx context.Context, id, action string,
|
||||
return model.CandidateRevision{}, mapError(err)
|
||||
}
|
||||
defer tx.Rollback(context.Background())
|
||||
revision, err := transitionCandidate(ctx, tx, id, action, input, actor, requestID)
|
||||
if err != nil {
|
||||
return revision, err
|
||||
}
|
||||
return revision, mapError(tx.Commit(ctx))
|
||||
}
|
||||
|
||||
func transitionCandidate(ctx context.Context, tx pgx.Tx, id, action string, input model.CandidateActionInput, actor, requestID string) (model.CandidateRevision, error) {
|
||||
revision, err := lockCandidate(ctx, tx, id)
|
||||
if err != nil {
|
||||
return revision, err
|
||||
@@ -241,5 +254,5 @@ func (r *CandidateRepository) Transition(ctx context.Context, id, action string,
|
||||
return revision, err
|
||||
}
|
||||
}
|
||||
return revision, mapError(tx.Commit(ctx))
|
||||
return revision, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user