UNPKG

framework-rai

Version:

Responsible AI framework for quick compliance documentation with AI-powered tips

637 lines (551 loc) 20.7 kB
#!/usr/bin/env node import inquirer from 'inquirer'; import fs from 'fs'; import { fileURLToPath } from 'url'; import { dirname } from 'path'; import { createRequire } from 'module'; import path from 'path'; import os from 'os'; const require = createRequire(import.meta.url); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Check for required packages let dotenv, fetch; try { dotenv = await import('dotenv'); dotenv.config(); } catch (e) { console.log('Required package "dotenv" not found. Installing it now...'); console.log('Please run: npm install dotenv'); process.exit(1); } try { fetch = (await import('node-fetch')).default; } catch (e) { console.log('Required package "node-fetch" not found. Installing it now...'); console.log('Please run: npm install node-fetch'); process.exit(1); } // Define config paths const LOCAL_ENV_PATH = path.join(process.cwd(), '.env'); const GLOBAL_CONFIG_DIR = path.join(os.homedir(), '.config', 'framework-rai'); const GLOBAL_CONFIG_PATH = path.join(GLOBAL_CONFIG_DIR, 'config'); // AI-related library patterns to detect const AI_LIBRARIES = [ // Python ML/AI libraries 'sklearn', 'scikit-learn', 'tensorflow', 'torch', 'pytorch', 'keras', 'xgboost', 'lightgbm', 'catboost', 'transformers', 'huggingface', 'spacy', 'nltk', 'gensim', 'fastai', 'opencv', 'cv2', // JavaScript ML/AI libraries '@tensorflow', 'ml5', 'brain.js', 'synaptic', '@huggingface', // R packages 'caret', 'randomForest', 'xgboost', 'kernlab', 'nnet', 'rpart' ]; // AI-related function patterns to detect const AI_FUNCTIONS = [ // Training/fitting 'fit', 'train', 'fit_transform', 'compile', // Prediction/inference 'predict', 'transform', 'inference', 'forward', // Evaluation 'score', 'evaluate', 'accuracy_score', 'classification_report', // Data preparation 'preprocessing', 'tokenize', 'encode', 'normalize' ]; // File extensions to scan const CODE_EXTENSIONS = ['.py', '.ipynb', '.js', '.ts', '.jsx', '.tsx', '.r', '.rmd', '.java', '.scala']; // Setup function to initialize API key if not present async function setupApiKey(options = {}) { // If key is provided directly via --key, use it without saving if (options.key) { return options.key; } // If --setup is specified, always prompt for new key if (options.setup) { return await promptAndSaveKey(options); } // Try to get key from environment or config files let apiKey = process.env.OPENAI_API_KEY; // If not in env, try local .env file (dotenv already loaded it) if (!apiKey && options.global) { // Try global config try { if (fs.existsSync(GLOBAL_CONFIG_PATH)) { const config = JSON.parse(fs.readFileSync(GLOBAL_CONFIG_PATH, 'utf8')); apiKey = config.OPENAI_API_KEY; } } catch (error) { // If error reading global config, continue without it } } // If still no key, prompt the user if (!apiKey && !options.ci) { console.log('OpenAI API key not found.'); const { shouldSetup } = await inquirer.prompt([ { type: 'confirm', name: 'shouldSetup', message: 'Would you like to set up your OpenAI API key now?', default: true, }, ]); if (shouldSetup) { apiKey = await promptAndSaveKey(options); } else { console.log('No API key provided. AI tips will be disabled.'); } } return apiKey; } async function promptAndSaveKey(options = {}) { const { key } = await inquirer.prompt([ { type: 'password', name: 'key', message: 'Enter your OpenAI API key:', validate: input => input.length > 0 ? true : 'API key cannot be empty', }, ]); const { saveLocation } = await inquirer.prompt([ { type: 'list', name: 'saveLocation', message: 'Where would you like to save your API key?', choices: [ { name: 'Project only (current directory)', value: 'local' }, { name: 'Global (available to all projects)', value: 'global' } ], default: options.global ? 'global' : 'local', }, ]); if (saveLocation === 'local') { fs.writeFileSync(LOCAL_ENV_PATH, `OPENAI_API_KEY=${key}\n`, { flag: 'w' }); console.log('API key saved to .env file in current directory.'); } else { // Global config if (!fs.existsSync(GLOBAL_CONFIG_DIR)) { fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true }); } fs.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify({ OPENAI_API_KEY: key }, null, 2)); console.log('API key saved globally in ~/.config/framework-rai/config.'); } return key; } // Parse command line arguments function parseArgs() { const args = process.argv.slice(2); const options = { setup: false, global: false, key: null, scan: false, ci: false }; for (const arg of args) { if (arg === '--setup') { options.setup = true; } else if (arg === '--global') { options.global = true; } else if (arg === '--scan') { options.scan = true; } else if (arg === '--ci') { options.ci = true; } else if (arg.startsWith('--key=')) { options.key = arg.split('=')[1]; } } return options; } const OPENAI_MODEL = 'gpt-4.1-nano-2025-04-14'; // Function to recursively scan directories for AI-related code function scanForAICode(directory, excludedDirs = ['node_modules', '.git']) { const result = { aiFiles: [], totalFiles: 0, aiLibraryMatches: {}, aiFunctionMatches: {} }; function scanDirectory(dir) { try { const items = fs.readdirSync(dir); for (const item of items) { // Skip excluded directories if (excludedDirs.includes(item)) { continue; } const fullPath = path.join(dir, item); const stats = fs.statSync(fullPath); if (stats.isDirectory()) { // Recursively scan subdirectory scanDirectory(fullPath); } else if (stats.isFile() && CODE_EXTENSIONS.includes(path.extname(fullPath).toLowerCase())) { // Scan code file result.totalFiles++; try { const fileContent = fs.readFileSync(fullPath, 'utf8'); let isAIFile = false; // Check for AI libraries for (const library of AI_LIBRARIES) { // Look for import/require statements with the library name const regex = new RegExp(`(import|from|require\\s*\\(\\s*['"'])\\s*${library}`, 'i'); if (regex.test(fileContent)) { isAIFile = true; result.aiLibraryMatches[fullPath] = result.aiLibraryMatches[fullPath] || []; result.aiLibraryMatches[fullPath].push(library); } } // Check for AI functions for (const func of AI_FUNCTIONS) { // Pattern matches function calls like functionName() or object.functionName() const regex = new RegExp(`[\\s\\.\(]${func}\\s*\\(`, 'g'); if (regex.test(fileContent)) { isAIFile = true; result.aiFunctionMatches[fullPath] = result.aiFunctionMatches[fullPath] || []; result.aiFunctionMatches[fullPath].push(func); } } if (isAIFile) { result.aiFiles.push(fullPath); } } catch (e) { console.error(`Error reading file ${fullPath}: ${e.message}`); } } } } catch (e) { console.error(`Error scanning directory ${dir}: ${e.message}`); } } scanDirectory(directory); // Remove duplicates in aiFiles result.aiFiles = [...new Set(result.aiFiles)]; return result; } // Generate suggestions based on code scan async function generateSuggestions(scanResults, apiKey) { if (!apiKey || !scanResults.aiFiles.length) { return null; } try { // Prepare a summary of the scan results const librariesFound = Object.values(scanResults.aiLibraryMatches).flat(); const functionsFound = Object.values(scanResults.aiFunctionMatches).flat(); // Count occurrences of each library and function const libraryCounts = {}; librariesFound.forEach(lib => { libraryCounts[lib] = (libraryCounts[lib] || 0) + 1; }); const functionCounts = {}; functionsFound.forEach(func => { functionCounts[func] = (functionCounts[func] || 0) + 1; }); // Get the top 5 most commonly used libraries and functions const topLibraries = Object.entries(libraryCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([lib, count]) => `${lib} (${count})`); const topFunctions = Object.entries(functionCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([func, count]) => `${func} (${count})`); const scanSummary = { aiFilesCount: scanResults.aiFiles.length, totalFiles: scanResults.totalFiles, topLibraries, topFunctions, fileCount: Math.min(scanResults.aiFiles.length, 5) // List up to 5 files }; const prompt = `Based on automatic code scanning, I found ${scanResults.aiFiles.length} files with AI-related code out of ${scanResults.totalFiles} total code files. Top libraries: ${topLibraries.join(', ')} Top functions: ${topFunctions.join(', ')} Based solely on this information, generate these sections: 1. Pre-filled checklist suggestions - key points to address for responsible AI based on the detected libraries and functions 2. Model card template - minimal starting template suitable for the detected AI technologies 3. Risk assessment - potential risks associated with the detected patterns Format the response in Markdown, with 3 sections (### headings).`; // Clean and validate API key const cleanApiKey = apiKey.trim().replace(/^Bearer\s+/i, ''); const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${cleanApiKey}` }, body: JSON.stringify({ model: OPENAI_MODEL, messages: [ { role: 'system', content: 'You are an expert in responsible AI development. Generate helpful, concise documentation templates.' }, { role: 'user', content: prompt } ], max_tokens: 700, temperature: 0.5 }) }); if (!response.ok) { throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`); } const data = await response.json(); if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) { return data.choices[0].message.content.trim(); } return null; } catch (e) { console.error('Error generating suggestions:', e); return null; } } async function getAITips(answers, apiKey) { if (!apiKey) { return 'AI tips disabled. Set OPENAI_API_KEY in .env file to enable.'; } try { const prompt = `Given the following answers about an AI feature, give only 23 practical, actionable tips for each category (checklist, model card, risk file). Tips should be specific actions the developer can take in the project. Format the output as:\n\nChecklist Tips:\n- ...\nModel Card Tips:\n- ...\nRisk File Tips:\n- ...\n\nAnswers: ${JSON.stringify(answers, null, 2)}`; const response = await fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}`, }, body: JSON.stringify({ model: OPENAI_MODEL, messages: [ { role: 'system', content: 'You are an expert in responsible AI. Give only 2–3 practical, actionable tips for each category, based on the user answers.' }, { role: 'user', content: prompt } ], max_tokens: 350, temperature: 0.5 }) }); const data = await response.json(); if (data.choices && data.choices[0] && data.choices[0].message && data.choices[0].message.content) { return data.choices[0].message.content.trim(); } return 'No tips generated.'; } catch (e) { return 'Could not fetch AI tips (API error).'; } } async function runCodeScan(options, apiKey) { console.log('Scanning codebase for AI-related patterns...'); const scanResults = scanForAICode(process.cwd()); console.log(`\nScan Results:`); console.log(`- Found ${scanResults.aiFiles.length} files with AI code out of ${scanResults.totalFiles} total code files`); if (scanResults.aiFiles.length > 0) { console.log('\nAI-related files detected:'); scanResults.aiFiles.slice(0, 10).forEach(file => { const relativePath = path.relative(process.cwd(), file); const libraries = scanResults.aiLibraryMatches[file] || []; const functions = scanResults.aiFunctionMatches[file] || []; console.log(`- ${relativePath}`); if (libraries.length) console.log(` Libraries: ${libraries.join(', ')}`); if (functions.length) console.log(` Functions: ${functions.join(', ')}`); }); if (scanResults.aiFiles.length > 10) { console.log(`\n...and ${scanResults.aiFiles.length - 10} more files`); } // Generate suggestions based on scan results console.log('\nGenerating documentation suggestions...'); const suggestions = await generateSuggestions(scanResults, apiKey); if (suggestions) { // Save suggestions to files console.log('Writing documentation templates...'); const sections = suggestions.split(/(?=###)/); // Extract checklist section const checklistSection = sections.find(s => /###.*checklist/i.test(s)) || ''; fs.writeFileSync('checklist.md', `# Responsible AI Feature Checklist\n\n${checklistSection.replace(/###.*checklist/i, '')}\n`); // Extract model card section const modelCardSection = sections.find(s => /###.*model card/i.test(s)) || ''; fs.writeFileSync('model_card.md', `# Model Card\n\n${modelCardSection.replace(/###.*model card/i, '')}\n`); // Extract risk file section const riskSection = sections.find(s => /###.*risk/i.test(s)) || ''; fs.writeFileSync('risk_file.md', `# AI Model Risk & Compliance\n\n${riskSection.replace(/###.*risk/i, '')}\n`); console.log('Documentation templates generated based on code scan.'); } else { console.log('Could not generate documentation suggestions. Please run the interactive mode.'); } return true; } else { console.log('No AI-related code detected.'); return false; } } async function main() { // Parse command line arguments const options = parseArgs(); // Setup API key if needed const apiKey = await setupApiKey(options); // If only --setup, configure API key and exit if (options.setup && !options.key && !options.scan && process.argv.length <= 3) { console.log('Setup complete. Run again without --setup to use the framework.'); process.exit(0); } // If the user just wants to set up the key or provide it directly, and not run the full tool if (options.setup && !options.scan && process.argv.length <= 4) { process.exit(0); } // If scan mode is requested, run the code scanner if (options.scan) { const foundAI = await runCodeScan(options, apiKey); // If in CI mode and AI code found, exit successfully if (options.ci && foundAI) { console.log('RAI documentation generated successfully in CI mode.'); process.exit(0); } // If in CI mode and no AI code found, exit successfully but with a message if (options.ci && !foundAI) { console.log('No AI code detected in CI mode, skipping documentation generation.'); process.exit(0); } // If not in CI mode, continue with interactive mode if AI code found if (!options.ci && foundAI) { console.log('\nWould you like to refine the documentation with interactive mode?'); if (!options.ci) { const { interactive } = await inquirer.prompt([ { type: 'confirm', name: 'interactive', message: 'Run interactive mode to refine documentation?', default: true, } ]); if (!interactive) { process.exit(0); } } else { process.exit(0); } } else if (!options.ci && !foundAI) { console.log('\nWould you like to manually document an AI feature?'); const { manualMode } = await inquirer.prompt([ { type: 'confirm', name: 'manualMode', message: 'Run in manual mode to document AI features?', default: true, } ]); if (!manualMode) { process.exit(0); } } } const { isAI } = await inquirer.prompt([ { type: 'confirm', name: 'isAI', message: 'Is this commit introducing or modifying an AI feature?', default: false, }, ]); if (!isAI) { console.log('No AI feature detected. Proceeding with commit.'); process.exit(0); } // Checklist (minimal) const checklist = await inquirer.prompt([ { type: 'list', name: 'purpose', message: 'Main purpose of this AI feature?', choices: [ 'User-facing feature', 'Internal analytics', 'Automation/decision support', 'Other' ] }, { type: 'list', name: 'data', message: 'What data does it use?', choices: [ 'Only anonymized data', 'Personal data with consent', 'No personal data', 'Other' ] }, { type: 'list', name: 'monitoring', message: 'Is there a monitoring plan?', choices: [ 'Automated monitoring', 'Manual review', 'User feedback', 'No monitoring' ] } ]); // Model Card (minimal) const modelCard = await inquirer.prompt([ { type: 'list', name: 'modelType', message: 'Model type?', choices: [ 'Classification', 'Regression', 'Clustering', 'NLP/Language', 'Other' ] }, { type: 'list', name: 'metric', message: 'Key metric?', choices: [ 'Accuracy', 'F1 Score', 'AUC/ROC', 'Other' ] }, { type: 'list', name: 'limitation', message: 'Main limitation?', choices: [ 'Data bias', 'Limited generalization', 'Explainability', 'None known' ] } ]); // Risk File (minimal) const riskFile = await inquirer.prompt([ { type: 'confirm', name: 'privacy', message: 'Are privacy and regulatory checks done?', default: true }, { type: 'confirm', name: 'bias', message: 'Is bias/fairness reviewed?', default: true } ]); // Generate summaries const checklistSummary = `This AI feature is mainly for ${checklist.purpose.toLowerCase()}, using ${checklist.data.toLowerCase()}. Monitoring approach: ${checklist.monitoring.toLowerCase()}.`; const modelCardSummary = `Model type: ${modelCard.modelType}. Key metric: ${modelCard.metric}. Main limitation: ${modelCard.limitation}.`; const riskSummary = `Privacy/regulatory checks: ${riskFile.privacy ? 'Yes' : 'No'}. Bias/fairness reviewed: ${riskFile.bias ? 'Yes' : 'No'}.`; // Get AI tips const allAnswers = { checklist, modelCard, riskFile }; let aiTips = ''; if (apiKey) { aiTips = await getAITips(allAnswers, apiKey); } else { aiTips = "\n*No tips available. Add an OpenAI API key to receive AI-powered recommendations.*"; } // Save files (overwrite) fs.writeFileSync('checklist.md', `# Responsible AI Feature Checklist\n\n${checklistSummary}\n\n## Tips\n${aiTips.match(/Checklist Tips:[\s\S]*?(?=Model Card Tips:|Risk File Tips:|$)/)?.[0] || aiTips}\n`); fs.writeFileSync('model_card.md', `# Model Card\n\n${modelCardSummary}\n\n## Tips\n${aiTips.match(/Model Card Tips:[\s\S]*?(?=Checklist Tips:|Risk File Tips:|$)/)?.[0] || aiTips}\n`); fs.writeFileSync('risk_file.md', `# AI Model Risk & Compliance\n\n${riskSummary}\n\n## Tips\n${aiTips.match(/Risk File Tips:[\s\S]*?(?=Checklist Tips:|Model Card Tips:|$)/)?.[0] || aiTips}\n`); console.log('Responsible AI documentation updated! Proceeding with commit.'); process.exit(0); } main();