Initial commit
This commit is contained in:
78
src/lib/provingGround.js
Normal file
78
src/lib/provingGround.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { base44 } from '@/api/base44Client';
|
||||
|
||||
const CHALLENGE_EVAL_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
verdict: { type: 'string', enum: ['verified', 'needs_work', 'failed'] },
|
||||
score: { type: 'number', description: '0-100 weighted confidence score' },
|
||||
rubric: { type: 'object', additionalProperties: { type: 'number' }, description: 'criterion -> 0-100' },
|
||||
feedback: { type: 'string', description: '2-3 sentences, direct to the worker' },
|
||||
strengths: { type: 'array', items: { type: 'string' } },
|
||||
concerns: { type: 'array', items: { type: 'string' } }
|
||||
}
|
||||
};
|
||||
|
||||
/** Real-work unlock gate — shifts, reliability, and badges must be earned first. */
|
||||
export function isUnlocked(course, profile = {}) {
|
||||
const req = course?.unlock_requirements;
|
||||
if (!req) return { unlocked: true, reasons: [] };
|
||||
const reasons = [];
|
||||
const shifts = Number(profile.shifts_completed) || 0;
|
||||
const rel = Number(profile.reliability_score) || 0;
|
||||
const badges = (profile.earned_badges || []).map((b) => b.name);
|
||||
if (req.min_shifts && shifts < req.min_shifts) reasons.push(`${req.min_shifts} shifts completed (you have ${shifts})`);
|
||||
if (req.min_reliability && rel < req.min_reliability) reasons.push(`Reliability ${req.min_reliability}+ (you have ${rel})`);
|
||||
(req.required_badges || []).forEach((b) => { if (!badges.includes(b)) reasons.push(`Badge: ${b}`); });
|
||||
return { unlocked: reasons.length === 0, reasons };
|
||||
}
|
||||
|
||||
/** One sharp follow-up question during a roleplay challenge. */
|
||||
export async function challengeFollowUp(course, history) {
|
||||
const convo = history.map((m) => `${m.role === 'user' ? 'Worker' : 'KROW'}: ${m.content}`).join('\n');
|
||||
const prompt = `You are "KROW", running a short proving-ground challenge for the skill "${course?.proof_skill || course?.title}".
|
||||
Challenge scenario: ${course?.challenge?.prompt || course?.description}
|
||||
You already posed the scenario. Now ask ONE sharp follow-up question that tests whether the worker can actually perform under pressure. Keep it under 25 words. Do not praise. Just ask.
|
||||
Conversation so far:
|
||||
${convo}
|
||||
Ask your follow-up now. Respond with only the question.`;
|
||||
const res = await base44.integrations.Core.InvokeLLM({ prompt, model: 'gemini_3_flash' });
|
||||
return typeof res === 'string' ? res : res.text || String(res);
|
||||
}
|
||||
|
||||
/** Evaluate a worker's proof — transcript for roleplay, attached media for photo/video,
|
||||
* identified hazards for photo_identify (with the scene image attached). */
|
||||
export async function evaluateChallenge(course, { type, mediaUrl, transcript, identified, workerName }) {
|
||||
const ch = course?.challenge || {};
|
||||
const skill = course?.proof_skill || course?.title;
|
||||
const criteria = (ch.rubric || []).map((r) => r.criterion).join(', ') || 'overall_performance';
|
||||
const rubricInstr = (ch.rubric || []).length
|
||||
? `Score each criterion 0-100 in the "rubric" object: ${criteria}.`
|
||||
: 'Score "overall_performance" 0-100 in the rubric object.';
|
||||
|
||||
let mediaPart;
|
||||
const call = { response_json_schema: CHALLENGE_EVAL_SCHEMA, model: 'claude_sonnet_4_6' };
|
||||
|
||||
if (type === 'photo_identify') {
|
||||
const list = (identified || []).map((h) => `- "${h.label}" at (${Math.round(h.x * 100)}%, ${Math.round(h.y * 100)}%)`).join('\n') || '(no hazards marked)';
|
||||
mediaPart = `The worker was shown a kitchen photo and asked to identify cross-contamination and food-safety hazards. They marked these hazards:\n${list}\n\nAnalyze the attached kitchen photo and judge whether they identified the REAL risks (e.g. raw meat next to ready-to-eat food, same board for raw and cooked, soiled towels on food surfaces, food left in the temperature danger zone, unwashed hands). Reward correct, specific identifications; penalize misses and false positives.`;
|
||||
if (mediaUrl) call.file_urls = [mediaUrl];
|
||||
} else if (type === 'photo' || type === 'video') {
|
||||
mediaPart = `The worker uploaded a ${type} demonstrating the challenge. Analyze the attached ${type} carefully and judge whether they actually performed the skill correctly.`;
|
||||
if (mediaUrl) call.file_urls = [mediaUrl];
|
||||
} else {
|
||||
mediaPart = `Worker's responses (transcript):\n"""\n${transcript || '(no response)'}\n"""`;
|
||||
}
|
||||
|
||||
call.prompt = `You are KROW's Proving Ground evaluator. A worker named ${workerName || 'the worker'} is proving the skill "${skill}" via a ${type} challenge.
|
||||
|
||||
Challenge: ${ch.prompt || course?.description || 'Demonstrate the skill.'}
|
||||
|
||||
${mediaPart}
|
||||
|
||||
${rubricInstr}
|
||||
Compute a weighted score (0-100). verdict: "verified" if score>=70 and clearly competent, "needs_work" if 50-69, "failed" if <50.
|
||||
Give feedback (2-3 sentences, direct and specific to the worker), strengths, and concerns.
|
||||
Be rigorous — employers will trust this evidence. Do not inflate.`;
|
||||
const res = await base44.integrations.Core.InvokeLLM(call);
|
||||
return res;
|
||||
}
|
||||
Reference in New Issue
Block a user