Adjusted Career Coach for milestones/impacts/tasks.
This commit is contained in:
parent
bb7ec5281e
commit
a72dc8bcfb
@ -8,6 +8,7 @@ import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import multer from 'multer';
|
||||
import authFetch from '../src/utils/authFetch.js'; // Adjust path as needed
|
||||
import mammoth from 'mammoth';
|
||||
import { fileURLToPath } from 'url';
|
||||
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)
|
||||
function buildUserSummary({ userProfile, scenarioRow, financialProfile, collegeProfile, aiRisk }) {
|
||||
const userName = userProfile.first_name || "N/A";
|
||||
const location = userProfile.location || "N/A";
|
||||
const userGoals = userProfile.goals || [];
|
||||
function buildUserSummary({
|
||||
userProfile = {},
|
||||
scenarioRow = {},
|
||||
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";
|
||||
const socCode = scenarioRow?.soc_code || "N/A";
|
||||
const jobDescription = scenarioRow?.job_description || "No description";
|
||||
const tasksList = scenarioRow?.tasks?.length
|
||||
? scenarioRow.tasks.join(", ")
|
||||
: "No tasks info";
|
||||
// RIASEC
|
||||
let riasecText = "None";
|
||||
if (userProfile.riasec_scores) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const income = financialProfile?.income ? `$${financialProfile.income}` : "N/A";
|
||||
const debt = financialProfile?.debt ? `$${financialProfile.debt}` : "N/A";
|
||||
const savings = financialProfile?.savings ? `$${financialProfile.savings}` : "N/A";
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
const major = collegeProfile?.major || "N/A";
|
||||
const creditsCompleted = collegeProfile?.credits_completed || 0;
|
||||
const graduationDate = collegeProfile?.expected_graduation || "Unknown";
|
||||
// 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(", ")
|
||||
: "No tasks info";
|
||||
|
||||
const aiRiskReport = aiRisk?.riskLevel
|
||||
? `Risk Level: ${aiRisk.riskLevel}\nReasoning: ${aiRisk.reasoning}`
|
||||
: "No AI risk info provided.";
|
||||
// 3) FINANCIAL PROFILE
|
||||
// your actual JSON uses e.g. "current_salary", "additional_income"
|
||||
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;
|
||||
|
||||
return `
|
||||
// 4) COLLEGE PROFILE
|
||||
// from your JSON:
|
||||
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";
|
||||
|
||||
// 5) AI RISK
|
||||
// from aiRisk object
|
||||
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 `
|
||||
[USER PROFILE]
|
||||
- Name: ${userName}
|
||||
- Full Name: ${fullName}
|
||||
- Username: ${username}
|
||||
- Location: ${location}
|
||||
- Goals: ${userGoals.length ? userGoals.join(", ") : "Not specified"}
|
||||
- Career Situation: ${careerSituation}
|
||||
- RIASEC:
|
||||
${riasecText}
|
||||
|
||||
Career Priorities:
|
||||
${careerPriorities}
|
||||
|
||||
[TARGET CAREER]
|
||||
- Career Name: ${careerName}
|
||||
- SOC Code: ${socCode}
|
||||
- Career Name: ${careerName} (SOC: ${socCode})
|
||||
- Job Description: ${jobDescription}
|
||||
- Typical Tasks: ${tasksList}
|
||||
|
||||
[FINANCIAL PROFILE]
|
||||
- Income: ${income}
|
||||
- Debt: ${debt}
|
||||
- Savings: ${savings}
|
||||
- Current Salary: $${currentSalary}
|
||||
- Additional Income: $${additionalIncome}
|
||||
- Monthly Expenses: $${monthlyExpenses}
|
||||
- Monthly Debt: $${monthlyDebt}
|
||||
- Retirement Savings: $${retirementSavings}
|
||||
- Emergency Fund: $${emergencyFund}
|
||||
|
||||
[COLLEGE / EDUCATION]
|
||||
- Major: ${major}
|
||||
- Credits Completed: ${creditsCompleted}
|
||||
- Expected Graduation Date: ${graduationDate}
|
||||
- Program: ${selectedProgram} (Status: ${enrollmentStatus})
|
||||
- Credits Completed: ${creditHoursCompleted}
|
||||
- Program Length: ${programLength}
|
||||
- Expected Graduation: ${expectedGraduation}
|
||||
|
||||
[AI RISK ANALYSIS]
|
||||
${aiRiskReport}
|
||||
`.trim();
|
||||
}
|
||||
${riskText}
|
||||
|
||||
[SALARY ANALYSIS]
|
||||
${salaryText}
|
||||
|
||||
[ECONOMIC PROJECTIONS]
|
||||
${econText}
|
||||
`.trim();
|
||||
}
|
||||
|
||||
|
||||
// (No changes to your environment configs)
|
||||
|
||||
@ -719,96 +849,125 @@ ${summaryText}
|
||||
model: "gpt-4",
|
||||
messages: messagesToSend,
|
||||
temperature: 0.7,
|
||||
max_tokens: 600
|
||||
max_tokens: 1000
|
||||
});
|
||||
|
||||
let coachResponse = completion?.choices?.[0]?.message?.content?.trim();
|
||||
|
||||
// ------------------------------------------------
|
||||
// 7. Detect and Possibly Save Milestones (unchanged)
|
||||
// ------------------------------------------------
|
||||
let milestones = [];
|
||||
let isMilestoneFormat = false;
|
||||
|
||||
try {
|
||||
milestones = JSON.parse(coachResponse);
|
||||
isMilestoneFormat = Array.isArray(milestones);
|
||||
} catch (e) {
|
||||
isMilestoneFormat = false;
|
||||
// 4) Grab the response text
|
||||
const rawReply = completion?.choices?.[0]?.message?.content?.trim() || "";
|
||||
if (!rawReply) {
|
||||
return res.json({
|
||||
reply: "Sorry, I didn't get a response. Could you please try again?"
|
||||
});
|
||||
}
|
||||
|
||||
if (isMilestoneFormat && milestones.length) {
|
||||
const MILESTONE_API_URL = process.env.APTIVA_INTERNAL_API
|
||||
? `${process.env.APTIVA_INTERNAL_API}/premium/milestone`
|
||||
: "http://localhost:5002/api/premium/milestone";
|
||||
// 5) Default: Just return raw text to front-end
|
||||
let replyToClient = rawReply;
|
||||
let createdMilestonesData = [];
|
||||
|
||||
const rawForMilestonesEndpoint = milestones.map(m => {
|
||||
return {
|
||||
title: m.title,
|
||||
description: m.description,
|
||||
date: m.date,
|
||||
career_profile_id: scenarioRow?.id
|
||||
};
|
||||
});
|
||||
// If the AI sent JSON (plan with milestones), parse & create in DB
|
||||
if (rawReply.startsWith("{") || rawReply.startsWith("[")) {
|
||||
try {
|
||||
const planObj = JSON.parse(rawReply);
|
||||
|
||||
const mileRes = await fetch(MILESTONE_API_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ milestones: rawForMilestonesEndpoint })
|
||||
});
|
||||
// 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
|
||||
};
|
||||
|
||||
if (!mileRes.ok) {
|
||||
console.error("Failed to save milestones =>", mileRes.status);
|
||||
coachResponse = "I prepared milestones, but couldn't save them right now. Please try again later.";
|
||||
} else {
|
||||
const createdMils = await mileRes.json();
|
||||
// Call your existing milestone endpoint
|
||||
const msRes = await authFetch("/api/premium/milestone", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(milestoneBody)
|
||||
});
|
||||
const createdMs = await msRes.json();
|
||||
|
||||
const IMPACT_API_URL = process.env.APTIVA_INTERNAL_API
|
||||
? `${process.env.APTIVA_INTERNAL_API}/premium/milestone-impacts`
|
||||
: "http://localhost:5002/api/premium/milestone-impacts";
|
||||
// Figure out the new milestone ID
|
||||
let newMilestoneId = null;
|
||||
if (Array.isArray(createdMs) && createdMs[0]) {
|
||||
newMilestoneId = createdMs[0].id;
|
||||
} else if (createdMs.id) {
|
||||
newMilestoneId = createdMs.id;
|
||||
}
|
||||
|
||||
for (let i = 0; i < milestones.length; i++) {
|
||||
const originalMilestone = milestones[i];
|
||||
const newMilestone = createdMils[i];
|
||||
|
||||
if (Array.isArray(originalMilestone.impacts) && originalMilestone.impacts.length) {
|
||||
for (const imp of originalMilestone.impacts) {
|
||||
const impactPayload = {
|
||||
milestone_id: newMilestone.id,
|
||||
impact_type: imp.impact_type,
|
||||
direction: imp.direction || 'subtract',
|
||||
amount: imp.amount || 0,
|
||||
start_date: imp.start_date || null,
|
||||
end_date: imp.end_date || null
|
||||
};
|
||||
|
||||
try {
|
||||
const impactRes = await fetch(IMPACT_API_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(impactPayload)
|
||||
});
|
||||
if (!impactRes.ok) {
|
||||
console.error(`Failed to create impact for milestone ${newMilestone.id}`);
|
||||
// If we have a milestoneId, create tasks & impacts
|
||||
if (newMilestoneId) {
|
||||
// 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)
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Error creating impact for milestone ${newMilestone.id}`, err);
|
||||
}
|
||||
|
||||
// impacts
|
||||
if (Array.isArray(milestone.impacts)) {
|
||||
for (const imp of milestone.impacts) {
|
||||
const impactBody = {
|
||||
milestone_id: newMilestoneId,
|
||||
impact_type: imp.impact_type,
|
||||
direction: imp.direction,
|
||||
amount: imp.amount,
|
||||
start_date: imp.start_date || null,
|
||||
end_date: imp.end_date || null
|
||||
};
|
||||
await authFetch("/api/premium/milestone-impacts", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(impactBody)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Keep track of the newly created milestone
|
||||
createdMilestonesData.push({
|
||||
milestoneId: newMilestoneId,
|
||||
title: milestone.title
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
coachResponse = "I've created some actionable milestones for you (with financial impacts). You can view them in your roadmap!";
|
||||
// If we successfully created at least 1 milestone,
|
||||
// override the reply with a success message
|
||||
if (createdMilestonesData.length > 0) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------
|
||||
// 8. Send JSON Response (unchanged)
|
||||
// ------------------------------------------------
|
||||
res.json({ reply: coachResponse, aiRisk });
|
||||
// 6) Finally, respond to front-end
|
||||
return res.json({
|
||||
reply: replyToClient,
|
||||
createdMilestones: createdMilestonesData
|
||||
});
|
||||
} catch (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." });
|
||||
}
|
||||
});
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user