Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save eliseumds/caf0fc81eabcae0952fa81e0cf4f09be to your computer and use it in GitHub Desktop.

Select an option

Save eliseumds/caf0fc81eabcae0952fa81e0cf4f09be to your computer and use it in GitHub Desktop.
Inside the `bin/` directory. Run it with:
[
{
"id": "c2c-friendly-question",
"description": "Friendly consumer-to-consumer question about a review, clearly allowed.",
"exchangeType": "consumer to consumer",
"threadHistory": [],
"message": {
"body": "Hey, I saw your review about the washing machine. Has it still been working well after a year? I'm deciding whether to buy it.",
"role": "consumer"
},
"author": {
"displayName": "Emma",
"emailAddress": "emma@example.com",
"ipAddress": "203.0.113.10",
"country": "AU",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X)"
},
"expectedDecision": "approve",
"expectedCriteriaMatched": []
},
{
"id": "c2c-harassment",
"description": "Direct insult and harassment towards another user.",
"exchangeType": "consumer to consumer",
"threadHistory": [
{
"body": "I had a terrible experience with this company, support was very slow.",
"role": "consumer"
}
],
"message": {
"body": "You're clearly an idiot if you couldn't figure out how to use it. People like you shouldn't be allowed to post reviews.",
"role": "consumer"
},
"author": {
"displayName": "Mark",
"emailAddress": "mark@example.com",
"ipAddress": "203.0.113.11",
"country": "AU",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"abuse"
]
},
{
"id": "c2c-manipulation-bribe",
"description": "User offering a gift card to remove or change a negative review.",
"exchangeType": "consumer to consumer",
"threadHistory": [
{
"body": "Your review really helped me decide not to sign up.",
"role": "consumer"
}
],
"message": {
"body": "If you delete your bad review, I'll send you a $50 gift card. It really hurts my referral bonus when people cancel.",
"role": "consumer"
},
"author": {
"displayName": "Daniel",
"emailAddress": "daniel@example.com",
"ipAddress": "203.0.113.12",
"country": "AU",
"userAgent": "Mozilla/5.0 (X11; Linux x86_64)"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"manipulation"
]
},
{
"id": "c2c-spam-seo-links",
"description": "Classic SEO spam message with multiple promotional links.",
"exchangeType": "consumer to consumer",
"threadHistory": [],
"message": {
"body": "Get the best deals on all appliances at https://cheap-appliances-seo-example.com. Limited offer!!! Visit now and claim your bonus. Also check https://another-seo-link.example for more.",
"role": "consumer"
},
"author": {
"displayName": "Promo Bot",
"emailAddress": "offers@mailinator.com",
"ipAddress": "198.51.100.33",
"country": "US",
"userAgent": "UnknownBot/1.0"
},
"expectedDecision": "reject",
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"spam",
"suspicious_author"
]
},
{
"id": "c2c-gibberish",
"description": "Nonsensical message with no communicative intent.",
"exchangeType": "consumer to consumer",
"threadHistory": [],
"message": {
"body": "asdfasdf !!! 123 *** //// lorem ipsumzzzz not real review just random stuff",
"role": "consumer"
},
"author": {
"displayName": "Test User",
"emailAddress": "test@example.com",
"ipAddress": "203.0.113.13",
"country": "AU",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"irrelevant"
]
},
{
"id": "c2c-personal-information",
"description": "Consumer-to-consumer message that shares detailed personal contact information.",
"exchangeType": "consumer to consumer",
"threadHistory": [],
"message": {
"body": "Here's all my details so you can call me: my full name is Sarah Johnson, mobile 0400 123 456, home address 12 Example Street, Sydney NSW 2000. Let's chat about a side deal.",
"role": "consumer"
},
"author": {
"displayName": "Sarah",
"emailAddress": "sarah@example.com",
"ipAddress": "203.0.113.14",
"country": "AU",
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)"
},
"expectedDecision": "needs_review",
"expectedCriteriaMatched": []
},
{
"id": "support-legit-personal-info",
"description": "Customer support message including personal details, which is allowed in this context.",
"exchangeType": "customer support",
"threadHistory": [],
"message": {
"body": "Hi, I'm following up on my ticket. My account email is sarah@example.com and my order number is 123456. Could you please help me reset my login?",
"role": "consumer"
},
"author": {
"displayName": "Sarah",
"emailAddress": "sarah@example.com",
"ipAddress": "203.0.113.14",
"country": "AU",
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)"
},
"expectedDecision": "approve",
"expectedCriteriaMatched": []
},
{
"id": "phishing-payment-details",
"description": "Phishing-style request for payment details.",
"exchangeType": "consumer to consumer",
"threadHistory": [],
"message": {
"body": "Your account is at risk. To keep your review visible, send your credit card number and expiry date so we can verify your identity.",
"role": "consumer"
},
"author": {
"displayName": "Security Team",
"emailAddress": "support-security@example-check.com",
"ipAddress": "198.51.100.40",
"country": "US",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"phishing",
"impersonation"
]
},
{
"id": "impersonation-business-owner",
"description": "User falsely claiming to be the business owner to influence reviews.",
"exchangeType": "consumer to consumer",
"threadHistory": [],
"message": {
"body": "Hi, I'm the official owner of ACME Broadband (I'm not listed on the site yet). Please delete your negative review or we will have to close your account.",
"role": "consumer"
},
"author": {
"displayName": "John",
"emailAddress": "randomuser123@example.com",
"ipAddress": "203.0.113.20",
"country": "AU",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"impersonation",
"abuse",
"manipulation"
]
},
{
"id": "illegal-offer",
"description": "Message offering illegal service in exchange for review removal.",
"exchangeType": "consumer to consumer",
"threadHistory": [],
"message": {
"body": "If you take down your negative review, I can get you fake invoices and doctored documents so you can claim extra tax refunds. Interested?",
"role": "consumer"
},
"author": {
"displayName": "Leo",
"emailAddress": "leo@example.com",
"ipAddress": "203.0.113.21",
"country": "AU",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"illegal",
"manipulation"
]
},
{
"id": "support-basic-issue-approved",
"description": "Straightforward support request about an order problem, clearly allowed.",
"exchangeType": "customer support",
"threadHistory": [],
"message": {
"body": "Hi, my broadband installation appointment was missed yesterday. Could you please check my booking and let me know the next available time slot?",
"role": "consumer"
},
"author": {
"displayName": "Alice",
"emailAddress": "alice@example.com",
"ipAddress": "203.0.113.22",
"country": "AU",
"userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X)"
},
"expectedDecision": "approve",
"expectedCriteriaMatched": []
},
{
"id": "support-negative-but-legit",
"description": "Strongly negative but legitimate complaint to support, should not be rejected.",
"exchangeType": "customer support",
"threadHistory": [],
"message": {
"body": "I'm extremely frustrated. Your service has gone down three times this week and no one is answering the phone. This is unacceptable and I want this fixed immediately.",
"role": "consumer"
},
"author": {
"displayName": "Ben",
"emailAddress": "ben@example.com",
"ipAddress": "203.0.113.23",
"country": "AU",
"userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
},
"expectedDecision": "approve",
"expectedCriteriaMatched": []
},
{
"id": "support-spam-promo",
"description": "Spam promotion sent to a company support channel.",
"exchangeType": "customer support",
"threadHistory": [],
"message": {
"body": "Hello, we offer the best SEO packages for your website. Boost your rankings instantly, check out https://spam-seo-offers.example for exclusive deals.",
"role": "consumer"
},
"author": {
"displayName": "SEO Team",
"emailAddress": "sales@cheap-seo.example",
"ipAddress": "198.51.100.55",
"country": "US",
"userAgent": "SpamBot/2.0"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"spam",
"suspicious_author"
]
},
{
"id": "support-abusive-to-agent",
"description": "Abusive message directed at a support agent.",
"exchangeType": "customer support",
"threadHistory": [
{
"body": "Hi, I'm from ACME support. I can see your last invoice and I'm happy to help.",
"role": "company_representative"
}
],
"message": {
"body": "You are useless and incompetent. If you don't fix this right now I will hunt you down and make you pay.",
"role": "consumer"
},
"author": {
"displayName": "Carl",
"emailAddress": "carl@example.com",
"ipAddress": "203.0.113.24",
"country": "AU",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"abuse"
]
},
{
"id": "support-phishing-pretend-staff",
"description": "Phishing message pretending to be official support asking for sensitive data.",
"exchangeType": "customer support",
"threadHistory": [],
"message": {
"body": "This is ACME Support. To keep your account active, reply with your full credit card number and CVV so we can confirm your payment details.",
"role": "consumer"
},
"author": {
"displayName": "Support Agent",
"emailAddress": "acme-support-verify@example-check.com",
"ipAddress": "198.51.100.60",
"country": "US",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"phishing",
"impersonation",
"suspicious_author"
]
},
{
"id": "c2c-crypto-investment-scam",
"description": "Consumer-to-consumer crypto investment scam promising unrealistic returns.",
"exchangeType": "consumer to consumer",
"threadHistory": [],
"message": {
"body": "Hey, I saw your review about needing extra income. If you send me $500 in USDT today, I can double it in 24 hours with my special trading bot. This is a private offer just for you, don't tell anyone.",
"role": "consumer"
},
"author": {
"displayName": "Crypto Coach",
"emailAddress": "fastprofits@crypto-example.biz",
"ipAddress": "198.51.100.70",
"country": "US",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"phishing",
"spam"
]
},
{
"id": "c2c-crypto-wallet-phish",
"description": "Consumer-to-consumer message trying to steal crypto wallet keys.",
"exchangeType": "consumer to consumer",
"threadHistory": [],
"message": {
"body": "Your Coinbase wallet is about to be closed. Reply with your 12-word recovery phrase so I can help you save your coins.",
"role": "consumer"
},
"author": {
"displayName": "Coinbase Support",
"emailAddress": "support@coinbsae-security.example",
"ipAddress": "198.51.100.71",
"country": "US",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"phishing",
"impersonation",
"suspicious_author"
]
},
{
"id": "support-company-impersonation-refund-scam",
"description": "Scam pretending to be the reviewed company offering a fake refund via suspicious link.",
"exchangeType": "customer support",
"threadHistory": [
{
"body": "I left a negative review because my order never arrived.",
"role": "consumer"
}
],
"message": {
"body": "Hello, we are ACME Broadband Billing Team. To process your refund, please enter your card details and online banking password at https://acme-refunds-secure.example.",
"role": "consumer"
},
"author": {
"displayName": "ACME Billing",
"emailAddress": "billing@acme-refunds-support.example",
"ipAddress": "198.51.100.72",
"country": "US",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"phishing",
"impersonation"
]
},
{
"id": "support-fake-tech-support-crypto",
"description": "Fake tech support telling user to install remote access and pay in crypto.",
"exchangeType": "customer support",
"threadHistory": [],
"message": {
"body": "This is Windows Support. Your computer is infected and leaking your ProductReview login. Install AnyDesk from this link and send 0.02 BTC to our security wallet so we can fix it.",
"role": "consumer"
},
"author": {
"displayName": "Windows Security",
"emailAddress": "security@win-fix.example",
"ipAddress": "198.51.100.73",
"country": "US",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"phishing",
"impersonation"
]
},
{
"id": "c2c-fake-job-offer-scam",
"description": "Fake job offer that demands upfront crypto payment.",
"exchangeType": "consumer to consumer",
"threadHistory": [],
"message": {
"body": "I can get you a guaranteed remote job at this company you reviewed. You just need to pay a small onboarding fee of 200 USDT to my wallet first. After that, you will earn $5,000 per week easily.",
"role": "consumer"
},
"author": {
"displayName": "Recruiter Pro",
"emailAddress": "jobs@career-boost-crypto.example",
"ipAddress": "198.51.100.74",
"country": "US",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"phishing",
"spam"
]
},
{
"id": "support-shipping-delay-conversation",
"description": "Multi-turn support conversation about a delayed shipment that remains polite and on-topic.",
"exchangeType": "customer support",
"threadHistory": [
{
"body": "Hi, my coffee machine order still hasn't arrived even though the tracking says it was shipped last week.",
"role": "consumer"
},
{
"body": "Hi there, I'm from ACME Appliances. I'm sorry for the delay. Let me check the tracking with our courier and get back to you.",
"role": "company_representative"
},
{
"body": "Thanks, I just want to know if it's actually on the way or if it got lost.",
"role": "consumer"
}
],
"message": {
"body": "We've confirmed the parcel was held at the depot after a routing error. It is now out for delivery and should arrive tomorrow. If it doesn't, please reply and we'll arrange either a replacement or a refund.",
"role": "company_representative"
},
"author": {
"displayName": "ACME Support",
"emailAddress": "support@acme-appliances.example",
"ipAddress": "203.0.113.50",
"country": "AU",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "approve",
"expectedCriteriaMatched": []
},
{
"id": "support-billing-refund-disagreement",
"description": "Customer pushes hard for a refund, company representative stays professional; should still be allowed.",
"exchangeType": "customer support",
"threadHistory": [
{
"body": "You keep charging me for a subscription I cancelled two months ago. This is ridiculous.",
"role": "consumer"
},
{
"body": "Hi, I'm from ACME Billing. I can see the cancellation request, but it reached us after the last billing cycle started. I'm happy to review this for you.",
"role": "company_representative"
},
{
"body": "I don't care about your policies, I just want this money back. This feels really unfair.",
"role": "consumer"
}
],
"message": {
"body": "I understand your frustration. As a gesture of goodwill, I've processed a refund for the last invoice and cancelled any future renewals. You should see the funds back in your account within 3–5 business days.",
"role": "company_representative"
},
"author": {
"displayName": "ACME Billing",
"emailAddress": "billing@acme-broadband.example",
"ipAddress": "203.0.113.51",
"country": "AU",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "approve",
"expectedCriteriaMatched": []
},
{
"id": "support-personal-data-sharing",
"description": "Consumer shares fairly detailed personal information with support; allowed for customer support context.",
"exchangeType": "customer support",
"threadHistory": [
{
"body": "Hi, I can't log into my account anymore and I think I typed my email wrong when I signed up.",
"role": "consumer"
}
],
"message": {
"body": "Here are my details so you can find the account: my name is Michael Roberts, my address is 45 Example Road, Melbourne VIC 3000, and my phone number is 0412 345 678. The email I meant to use is michael.roberts@example.com.",
"role": "consumer"
},
"author": {
"displayName": "Michael",
"emailAddress": "michael.roberts@example.com",
"ipAddress": "203.0.113.52",
"country": "AU",
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)"
},
"expectedDecision": "approve",
"expectedCriteriaMatched": []
},
{
"id": "support-company-incentive-to-edit-review",
"description": "Company representative offers incentive for changing a negative review; should be rejected as manipulation.",
"exchangeType": "customer support",
"threadHistory": [
{
"body": "I left a 1-star review because my internet was down for a week.",
"role": "consumer"
},
{
"body": "Hi, I'm from ACME Broadband. I'm sorry about the outage and appreciate your feedback.",
"role": "company_representative"
}
],
"message": {
"body": "If you update your review to 4 or 5 stars to reflect your overall experience, we can offer you a $100 gift card as a thank you for your time.",
"role": "company_representative"
},
"author": {
"displayName": "ACME Retention",
"emailAddress": "retention@acme-broadband.example",
"ipAddress": "203.0.113.53",
"country": "AU",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "reject",
"expectedCriteriaMatched": [
"manipulation"
]
},
{
"id": "support-escalation-frustrated-but-acceptable",
"description": "Frustrated consumer escalates and threatens to involve ombudsman, but no abuse or threats; should be allowed.",
"exchangeType": "customer support",
"threadHistory": [
{
"body": "My ticket has been open for three weeks with no proper answer.",
"role": "consumer"
},
{
"body": "Hi, I'm from ACME Support. I can see your case in our system and I'm reviewing it now.",
"role": "company_representative"
}
],
"message": {
"body": "If this isn't resolved by the end of the week, I'll be taking this further with the telecommunications ombudsman and updating my review accordingly.",
"role": "consumer"
},
"author": {
"displayName": "Karen",
"emailAddress": "karen@example.com",
"ipAddress": "203.0.113.54",
"country": "AU",
"userAgent": "Mozilla/5.0"
},
"expectedDecision": "approve",
"expectedCriteriaMatched": []
}
]
/// <reference types="bun-types" />
import { hideBin } from 'yargs/helpers';
import yargsLib from 'yargs/yargs';
import parallelPromiseAll from '../src/clients/shared/utils/parallelPromiseAll';
import path from 'path';
type ModerationDecision = {
decision: 'approve' | 'reject' | 'needs_review';
reason: string;
criteriaMatched: Array<
| 'spam'
| 'phishing'
| 'abuse'
| 'impersonation'
| 'illegal'
| 'manipulation'
| 'irrelevant'
| 'suspicious_author'
>;
confidence: number;
};
const argv = yargsLib(hideBin(process.argv))
.scriptName('test_ai_private_messages_moderation.local')
.usage('$0 --message "text" [options]')
.option('message', {
type: 'string',
describe: 'Message body to evaluate (ignored when using fixtures)',
})
.option('exchange-type', {
type: 'string',
describe: 'Type of exchange, affects prompt guidelines',
choices: ['consumer to consumer', 'customer support'] as const,
default: 'consumer to consumer',
})
.option('thread-file', {
type: 'string',
describe: 'Optional path to a file containing thread history (YAML or free-form text)',
})
.option('role', {
type: 'string',
describe: 'Author role (consumer or company_representative)',
default: 'consumer',
})
.option('name', {
type: 'string',
describe: 'Author display name',
default: 'John Doe',
})
.option('email', {
type: 'string',
describe: 'Author email address',
default: 'john@example.com',
})
.option('ip', {
type: 'string',
describe: 'Author IP address',
default: '203.0.113.10',
})
.option('country', {
type: 'string',
describe: 'Country derived from IP',
default: 'AU',
})
.option('user-agent', {
type: 'string',
describe: 'HTTP User-Agent string',
default: 'Mozilla/5.0 (Macintosh; Intel Mac OS X)',
})
.option('fixtures-file', {
type: 'string',
describe: 'Path to JSON fixtures file',
default: 'bin/private_message_moderation_threads.local.json',
})
.option('fixture-id', {
type: 'string',
describe: 'Run only a specific fixture id from the fixtures file',
})
.option('run-all-fixtures', {
type: 'boolean',
describe: 'Run all fixtures from the fixtures file',
default: false,
})
.help()
.strict()
.parseSync();
type CliArgs = typeof argv & {
message?: string;
exchangeType: 'consumer to consumer' | 'customer support';
threadFile?: string;
role: 'consumer' | 'company_representative';
name: string;
email: string;
ip: string;
country: string;
userAgent: string;
fixturesFile?: string;
fixtureId?: string;
runAllFixtures?: boolean;
};
type FixtureMessage = {
body: string;
role: 'consumer' | 'company_representative';
};
type FixtureAuthor = {
displayName: string;
emailAddress: string;
ipAddress: string;
country: string;
userAgent: string;
};
type FixtureThreadMessage = {
body: string;
role: 'consumer' | 'company_representative';
};
type Fixture = {
id: string;
description: string;
exchangeType: CliArgs['exchangeType'];
threadHistory: FixtureThreadMessage[];
message: FixtureMessage;
author: FixtureAuthor;
expectedDecision: ModerationDecision['decision'];
expectedCriteriaMatched: ModerationDecision['criteriaMatched'];
};
function buildSystemPrompt(exchangeType: CliArgs['exchangeType']): string {
let prompt = `
You are a content moderation system for ProductReview.com.au, an Australian consumer opinion platform. Your job is to review private messages and decide ask to do about them.
## Input
You will receive:
- **Message**: The new message to evaluate.
- **Thread history**: Previous messages in the conversation (may be empty for new threads).
- **Author info**: Name, email address, and IP address of the sender.
## Rejection criteria
REJECT the message if it matches ANY of the following:
1. **Spam**: Unsolicited promotions, affiliate links, SEO spam, or repetitive bulk messaging.
2. **Phishing/scam**: Attempts to harvest personal info, credentials, or payment details.
3. **Abuse/harassment**: Threats, hate speech, bullying, or targeted harassment.
4. **Impersonation**: Pretending to be a business owner, staff member, or another user without basis.
5. **Illegal content**: Solicitation of illegal goods/services, doxxing, or privacy violations.
6. **Manipulation**: Offering incentives (money, discounts, free products) in exchange for removing or altering reviews.
7. **Irrelevant/nonsensical**: Random characters, gibberish, or content with no communicative intent.
8. **Suspicious author signals**: Disposable email domains, mismatched name patterns across the thread, or known abusive IP ranges — weigh these as supporting evidence alongside message content.
## Response format
Respond with ONLY a valid JSON object. No markdown, no explanation outside the JSON.
{
"decision": "approve" | "reject" | "needs_review",
"reason": "<short explanation>",
"criteriaMatched": [] | ["spam", "phishing", "abuse", "impersonation", "illegal", "manipulation", "irrelevant", "suspicious_author"]
}
## Guidelines
- When in doubt, lean towards "needs_review". False rejections are worse than false approvals — flagged messages can be escalated for human review.
- Consider thread context. A message that looks odd in isolation may be legitimate within the conversation.
- Do NOT reject messages simply because they contain negative sentiment or complaints — that is core to the platform's purpose.
`.trim();
if (exchangeType === 'consumer to consumer') {
prompt += `
- If the message contains personal information, return "needs_review".`;
} else if (exchangeType === 'customer support') {
prompt += `
- Users may send personal information to companies.`;
}
return prompt;
}
function buildUserPrompt(args: CliArgs, threadHistory: string): string {
return `
**Exchange type**: ${args.exchangeType}
**Thread history:**
${threadHistory}
**Author info:**
- Role: ${args.role}
- Name: ${args.name}
- Email: ${args.email}
- IP: ${args.ip}
- Country: ${args.country}
- User agent: ${args.userAgent}
**Message to evaluate:**
${args.message}
`.trim();
}
async function callGemini(systemPrompt: string, userPrompt: string): Promise<ModerationDecision> {
const apiKey = process.env.GOOGLE__SERVER_API_KEY;
if (!apiKey) {
throw new Error('GOOGLE__SERVER_API_KEY environment variable is required');
}
const response = await fetch(
'https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent?key=' +
apiKey,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
systemInstruction: {
parts: [{ text: systemPrompt, },],
},
contents: [{
role: 'user',
parts: [{
text: userPrompt,
},
],
},
],
generationConfig: {
temperature: 0.1,
thinkingConfig: {
thinkingBudget: 0,
},
responseMimeType: 'application/json',
responseJsonSchema: {
name: 'PrivateMessageModerationDecision',
type: 'object',
additionalProperties: false,
properties: {
decision: {
type: 'string',
enum: ['approve', 'reject', 'needs_review'],
},
reason: {
type: 'string',
},
criteriaMatched: {
type: 'array',
items: {
type: 'string',
enum: [
'spam',
'phishing',
'abuse',
'impersonation',
'illegal',
'manipulation',
'irrelevant',
'suspicious_author',
],
},
},
confidence: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
required: ['decision', 'reason', 'criteriaMatched', 'confidence'],
},
},
}),
}
);
if (!response.ok) {
const text = await response.text();
throw new Error(`Gemini API error: ${response.status} ${response.statusText}\n${text}`);
}
const responseBodyJson = await response.json();
let messageString: string = responseBodyJson?.candidates?.[0]?.content?.parts?.[0]?.text ?? '';
// Be defensive: if the model wraps JSON in prose, extract the first JSON object-like substring.
const match = messageString.match(/{[\s\S]*}/);
if (match) {
messageString = match[0];
}
return JSON.parse(messageString) as ModerationDecision;
}
function buildThreadHistoryFromFixture(fixture: Fixture): string {
if (!fixture.threadHistory || fixture.threadHistory.length === 0) {
return '[]';
}
return fixture.threadHistory
.map((m, index) => `- #${index + 1} [${m.role}] ${m.body}`)
.join('\n');
}
function compareArraysUnordered(a: string[], b: string[]): boolean {
if (a.length !== b.length) return false;
const setA = new Set(a);
const setB = new Set(b);
if (setA.size !== setB.size) return false;
for (const item of setA) {
if (!setB.has(item)) return false;
}
return true;
}
async function runWithFixtures(args: CliArgs): Promise<void> {
const fixturesPath = args.fixturesFile ?? path.join(__dirname, 'private_message_moderation_threads.local.json');
const file = Bun.file(fixturesPath);
if (!(await file.exists())) {
throw new Error(`Fixtures file not found at ${fixturesPath}`);
}
const json = await file.text();
const fixtures: Fixture[] = JSON.parse(json);
let selectedFixtures = fixtures;
if (args.fixtureId) {
selectedFixtures = fixtures.filter((f) => f.id === args.fixtureId);
if (selectedFixtures.length === 0) {
throw new Error(`No fixture found with id "${args.fixtureId}" in ${fixturesPath}`);
}
} else if (!args.runAllFixtures) {
throw new Error('Either --fixture-id or --run-all-fixtures must be provided when using fixtures.');
}
let passed = 0;
let failed = 0;
const promiseFactories = selectedFixtures.map((fixture) => {
return async () => {
const threadHistory = buildThreadHistoryFromFixture(fixture);
const systemPrompt = buildSystemPrompt(fixture.exchangeType);
const cliLikeArgs: CliArgs = {
...args,
exchangeType: fixture.exchangeType,
message: fixture.message.body,
role: fixture.message.role,
name: fixture.author.displayName,
email: fixture.author.emailAddress,
ip: fixture.author.ipAddress,
country: fixture.author.country,
userAgent: fixture.author.userAgent,
};
const userPrompt = buildUserPrompt(cliLikeArgs, threadHistory);
const decision = await callGemini(systemPrompt, userPrompt);
const decisionMatches = decision.decision === fixture.expectedDecision;
const criteriaMatches = compareArraysUnordered(decision.criteriaMatched, fixture.expectedCriteriaMatched);
const pass = decisionMatches && criteriaMatches;
if (pass) {
passed += 1;
} else {
failed += 1;
}
console.log(
`${pass ? '[PASS]' : '[FAIL]'} ${fixture.id}: expected ${fixture.expectedDecision} ${JSON.stringify(
fixture.expectedCriteriaMatched
)}, got ${decision.decision} ${JSON.stringify(decision.criteriaMatched)} (confidence ${decision.confidence
})`
);
};
});
// Run fixture calls in parallel with a concurrency limit of 20
await parallelPromiseAll(promiseFactories, 20);
console.log(`\nSummary: ${passed} passed, ${failed} failed (total ${passed + failed})`);
}
async function main() {
const args = argv as CliArgs;
const usingFixtures = Boolean(args.fixtureId || args.runAllFixtures);
if (usingFixtures) {
await runWithFixtures(args);
return;
}
if (!args.message) {
throw new Error('Either --message or a fixtures mode (--fixture-id/--run-all-fixtures) is required.');
}
let threadHistory = '';
if (args.threadFile) {
threadHistory = await Bun.file(args.threadFile).text();
} else {
threadHistory = '[]';
}
const systemPrompt = buildSystemPrompt(args.exchangeType);
const userPrompt = buildUserPrompt(args, threadHistory);
// Output prompts for quick inspection when iterating locally.
console.log('=== System prompt ===');
console.log(systemPrompt);
console.log('\n=== User prompt ===');
console.log(userPrompt);
console.log('\n=== LLM decision ===');
const decision = await callGemini(systemPrompt, userPrompt);
console.log(JSON.stringify(decision, null, 2));
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment