← 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,
  };
};