-
Notifications
You must be signed in to change notification settings - Fork 5
PM-3926 ai screening phase #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
cb6a883
da5ac6f
ea21c0c
da805ad
6905bed
302a12e
e202ccc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| -- Insert AI Screening phase | ||
| INSERT INTO "Phase" ( | ||
| "id", | ||
| "name", | ||
| "description", | ||
| "isOpen", | ||
| "duration", | ||
| "createdAt", | ||
| "createdBy", | ||
| "updatedAt", | ||
| "updatedBy" | ||
| ) VALUES ( | ||
| '9f4e3b2a-7c1d-4e9f-b8a6-5d3c1a9f2b4e', | ||
| 'AI Screening', | ||
| 'AI Screening Phase', | ||
| true, | ||
| 14400, | ||
| '2025-03-10T13:08:02.378Z', | ||
kkartunov marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 'topcoder user', | ||
| '2025-03-10T13:08:02.378Z', | ||
| 'topcoder user' | ||
| ) | ||
| ON CONFLICT DO NOTHING; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,9 +3,11 @@ const challengeTrackService = require("../services/ChallengeTrackService"); | |
| const timelineTemplateService = require("../services/TimelineTemplateService"); | ||
| const HttpStatus = require("http-status-codes"); | ||
| const _ = require("lodash"); | ||
| const moment = require("moment"); | ||
| const errors = require("./errors"); | ||
| const config = require("config"); | ||
| const helper = require("./helper"); | ||
| const phaseHelper = require("./phase-helper"); | ||
| const axios = require("axios"); | ||
| const { getM2MToken } = require("./m2m-helper"); | ||
| const { hasAdminRole } = require("./role-helper"); | ||
|
|
@@ -509,6 +511,138 @@ class ChallengeHelper { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Add AI Screening phase for challenges with AI reviewers. | ||
| * The AI screening phase is positioned after submission and allocated 4 hours by default. | ||
| * | ||
| * @param {Object} challenge challenge payload (mutated in-place) | ||
| * @param {Object} prisma Prisma client | ||
| * @param {Function} logDebugMessage optional logging function | ||
| */ | ||
| async addAIScreeningPhaseForChallengeCreation(challenge, prisma, logDebugMessage = () => {}) { | ||
| if (!challenge || !challenge.phases || !Array.isArray(challenge.reviewers)) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| return; | ||
| } | ||
|
|
||
| // Check if there are any AI reviewers | ||
| const hasAIReviewers = challenge.reviewers.some((reviewer) => !reviewer.isMemberReview && reviewer.aiWorkflowId); | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| if (!hasAIReviewers) { | ||
| logDebugMessage("no AI reviewers found, skipping AI screening phase creation"); | ||
| return; | ||
| } | ||
|
|
||
| // Check if AI Screening phase already exists | ||
| const aiScreeningPhaseExists = challenge.phases.some((phase) => phase.name === "AI Screening"); | ||
| if (aiScreeningPhaseExists) { | ||
| logDebugMessage("AI screening phase already exists, skipping creation"); | ||
| return; | ||
| } | ||
|
|
||
| // Find the submission phase | ||
| const submissionPhaseName = SUBMISSION_PHASE_PRIORITY.find((name) => | ||
| challenge.phases.some((phase) => phase.name === name) | ||
| ); | ||
|
|
||
| if (!submissionPhaseName) { | ||
| throw new errors.BadRequestError( | ||
| `Cannot add AI screening phase: no submission phase found in challenge` | ||
| ); | ||
| } | ||
|
|
||
| // Get the AI Screening phase definition from the database | ||
| const { phaseDefinitionMap } = await phaseHelper.getPhaseDefinitionsAndMap(); | ||
| const aiScreeningPhaseDefEntry = Array.from(phaseDefinitionMap.entries()).find( | ||
| ([_, phase]) => phase.name === "AI Screening" | ||
| ); | ||
|
|
||
| if (!aiScreeningPhaseDefEntry) { | ||
| throw new errors.BadRequestError( | ||
| `AI Screening phase definition not found in the system` | ||
| ); | ||
| } | ||
|
|
||
| const [aiScreeningPhaseId, aiScreeningPhaseDef] = aiScreeningPhaseDefEntry; | ||
|
|
||
| // Find the submission phase in the challenge phases | ||
| const submissionPhase = challenge.phases.find((phase) => phase.name === submissionPhaseName); | ||
| if (!submissionPhase) { | ||
| throw new errors.BadRequestError( | ||
| `Cannot add AI screening phase: submission phase not found in challenge phases` | ||
| ); | ||
| } | ||
|
|
||
| const reviewPhases = challenge.phases.filter( | ||
| (phase) => | ||
| phase.name && | ||
| phase.name.toLowerCase().includes("review") && | ||
| phase.predecessor === submissionPhase.phaseId | ||
| ); | ||
|
|
||
| // Create the AI Screening challenge phase | ||
| const aiScreeningPhase = { | ||
| phaseId: aiScreeningPhaseId, | ||
| name: "AI Screening", | ||
| description: aiScreeningPhaseDef.description, | ||
| duration: 14400, // 4 hours in seconds | ||
| isOpen: false, | ||
| predecessor: submissionPhase.phaseId, // predecessor is the submission phase's phaseId | ||
| constraints: [], | ||
| scheduledStartDate: undefined, | ||
| scheduledEndDate: undefined, | ||
| actualStartDate: undefined, | ||
| actualEndDate: undefined, | ||
| }; | ||
|
|
||
| // Ensure AI Screening is ordered between submission and review phases in the phases array. | ||
| const firstReviewIndex = challenge.phases.indexOf(reviewPhases[0]); | ||
| logDebugMessage(`(firstReviewIndex=${firstReviewIndex})`) | ||
| if (firstReviewIndex >= 0) { | ||
| challenge.phases.splice(firstReviewIndex, 0, aiScreeningPhase); | ||
| } else { | ||
| challenge.phases.push(aiScreeningPhase); | ||
| } | ||
|
|
||
| // Re-link review phase(s) so they start after AI Screening instead of submission. | ||
| reviewPhases.forEach((phase) => { | ||
| phase.predecessor = aiScreeningPhase.phaseId; | ||
| }); | ||
|
|
||
| // Recalculate phase dates to keep timeline in sync | ||
| if (submissionPhase.scheduledEndDate) { | ||
| aiScreeningPhase.scheduledStartDate = submissionPhase.scheduledEndDate; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| aiScreeningPhase.scheduledEndDate = moment(aiScreeningPhase.scheduledStartDate) | ||
| .add(aiScreeningPhase.duration, "seconds") | ||
| .toDate() | ||
| .toISOString(); | ||
|
|
||
| logDebugMessage( | ||
| `AI screening phase dates calculated (start=${aiScreeningPhase.scheduledStartDate}, end=${aiScreeningPhase.scheduledEndDate})` | ||
| ); | ||
|
|
||
| // Update dates for review phases that now depend on AI Screening | ||
| reviewPhases.forEach((phase) => { | ||
| if (_.isNil(phase.actualStartDate)) { | ||
| phase.scheduledStartDate = aiScreeningPhase.scheduledEndDate; | ||
| if (phase.duration) { | ||
| phase.scheduledEndDate = moment(phase.scheduledStartDate) | ||
| .add(phase.duration, "seconds") | ||
| .toDate() | ||
| .toISOString(); | ||
|
|
||
| logDebugMessage( | ||
| `Updated ${phase.name} phase dates (start=${phase.scheduledStartDate}, end=${phase.scheduledEndDate})` | ||
| ); | ||
| } | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| logDebugMessage( | ||
| `AI screening phase added (phaseId=${aiScreeningPhase.phaseId}), updated ${reviewPhases.length} review predecessor(s)` | ||
| ); | ||
| } | ||
|
|
||
| async validateChallengeUpdateRequest(currentUser, challenge, data, challengeResources) { | ||
| if (process.env.LOCAL != "true") { | ||
| await helper.ensureUserCanModifyChallenge(currentUser, challenge, challengeResources); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,8 @@ const errors = require("./errors"); | |
| const timelineTemplateService = require("../services/TimelineTemplateService"); | ||
| const prisma = require("../common/prisma").getClient(); | ||
|
|
||
| const SUBMISSION_PHASE_PRIORITY = ["Topgear Submission", "Topcoder Submission", "Submission"]; | ||
|
|
||
| class ChallengePhaseHelper { | ||
| phaseDefinitionMap = {}; | ||
| timelineTemplateMap = {}; | ||
|
|
@@ -98,9 +100,27 @@ class ChallengePhaseHelper { | |
| // to incorrectly push earlier phases forward. Sorting by template order prevents that. | ||
| const orderIndex = new Map(); | ||
| _.each(timelineTempate, (tplPhase, idx) => orderIndex.set(tplPhase.phaseId, idx)); | ||
| const submissionPhaseName = SUBMISSION_PHASE_PRIORITY.find((name) => | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [ |
||
| _.some(challengePhases, (phase) => phase.name === name) | ||
| ); | ||
| const submissionPhase = submissionPhaseName | ||
| ? _.find(challengePhases, (phase) => phase.name === submissionPhaseName) | ||
| : null; | ||
| const submissionOrderIndex = _.isNil(submissionPhase) | ||
| ? null | ||
| : orderIndex.get(submissionPhase.phaseId); | ||
| const challengePhasesOrdered = _.sortBy( | ||
| challengePhases, | ||
| (p) => orderIndex.get(p.phaseId) ?? Number.MAX_SAFE_INTEGER | ||
| (p) => { | ||
| const templateOrder = orderIndex.get(p.phaseId); | ||
| if (!_.isNil(templateOrder)) { | ||
| return templateOrder; | ||
| } | ||
| if (p.name === "AI Screening" && !_.isNil(submissionOrderIndex)) { | ||
| return submissionOrderIndex + 0.5; | ||
| } | ||
| return Number.MAX_SAFE_INTEGER; | ||
| } | ||
| ); | ||
|
|
||
| let fixedStartDate = undefined; | ||
|
|
@@ -159,6 +179,27 @@ class ChallengePhaseHelper { | |
| } | ||
| return updatedPhase; | ||
| }); | ||
|
|
||
| const aiScreeningPhase = _.find(updatedPhases, (phase) => phase.name === "AI Screening"); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [💡 |
||
| const updateSubmissionPhaseName = SUBMISSION_PHASE_PRIORITY.find((name) => | ||
| _.some(updatedPhases, (phase) => phase.name === name) | ||
| ); | ||
| const updateSubmissionPhase = updateSubmissionPhaseName | ||
| ? _.find(updatedPhases, (phase) => phase.name === updateSubmissionPhaseName) | ||
| : null; | ||
| if (!_.isNil(aiScreeningPhase) && !_.isNil(updateSubmissionPhase)) { | ||
| aiScreeningPhase.predecessor = updateSubmissionPhase.phaseId; | ||
| _.each(updatedPhases, (phase) => { | ||
| if ( | ||
| phase.name && | ||
| phase.name.toLowerCase().includes("review") && | ||
| phase.predecessor === updateSubmissionPhase.phaseId | ||
| ) { | ||
| phase.predecessor = aiScreeningPhase.phaseId; | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| let iterativeReviewSet = false; | ||
| for (let phase of updatedPhases) { | ||
| if (_.isNil(phase.predecessor)) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.