Skip to content

Commit

Permalink
complete risk score generator
Browse files Browse the repository at this point in the history
  • Loading branch information
ilhamfadheel committed Oct 9, 2024
1 parent 4b22309 commit e079881
Show file tree
Hide file tree
Showing 12 changed files with 5,070 additions and 232 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
OPENAI_API_KEY=
237 changes: 180 additions & 57 deletions app/api/generate-risk-score/route.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,195 @@
import { NextResponse } from 'next/server';
import OpenAI from 'openai';
import { openai } from '@ai-sdk/openai'; // Assuming this is part of some SDK you're using
import { generateText } from 'ai';
import puppeteer, { Page } from 'puppeteer';

interface TwitterProfile {
handle: string;
userDescription: string;
createdAt: string;
followers: number;
following: number;
verified: boolean;
locked: boolean;
website: string;
recentTweets: { text: string }[];
}


export async function POST(request: Request) {
const { twitterHandle, apiKey } = await request.json();
const { twitterHandle } = await request.json();

if (!twitterHandle) {
return NextResponse.json({ error: 'Twitter handle is required' }, { status: 400 });
}

if (!apiKey) {
return NextResponse.json({ error: 'OpenAI API key is required' }, { status: 400 });
}

// Initialize OpenAI client with the provided API key
const openai = new OpenAI({
apiKey: apiKey,
});

try {
// Fetch Twitter profile data (mock implementation)
// Fetch Twitter profile data
const twitterProfile = await fetchTwitterProfile(twitterHandle);
console.log('Twitter Profile:', JSON.stringify(twitterProfile, null, 2));

// Check if the account is locked
if (twitterProfile.locked) {
return NextResponse.json({ error: 'Unable to verify the account as the account is locked' }, { status: 403 });
}

// Generate analysis prompt
const prompt = generateAnalysisPrompt(twitterProfile);
console.log('Generated Analysis Prompt:', prompt);

// Call OpenAI API for analysis
const completion = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: [
{ role: "system", content: "You are an AI assistant that analyzes Twitter profiles for Web3 project risk assessment." },
{ role: "user", content: prompt }
],
const { text } = await generateText({
model: openai('gpt-4-turbo'),
system: "You are an AI assistant that analyzes Twitter profiles for Web3 project risk assessment. Your output for the reasoning will always be in markdown.",
prompt: prompt,
});

const analysis = completion.choices[0].message.content;
console.log('result by openAI:', text);

// Parse the analysis to extract scores
const scores = parseAnalysis(analysis);
// Parse the analysis to extract scores and overall risk score
const medianScore = getMedian(text);

// Calculate overall risk score
const overallScore = calculateOverallScore(scores);

return NextResponse.json({ riskScore: overallScore, detailedScores: scores });
// Return the analysis result and overall risk score
return NextResponse.json({ result: text, riskScore: medianScore });
} catch (error) {
console.error('Error generating risk score:', error);
return NextResponse.json({ error: 'Failed to generate risk score' }, { status: 500 });
return NextResponse.json({ error: error }, { status: 500 });
}
}

async function fetchTwitterProfile(handle: string) {
// Mock implementation - replace with actual Twitter API call
return {
handle,
createdAt: '2020-01-01',
followers: 15000,
verified: true,
tweetCount: 1000,
// Add more fields as needed
};
try {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto(`https://twitter.com/${handle}`, { waitUntil: 'networkidle0' });

// Scroll down to load more tweets
await autoScroll(page);


const profileData = await page.evaluate(() => {
const followingElement = document.querySelector('a[href$="/following"] span');
const userDescriptionElement = document.querySelector('div[data-testid="UserDescription"]');
const followersElement = document.querySelector('a[href$="/verified_followers"] span');
const joinDateElement = document.querySelector('span[data-testid="UserJoinDate"]');
const verifiedElement = document.querySelector('svg[aria-label="Verified account"]');
const lockedElement = document.querySelector('svg[aria-label="Protected account"]');
const websiteElement = document.querySelector('a[data-testid="UserUrl"] span');

// Updated tweet extraction
const tweetElements = document.querySelectorAll('div[data-testid="tweetText"]');

const tweets = Array.from(tweetElements)
.map(tweet => ({
text: tweet.textContent?.replace(/\n/g, ' ').trim() ?? ''
}))
.filter(tweet => tweet.text !== ''); // Filter out any empty tweets

console.log(tweets);



return {
userDescription: userDescriptionElement
? Array.from(userDescriptionElement.childNodes)
.map(node => node.textContent?.trim() ?? '')
.filter(text => text !== '')
.join(' ')
: '',
following: followingElement?.textContent?.trim() ?? '0',
followers: followersElement?.textContent?.trim() ?? '0',
joinDate: joinDateElement?.textContent?.trim() ?? '',
verified: !!verifiedElement,
locked: !!lockedElement,
website: websiteElement?.textContent?.trim() ?? '',
recentTweets: tweets,
};
});

await browser.close();

const cleanedTweets = profileData.recentTweets.map(tweet => ({
text: cleanTweetText(tweet.text)
})).filter(tweet => tweet.text !== '');

return {
handle,
userDescription: profileData.userDescription,
createdAt: profileData.joinDate,
followers: parseTwitterNumber(profileData.followers),
following: parseTwitterNumber(profileData.following),
verified: profileData.verified,
locked: profileData.locked,
website: profileData.website,
recentTweets: cleanedTweets,
};
} catch (error) {
console.error('Error fetching Twitter profile:', error);
throw new Error('Failed to fetch Twitter profile');
}
}

function generateAnalysisPrompt(profile: any) {
async function autoScroll(page: Page) {
await page.evaluate(async () => {
await new Promise<void>((resolve) => {
const startTime = Date.now(); // Track the start time
const duration = 15000; // Minimum 10 seconds (in ms)
let totalHeight = 0;
const distance = 1000; // Scroll 100px on each step

const timer = setInterval(() => {
const now = Date.now();
const scrollHeight = document.documentElement.scrollHeight;
window.scrollBy(0, distance);
totalHeight += distance;

// Stop scrolling if we've reached the end of the page or if 10 seconds have passed
if (
totalHeight >= scrollHeight - window.innerHeight || // Scroll till the bottom of the page
now - startTime >= duration // Ensure 10 seconds duration
) {
clearInterval(timer);
resolve();
}
}, 100); // Scroll every 100 milliseconds for stability
});
});
}


function cleanTweetText(text: string): string {
let cleaned = text.replace(/^.*?·.*?(\n|$)/, '');
cleaned = cleaned.replace(/Show more$/, '');
return cleaned.trim();
}

function parseTwitterNumber(str: string): number {
const multipliers: { [key: string]: number } = { K: 1000, M: 1000000, B: 1000000000 };
const match = str.match(/^(\d+(?:\.\d+)?)\s*([KMB])?$/);
if (match) {
const [, num, unit] = match;
return Math.round(parseFloat(num) * (unit ? multipliers[unit] : 1));
}
return 0;
}


function generateAnalysisPrompt(profile: TwitterProfile) {
return `
Analyze the following Twitter profile for a Web3 project risk assessment:
Twitter Handle: ${profile.handle}
User Description: ${profile.userDescription}
Account Created: ${profile.createdAt}
Website: ${profile.website}
Followers: ${profile.followers}
Following: ${profile.following}
Verified: ${profile.verified}
Tweet Count: ${profile.tweetCount}
Please provide a score from 1 to 10 for each of the following criteria, where 1 is the lowest (highest risk) and 10 is the highest (lowest risk):
Recent Tweets:
${profile.recentTweets.map((tweet, index) => `${index + 1}. ${tweet.text}`).join('\n')}
Please provide a score from 1 to 10 for each of the following criteria, where 0 is the lowest (highest risk) and 10 is the highest (lowest risk):
1. Account Longevity: Established accounts (>6 months) indicate more stability
2. Follower Base: A substantial following (>10,000) suggests broader recognition
Expand All @@ -83,35 +202,39 @@ Please provide a score from 1 to 10 for each of the following criteria, where 1
9. Team Visibility: Clear information about team members builds trust
10. Industry Connections: Interactions with reputable projects suggest legitimacy
Provide your scores in the following format:
1. Account Longevity: [SCORE]
2. Follower Base: [SCORE]
...
10. Industry Connections: [SCORE]
Follow each score with a brief explanation of your reasoning.
Provide your scores and reasoning for each criterion. Make sure your output is in markdown style.
`;
}

function parseAnalysis(analysis: string | null): Record<string, number> {
if (!analysis) return {};
function getMedian(analysis: string | null): number {
if (!analysis) return 0;

const scores: Record<string, number> = {};
const scores: number[] = [];
const lines = analysis.split('\n');

for (const line of lines) {
const match = line.match(/^(\d+)\.\s+(.+):\s+(\d+)/);
if (match) {
const [, , category, score] = match;
scores[category] = parseInt(score, 10);
if (line.includes('Score:')) {
const scoreMatch = line.match(/Score:\s*(\d+)/);
if (scoreMatch) {
const score = parseInt(scoreMatch[1], 10);
scores.push(score);
}
}
}

return scores;
return calculateMedian(scores);
}

function calculateOverallScore(scores: Record<string, number>): number {
const totalScore = Object.values(scores).reduce((sum, score) => sum + score, 0);
const averageScore = totalScore / Object.keys(scores).length;
return Math.round(averageScore * 10) / 10; // Round to one decimal place
// Helper function to calculate the median
function calculateMedian(numbers: number[]): number {
if (numbers.length === 0) return 0;

const sorted = [...numbers].sort((a, b) => a - b);
const middle = Math.floor(sorted.length / 2);

if (sorted.length % 2 === 0) {
return (sorted[middle - 1] + sorted[middle]) / 2;
}

return sorted[middle];
}
6 changes: 3 additions & 3 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { HeaderComponent } from "@/components/header";
import MaskedAPIKey from "@/components/masked-api-key";
// import MaskedAPIKey from "@/components/masked-api-key";
import RiskScoreCard from "@/components/risk-score-card";

export default function DashboardPage() {
Expand All @@ -10,8 +10,8 @@ export default function DashboardPage() {
<>
<HeaderComponent />
<div className="flex flex-col min-h-screen items-center justify-center">
<MaskedAPIKey/>
<RiskScoreCard/>
{/* <MaskedAPIKey/> */}
<RiskScoreCard />
</div >
</>
);
Expand Down
5 changes: 4 additions & 1 deletion app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";
import { ApiKeyProvider } from "@/hooks/ApiKeyContext";

const geistSans = localFont({
src: "./fonts/GeistVF.woff",
Expand Down Expand Up @@ -28,7 +29,9 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<ApiKeyProvider>
{children}
</ApiKeyProvider>
</body>
</html>
);
Expand Down
8 changes: 7 additions & 1 deletion components/masked-api-key.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@ import { Input } from "@/components/ui/input";
import { useApiKey } from "@/hooks/useApiKey";

export default function MaskedAPIKey() {
const [apiKey, updateApiKey] = useApiKey();
const { apiKey, updateApiKey } = useApiKey();
const [showApiKey, setShowApiKey] = useState(false);

const handleApiKeyChange = (e: React.ChangeEvent<HTMLInputElement>) => {
updateApiKey(e.target.value);
};

const handleApiKeyPaste = (e: React.ClipboardEvent<HTMLInputElement>) => {
const pastedText = e.clipboardData.getData("text");
updateApiKey(pastedText);
};

const maskApiKey = (key: string) => {
if (key.length <= 6) return key;
return key.slice(0, 6) + "*".repeat(Math.max(0, key.length - 11));
Expand All @@ -34,6 +39,7 @@ export default function MaskedAPIKey() {
type={showApiKey ? "text" : "password"}
value={apiKey}
onChange={handleApiKeyChange}
onPaste={handleApiKeyPaste}
className="pr-10"
placeholder="Enter your OpenAI API key"
/>
Expand Down
Loading

0 comments on commit e079881

Please sign in to comment.