Skip to content

Commit eba296c

Browse files
authored
feat: add evaluation api call (CM-1168) (#4140)
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent c935e95 commit eba296c

6 files changed

Lines changed: 110 additions & 13 deletions

File tree

services/apps/projects_evaluation_worker/src/activities/activities.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
findProjectCatalogById,
23
findProjectCatalogPendingEvaluation,
34
promoteProjectsToEvaluate,
45
updateProjectCatalog,
@@ -43,6 +44,17 @@ export async function evaluateAndUpdateProject(project: IDbProjectCatalog): Prom
4344
const qx = pgpQx(svc.postgres.writer.connection())
4445
const startTime = Date.now()
4546

47+
// Guard: fetch fresh state to ensure the API is called at most once per project.
48+
// Uses the writer connection to avoid replica lag missing a just-written evaluatedAt.
49+
const fresh = await findProjectCatalogById(qx, project.id)
50+
if (fresh?.evaluatedAt) {
51+
log.info(
52+
{ id: project.id, repoUrl: project.repoUrl, evaluatedAt: fresh.evaluatedAt },
53+
'Project already evaluated, skipping API call.',
54+
)
55+
return
56+
}
57+
4658
log.info({ id: project.id, repoUrl: project.repoUrl }, 'Starting evaluation.')
4759

4860
const result = await evaluateProject({
@@ -56,6 +68,8 @@ export async function evaluateAndUpdateProject(project: IDbProjectCatalog): Prom
5668

5769
await updateProjectCatalog(qx, project.id, {
5870
action: result.outcome,
71+
evaluationResult: result.evaluationResult,
72+
evaluationReason: result.evaluationReason,
5973
evaluatedAt: new Date().toISOString(),
6074
})
6175

@@ -66,7 +80,8 @@ export async function evaluateAndUpdateProject(project: IDbProjectCatalog): Prom
6680
id: project.id,
6781
repoUrl: project.repoUrl,
6882
outcome: result.outcome,
69-
reason: result.reason,
83+
evaluationResult: result.evaluationResult,
84+
evaluationReason: result.evaluationReason,
7085
elapsedSeconds,
7186
},
7287
'Evaluation complete.',
Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,86 @@
11
import { IEvaluationInput, IEvaluationResult } from './types'
22

3-
// TODO: Replace with the actual AI evaluation algorithm once the external repo is integrated.
4-
// The algorithm is described in the technical spec and currently takes ~30-40s per project
5-
// at ~$0.15/project. Reference: https://github.com/... (link TBD).
3+
interface IApiResponseContent {
4+
onboard: boolean
5+
non_onboard_reason?: string
6+
}
7+
68
export async function evaluateProject(input: IEvaluationInput): Promise<IEvaluationResult> {
7-
console.error(`evaluateProject is not implemented yet for repo: ${input.repoUrl}`)
8-
return { outcome: 'unsure', reason: 'evaluator not implemented' }
9+
const endpoint = process.env.CROWD_PROJECT_EVALUATION_API_ENDPOINT
10+
const userId = process.env.CROWD_PROJECT_EVALUATION_API_USER_ID
11+
const secret = process.env.CROWD_PROJECT_EVALUATION_API_SECRET
12+
13+
if (!endpoint || !userId || !secret) {
14+
return {
15+
outcome: 'unsure',
16+
evaluationResult: 'error',
17+
evaluationReason:
18+
'Missing API configuration: CROWD_PROJECT_EVALUATION_API_ENDPOINT, CROWD_PROJECT_EVALUATION_API_USER_ID, or CROWD_PROJECT_EVALUATION_API_SECRET',
19+
}
20+
}
21+
22+
const body = new URLSearchParams()
23+
body.append('message', JSON.stringify({ repo_url: input.repoUrl }))
24+
body.append('stream', 'false')
25+
body.append('user_id', userId)
26+
27+
let response: Response
28+
try {
29+
response = await fetch(endpoint, {
30+
method: 'POST',
31+
headers: {
32+
'Content-Type': 'application/x-www-form-urlencoded',
33+
Authorization: `Bearer ${secret}`,
34+
},
35+
body,
36+
})
37+
} catch (err) {
38+
const message = err instanceof Error ? err.message : String(err)
39+
return {
40+
outcome: 'unsure',
41+
evaluationResult: 'error',
42+
evaluationReason: `API request failed: ${message}`,
43+
}
44+
}
45+
46+
if (!response.ok) {
47+
return {
48+
outcome: 'unsure',
49+
evaluationResult: 'error',
50+
evaluationReason: `API returned HTTP ${response.status}: ${response.statusText}`,
51+
}
52+
}
53+
54+
let responseBody: unknown
55+
try {
56+
responseBody = await response.json()
57+
} catch (err) {
58+
const message = err instanceof Error ? err.message : String(err)
59+
return {
60+
outcome: 'unsure',
61+
evaluationResult: 'error',
62+
evaluationReason: `Failed to parse API response: ${message}`,
63+
}
64+
}
65+
66+
const content = (responseBody as { content?: unknown } | null)?.content
67+
if (
68+
!content ||
69+
typeof content !== 'object' ||
70+
typeof (content as IApiResponseContent).onboard !== 'boolean'
71+
) {
72+
return {
73+
outcome: 'unsure',
74+
evaluationResult: 'error',
75+
evaluationReason: `Unexpected API response shape: ${JSON.stringify(responseBody)}`,
76+
}
77+
}
78+
79+
const { onboard, non_onboard_reason } = content as IApiResponseContent
80+
81+
return {
82+
outcome: onboard ? 'onboard' : 'skip',
83+
evaluationResult: String(onboard),
84+
evaluationReason: non_onboard_reason ?? null,
85+
}
986
}

services/apps/projects_evaluation_worker/src/evaluator/types.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ export interface IEvaluationInput {
99
source: string | null
1010
}
1111

12-
// Evaluation can only resolve to 'onboard' or 'unsure' — never back to 'evaluate' or 'auto'.
13-
export type EvaluationOutcome = Extract<ProjectCatalogAction, 'onboard' | 'unsure'>
12+
// Evaluation can only resolve to 'onboard', 'skip', or 'unsure' — never back to 'evaluate' or 'auto'.
13+
export type EvaluationOutcome = Extract<ProjectCatalogAction, 'onboard' | 'skip' | 'unsure'>
1414

1515
export interface IEvaluationResult {
1616
outcome: EvaluationOutcome
17-
reason: string
17+
evaluationResult: string
18+
evaluationReason: string | null
1819
}

services/apps/projects_evaluation_worker/src/main.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { Options, ServiceWorker } from '@crowd/archetype-worker'
44
import { scheduleProjectsEvaluation } from './schedules/scheduleProjectsEvaluation'
55

66
const config: Config = {
7-
envvars: [],
7+
envvars: [
8+
'CROWD_PROJECT_EVALUATION_API_ENDPOINT',
9+
'CROWD_PROJECT_EVALUATION_API_USER_ID',
10+
'CROWD_PROJECT_EVALUATION_API_SECRET',
11+
],
812
producer: {
913
enabled: false,
1014
},

services/libs/data-access-layer/src/project-catalog/projectCatalog.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ export async function upsertProjectCatalog(
316316
"repoName" = EXCLUDED."repoName",
317317
"source" = COALESCE(EXCLUDED."source", "projectCatalog"."source"),
318318
"action" = CASE
319-
WHEN "projectCatalog"."action" IN ('onboard', 'unsure') THEN "projectCatalog"."action"
319+
WHEN "projectCatalog"."action" IN ('onboard', 'skip', 'unsure') THEN "projectCatalog"."action"
320320
WHEN EXCLUDED.action = 'evaluate' THEN 'evaluate'
321321
ELSE "projectCatalog"."action"
322322
END,
@@ -389,7 +389,7 @@ export async function bulkUpsertProjectCatalog(
389389
"repoName" = EXCLUDED."repoName",
390390
"source" = COALESCE(EXCLUDED."source", "projectCatalog"."source"),
391391
"action" = CASE
392-
WHEN "projectCatalog"."action" IN ('onboard', 'unsure') THEN "projectCatalog"."action"
392+
WHEN "projectCatalog"."action" IN ('onboard', 'skip', 'unsure') THEN "projectCatalog"."action"
393393
WHEN EXCLUDED.action = 'evaluate' THEN 'evaluate'
394394
ELSE "projectCatalog"."action"
395395
END,

services/libs/data-access-layer/src/project-catalog/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export type ProjectCatalogAction = 'auto' | 'evaluate' | 'onboard' | 'unsure'
1+
export type ProjectCatalogAction = 'auto' | 'evaluate' | 'onboard' | 'skip' | 'unsure'
22

33
export interface IDbProjectCatalog {
44
id: string

0 commit comments

Comments
 (0)