Adjusted Career Coach for milestones/impacts/tasks.

This commit is contained in:
Josh 2025-06-10 15:46:13 +00:00
parent bb7ec5281e
commit a72dc8bcfb

View File

@ -8,6 +8,7 @@ import dotenv from 'dotenv';
import path from 'path'; import path from 'path';
import fs from 'fs/promises'; import fs from 'fs/promises';
import multer from 'multer'; import multer from 'multer';
import authFetch from '../src/utils/authFetch.js'; // Adjust path as needed
import mammoth from 'mammoth'; import mammoth from 'mammoth';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
@ -557,56 +558,185 @@ I'm here to support you with personalized coaching—what would you like to focu
} }
// B. Build a user summary that references all available info (unchanged from your code) // B. Build a user summary that references all available info (unchanged from your code)
function buildUserSummary({ userProfile, scenarioRow, financialProfile, collegeProfile, aiRisk }) { function buildUserSummary({
const userName = userProfile.first_name || "N/A"; userProfile = {},
const location = userProfile.location || "N/A"; scenarioRow = {},
const userGoals = userProfile.goals || []; financialProfile = {},
collegeProfile = {},
aiRisk = null,
salaryAnalysis = null,
economicProjections = null
}) {
// 1) USER PROFILE
const firstName = userProfile.firstname || "N/A";
const lastName = userProfile.lastname || "N/A";
const fullName = `${firstName} ${lastName}`;
const username = userProfile.username || "N/A";
const location = userProfile.area || userProfile.state || "Unknown Region";
// userProfile.career_situation might be "enhancing", "preparing", etc.
const careerSituation = userProfile.career_situation || "Not provided";
const careerName = scenarioRow?.career_name || "this career"; // RIASEC
const socCode = scenarioRow?.soc_code || "N/A"; let riasecText = "None";
const jobDescription = scenarioRow?.job_description || "No description"; if (userProfile.riasec_scores) {
const tasksList = scenarioRow?.tasks?.length try {
const rScores = JSON.parse(userProfile.riasec_scores);
// { "R":23,"I":25,"A":23,"S":16,"E":15,"C":22 }
riasecText = `
(R) Realistic: ${rScores.R}
(I) Investigative: ${rScores.I}
(A) Artistic: ${rScores.A}
(S) Social: ${rScores.S}
(E) Enterprising: ${rScores.E}
(C) Conventional: ${rScores.C}
`.trim();
} catch(e) {
console.error("Error parsing RIASEC JSON =>", e);
}
}
// Possibly parse "career_priorities" if you need them
let careerPriorities = "Not provided";
if (userProfile.career_priorities) {
// e.g. "career_priorities": "{\"interests\":\"Somewhat important\",\"meaning\":\"Somewhat important\",\"stability\":\"Very important\", ...}"
try {
const cP = JSON.parse(userProfile.career_priorities);
// Build a bullet string
careerPriorities = Object.entries(cP).map(([k,v]) => `- ${k}: ${v}`).join("\n");
} catch(e) {
console.error("Error parsing career_priorities =>", e);
}
}
// 2) CAREER SCENARIO
// scenarioRow might have career_name, job_description, tasks
// but you said sometimes you store them in scenarioRow or pass them in a separate param
const careerName = scenarioRow.career_name || "No career selected";
const socCode = scenarioRow.soc_code || "N/A";
const jobDescription = scenarioRow.job_description || "No jobDescription info";
// scenarioRow.tasks might be an array
const tasksList = Array.isArray(scenarioRow.tasks) && scenarioRow.tasks.length
? scenarioRow.tasks.join(", ") ? scenarioRow.tasks.join(", ")
: "No tasks info"; : "No tasks info";
const income = financialProfile?.income ? `$${financialProfile.income}` : "N/A"; // 3) FINANCIAL PROFILE
const debt = financialProfile?.debt ? `$${financialProfile.debt}` : "N/A"; // your actual JSON uses e.g. "current_salary", "additional_income"
const savings = financialProfile?.savings ? `$${financialProfile.savings}` : "N/A"; const currentSalary = financialProfile.current_salary || 0;
const additionalIncome = financialProfile.additional_income || 0;
const monthlyExpenses = financialProfile.monthly_expenses || 0;
const monthlyDebt = financialProfile.monthly_debt_payments || 0;
const retirementSavings = financialProfile.retirement_savings || 0;
const emergencyFund = financialProfile.emergency_fund || 0;
const major = collegeProfile?.major || "N/A"; // 4) COLLEGE PROFILE
const creditsCompleted = collegeProfile?.credits_completed || 0; // from your JSON:
const graduationDate = collegeProfile?.expected_graduation || "Unknown"; const selectedProgram = collegeProfile.selected_program || "N/A";
const enrollmentStatus = collegeProfile.college_enrollment_status || "Not enrolled";
const creditHoursCompleted = parseFloat(collegeProfile.hours_completed) || 0;
const programLength = parseFloat(collegeProfile.program_length) || 0;
const expectedGraduation = collegeProfile.expected_graduation || "Unknown";
const aiRiskReport = aiRisk?.riskLevel // 5) AI RISK
? `Risk Level: ${aiRisk.riskLevel}\nReasoning: ${aiRisk.reasoning}` // from aiRisk object
: "No AI risk info provided."; let riskText = "No AI risk info provided.";
if (aiRisk?.riskLevel) {
riskText = `Risk Level: ${aiRisk.riskLevel}
Reasoning: ${aiRisk.reasoning}`;
}
// 6) SALARY ANALYSIS
// e.g. { "regional": { ... }, "national": { ... } }
let salaryText = "No salary analysis provided.";
if (salaryAnalysis && salaryAnalysis.regional && salaryAnalysis.national) {
salaryText = `
[Regional Salary Range]
10th Percentile: $${salaryAnalysis.regional.regional_PCT10}
25th Percentile: $${salaryAnalysis.regional.regional_PCT25}
Median: $${salaryAnalysis.regional.regional_MEDIAN}
75th: $${salaryAnalysis.regional.regional_PCT75}
90th: $${salaryAnalysis.regional.regional_PCT90}
[National Salary Range]
10th Percentile: $${salaryAnalysis.national.national_PCT10}
25th Percentile: $${salaryAnalysis.national.national_PCT25}
Median: $${salaryAnalysis.national.national_MEDIAN}
75th: $${salaryAnalysis.national.national_PCT75}
90th: $${salaryAnalysis.national.national_PCT90}
`.trim();
}
// 7) ECONOMIC PROJECTIONS
// e.g. { "state": { ... }, "national": { ... } }
let econText = "No economic projections provided.";
if (economicProjections?.state && economicProjections.national) {
econText = `
[State Projections]
Area: ${economicProjections.state.area}
Base Year: ${economicProjections.state.baseYear}
Base Employment: ${economicProjections.state.base}
Projected Year: ${economicProjections.state.projectedYear}
Projected Employment: ${economicProjections.state.projection}
Change: ${economicProjections.state.change}
Percent Change: ${economicProjections.state.percentChange}%
Annual Openings: ${economicProjections.state.annualOpenings}
Occupation: ${economicProjections.state.occupationName}
[National Projections]
Area: ${economicProjections.national.area}
Base Year: ${economicProjections.national.baseYear}
Base Employment: ${economicProjections.national.base}
Projected Year: ${economicProjections.national.projectedYear}
Projected Employment: ${economicProjections.national.projection}
Change: ${economicProjections.national.change}
Percent Change: ${economicProjections.national.percentChange}%
Annual Openings: ${economicProjections.national.annualOpenings}
Occupation: ${economicProjections.national.occupationName}
`.trim();
}
// 8) BUILD THE FINAL TEXT
return ` return `
[USER PROFILE] [USER PROFILE]
- Name: ${userName} - Full Name: ${fullName}
- Username: ${username}
- Location: ${location} - Location: ${location}
- Goals: ${userGoals.length ? userGoals.join(", ") : "Not specified"} - Career Situation: ${careerSituation}
- RIASEC:
${riasecText}
Career Priorities:
${careerPriorities}
[TARGET CAREER] [TARGET CAREER]
- Career Name: ${careerName} - Career Name: ${careerName} (SOC: ${socCode})
- SOC Code: ${socCode}
- Job Description: ${jobDescription} - Job Description: ${jobDescription}
- Typical Tasks: ${tasksList} - Typical Tasks: ${tasksList}
[FINANCIAL PROFILE] [FINANCIAL PROFILE]
- Income: ${income} - Current Salary: $${currentSalary}
- Debt: ${debt} - Additional Income: $${additionalIncome}
- Savings: ${savings} - Monthly Expenses: $${monthlyExpenses}
- Monthly Debt: $${monthlyDebt}
- Retirement Savings: $${retirementSavings}
- Emergency Fund: $${emergencyFund}
[COLLEGE / EDUCATION] [COLLEGE / EDUCATION]
- Major: ${major} - Program: ${selectedProgram} (Status: ${enrollmentStatus})
- Credits Completed: ${creditsCompleted} - Credits Completed: ${creditHoursCompleted}
- Expected Graduation Date: ${graduationDate} - Program Length: ${programLength}
- Expected Graduation: ${expectedGraduation}
[AI RISK ANALYSIS] [AI RISK ANALYSIS]
${aiRiskReport} ${riskText}
`.trim();
} [SALARY ANALYSIS]
${salaryText}
[ECONOMIC PROJECTIONS]
${econText}
`.trim();
}
// (No changes to your environment configs) // (No changes to your environment configs)
@ -719,96 +849,125 @@ ${summaryText}
model: "gpt-4", model: "gpt-4",
messages: messagesToSend, messages: messagesToSend,
temperature: 0.7, temperature: 0.7,
max_tokens: 600 max_tokens: 1000
}); });
let coachResponse = completion?.choices?.[0]?.message?.content?.trim(); // 4) Grab the response text
const rawReply = completion?.choices?.[0]?.message?.content?.trim() || "";
// ------------------------------------------------ if (!rawReply) {
// 7. Detect and Possibly Save Milestones (unchanged) return res.json({
// ------------------------------------------------ reply: "Sorry, I didn't get a response. Could you please try again?"
let milestones = []; });
let isMilestoneFormat = false;
try {
milestones = JSON.parse(coachResponse);
isMilestoneFormat = Array.isArray(milestones);
} catch (e) {
isMilestoneFormat = false;
} }
if (isMilestoneFormat && milestones.length) { // 5) Default: Just return raw text to front-end
const MILESTONE_API_URL = process.env.APTIVA_INTERNAL_API let replyToClient = rawReply;
? `${process.env.APTIVA_INTERNAL_API}/premium/milestone` let createdMilestonesData = [];
: "http://localhost:5002/api/premium/milestone";
const rawForMilestonesEndpoint = milestones.map(m => { // If the AI sent JSON (plan with milestones), parse & create in DB
return { if (rawReply.startsWith("{") || rawReply.startsWith("[")) {
title: m.title, try {
description: m.description, const planObj = JSON.parse(rawReply);
date: m.date,
career_profile_id: scenarioRow?.id // The AI plan is expected to have: planObj.milestones[]
if (planObj && Array.isArray(planObj.milestones)) {
for (const milestone of planObj.milestones) {
// Create the milestone
const milestoneBody = {
title: milestone.title,
description: milestone.description || "",
date: milestone.date,
career_profile_id: scenarioRow.id, // or scenarioRow.career_profile_id
status: "planned",
progress: 0,
is_universal: false
}; };
});
const mileRes = await fetch(MILESTONE_API_URL, { // Call your existing milestone endpoint
const msRes = await authFetch("/api/premium/milestone", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ milestones: rawForMilestonesEndpoint }) body: JSON.stringify(milestoneBody)
}); });
const createdMs = await msRes.json();
if (!mileRes.ok) { // Figure out the new milestone ID
console.error("Failed to save milestones =>", mileRes.status); let newMilestoneId = null;
coachResponse = "I prepared milestones, but couldn't save them right now. Please try again later."; if (Array.isArray(createdMs) && createdMs[0]) {
} else { newMilestoneId = createdMs[0].id;
const createdMils = await mileRes.json(); } else if (createdMs.id) {
newMilestoneId = createdMs.id;
}
const IMPACT_API_URL = process.env.APTIVA_INTERNAL_API // If we have a milestoneId, create tasks & impacts
? `${process.env.APTIVA_INTERNAL_API}/premium/milestone-impacts` if (newMilestoneId) {
: "http://localhost:5002/api/premium/milestone-impacts"; // tasks
if (Array.isArray(milestone.tasks)) {
for (const t of milestone.tasks) {
const taskBody = {
milestone_id: newMilestoneId,
title: t.title,
description: t.description || "",
due_date: t.due_date || null
};
await authFetch("/api/premium/tasks", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(taskBody)
});
}
}
for (let i = 0; i < milestones.length; i++) { // impacts
const originalMilestone = milestones[i]; if (Array.isArray(milestone.impacts)) {
const newMilestone = createdMils[i]; for (const imp of milestone.impacts) {
const impactBody = {
if (Array.isArray(originalMilestone.impacts) && originalMilestone.impacts.length) { milestone_id: newMilestoneId,
for (const imp of originalMilestone.impacts) {
const impactPayload = {
milestone_id: newMilestone.id,
impact_type: imp.impact_type, impact_type: imp.impact_type,
direction: imp.direction || 'subtract', direction: imp.direction,
amount: imp.amount || 0, amount: imp.amount,
start_date: imp.start_date || null, start_date: imp.start_date || null,
end_date: imp.end_date || null end_date: imp.end_date || null
}; };
await authFetch("/api/premium/milestone-impacts", {
try {
const impactRes = await fetch(IMPACT_API_URL, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify(impactPayload) body: JSON.stringify(impactBody)
}); });
if (!impactRes.ok) {
console.error(`Failed to create impact for milestone ${newMilestone.id}`);
}
} catch (err) {
console.error(`Error creating impact for milestone ${newMilestone.id}`, err);
}
}
} }
} }
coachResponse = "I've created some actionable milestones for you (with financial impacts). You can view them in your roadmap!"; // Keep track of the newly created milestone
createdMilestonesData.push({
milestoneId: newMilestoneId,
title: milestone.title
});
} }
} }
// ------------------------------------------------ // If we successfully created at least 1 milestone,
// 8. Send JSON Response (unchanged) // override the reply with a success message
// ------------------------------------------------ if (createdMilestonesData.length > 0) {
res.json({ reply: coachResponse, aiRisk }); replyToClient = `
I've created ${createdMilestonesData.length} milestones (with tasks & impacts) for you in this scenario.
Check your Milestones tab. Let me know if you want any changes!
`.trim();
}
}
} catch (parseErr) {
console.error("Error parsing AI JSON =>", parseErr);
// We'll just keep the raw AI text if parsing fails
}
}
// 6) Finally, respond to front-end
return res.json({
reply: replyToClient,
createdMilestones: createdMilestonesData
});
} catch (err) { } catch (err) {
console.error("Error in /api/premium/ai/chat =>", err); console.error("Error in /api/premium/ai/chat =>", err);
res.status(500).json({ error: "Failed to generate conversational response." }); return res.status(500).json({ error: "Failed to process AI chat." });
} }
}); });