Skip to content

Instantly share code, notes, and snippets.

@ilhamsj
Last active November 20, 2025 14:30
Show Gist options
  • Select an option

  • Save ilhamsj/9842827f3f6c99044af58f00118c9e4f to your computer and use it in GitHub Desktop.

Select an option

Save ilhamsj/9842827f3f6c99044af58f00118c9e4f to your computer and use it in GitHub Desktop.
/**
* AMARTHA HACKATHON 2025 - TRUST SCORE ENGINE
* Tech Stack: Firebase Cloud Functions (2nd Gen), Vertex AI (Gemini), Cloud Vision
*/
const { onRequest } = require("firebase-functions/v2/https");
const logger = require("firebase-functions/logger");
const admin = require("firebase-admin");
const { VertexAI } = require("@google-cloud/vertexai");
const vision = require("@google-cloud/vision");
// Initialize Firebase Admin
admin.initializeApp();
// --- CONFIGURATION ---
// TODO: Replace with your actual Google Cloud Project ID
const PROJECT_ID = "amartha-hackathon-team-id";
const LOCATION = "us-central1"; // Vertex AI location
// Initialize Clients
const vertexAI = new VertexAI({ project: PROJECT_ID, location: LOCATION });
const visionClient = new vision.ImageAnnotatorClient();
/**
* MOCK DATA: Internal Amartha History
* In a real app, this would query Amartha's actual internal API or database.
*/
const getMockRepaymentHistory = (userId) => {
// Simulating a user with "Thin File" (little history)
return {
userId: userId,
loansTaken: 1,
loansRepaid: 1,
daysOverdueAvg: 0,
baseCreditScore: 300, // Low base score because no history
};
};
/**
* ENDPOINT 1: Analyze Sentiment (The "AI Radar")
* input: { "agentReportText": "..." }
*/
exports.analyzeSentiment = onRequest(async (req, res) => {
try {
const { agentReportText } = req.body;
if (!agentReportText) {
return res.status(400).json({ error: "No text provided" });
}
// 1. Instantiate Gemini Model
const model = vertexAI.getGenerativeModel({ model: "gemini-1.5-flash-001" });
// 2. Craft the Prompt
const prompt = `
You are a Risk Analyst for Amartha. Analyze this field agent's note about a borrower.
Return ONLY raw JSON (no markdown) with these fields:
- sentiment: "POSITIVE", "NEUTRAL", or "NEGATIVE"
- confidence: number 0-1
- riskFlags: array of strings (e.g. "business closed", "evasive")
- positiveSignals: array of strings (e.g. "motivated", "shop clean")
Agent Note: "${agentReportText}"
`;
// 3. Generate Content
const result = await model.generateContent(prompt);
const response = result.response;
const text = response.candidates[0].content.parts[0].text;
// Clean up code blocks if Gemini adds them
const cleanJson = text.replace(/```json/g, "").replace(/```/g, "").trim();
res.status(200).json(JSON.parse(cleanJson));
} catch (error) {
logger.error("Sentiment Analysis Failed", error);
res.status(500).json({ error: error.message });
}
});
/**
* ENDPOINT 2: Calculate Trust Score (The Core Feature)
* input: { "userId": "123", "businessPhotoUrl": "...", "agentNotes": "..." }
*/
exports.calculateScore = onRequest(async (req, res) => {
try {
const { userId, businessPhotoUrl, agentNotes } = req.body;
// --- STEP 1: Get Baseline (Mock) ---
const history = getMockRepaymentHistory(userId);
let trustScore = history.baseCreditScore; // Start at 300
// --- STEP 2: Visual Analysis (Cloud Vision) ---
// Detect objects in the business photo (e.g., look for stock, tools)
// Note: For hackathon, ensure 'businessPhotoUrl' is a public GS util or URL
let assetBonus = 0;
let detectedAssets = [];
if (businessPhotoUrl) {
const [result] = await visionClient.labelDetection(businessPhotoUrl);
const labels = result.labelAnnotations;
// Simple logic: More business-related objects = higher score
const relevantKeywords = ["shop", "store", "product", "shelf", "machine", "food", "textile"];
labels.forEach(label => {
const desc = label.description.toLowerCase();
if (relevantKeywords.some(k => desc.includes(k))) {
assetBonus += 10; // +10 points per relevant asset
detectedAssets.push(desc);
}
});
trustScore += Math.min(assetBonus, 100); // Cap visual bonus at 100
}
// --- STEP 3: Behavioral Analysis (Gemini) ---
// Re-using the logic from endpoint 1 but integrated here
const model = vertexAI.getGenerativeModel({ model: "gemini-1.5-flash-001" });
const prompt = `
Analyze this note for credit risk. Return ONLY a number between 0 and 100 representing reliability.
0 = High Risk, 100 = High Trust.
Note: "${agentNotes}"
`;
const aiResult = await model.generateContent(prompt);
const sentimentScoreStr = aiResult.response.candidates[0].content.parts[0].text.trim();
const sentimentScore = parseInt(sentimentScoreStr) || 50;
// Weight the AI score (20% weight)
const sentimentBonus = (sentimentScore - 50) * 2; // +/- points based on sentiment
trustScore += sentimentBonus;
// --- STEP 4: Final Decision ---
const recommendedLimit = trustScore > 400 ? 5000000 : 2000000;
const riskLevel = trustScore > 450 ? "LOW" : (trustScore > 350 ? "MEDIUM" : "HIGH");
const finalResult = {
userId,
trustScore: Math.floor(trustScore),
riskLevel,
recommendedLimit,
breakdown: {
baseScore: history.baseCreditScore,
visualAssetsDetected: detectedAssets,
visualBonus: assetBonus,
agentSentimentScore: sentimentScore,
sentimentBonus: sentimentBonus
}
};
// Save result to Firestore (optional for Hackathon, but good for demo)
await admin.firestore().collection('scoring_logs').add({
...finalResult,
timestamp: admin.firestore.FieldValue.serverTimestamp()
});
res.status(200).json(finalResult);
} catch (error) {
logger.error("Scoring Failed", error);
res.status(500).json({ error: error.message });
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment