Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .coderabbit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,4 @@ reviews:

# Optional: By default, draft pull requests are not reviewed.
# Set to true if you want Coderabbit to review drafts as well.
drafts: false
drafts: false
10 changes: 10 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"printWidth": 120,
"trailingComma": "none",
"singleQuote": true,
"semi": true,
"useTabs": false,
"tabWidth": 2,
"arrowParens": "always",
"jsxSingleQuote": true
}
44 changes: 21 additions & 23 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,22 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug redisDedupeListener",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/src/redisDedupeListener.ts",
"runtimeExecutable": "npx",
"runtimeArgs": ["tsx"],
"console": "integratedTerminal",
"internalConsoleOptions": "openOnSessionStart",
"env": {
"NODE_OPTIONS": "--enable-source-maps"
}
}
]
}
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug redisDedupeListener",
"skipFiles": ["<node_internals>/**"],
"program": "${workspaceFolder}/src/redisDedupeListener.ts",
"runtimeExecutable": "npx",
"runtimeArgs": ["tsx"],
"console": "integratedTerminal",
"internalConsoleOptions": "openOnSessionStart",
"env": {
"NODE_OPTIONS": "--enable-source-maps"
}
}
]
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# classifier

AI engine for classifying market sentiment, topics, and trustworthiness from raw data.
2 changes: 1 addition & 1 deletion data/sample_posts.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@
"url": "https://x.com/1003",
"date": "2025-09-03T18:20:00.000Z"
}
]
]
16 changes: 16 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"start": "node dist/run-classifier.js",
"seed": "tsx src/seed.ts",
"seed:clear": "tsx src/seed.ts clear",
"seed:verify": "tsx src/seed.ts verify"
"seed:verify": "tsx src/seed.ts verify",
"format": "npx prettier --write ."
},
"dependencies": {
"@prisma/client": "^6.15.0",
Expand All @@ -26,6 +27,7 @@
},
"devDependencies": {
"@types/node": "^24.3.1",
"prettier": "^3.6.2",
"tsx": "^4.20.5",
"typescript": "^5.9.2"
}
Expand Down
98 changes: 49 additions & 49 deletions run-classifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,62 +9,62 @@ import { generateTitlesForPosts } from './src/generateTitle.js';
const command = process.argv[2];

async function main() {
console.log("🤖 SentioPulse Classifier Tool");
console.log("==============================\n");
console.log('🤖 SentioPulse Classifier Tool');
console.log('==============================\n');

switch (command) {
case 'complete':
case 'all':
console.log("Running complete analysis (categorization + sentiment + title generation)...");
await runCompleteAnalysisDemo();
break;
switch (command) {
case 'complete':
case 'all':
console.log('Running complete analysis (categorization + sentiment + title generation)...');
await runCompleteAnalysisDemo();
break;

case 'categorize':
case 'category':
console.log("Running categorization only...");
await runCategorization();
break;
case 'categorize':
case 'category':
console.log('Running categorization only...');
await runCategorization();
break;

case 'sentiment': {
console.log("Running sentiment analysis only...");
const samplePosts = [
"Bitcoin is going to skyrocket after the halving event next month! The fundamentals are incredibly strong and institutional adoption is accelerating. This could be the start of the next major bull run.",
"Ethereum might drop below $1000 soon due to the current risky market conditions. The macro environment is deteriorating and there's too much leverage in the system right now.",
"The market seems calm today with no major moves in either direction. Bitcoin is trading sideways and most altcoins are following suit. It's a good time to accumulate quality projects."
];
const sentimentResults = await analyzeMultiplePosts(samplePosts);
console.log("Sentiment Results:", JSON.stringify(sentimentResults, null, 2));
break;
}
case 'sentiment': {
console.log('Running sentiment analysis only...');
const samplePosts = [
'Bitcoin is going to skyrocket after the halving event next month! The fundamentals are incredibly strong and institutional adoption is accelerating. This could be the start of the next major bull run.',
"Ethereum might drop below $1000 soon due to the current risky market conditions. The macro environment is deteriorating and there's too much leverage in the system right now.",
"The market seems calm today with no major moves in either direction. Bitcoin is trading sideways and most altcoins are following suit. It's a good time to accumulate quality projects."
];
const sentimentResults = await analyzeMultiplePosts(samplePosts);
console.log('Sentiment Results:', JSON.stringify(sentimentResults, null, 2));
break;
}

case 'title':
case 'titles': {
console.log("Running title generation only...");
const titlePosts = [
"Benchmarking tiny on-device ML models for edge inference — latency down 40% with the new quantization pipeline.",
"Q2 fintech update: payments startup doubled TPV and improved take rate; unit economics are trending positive.",
"Reading a new whitepaper on Web3 compliance and institutional custody — regulatory clarity is the next catalyst for adoption."
];
const titleResults = await generateTitlesForPosts(titlePosts);
console.log("Title Results:", JSON.stringify(titleResults, null, 2));
break;
}
case 'title':
case 'titles': {
console.log('Running title generation only...');
const titlePosts = [
'Benchmarking tiny on-device ML models for edge inference — latency down 40% with the new quantization pipeline.',
'Q2 fintech update: payments startup doubled TPV and improved take rate; unit economics are trending positive.',
'Reading a new whitepaper on Web3 compliance and institutional custody — regulatory clarity is the next catalyst for adoption.'
];
const titleResults = await generateTitlesForPosts(titlePosts);
console.log('Title Results:', JSON.stringify(titleResults, null, 2));
break;
}

case 'help':
case '--help':
case '-h':
showHelp();
break;
case 'help':
case '--help':
case '-h':
showHelp();
break;

default:
console.log("No command specified. Running complete analysis by default...");
await runCompleteAnalysisDemo();
break;
}
default:
console.log('No command specified. Running complete analysis by default...');
await runCompleteAnalysisDemo();
break;
}
}

function showHelp() {
console.log(`
console.log(`
Usage: npm run classify [command]

Commands:
Expand All @@ -86,6 +86,6 @@ If no command is provided, complete analysis will run by default.

// Handle errors gracefully
main().catch((error) => {
console.error("❌ Error running classifier:", error);
process.exit(1);
console.error('❌ Error running classifier:', error);
process.exit(1);
});
88 changes: 44 additions & 44 deletions src/analyzeSentiment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@ import { callOpenAIWithValidation } from './openaiValidationUtil.js';
import { z } from 'zod';
import { generateTitleForPost } from './generateTitle.js';

// Sentiment analysis result type
// Sentiment analysis result type
export type SentimentResult = {
post: string;
sentiment: "BULLISH" | "BEARISH" | "NEUTRAL";
post: string;
sentiment: 'BULLISH' | 'BEARISH' | 'NEUTRAL';
};

// Zod schema for sentiment validation
const SentimentSchema = z.object({
sentiment: z.enum(["BULLISH", "BEARISH", "NEUTRAL"])
sentiment: z.enum(['BULLISH', 'BEARISH', 'NEUTRAL'])
});

// Analyze multiple posts at once
export async function analyzeMultiplePosts(posts: string[]): Promise<SentimentResult[]> {
const results: SentimentResult[] = [];
const systemPrompt = `You are a sentiment analysis system for crypto-related posts.
const results: SentimentResult[] = [];
const systemPrompt = `You are a sentiment analysis system for crypto-related posts.
Classify the sentiment of posts into one of: BULLISH, BEARISH, NEUTRAL.

Important: Always return the sentiment in uppercase letters exactly like this: BULLISH, BEARISH, NEUTRAL,
Expand All @@ -29,52 +29,52 @@ Return only valid JSON in this format:
"sentiment": "BULLISH"
}`;

for (const post of posts) {
try {
const validated = await callOpenAIWithValidation({
client: openai,
systemPrompt,
userPrompt: post,
schema: SentimentSchema,
retryCount: 3
});
for (const post of posts) {
try {
const validated = await callOpenAIWithValidation({
client: openai,
systemPrompt,
userPrompt: post,
schema: SentimentSchema,
retryCount: 3
});

if (validated) {
results.push({ post, sentiment: validated.sentiment });
}
} catch (e) {
console.error("Error analyzing post:", post, e);
continue;
}
if (validated) {
results.push({ post, sentiment: validated.sentiment });
}
} catch (e) {
console.error('Error analyzing post:', post, e);
continue;
}
return results;
}
return results;
}

async function runExample() {
// Example runner
const posts = [
"Bitcoin is going to skyrocket after the halving event next month! The fundamentals are incredibly strong and institutional adoption is accelerating. This could be the start of the next major bull run that takes us to new all-time highs.",
"Ethereum might drop below $1000 soon due to the current risky market conditions. The macro environment is deteriorating and there's too much leverage in the system. I'm expecting a significant correction in the coming weeks.",
"The market seems calm today with no major moves in either direction. Bitcoin is trading sideways and most altcoins are following suit. It's a good time to accumulate quality projects at these levels."
];
// Example runner
const posts = [
'Bitcoin is going to skyrocket after the halving event next month! The fundamentals are incredibly strong and institutional adoption is accelerating. This could be the start of the next major bull run that takes us to new all-time highs.',
"Ethereum might drop below $1000 soon due to the current risky market conditions. The macro environment is deteriorating and there's too much leverage in the system. I'm expecting a significant correction in the coming weeks.",
"The market seems calm today with no major moves in either direction. Bitcoin is trading sideways and most altcoins are following suit. It's a good time to accumulate quality projects at these levels."
];

const results = await analyzeMultiplePosts(posts);
const resultsByPost = new Map(results.map(r => [r.post, r.sentiment]));
for (const post of posts) {
const sentiment = resultsByPost.get(post);
const title = await generateTitleForPost(post);
console.log(`Post: ${post}`);
if (title) {
console.log(`Title: ${title}`);
}
if (sentiment) {
console.log(`Sentiment: ${sentiment}`);
}
console.log('─'.repeat(40));
const results = await analyzeMultiplePosts(posts);
const resultsByPost = new Map(results.map((r) => [r.post, r.sentiment]));
for (const post of posts) {
const sentiment = resultsByPost.get(post);
const title = await generateTitleForPost(post);
console.log(`Post: ${post}`);
if (title) {
console.log(`Title: ${title}`);
}
if (sentiment) {
console.log(`Sentiment: ${sentiment}`);
}
console.log('─'.repeat(40));
}
}

// Run example if this file is executed directly (not imported)
if (require.main === module) {
runExample();
}
runExample();
}
Loading