Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
findProjectCatalogById,
findProjectCatalogPendingEvaluation,
promoteProjectsToEvaluate,
updateProjectCatalog,
Expand Down Expand Up @@ -43,6 +44,17 @@ export async function evaluateAndUpdateProject(project: IDbProjectCatalog): Prom
const qx = pgpQx(svc.postgres.writer.connection())
const startTime = Date.now()

// Guard: fetch fresh state to ensure the API is called at most once per project.
// Uses the writer connection to avoid replica lag missing a just-written evaluatedAt.
const fresh = await findProjectCatalogById(qx, project.id)
if (fresh?.evaluatedAt) {
log.info(
{ id: project.id, repoUrl: project.repoUrl, evaluatedAt: fresh.evaluatedAt },
'Project already evaluated, skipping API call.',
)
return
}
Comment thread
ulemons marked this conversation as resolved.

log.info({ id: project.id, repoUrl: project.repoUrl }, 'Starting evaluation.')

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

await updateProjectCatalog(qx, project.id, {
action: result.outcome,
evaluationResult: result.evaluationResult,
evaluationReason: result.evaluationReason,
evaluatedAt: new Date().toISOString(),
})

Expand All @@ -66,7 +80,8 @@ export async function evaluateAndUpdateProject(project: IDbProjectCatalog): Prom
id: project.id,
repoUrl: project.repoUrl,
outcome: result.outcome,
reason: result.reason,
evaluationResult: result.evaluationResult,
evaluationReason: result.evaluationReason,
elapsedSeconds,
},
'Evaluation complete.',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,86 @@
import { IEvaluationInput, IEvaluationResult } from './types'

// TODO: Replace with the actual AI evaluation algorithm once the external repo is integrated.
// The algorithm is described in the technical spec and currently takes ~30-40s per project
// at ~$0.15/project. Reference: https://github.com/... (link TBD).
interface IApiResponseContent {
onboard: boolean
non_onboard_reason?: string
}

export async function evaluateProject(input: IEvaluationInput): Promise<IEvaluationResult> {
console.error(`evaluateProject is not implemented yet for repo: ${input.repoUrl}`)
return { outcome: 'unsure', reason: 'evaluator not implemented' }
const endpoint = process.env.CROWD_PROJECT_EVALUATION_API_ENDPOINT
const userId = process.env.CROWD_PROJECT_EVALUATION_API_USER_ID
const secret = process.env.CROWD_PROJECT_EVALUATION_API_SECRET

if (!endpoint || !userId || !secret) {
return {
outcome: 'unsure',
evaluationResult: 'error',
evaluationReason:
'Missing API configuration: CROWD_PROJECT_EVALUATION_API_ENDPOINT, CROWD_PROJECT_EVALUATION_API_USER_ID, or CROWD_PROJECT_EVALUATION_API_SECRET',
}
}

const body = new URLSearchParams()
body.append('message', JSON.stringify({ repo_url: input.repoUrl }))
body.append('stream', 'false')
body.append('user_id', userId)

let response: Response
try {
response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Bearer ${secret}`,
},
body,
})
Comment thread
ulemons marked this conversation as resolved.
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
return {
outcome: 'unsure',
evaluationResult: 'error',
evaluationReason: `API request failed: ${message}`,
}
}

if (!response.ok) {
return {
outcome: 'unsure',
evaluationResult: 'error',
evaluationReason: `API returned HTTP ${response.status}: ${response.statusText}`,
}
}

let responseBody: unknown
try {
responseBody = await response.json()
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
return {
outcome: 'unsure',
evaluationResult: 'error',
evaluationReason: `Failed to parse API response: ${message}`,
}
}

const content = (responseBody as { content?: unknown } | null)?.content
if (
!content ||
typeof content !== 'object' ||
typeof (content as IApiResponseContent).onboard !== 'boolean'
) {
return {
outcome: 'unsure',
evaluationResult: 'error',
evaluationReason: `Unexpected API response shape: ${JSON.stringify(responseBody)}`,
}
}

const { onboard, non_onboard_reason } = content as IApiResponseContent

return {
outcome: onboard ? 'onboard' : 'skip',
evaluationResult: String(onboard),
evaluationReason: non_onboard_reason ?? null,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ export interface IEvaluationInput {
source: string | null
}

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

export interface IEvaluationResult {
outcome: EvaluationOutcome
reason: string
evaluationResult: string
evaluationReason: string | null
}
6 changes: 5 additions & 1 deletion services/apps/projects_evaluation_worker/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { Options, ServiceWorker } from '@crowd/archetype-worker'
import { scheduleProjectsEvaluation } from './schedules/scheduleProjectsEvaluation'

const config: Config = {
envvars: [],
envvars: [
'CROWD_PROJECT_EVALUATION_API_ENDPOINT',
'CROWD_PROJECT_EVALUATION_API_USER_ID',
'CROWD_PROJECT_EVALUATION_API_SECRET',
],
producer: {
enabled: false,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ export async function upsertProjectCatalog(
"repoName" = EXCLUDED."repoName",
"source" = COALESCE(EXCLUDED."source", "projectCatalog"."source"),
"action" = CASE
WHEN "projectCatalog"."action" IN ('onboard', 'unsure') THEN "projectCatalog"."action"
WHEN "projectCatalog"."action" IN ('onboard', 'skip', 'unsure') THEN "projectCatalog"."action"
WHEN EXCLUDED.action = 'evaluate' THEN 'evaluate'
ELSE "projectCatalog"."action"
END,
Expand Down Expand Up @@ -389,7 +389,7 @@ export async function bulkUpsertProjectCatalog(
"repoName" = EXCLUDED."repoName",
"source" = COALESCE(EXCLUDED."source", "projectCatalog"."source"),
"action" = CASE
WHEN "projectCatalog"."action" IN ('onboard', 'unsure') THEN "projectCatalog"."action"
WHEN "projectCatalog"."action" IN ('onboard', 'skip', 'unsure') THEN "projectCatalog"."action"
WHEN EXCLUDED.action = 'evaluate' THEN 'evaluate'
ELSE "projectCatalog"."action"
END,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type ProjectCatalogAction = 'auto' | 'evaluate' | 'onboard' | 'unsure'
export type ProjectCatalogAction = 'auto' | 'evaluate' | 'onboard' | 'skip' | 'unsure'

export interface IDbProjectCatalog {
id: string
Expand Down
Loading