-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add ai to detect clankers #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ac2a620
8eac1cd
b05ca50
f39148d
c4bd250
f59d4f1
14b9bc1
e10c844
91b482b
62ca24d
602a17d
e2d154b
a58b9f3
4079fdd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| import { identify } from "../src/index"; | ||
| import { getAIAnalysis } from "../src/ai/index"; | ||
|
|
||
| const username = process.argv[2]; | ||
| const model = process.argv[3] || "openai/gpt-4o-mini"; | ||
| const token = process.env.GITHUB_TOKEN; | ||
|
|
||
| if (!username) { | ||
| console.error("Usage: GITHUB_TOKEN=<token> npx jiti scripts/ai-analyser-user.ts <username> [model]"); | ||
| console.error("Example: npx jiti scripts/ai-analyser-user.ts octocat openai/gpt-4o"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| if (!token) { | ||
| console.error("Error: GITHUB_TOKEN environment variable is required"); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| async function run() { | ||
| console.log(`Fetching data for: ${username}`); | ||
|
|
||
| const userRes = await fetch(`https://api.github.com/users/${username}`, { | ||
| headers: { Authorization: `Bearer ${token}` }, | ||
| }); | ||
| if (!userRes.ok) throw new Error(`GitHub API error: ${userRes.status} ${userRes.statusText}`); | ||
| const user = await userRes.json(); | ||
|
|
||
| console.log(`Fetching orgs...`); | ||
| const orgsRes = await fetch(`https://api.github.com/users/${username}/orgs`, { | ||
| headers: { Authorization: `Bearer ${token}` }, | ||
| }); | ||
| const orgs = orgsRes.ok | ||
| ? (await orgsRes.json()).map((o: { login: string }) => o.login) | ||
| : []; | ||
| console.log(`Fetching events...`); | ||
| const events = []; | ||
| for (let page = 1; page <= 3; page++) { | ||
| const res = await fetch( | ||
| `https://api.github.com/users/${username}/events?per_page=100&page=${page}`, | ||
| { headers: { Authorization: `Bearer ${token}` } }, | ||
| ); | ||
| if (!res.ok) throw new Error(`GitHub API error: ${res.status} ${res.statusText}`); | ||
| const page_events = await res.json(); | ||
| if (page_events.length === 0) break; | ||
| events.push(...page_events); | ||
| if (page_events.length < 100) break; | ||
| } | ||
|
|
||
| console.log(`Running heuristic analysis...`); | ||
| const analysis = identify({ | ||
| createdAt: user.created_at, | ||
| reposCount: user.public_repos, | ||
| accountName: user.login, | ||
| events, | ||
| }); | ||
|
|
||
| console.log("\n--- Heuristic Result ---"); | ||
| console.log(`Classification: ${analysis.classification}`); | ||
| console.log(`Score: ${analysis.score}`); | ||
| if (analysis.flags.length) { | ||
| console.log("Flags:"); | ||
| for (const flag of analysis.flags) { | ||
| console.log(` - ${flag.label} (${flag.points} pts): ${flag.detail}`); | ||
| } | ||
| } | ||
|
|
||
| console.log(`\nRunning AI analysis with model: ${model}...`); | ||
| const aiResult = await getAIAnalysis({ | ||
| token: token!, | ||
| model, | ||
| username: user.login, | ||
| // let's test without. Feels like this influence too much the LLM | ||
| // analysis, | ||
| accountCreatedAt: user.created_at, | ||
| publicRepos: user.public_repos, | ||
| events, | ||
| orgs, | ||
| }); | ||
|
|
||
| if (!aiResult) { | ||
| console.error("AI analysis returned no result."); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| console.log("\n--- AI Result ---"); | ||
| console.log(`Classification: ${aiResult.classification}`); | ||
| console.log(`Confidence: ${aiResult.confidence}`); | ||
| console.log(`Reasoning: ${aiResult.reasoning}`); | ||
| } | ||
|
|
||
| run().catch((err) => { | ||
| console.error(`Error: ${err.message}`); | ||
| process.exit(1); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { z } from "zod/mini"; | ||
| import { buildUserPrompt, SYSTEM_PROMPT } from "./prompt"; | ||
| import type { AIAnalysisInput, AIAnalysisResult } from "./types"; | ||
|
|
||
| const aiAnalysisResultSchema = z.object({ | ||
| classification: z.enum(["organic", "mixed", "automation"]), | ||
| confidence: z.number(), | ||
| reasoning: z.string(), | ||
| }); | ||
|
|
||
| export async function getAIAnalysis(input: AIAnalysisInput): Promise<AIAnalysisResult | null> { | ||
| const { token = process.env.GITHUB_TOKEN, model = 'openai/gpt-4o-mini', username, analysis, accountCreatedAt, publicRepos, events, orgs } = input; | ||
|
|
||
| if (!token) { | ||
| throw new Error("GitHub token is required for AI analysis. Please provide it in the input or set it as an environment variable GITHUB_TOKEN."); | ||
| } | ||
|
|
||
| const prompt = buildUserPrompt({ token, model, username, analysis, accountCreatedAt, publicRepos, events, orgs }); | ||
|
|
||
|
Comment on lines
+11
to
+19
|
||
| // todo: extract into separate module for calling different AI providers and handling their specific quirks | ||
| const response = await fetch( | ||
| "https://models.github.ai/inference/chat/completions", | ||
| { | ||
| method: "POST", | ||
| headers: { | ||
| Authorization: `Bearer ${token}`, | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ | ||
| model, | ||
| messages: [ | ||
| { role: "system", content: SYSTEM_PROMPT }, | ||
| { role: "user", content: prompt }, | ||
| ], | ||
| temperature: 0.3, | ||
| }), | ||
| }, | ||
| ); | ||
|
|
||
| if (!response.ok) { | ||
| const body = await response.text(); | ||
| throw new Error(`${response.status} ${response.statusText}: ${body}`); | ||
| } | ||
|
|
||
| const data = (await response.json()) as { | ||
| choices?: { message?: { content?: string } }[]; | ||
| }; | ||
| let content = data.choices?.[0]?.message?.content?.trim() ?? null; | ||
| if (!content) return null; | ||
|
|
||
| if (model.includes("deepseek")) { | ||
| // remove DeepSeek-R1 markers if present | ||
| content = content.replace(/<think>[\s\S]*?<\/think>/g, "").trim(); | ||
| } | ||
|
|
||
| return aiAnalysisResultSchema.parse(JSON.parse(content)); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,3 @@ | ||||||||||||||
| export * from './analysis' | ||||||||||||||
| export * from './types' | ||||||||||||||
| export * from './prompt' | ||||||||||||||
|
Comment on lines
+1
to
+3
|
||||||||||||||
| export * from './analysis' | |
| export * from './types' | |
| export * from './prompt' | |
| export * from "./analysis"; | |
| export * from "./types"; | |
| export * from "./prompt"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The README says
getAIAnalysis“returnsnullif the model produces no usable response,” but the current implementation throws on HTTP errors and JSON parse failures. Either update the README to describe the throwing behavior, or change the implementation to catch/returnnullfor these failure modes.