251 lines
7.6 KiB
JavaScript

#!/usr/bin/env node
/**
* AI Analyzer CLI
*
* Command-line interface for the ai-analyzer package
* Can be used by any parser to analyze JSON files
*/
const fs = require("fs");
const path = require("path");
// Import AI utilities from this package
const {
logger,
analyzeBatch,
checkOllamaStatus,
findLatestResultsFile,
} = require("./index");
// Default configuration
const DEFAULT_CONTEXT =
process.env.AI_CONTEXT || "job market analysis and trends";
const DEFAULT_MODEL = process.env.OLLAMA_MODEL || "mistral";
const DEFAULT_RESULTS_DIR = "results";
// Parse command line arguments
const args = process.argv.slice(2);
let inputFile = null;
let outputFile = null;
let context = DEFAULT_CONTEXT;
let model = DEFAULT_MODEL;
let findLatest = false;
let resultsDir = DEFAULT_RESULTS_DIR;
for (const arg of args) {
if (arg.startsWith("--input=")) {
inputFile = arg.split("=")[1];
} else if (arg.startsWith("--output=")) {
outputFile = arg.split("=")[1];
} else if (arg.startsWith("--context=")) {
context = arg.split("=")[1];
} else if (arg.startsWith("--model=")) {
model = arg.split("=")[1];
} else if (arg.startsWith("--dir=")) {
resultsDir = arg.split("=")[1];
} else if (arg === "--latest") {
findLatest = true;
} else if (arg === "--help" || arg === "-h") {
console.log(`
AI Analyzer CLI
Usage: node cli.js [options]
Options:
--input=FILE Input JSON file
--output=FILE Output file (default: ai-analysis-{timestamp}.json)
--context="description" Analysis context (default: "${DEFAULT_CONTEXT}")
--model=MODEL Ollama model (default: ${DEFAULT_MODEL})
--latest Use latest results file from results directory
--dir=PATH Directory to look for results (default: 'results')
--help, -h Show this help
Examples:
node cli.js --input=results.json
node cli.js --latest --dir=results
node cli.js --input=results.json --context="job trends" --model=mistral
Environment Variables:
AI_CONTEXT Default analysis context
OLLAMA_MODEL Default Ollama model
`);
process.exit(0);
}
}
async function main() {
try {
// Determine input file
if (findLatest) {
try {
inputFile = findLatestResultsFile(resultsDir);
logger.info(`Found latest results file: ${inputFile}`);
} catch (error) {
logger.error(
`❌ No results files found in '${resultsDir}': ${error.message}`
);
logger.info(`💡 To create results files:`);
logger.info(
` 1. Run a parser first (e.g., npm start in linkedin-parser)`
);
logger.info(` 2. Or provide a specific file with --input=FILE`);
logger.info(` 3. Or create a sample JSON file to test with`);
process.exit(1);
}
}
// If inputFile is a relative path and --dir is set, resolve it
if (inputFile && !path.isAbsolute(inputFile) && !fs.existsSync(inputFile)) {
const candidate = path.join(resultsDir, inputFile);
if (fs.existsSync(candidate)) {
inputFile = candidate;
}
}
if (!inputFile) {
logger.error("❌ Input file required. Use --input=FILE or --latest");
logger.info(`💡 Examples:`);
logger.info(` node cli.js --input=results.json`);
logger.info(` node cli.js --latest --dir=results`);
logger.info(` node cli.js --help`);
process.exit(1);
}
// Load input file
logger.step(`Loading input file: ${inputFile}`);
if (!fs.existsSync(inputFile)) {
throw new Error(`Input file not found: ${inputFile}`);
}
const data = JSON.parse(fs.readFileSync(inputFile, "utf-8"));
// Extract posts from different formats
let posts = [];
if (data.results && Array.isArray(data.results)) {
posts = data.results;
logger.info(`Found ${posts.length} items in results array`);
} else if (Array.isArray(data)) {
posts = data;
logger.info(`Found ${posts.length} items in array`);
} else {
throw new Error("Invalid JSON format - need array or {results: [...]}");
}
if (posts.length === 0) {
throw new Error("No items found to analyze");
}
// Check AI availability
logger.step("Checking AI availability");
const aiAvailable = await checkOllamaStatus(model);
if (!aiAvailable) {
throw new Error(
`AI not available. Make sure Ollama is running and model '${model}' is installed.`
);
}
// Check if results already have AI analysis
const hasExistingAI = posts.some((post) => post.aiAnalysis);
if (hasExistingAI) {
logger.info(
`📋 Results already contain AI analysis - will update with new context`
);
}
// Prepare data for analysis
const analysisData = posts.map((post, i) => ({
text: post.text || post.content || post.post || "",
location: post.location || "Unknown",
keyword: post.keyword || "Unknown",
timestamp: post.timestamp || new Date().toISOString(),
}));
// Run analysis
logger.step(`Running AI analysis with context: "${context}"`);
const analysis = await analyzeBatch(analysisData, context, model);
// Integrate AI analysis back into the original results
const updatedPosts = posts.map((post, index) => {
const aiResult = analysis[index];
return {
...post,
aiAnalysis: {
isRelevant: aiResult.isRelevant,
confidence: aiResult.confidence,
reasoning: aiResult.reasoning,
context: context,
model: model,
analyzedAt: new Date().toISOString(),
},
};
});
// Update the original data structure
if (data.results && Array.isArray(data.results)) {
data.results = updatedPosts;
// Update metadata
data.metadata = data.metadata || {};
data.metadata.aiAnalysisUpdated = new Date().toISOString();
data.metadata.aiContext = context;
data.metadata.aiModel = model;
} else {
// If it's a simple array, create a proper structure
data = {
metadata: {
timestamp: new Date().toISOString(),
totalItems: updatedPosts.length,
aiContext: context,
aiModel: model,
analysisType: "cli",
},
results: updatedPosts,
};
}
// Generate output filename if not provided
if (!outputFile) {
// Use the original filename with -ai suffix
const originalName = path.basename(inputFile, path.extname(inputFile));
outputFile = path.join(
path.dirname(inputFile),
`${originalName}-ai.json`
);
}
// Save updated results back to file
fs.writeFileSync(outputFile, JSON.stringify(data, null, 2));
// Show summary
const relevant = analysis.filter((a) => a.isRelevant).length;
const irrelevant = analysis.filter((a) => !a.isRelevant).length;
const avgConfidence =
analysis.reduce((sum, a) => sum + a.confidence, 0) / analysis.length;
logger.success("✅ AI analysis completed and integrated");
logger.info(`📊 Context: "${context}"`);
logger.info(`📈 Total items analyzed: ${analysis.length}`);
logger.info(
`✅ Relevant items: ${relevant} (${(
(relevant / analysis.length) *
100
).toFixed(1)}%)`
);
logger.info(
`❌ Irrelevant items: ${irrelevant} (${(
(irrelevant / analysis.length) *
100
).toFixed(1)}%)`
);
logger.info(`🎯 Average confidence: ${avgConfidence.toFixed(2)}`);
logger.file(`🧠 Updated results saved to: ${outputFile}`);
} catch (error) {
logger.error(`❌ Analysis failed: ${error.message}`);
process.exit(1);
}
}
// Run the CLI
main();