UNPKG

@access-mcp/xdmod-metrics

Version:
1,034 lines 65.2 kB
#!/usr/bin/env node import { BaseAccessServer } from "@access-mcp/shared"; class XDMoDMetricsServer extends BaseAccessServer { apiToken; constructor() { super("xdmod-metrics", "0.4.0", "https://xdmod.access-ci.org"); // Get API token from environment variable OR process arguments this.apiToken = process.env.XDMOD_API_TOKEN; // Also check for token in command line arguments (for Claude Desktop config) const args = process.argv; const tokenArgIndex = args.findIndex(arg => arg === '--api-token'); if (tokenArgIndex !== -1 && tokenArgIndex + 1 < args.length) { this.apiToken = args[tokenArgIndex + 1]; // console.log(`[XDMoD] Using API token from command line argument`); } // Debug logging (commented out for production) // console.log(`[XDMoD] API Token present: ${!!this.apiToken}`); // console.log(`[XDMoD] Token source: ${process.env.XDMOD_API_TOKEN ? 'environment' : (tokenArgIndex !== -1 ? 'command-line' : 'none')}`); // if (this.apiToken) { // console.log(`[XDMoD] Token length: ${this.apiToken.length}`); // console.log(`[XDMoD] Token preview: ${this.apiToken.substring(0, 10)}...`); // } } getAuthHeaders() { const headers = { "Content-Type": "application/x-www-form-urlencoded", }; if (this.apiToken) { // XDMoD uses "Token" header (not Authorization) headers["Token"] = this.apiToken; } return headers; } isAuthenticated() { return !!this.apiToken; } getTools() { const tools = [ { name: "get_dimensions", description: "Get all available dimensions from XDMoD Usage Tab", inputSchema: { type: "object", properties: {}, required: [], }, }, { name: "get_statistics", description: "Get available statistics for a specific dimension", inputSchema: { type: "object", properties: { dimension_id: { type: "string", description: 'The dimension ID (e.g., "Jobs_none")', }, category: { type: "string", description: 'The realm/category (e.g., "Jobs")', }, group_by: { type: "string", description: 'The group by field (e.g., "none")', }, }, required: ["dimension_id", "category", "group_by"], }, }, { name: "get_chart_data", description: "Get chart data and metadata for a specific statistic", inputSchema: { type: "object", properties: { realm: { type: "string", description: 'The realm (e.g., "Jobs", "SUPREMM")', }, group_by: { type: "string", description: 'The group by field (e.g., "none", "resource")', }, statistic: { type: "string", description: 'The statistic name (e.g., "total_cpu_hours", "gpu_time")', }, start_date: { type: "string", description: "Start date in YYYY-MM-DD format", }, end_date: { type: "string", description: "End date in YYYY-MM-DD format", }, dataset_type: { type: "string", description: 'Dataset type (default: "timeseries")', enum: ["timeseries", "aggregate"], default: "timeseries", }, display_type: { type: "string", description: 'Display type (default: "line")', enum: ["line", "bar", "pie", "scatter"], default: "line", }, combine_type: { type: "string", description: 'How to combine data (default: "side")', enum: ["side", "stack", "percent"], default: "side", }, limit: { type: "number", description: "Maximum number of data series to return (default: 10)", default: 10, }, offset: { type: "number", description: "Offset for pagination (default: 0)", default: 0, }, log_scale: { type: "string", description: 'Use logarithmic scale (default: "n")', enum: ["y", "n"], default: "n", }, filters: { type: "object", description: "Optional filters to apply (e.g., {resource: 'delta.ncsa.xsede.org'})", additionalProperties: { type: "string" } }, }, required: [ "realm", "group_by", "statistic", "start_date", "end_date", ], }, }, { name: "get_chart_image", description: "Get chart image (SVG, PNG, or PDF) for a specific statistic", inputSchema: { type: "object", properties: { realm: { type: "string", description: 'The realm (e.g., "Jobs", "SUPREMM")', }, group_by: { type: "string", description: 'The group by field (e.g., "none", "resource")', }, statistic: { type: "string", description: 'The statistic name (e.g., "total_cpu_hours", "gpu_time")', }, start_date: { type: "string", description: "Start date in YYYY-MM-DD format", }, end_date: { type: "string", description: "End date in YYYY-MM-DD format", }, format: { type: "string", description: "Image format (svg, png, pdf)", enum: ["svg", "png", "pdf"], default: "svg", }, width: { type: "number", description: "Image width in pixels", default: 916, }, height: { type: "number", description: "Image height in pixels", default: 484, }, dataset_type: { type: "string", description: 'Dataset type (default: "timeseries")', enum: ["timeseries", "aggregate"], default: "timeseries", }, display_type: { type: "string", description: 'Display type (default: "line")', enum: ["line", "bar", "pie", "scatter"], default: "line", }, combine_type: { type: "string", description: 'How to combine data (default: "side")', enum: ["side", "stack", "percent"], default: "side", }, limit: { type: "number", description: "Maximum number of data series to return (default: 10)", default: 10, }, offset: { type: "number", description: "Offset for pagination (default: 0)", default: 0, }, log_scale: { type: "string", description: 'Use logarithmic scale (default: "n")', enum: ["y", "n"], default: "n", }, filters: { type: "object", description: "Optional filters to apply (e.g., {resource: 'delta.ncsa.xsede.org'})", additionalProperties: { type: "string" } }, }, required: [ "realm", "group_by", "statistic", "start_date", "end_date", ], }, }, { name: "get_chart_link", description: "Generate a direct link to view the chart in XDMoD portal", inputSchema: { type: "object", properties: { realm: { type: "string", description: 'The realm (e.g., "Jobs", "SUPREMM")', }, group_by: { type: "string", description: 'The group by field (e.g., "none", "resource")', }, statistic: { type: "string", description: 'The statistic name (e.g., "total_cpu_hours", "gpu_time")', }, }, required: ["realm", "group_by", "statistic"], }, }, ]; // Data Analytics Framework test tools removed - endpoints were not accessible // NSF-Enhanced XDMoD Integration tools tools.push({ name: "get_usage_with_nsf_context", description: "Get XDMoD usage data enriched with NSF funding context for a researcher or institution", inputSchema: { type: "object", properties: { researcher_name: { type: "string", description: "Researcher name to analyze (will search both XDMoD usage and NSF awards)", }, realm: { type: "string", description: 'XDMoD realm to analyze (e.g., "Jobs", "SUPREMM")', default: "Jobs", }, start_date: { type: "string", description: "Start date for usage analysis in YYYY-MM-DD format", }, end_date: { type: "string", description: "End date for usage analysis in YYYY-MM-DD format", }, limit: { type: "number", description: "Maximum number of NSF awards to include (default: 5)", default: 5, }, }, required: ["researcher_name", "start_date", "end_date"], }, }, { name: "analyze_funding_vs_usage", description: "Compare NSF funding amounts with actual XDMoD computational usage patterns", inputSchema: { type: "object", properties: { nsf_award_number: { type: "string", description: "NSF award number to analyze (e.g., '2138259')", }, usage_metric: { type: "string", description: 'XDMoD metric to analyze (e.g., "total_cpu_hours", "gpu_time")', default: "total_cpu_hours", }, start_date: { type: "string", description: "Start date for analysis in YYYY-MM-DD format", }, end_date: { type: "string", description: "End date for analysis in YYYY-MM-DD format", }, }, required: ["nsf_award_number", "start_date", "end_date"], }, }, { name: "institutional_research_profile", description: "Generate a comprehensive research profile combining XDMoD usage patterns with NSF funding for an institution", inputSchema: { type: "object", properties: { institution_name: { type: "string", description: "Institution name to analyze (e.g., 'University of Colorado Boulder')", }, start_date: { type: "string", description: "Start date for analysis in YYYY-MM-DD format", }, end_date: { type: "string", description: "End date for analysis in YYYY-MM-DD format", }, top_researchers: { type: "number", description: "Number of top researchers to highlight (default: 10)", default: 10, }, }, required: ["institution_name", "start_date", "end_date"], }, }); // Always add debug tool tools.push({ name: "debug_auth_status", description: "Check authentication status and debug information", inputSchema: { type: "object", properties: {}, required: [], }, }); // User-specific tools removed - see user-specific.ts for experimental user functionality return tools; } getResources() { return []; } async handleToolCall(request) { const { name, arguments: args } = request.params; // console.log(`[XDMoD] Tool called: ${name}`, args); switch (name) { case "get_dimensions": return await this.getDimensions(); case "get_statistics": return await this.getStatistics(args.dimension_id, args.category, args.group_by); case "get_chart_data": return await this.getChartData({ realm: args.realm, group_by: args.group_by, statistic: args.statistic, start_date: args.start_date, end_date: args.end_date, dataset_type: args.dataset_type || "timeseries", display_type: args.display_type || "line", combine_type: args.combine_type || "side", limit: args.limit || 10, offset: args.offset || 0, log_scale: args.log_scale || "n", filters: args.filters, }); case "get_chart_image": return await this.getChartImage({ realm: args.realm, group_by: args.group_by, statistic: args.statistic, start_date: args.start_date, end_date: args.end_date, format: args.format || "svg", width: args.width || 916, height: args.height || 484, dataset_type: args.dataset_type || "timeseries", display_type: args.display_type || "line", combine_type: args.combine_type || "side", limit: args.limit || 10, offset: args.offset || 0, log_scale: args.log_scale || "n", filters: args.filters, }); case "get_chart_link": return await this.getChartLink(args.realm, args.group_by, args.statistic); case "debug_auth_status": return await this.debugAuthStatus(); case "get_usage_with_nsf_context": return await this.getUsageWithNSFContext(args.researcher_name, args.realm || "Jobs", args.start_date, args.end_date, args.limit || 5); case "analyze_funding_vs_usage": return await this.analyzeFundingVsUsage(args.nsf_award_number, args.usage_metric || "total_cpu_hours", args.start_date, args.end_date); case "institutional_research_profile": return await this.generateInstitutionalProfile(args.institution_name, args.start_date, args.end_date, args.top_researchers || 10); // Data Analytics Framework cases removed // User-specific cases removed - see user-specific.ts default: throw new Error(`Unknown tool: ${name}`); } } async handleResourceRead(request) { throw new Error("Resources not implemented"); } async getDimensions() { try { const response = await fetch(`${this.baseURL}/controllers/user_interface.php`, { method: "POST", headers: this.getAuthHeaders(), body: new URLSearchParams({ operation: "get_menus", public_user: "true", node: "category_", }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const dimensions = await response.json(); // Filter out dimensions with "-111" in the ID (these are typically invalid/test entries) const cleanedDimensions = dimensions.filter((dim) => !dim.id.includes("-111")); return { content: [ { type: "text", text: `Found ${cleanedDimensions.length} available dimensions in XDMoD Usage Tab:\n\n` + cleanedDimensions .map((dim) => `• ${dim.text} (ID: ${dim.id}, Category: ${dim.category}, Group By: ${dim.group_by})`) .join("\n"), }, ], }; } catch (error) { throw new Error(`Failed to fetch dimensions: ${error instanceof Error ? error.message : String(error)}`); } } async getStatistics(dimensionId, category, groupBy) { try { const response = await fetch(`${this.baseURL}/controllers/user_interface.php`, { method: "POST", headers: this.getAuthHeaders(), body: new URLSearchParams({ operation: "get_menus", public_user: "true", category: category, group_by: groupBy, node: dimensionId, }), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const statistics = await response.json(); return { content: [ { type: "text", text: `Found ${statistics.length} available statistics for dimension "${dimensionId}":\n\n` + statistics .map((stat) => `• ${stat.text} (ID: ${stat.id}${stat.statistic ? `, Statistic: ${stat.statistic}` : ""})`) .join("\n"), }, ], }; } catch (error) { throw new Error(`Failed to fetch statistics: ${error instanceof Error ? error.message : String(error)}`); } } async getChartData(params) { try { const response = await fetch(`${this.baseURL}/controllers/user_interface.php`, { method: "POST", headers: this.getAuthHeaders(), body: (() => { const urlParams = new URLSearchParams({ operation: "get_charts", public_user: "true", dataset_type: params.dataset_type, format: "hc_jsonstore", width: "916", height: "484", realm: params.realm, group_by: params.group_by, statistic: params.statistic, start_date: params.start_date, end_date: params.end_date, }); // Add optional parameters if (params.display_type) { urlParams.append("display_type", params.display_type); } if (params.combine_type) { urlParams.append("combine_type", params.combine_type); } if (params.limit !== undefined) { urlParams.append("limit", params.limit.toString()); } if (params.offset !== undefined) { urlParams.append("offset", params.offset.toString()); } if (params.log_scale) { urlParams.append("log_scale", params.log_scale); } // Add filters if (params.filters) { for (const [key, value] of Object.entries(params.filters)) { urlParams.append(`${key}_filter`, value); } } return urlParams; })(), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); let resultText = `Chart Data for ${params.statistic} (${params.realm}):\n\n`; if (data.data && data.data.length > 0) { const chartInfo = data.data[0]; if (chartInfo.group_description) { resultText += `**Group Description:**\n${chartInfo.group_description}\n\n`; } if (chartInfo.description) { resultText += `**Chart Description:**\n${chartInfo.description}\n\n`; } if (chartInfo.chart_title) { resultText += `**Chart Title:** ${chartInfo.chart_title}\n\n`; } resultText += `**Raw Data:**\n\`\`\`json\n${JSON.stringify(data, null, 2)}\n\`\`\``; } else { resultText += "No data available for the specified parameters."; } return { content: [ { type: "text", text: resultText, }, ], }; } catch (error) { throw new Error(`Failed to fetch chart data: ${error instanceof Error ? error.message : String(error)}`); } } async getChartImage(params) { try { const response = await fetch(`${this.baseURL}/controllers/user_interface.php`, { method: "POST", headers: this.getAuthHeaders(), body: (() => { const urlParams = new URLSearchParams({ operation: "get_charts", public_user: "true", dataset_type: params.dataset_type, format: params.format, width: params.width.toString(), height: params.height.toString(), realm: params.realm, group_by: params.group_by, statistic: params.statistic, start_date: params.start_date, end_date: params.end_date, }); // Add optional parameters if (params.display_type) { urlParams.append("display_type", params.display_type); } if (params.combine_type) { urlParams.append("combine_type", params.combine_type); } if (params.limit !== undefined) { urlParams.append("limit", params.limit.toString()); } if (params.offset !== undefined) { urlParams.append("offset", params.offset.toString()); } if (params.log_scale) { urlParams.append("log_scale", params.log_scale); } // Add filters if (params.filters) { for (const [key, value] of Object.entries(params.filters)) { urlParams.append(`${key}_filter`, value); } } return urlParams; })(), }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } if (params.format === "png") { // For PNG, get binary data and convert to base64 const imageBuffer = await response.arrayBuffer(); const base64Data = Buffer.from(imageBuffer).toString('base64'); // Return with MCP-compliant image format that was working return { content: [ { type: "image", data: base64Data, mimeType: "image/png", }, { type: "text", text: `\nChart Details:\n` + `- Statistic: ${params.statistic}\n` + `- Realm: ${params.realm}\n` + `- Group By: ${params.group_by}\n` + `- Date Range: ${params.start_date} to ${params.end_date}\n` + `- Size: ${params.width}x${params.height} pixels`, }, ], }; } else { // For SVG and other text formats const imageData = await response.text(); if (params.format === "svg") { // For SVG, provide helpful message about using PNG instead return { content: [ { type: "text", text: `SVG Chart for ${params.statistic} (${params.realm})\n\n` + `⚠️ SVG format doesn't display directly in Claude Desktop.\n\n` + `**Recommended:** Use PNG format for direct image display:\n` + `\`\`\`\n` + `format: "png"\n` + `\`\`\`\n\n` + `**Chart Details:**\n` + `- Statistic: ${params.statistic}\n` + `- Realm: ${params.realm}\n` + `- Group By: ${params.group_by}\n` + `- Date Range: ${params.start_date} to ${params.end_date}\n` + `- Size: ${params.width}x${params.height} pixels\n\n` + `**To view this SVG chart:**\n` + `1. Copy the SVG code below\n` + `2. Save it to a .svg file and open in your browser\n\n` + `\`\`\`svg\n${imageData}\n\`\`\`` } ], }; } else { // For PDF and other formats, return as text return { content: [ { type: "text", text: `Chart Image (${params.format.toUpperCase()}) for ${params.statistic}:\n\n` + `**Parameters:** Realm: ${params.realm}, Group By: ${params.group_by}, ` + `Date Range: ${params.start_date} to ${params.end_date}\n\n` + `**To view this chart:**\n` + `1. Copy the ${params.format.toUpperCase()} data below\n` + `2. Save it to a file with .${params.format} extension\n` + `3. Open the file in your browser or image viewer\n\n` + `**${params.format.toUpperCase()} Data:**\n\`\`\`${params.format}\n${imageData}\n\`\`\``, }, ], }; } } } catch (error) { throw new Error(`Failed to fetch chart image: ${error instanceof Error ? error.message : String(error)}`); } } async getChartLink(realm, groupBy, statistic) { // Construct the URL parameters for XDMoD portal const urlParams = new URLSearchParams({ node: 'statistic', realm: realm, group_by: groupBy, statistic: statistic }); const chartUrl = `https://xdmod.access-ci.org/index.php#tg_usage?${urlParams.toString()}`; const responseText = `Direct link to view chart in XDMoD portal:\n\n${chartUrl}\n\n` + `**Chart Parameters:**\n` + `- Realm: ${realm}\n` + `- Group By: ${groupBy}\n` + `- Statistic: ${statistic}\n\n` + `You can use this URL to view the interactive chart directly in the XDMoD web interface. ` + `Use the portal's filtering options to narrow down to specific resources, users, or other criteria.`; return { content: [ { type: "text", text: responseText, }, ], }; } async debugAuthStatus() { const envToken = process.env.XDMOD_API_TOKEN; const args = process.argv; const tokenArgIndex = args.findIndex(arg => arg === '--api-token'); const argToken = tokenArgIndex !== -1 && tokenArgIndex + 1 < args.length ? args[tokenArgIndex + 1] : null; // Get all environment variables that might be relevant const allEnvVars = Object.keys(process.env).filter(key => key.includes('XDMOD') || key.includes('TOKEN') || key.includes('API')); return { content: [ { type: "text", text: `🔍 **XDMoD Authentication Debug Information**\n\n` + `**Environment Variables:**\n` + `- XDMOD_API_TOKEN present: ${!!envToken}\n` + `- Token length: ${envToken ? envToken.length : 'N/A'}\n` + `- Token preview: ${envToken ? envToken.substring(0, 10) + '...' : 'N/A'}\n` + `- All relevant env vars: ${allEnvVars.join(', ') || 'none'}\n\n` + `**Command Line Arguments:**\n` + `- Process argv: ${JSON.stringify(args)}\n` + `- --api-token argument: ${!!argToken}\n` + `- Arg token length: ${argToken ? argToken.length : 'N/A'}\n` + `- Arg token preview: ${argToken ? argToken.substring(0, 10) + '...' : 'N/A'}\n\n` + `**Current Configuration:**\n` + `- API Token active: ${this.isAuthenticated()}\n` + `- Active token length: ${this.apiToken ? this.apiToken.length : 'N/A'}\n` + `- Active token preview: ${this.apiToken ? this.apiToken.substring(0, 10) + '...' : 'N/A'}\n` + `- Token source: ${this.apiToken === envToken ? 'environment' : (this.apiToken === argToken ? 'command-line' : 'unknown')}\n\n` + `**Available Tools:**\n` + `- get_dimensions: ✅\n` + `- get_statistics: ✅\n` + `- get_chart_data: ✅\n` + `- get_chart_image: ✅\n` + `- get_chart_link: ✅\n` + `- get_nsf_award: ✅\n` + `- find_nsf_awards_by_pi: ✅\n` + `- find_nsf_awards_by_personnel: ✅\n` + `- get_usage_with_nsf_context: ✅ (NSF-enhanced)\n` + `- analyze_funding_vs_usage: ✅ (NSF-enhanced)\n` + `- institutional_research_profile: ✅ (NSF-enhanced)\n` + `- debug_auth_status: ✅\n` + `- get_current_user: ❌ (moved to user-specific.ts)\n` + `- get_my_usage: ❌ (moved to user-specific.ts)\n\n` + `**Troubleshooting:**\n` + `Environment variable should be set in Claude Desktop config under "env" section.\n` + `If still not working, the environment variable might not be passed correctly by Claude Desktop.` } ] }; } // getCurrentUser method moved to user-specific.ts // Data Analytics Framework test methods removed - endpoints were not accessible // getMyUsage and formatUsageResponse methods moved to user-specific.ts // NSF-Enhanced XDMoD Integration Methods async getUsageWithNSFContext(researcherName, realm, startDate, endDate, limit) { try { // Search for NSF awards for this researcher const nsfAwards = await this.searchNSFAwardsByPI(researcherName, limit); // Get XDMoD usage statistics for the same period // Note: This would ideally filter by user, but public XDMoD API doesn't support user filtering // So we provide general usage context instead const usageData = await this.getChartData({ realm, group_by: "none", statistic: "total_cpu_hours", // Default metric start_date: startDate, end_date: endDate, dataset_type: "timeseries", }); return { content: [ { type: "text", text: this.formatUsageWithNSFContext(researcherName, nsfAwards, usageData, startDate, endDate), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error analyzing ${researcherName}: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } async analyzeFundingVsUsage(awardNumber, usageMetric, startDate, endDate) { try { // Get NSF award details const nsfAward = await this.fetchNSFAwardData(awardNumber); // Get corresponding XDMoD usage data const usageData = await this.getChartData({ realm: "Jobs", // Default to Jobs realm group_by: "none", statistic: usageMetric, start_date: startDate, end_date: endDate, dataset_type: "aggregate", }); return { content: [ { type: "text", text: this.formatFundingVsUsageAnalysis(nsfAward, usageData, usageMetric, startDate, endDate), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error analyzing award ${awardNumber}: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } async generateInstitutionalProfile(institutionName, startDate, endDate, topResearchers) { try { // Search for NSF awards by institution (using institution name as keyword) const institutionAwards = await this.searchNSFAwardsByInstitution(institutionName, topResearchers * 2); // Get XDMoD aggregate usage data for the period const usageData = await this.getChartData({ realm: "Jobs", group_by: "resource", // Group by resource to show institutional usage patterns statistic: "total_cpu_hours", start_date: startDate, end_date: endDate, dataset_type: "aggregate", }); return { content: [ { type: "text", text: this.formatInstitutionalProfile(institutionName, institutionAwards, usageData, topResearchers, startDate, endDate), }, ], }; } catch (error) { return { content: [ { type: "text", text: `Error generating profile for ${institutionName}: ${error instanceof Error ? error.message : String(error)}`, }, ], }; } } // Enhanced formatting methods for integrated NSF-XDMoD analysis formatUsageWithNSFContext(researcherName, nsfAwards, usageData, startDate, endDate) { let result = `🔬 **Research Profile: ${researcherName}**\n\n`; let totalFunding = 0; // NSF Funding Context if (nsfAwards.length > 0) { result += `🏆 **NSF Funding Portfolio** (${nsfAwards.length} award${nsfAwards.length === 1 ? '' : 's'}):\n\n`; for (const award of nsfAwards) { result += `• **${award.awardNumber}**: ${award.title}\n`; result += ` - Amount: ${award.totalIntendedAward}\n`; result += ` - Period: ${award.startDate} to ${award.endDate}\n`; result += ` - Institution: ${award.institution}\n\n`; // Try to extract numeric amount for totaling if (award.totalIntendedAward) { const match = award.totalIntendedAward.match(/[\d,]+/); if (match) { const amount = parseFloat(match[0].replace(/,/g, '')); if (!isNaN(amount)) totalFunding += amount; } } } if (totalFunding > 0) { result += `**Total NSF Funding**: $${totalFunding.toLocaleString()}\n\n`; } } else { result += `**NSF Funding**: No recent NSF awards found for ${researcherName}\n\n`; } // XDMoD Usage Context result += `📊 **ACCESS-CI Usage Context** (${startDate} to ${endDate}):\n\n`; result += `*Note: XDMoD public API provides system-wide metrics. Individual user usage*\n`; result += `*data requires authentication and is not available in this analysis.*\n\n`; if (usageData?.content?.[0]?.text) { // Extract key metrics from usage data const usageText = usageData.content[0].text; if (usageText.includes('CPU hours')) { result += `**System-wide CPU Usage**: Available in detailed XDMoD analysis above\n`; } } // Integration insights result += `\n---\n**🔗 Research Integration Insights:**\n\n`; if (nsfAwards.length > 0 && totalFunding > 0) { result += `• ${researcherName} has received $${totalFunding.toLocaleString()} in NSF funding\n`; result += `• Research areas: ${nsfAwards.map(a => a.primaryProgram).filter(p => p).join(', ') || 'Various'}\n`; result += `• This funding likely supports computational work on ACCESS-CI resources\n`; result += `• Use XDMoD institutional analysis to see usage patterns at ${nsfAwards[0]?.institution || 'their institution'}\n\n`; result += `**💡 Recommendations:**\n`; result += `• Cross-reference award periods with XDMoD usage spikes\n`; result += `• Analyze computational requirements vs. funding amounts\n`; result += `• Compare usage patterns across different award types\n`; } else { result += `• No NSF funding context available for computational usage analysis\n`; result += `• Consider searching variations of the researcher name\n`; result += `• Institutional analysis may reveal collaborative usage patterns\n`; } return result; } formatFundingVsUsageAnalysis(nsfAward, usageData, usageMetric, startDate, endDate) { let result = `💰 **Funding vs. Usage Analysis**\n\n`; // NSF Award Summary result += `🏆 **NSF Award ${nsfAward.awardNumber}**\n`; result += `• **Title**: ${nsfAward.title}\n`; result += `• **PI**: ${nsfAward.principalInvestigator}\n`; result += `• **Institution**: ${nsfAward.institution}\n`; result += `• **Award Amount**: ${nsfAward.totalIntendedAward}\n`; result += `• **Award Period**: ${nsfAward.startDate} to ${nsfAward.endDate}\n\n`; // Usage Analysis Period result += `📊 **Computational Usage Analysis** (${startDate} to ${endDate}):\n`; result += `• **Metric Analyzed**: ${usageMetric}\n`; result += `• **Analysis Period**: ${startDate} to ${endDate}\n\n`; // Usage Data Context if (usageData?.content?.[0]?.text) { result += `**System-wide Usage During Analysis Period:**\n`; result += `*Note: Individual project usage requires authentication*\n\n`; // Try to extract meaningful metrics from the usage data const usageText = usageData.content[0].text; if (usageText.includes('CPU hours') || usageText.includes('total_cpu_hours')) { result += `• ACCESS-CI systems show active computational usage during this period\n`; result += `• Detailed metrics available through authenticated XDMoD access\n`; } } result += `\n---\n**🔍 Analysis Insights:**\n\n`; // Temporal Analysis const awardStartYear = new Date(nsfAward.startDate).getFullYear(); const awardEndYear = new Date(nsfAward.endDate).getFullYear(); const analysisStartYear = new Date(startDate).getFullYear(); const analysisEndYear = new Date(endDate).getFullYear(); if (analysisStartYear >= awardStartYear && analysisEndYear <= awardEndYear) { result += `• ✅ Analysis period falls within NSF award timeframe\n`; result += `• This computational usage likely relates to funded research activities\n`; } else { result += `• ⚠️ Analysis period extends beyond NSF award timeframe\n`; result += `• Usage may include follow-up work or other funding sources\n`; } result += `• **Research Domain**: ${nsfAward.primaryProgram || 'General NSF research'}\n`; result += `• **Computational Focus**: ${nsfAward.abstract.substring(0, 200)}...\n\n`; result += `**💡 Research Impact Indicators:**\n`; result += `• NSF investment of ${nsfAward.totalIntendedAward} supports computational research\n`; result += `• ACCESS-CI provides the cyberinfrastructure platform for this work\n`; result += `• Usage patterns can indicate research productivity and impact\n\n`; result += `**🔧 Deeper Analysis Recommendations:**\n`; result += `• Use authenticated XDMoD access for specific user/project metrics\n`; result += `• Correlate usage spikes with publication dates or milestones\n`; result += `• Compare similar awards to benchmark computational intensity\n`; return result; } formatInstitutionalProfile(institutionName, awards, usageData, topResearchers, startDate, endDate) { let result = `🏛️ **Institutional Research Profile: ${institutionName}**\n\n`; // NSF Funding Overview result += `🏆 **NSF Research Portfolio** (${startDate} to ${endDate}):\n\n`; let totalFunding = 0; const researchAreas = new Set(); const topPIs = new Map(); if (awards.length > 0) { result += `**Active NSF Awards**: ${awards.length}\n\n`; // Analyze awards for (const award of awards.slice(0, topResearchers)) { result += `• **${award.awardNumber}**: ${award.title}\n`; result += ` - PI: ${award.principalInvestigator}\n`; result += ` - Amount: ${award.totalIntendedAward}\n`; if (award.primaryProgram) { result += ` - Program: ${award.primaryProgram}\n`; researchAreas.add(award.primaryProgram); } result += `\n`; // Track PI activity const piCount = topPIs.get(award.principalInvestigator) || 0; topPIs.set(award.principalInvestigator, piCount + 1); // Sum funding if (award.totalIntendedAward) { const match = award.totalIntendedAward.match(/[\d,]+/); if (match) { const amount = parseFloat(match[0].replace(/,/g, '')); if (!isNaN(amount)) totalFunding += amount; } } } result += `**Research Portfolio Summary:**\n`; result += `• **Total NSF Funding**: $${totalFunding.toLocaleString()}\n`; result += `• **Research Areas**: ${Array.from(researchAreas).join(', ') || 'Various disciplines'}\n`; // Top researchers const sortedPIs = Array.from(topPIs.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 5); if (sortedPIs.length > 0) { result += `• **Most Active PIs**: ${sortedPIs.map(([pi, count]) => `${pi} (${count} award${count > 1 ? 's' : ''})`).join(', ')}\n`; } } else { result += `**No recent NSF awards found** for "${institutionName}"\n\n`; result += `**Troubleshooting NSF Award Search:**\n`; result += `• Institution names must match NSF records exactly\n`; result += `• Try variations: "${institutionName.replace("University of ", "")}", "${institutionName.replace(/ University$/, "")}", etc.\n`; result += `• Search for specific researchers instead using other tools\n`; result += `• Recent awards (2024+) may not be indexed yet\n`; } result += `\n📊 **ACCESS-CI Usage Profile** (${startDate} to ${endDate}):\n\n`; if (usageData?.content?.[0]?.text) { result += `**Computati