← back to Norma
agents/onboard-agent/skills/post.js
151 lines
/**
* OnBoard Post Skill
*
* Generates a partnership outreach email draft for a discovered organization.
* Uses Gemini 2.0 Flash to create a personalized outreach message based on
* the discovery context, match score, and organization details.
*/
const { query } = require('../../shared/db');
const { logAction } = require('../../shared/audit-logger');
const fetch = require('node-fetch');
const AGENT = 'onboard-agent';
const PLATFORM = 'email';
const GEMINI_API_KEY = '${GOOGLE_API_KEY}';
const GEMINI_URL = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${GEMINI_API_KEY}`;
/**
* Call Gemini to generate outreach email content.
*
* @param {string} prompt - Full prompt
* @returns {Promise<string>} Generated text
*/
async function geminiGenerate(prompt) {
try {
const res = await fetch(GEMINI_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }],
generationConfig: { temperature: 0.7, maxOutputTokens: 1500 },
}),
});
const data = await res.json();
return data?.candidates?.[0]?.content?.parts?.[0]?.text || '';
} catch (err) {
console.error(`[${AGENT}] Gemini generate error:`, err.message);
return '';
}
}
/**
* @param {Object} body - Request body
* @param {string} body.discovery_id - UUID of the onboard_discovery record
* @returns {Promise<Object>} Draft email content
*/
module.exports = async function post(body = {}) {
const { discovery_id } = body;
if (!discovery_id) {
throw new Error('discovery_id is required');
}
// Load discovery + org details
const { rows } = await query(
`SELECT d.id AS discovery_id, d.match_score, d.match_details,
d.trigger_label, d.trigger_type, d.status,
o.id AS org_id, o.name AS org_name, o.city, o.state,
o.ntee_code, o.description AS org_description, o.mission,
o.website, o.email AS org_email, o.executive_director,
o.annual_revenue, o.key_issues
FROM onboard_discoveries d
JOIN organizations o ON d.org_id = o.id
WHERE d.id = $1`,
[discovery_id]
);
if (rows.length === 0) {
throw new Error(`Discovery ${discovery_id} not found`);
}
const disc = rows[0];
const matchDetails = disc.match_details || {};
// Build prompt for outreach email
const prompt = `You are drafting a partnership outreach email from a nonprofit organization to another nonprofit. The email should be professional, warm, and highlight mutual benefit.
ABOUT THE SENDER ORG:
- A nonprofit organization focused on advocacy and community impact
- We advocate for policy changes, provide resources, and build coalitions with aligned organizations
- We are looking for nonprofit partners who share our mission areas
ABOUT THE PROSPECTIVE PARTNER:
- Name: ${disc.org_name}
- Location: ${disc.city || 'Unknown'}, ${disc.state || 'Unknown'}
- NTEE Code: ${disc.ntee_code || 'Unknown'}
- Mission: ${disc.mission || disc.org_description || 'Not available'}
- Executive Director: ${disc.executive_director || 'Not available'}
- Annual Revenue: ${disc.annual_revenue ? '$' + Number(disc.annual_revenue).toLocaleString() : 'Not available'}
- Key Issues: ${Array.isArray(disc.key_issues) ? disc.key_issues.join(', ') : 'Not specified'}
- Match Score: ${disc.match_score}/100
- Triggered By Topic: "${disc.trigger_label}"
- Match Areas: ${matchDetails.org_issues ? matchDetails.org_issues.join(', ') : 'education, advocacy'}
INSTRUCTIONS:
Generate a JSON object with exactly these fields:
{
"subject": "Email subject line (concise, compelling)",
"body": "Full email body in plain text. Include greeting, intro paragraph, why we're reaching out, what partnership could look like, and a call to action. Sign off as the Partnership Team."
}
Reply with ONLY the JSON object, no markdown formatting or code blocks.`;
const aiResult = await geminiGenerate(prompt);
// Parse the AI response
let draft;
try {
// Try to extract JSON from the response (handle possible markdown wrapping)
const jsonMatch = aiResult.match(/\{[\s\S]*\}/);
if (jsonMatch) {
draft = JSON.parse(jsonMatch[0]);
} else {
throw new Error('No JSON found in AI response');
}
} catch (parseErr) {
console.error(`[${AGENT}] Failed to parse AI email draft:`, parseErr.message);
// Fallback template
draft = {
subject: `Partnership Opportunity with ${disc.org_name}`,
body: `Dear ${disc.executive_director || 'Team'},\n\nI'm reaching out regarding a potential partnership with ${disc.org_name}. Our organizations share common ground in ${disc.trigger_label}, and we believe there's an opportunity to collaborate on meaningful impact.\n\nWe'd love to schedule a brief conversation to explore how we might work together.\n\nBest regards,\nPartnership Team`,
};
}
// Log the action
await logAction({
agent: AGENT,
actionType: 'post',
platform: PLATFORM,
targetId: discovery_id,
content: draft.subject,
responseData: {
org_name: disc.org_name,
match_score: disc.match_score,
draft_length: (draft.body || '').length,
},
status: 'success',
});
return {
discovery_id: disc.discovery_id,
org_name: disc.org_name,
org_email: disc.org_email || null,
match_score: disc.match_score,
draft_subject: draft.subject,
draft_body: draft.body,
};
};