From a72dc8bcfb9fbd9bfe9e94e3f424ca248c702059 Mon Sep 17 00:00:00 2001 From: Josh Date: Tue, 10 Jun 2025 15:46:13 +0000 Subject: [PATCH] Adjusted Career Coach for milestones/impacts/tasks. --- backend/server3.js | 367 ++++++++++++++++++++++++++++++++------------- 1 file changed, 263 insertions(+), 104 deletions(-) diff --git a/backend/server3.js b/backend/server3.js index 1f31f67..14574eb 100644 --- a/backend/server3.js +++ b/backend/server3.js @@ -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"; + + // 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 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"; + // 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 income = financialProfile?.income ? `$${financialProfile.income}` : "N/A"; - const debt = financialProfile?.debt ? `$${financialProfile.debt}` : "N/A"; - const savings = financialProfile?.savings ? `$${financialProfile.savings}` : "N/A"; + // 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 major = collegeProfile?.major || "N/A"; - const creditsCompleted = collegeProfile?.credits_completed || 0; - const graduationDate = collegeProfile?.expected_graduation || "Unknown"; + // 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; - const aiRiskReport = aiRisk?.riskLevel - ? `Risk Level: ${aiRisk.riskLevel}\nReasoning: ${aiRisk.reasoning}` - : "No AI risk info provided."; + // 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"; - return ` + // 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." }); } });