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 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." });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user