Last active
November 20, 2025 14:30
-
-
Save ilhamsj/9842827f3f6c99044af58f00118c9e4f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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