// Placeholder for aggregator: mimic Python aggregator semantics by reading from the same tables/blobs in future. // For MVP, return a minimal shape merged from UserTable and known analysis keys if already present in user record. import { DynamoDBClient } from '@aws-sdk/client-dynamodb'; import { DynamoDBDocumentClient, GetCommand } from '@aws-sdk/lib-dynamodb'; import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'; // import { mkdir, writeFile } from 'fs/promises'; import { GraphInsightsPayload } from '../types'; const region = process.env.AWS_REGION!; const usersTableName = process.env.USER_TABLE_NAME const doc = DynamoDBDocumentClient.from(new DynamoDBClient({ region })); const s3 = new S3Client({ region }); const providerDataBucket = process.env.PROVIDER_DATA_BUCKET_NAME!; const badgeTableName = process.env.BADGE_TABLE_NAME!; export async function aggregateUserData(userId: string, enterpriseMode?: boolean, companyId?: string | null): Promise { let userResp; if (enterpriseMode) { const linkTableName = process.env.USER_COMPANY_LINK_TABLE_NAME userResp = await doc.send(new GetCommand({ TableName: linkTableName, Key: { userId, companyId } })); } else { userResp = await doc.send(new GetCommand({ TableName: usersTableName, Key: { userId } })); } const user = userResp.Item || {}; if (enterpriseMode && companyId) { // Optionally merge enterprise overlay later; keeping simple for MVP. user.companyId = companyId; } // For each provider_* entry, fetch filtered_data_key JSON from S3 and attach under _analysis const providerKeys = Object.keys(user).filter(k => k.startsWith('provider_')); const fetches = providerKeys.map(async (key) => { try { const providerData = user[key]; if (!providerData || typeof providerData !== 'object') return; const filteredKey = providerData['filtered_data_key']; if (!filteredKey || typeof filteredKey !== 'string') return; const obj = await getJsonFromS3(providerDataBucket, filteredKey); const providerName = key.replace(/^provider_/, ''); const analysisKey = `${providerName}_analysis`; user[analysisKey] = obj; } catch (e) { console.error('Failed to load provider filtered data', e); // Non-fatal; annotate error try { const providerName = key.replace(/^provider_/, ''); user[`${providerName}_analysis`] = { error: 'Failed to load provider filtered data' }; } catch {} } }); await Promise.all(fetches); return user; } // eslint-disable-next-line @typescript-eslint/no-explicit-any export function cleanDeveloperProfile(data: any): any { if (Array.isArray(data)) return data.map(cleanDeveloperProfile); if (data === null || typeof data !== 'object') return data; const cleaned = {}; const keysToRemove = new Set([ 'accessToken','refreshToken','access_token','refresh_token','id_token', 'stripeCustomerId','password','secret', 'gravatar_id','node_id','id','site_admin', 'expires_in','expires_at','token_type','scope','avatarUrls', 'iconUrl','extern_uid','saml_provider_id','profile_image', 'account_id','user_id','prompt','expiresAt', 'source_files_for_analysis','prompt_location','prompt_location_s3_key', 's3_archive_key','repos','profilePic','linkedinUrl','url','profilePictureUrl', 'userId','key_data_points','projects_limit','web_url','linkedinUrl', 'requestHistory','domains_checked','connectedAccounts', 'analysisCache', 'filtered_data_key', 'raw_data_key' ]); const urlKeysToKeep = new Set(['html_url','web_url','blog']); for (const [key, value] of Object.entries(data)) { if (keysToRemove.has(key)) continue; if (key.includes('avatar') || key.includes('picture')) continue; if (key.endsWith('_url') && !urlKeysToKeep.has(key)) continue; const v = cleanDeveloperProfile(value); if (key === 'screening_sources' && v && typeof v === 'object') { delete v['ofac_lists']; delete v['additional_watchlists']; delete v['risk_profile_domains']; } // @ts-expect-error - ignoring cleaned[key] = v; } return cleaned; } // // eslint-disable-next-line @typescript-eslint/no-explicit-any export function buildAllContextPrompt(cleanedData: any, reportGraphData: GraphInsightsPayload | undefined, options?: { concise?: boolean, debugName?: string }): string { const instructions = [ 'You are KYD, a career coach specializing in technical talent, speaking to a developer who wants to improve their score and be considered for more senior roles', 'Give customer-facing answers. ', 'Prefer concise, direct answers.', 'Use ONLY the JSON provided to answer the user.', 'Never invent or fetch external data.', 'If data is missing, say so briefly. Assume the question is about the report, and assume the user is asking about the report, do not assume the user is asking you to perform a task other than looking at the report information.', 'Return natural language for the main reply.', 'When referring to the input JSON, use the term "Report Information" or "Report"', 'You are speaking to a non-technical user, ensure you do not use variable-esk language like "based on linkedin_provider", instead use "based the LinkedIn profile"', 'Do NOT recommend reviewing external sources or contacting anyone. Do NOT suggest actions such as "reach out", "discuss directly", "interview", or "collect more information". If the report lacks information to answer, state that plainly and stop.', 'Avoid advisory phrasing like "I recommend", "you should", or "consider". Provide only what the Report contains or explicitly lacks.', // 'Optionally append a fenced JSON evidence block at the end if relevant.', // 'If you output evidence, follow the exact evidence schema described.', ]; const jsonOnlyNote = 'Do not include the input JSON in your reply.'; // const evidenceSchema = '{ "type": "evidence", "claims": [ { "claim": str, "items": [ { "provider": str, "title": str, "url": str, "date": str } ] } ] }'; const out = [ instructions.join(' '), jsonOnlyNote, '-----BEGIN REPORT GRAPH DATA-----', JSON.stringify(reportGraphData), '-----END REPORT GRAPH DATA-----', '-----BEGIN USER INPUT DATA-----', JSON.stringify(cleanedData), '-----END USER INPUT DATA-----', ].join('\n'); // Fire-and-forget debug write if enabled via env // if (process.env.NODE_ENV === 'development') { // void maybeWriteDebugContext(cleanedData, out, options?.debugName).catch(() => {}); // } return out; } // eslint-disable-next-line @typescript-eslint/no-explicit-any async function getJsonFromS3(bucket: string, key: string): Promise { const res = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key })); const text = await res.Body?.transformToString('utf-8'); return JSON.parse(text || '{}'); } // async function maybeWriteDebugContext(cleanedData: any, promptText: string, nameHint?: string): Promise { // try { // const dir = './debug'; // await mkdir(dir, { recursive: true }); // const sanitize = (s: string) => s.replace(/[^a-zA-Z0-9_\-.]/g, '_').slice(0, 64) || 'context'; // const base = sanitize(nameHint || 'context'); // const ts = new Date().toISOString().replace(/[:.]/g, '-'); // const jsonPath = `${dir}/kyd-${base}-${ts}.json`; // const txtPath = `${dir}/kyd-${base}-${ts}.txt`; // const jsonPretty = JSON.stringify(cleanedData, null, 2); // await writeFile(jsonPath, jsonPretty, 'utf8'); // await writeFile(txtPath, promptText, 'utf8'); // } catch (e) { // console.error('Context debug write failed', e); // } // } export async function getReportGraphData(badgeId: string): Promise { const badge = await doc.send(new GetCommand({ TableName: badgeTableName, Key: { badgeId } })); return badge.Item?.assessmentResult?.graph_insights as GraphInsightsPayload | undefined; } export async function getBadgeUserId(badgeId: string): Promise { const badge = await doc.send(new GetCommand({ TableName: badgeTableName, Key: { badgeId } })); return badge.Item?.userId as string; }