title: Regnify Tool: draft_cpd_reminder¶
Regnify Tool: draft_cpd_reminder¶
Purpose¶
draft_cpd_reminder drafts (but does NOT send) a polite CPD-reminder email for a specific representative. The tool fetches the rep's user profile (name + email), current-year CPD hour totals, and remaining gap against the annual CPD floor, then asks the LLM to compose a 2-paragraph professional reminder. The returned draft is meant for a UI approval step -- a COMP or HR_ADMIN user must review and approve before the email is dispatched.
Critical: This tool NEVER auto-sends email. The return payload has a {PORTAL_URL} placeholder for the frontend to substitute before actual dispatch.
Signature¶
draft_cpd_reminder(ctx, rep: RepProfileRef, confirm_write: bool = False) -> dict
Parameters¶
| Parameter | Type | Required | Description |
|---|---|---|---|
rep |
{"kind": "rep_profile", "id": <int>} |
Yes | Tagged rep-profile reference. id must be the numeric rep_profile_id. |
confirm_write |
bool |
No (default False) |
Safety gate. False returns an abort_and_redirect bounce. Set True ONLY when the user's current message uses an explicit issuance verb (e.g., "draft a CPD reminder for rep X", "send a reminder", "draft and send"). |
confirm_write Safety Gate¶
The confirm_write parameter prevents the tool from being called when the user is merely asking about CPD reminders rather than commanding one:
| User Intent | confirm_write | Tool Behaviour |
|---|---|---|
| "draft a CPD reminder for rep 42" | True |
Drafts and returns the reminder |
| "send rep 12 a CPD nudge" | True |
Drafts and returns the reminder |
| "what is the CPD requirement?" | False or omitted |
Returns abort_and_redirect bounce |
| "how do CPD reminders work?" | False or omitted |
Returns abort_and_redirect bounce |
| "should I send a CPD reminder?" | False or omitted |
Returns abort_and_redirect bounce |
abort_and_redirect Bounce Shape¶
When confirm_write is False, the tool returns:
{
"status": "abort_and_redirect",
"tool_unavailable": true,
"next_action": "answer_from_search_mas_knowledge",
"internal_only_note": "..."
}
Do NOT relay this dict to the user. Instead, call search_mas_knowledge to answer the user's actual question about CPD or compliance. The internal_only_note is a system-level instruction for the LLM, not user-facing text.
Internal Data Gathering¶
When confirm_write=True, the tool gathers three data pieces:
- Rep profile -> user_id:
repProfile(id)GraphQL query resolves theuserIdforeign key. - User profile (name + email):
user(id)GraphQL query returnsname,emailAddress,username. - CPD totals: Internal call to
get_cpd_status(ctx, rep, cycle_year)wherecycle_yearis the current calendar year (datetime.now(UTC).year). Returnshours_ethics,hours_product,hours_other,total,records.
LLM Drafting¶
The tool passes the gathered data to ctx.llm_client.generate() with the model tier standard (Qwen 3.6 35B, medium reasoning effort, 4096 thinking tokens):
System prompt instructs: output strict JSON only -- {"subject": "<short subject>", "body": "<email body, exactly 2 paragraphs separated by a blank line>"}. The body must greet the recipient by name, state their current CPD hours and the annual deadline (31 December of the cycle year), and include the literal placeholder {PORTAL_URL} as the link the recipient should follow.
User prompt provides: recipient name, cycle year, hours completed, hours remaining (vs 9-hour annual floor).
Annual CPD Floor¶
The tool uses 9 hours as the Singapore CPD annual requirement for the reminder calculation:
hours_remaining = max(0.0, 9.0 - cpd["total"])
This 9-hour floor is the basic regulatory minimum. It is used for the reminder phrasing only. The server-side health badge computation uses the full FAA-N26 thresholds (30h total: ≥6h Core CPD [ethics/rules, IBF/SCI-accredited] + ≥24h Supplementary CPD), applied per calendar year.
Return Shape (Success)¶
{
"rep_profile_id": <int>,
"draft": {
"subject": "<short subject line>",
"body": "<2 paragraphs separated by a blank line, with {PORTAL_URL} placeholder>"
},
"hours_remaining": <float>
}
- draft.subject: Short email subject (e.g., "Your CPD Hours Update -- Action Required by 31 Dec 2026").
- draft.body: Exactly two paragraphs. Greets the rep by name, states current hours, remaining gap, annual deadline (31 December), and includes
{PORTAL_URL}. The caller (frontend or user) substitutes{PORTAL_URL}with the actual Rep Portal CPD page URL before sending. - hours_remaining: Non-negative float.
0.0means the rep has met or exceeded the 9-hour annual floor (but may still fall below FAA-N26 30h total / 6h Core CPD / 24h Supplementary CPD thresholds).
Error Conditions¶
| Condition | Behaviour |
|---|---|
| rep_profile_id not found | LookupError: "draft_cpd_reminder: rep_profile_id=... not found" |
| user_id (from rep) not found | LookupError: "draft_cpd_reminder: user_id=... (from rep ...) not found" |
| LLM returns malformed JSON (missing subject/body) | RuntimeError: "draft_cpd_reminder: LLM returned malformed draft (missing subject or body)" |
| GraphQL transport error | RuntimeError -- surfaces verbatim |
Draft-Then-Approve Workflow¶
The full lifecycle of a CPD reminder in Regnify:
- COMP/HR_ADMIN requests draft -- chatbot calls
draft_cpd_reminderwithconfirm_write=True. - Chatbot returns draft -- subject + body displayed in the chat UI for review.
- User reviews and edits -- the draft is editable before dispatch.
- User approves -- the frontend sends the finalized email via the notification/email system (separate from this tool).
- Email dispatched -- the
{PORTAL_URL}placeholder is substituted with the actual Rep Portal CPD Log URL (/rep/cpd).
NOT automatic: The tool only drafts. Compliance staff (COMP or HR_ADMIN) must actively trigger and approve. Representatives cannot trigger their own reminders.
Rep Self-Service Alternative¶
Representatives can view their own CPD hours without needing a reminder. Direct them to the Rep Portal CPD Log page at /rep/cpd which displays:
- Current year CPD hours (ethics, product, other, total)
- Historical CPD records
- Gap against FAA-N26 thresholds
When a rep asks "how many CPD hours do I have?", use get_cpd_status to fetch their totals -- do NOT call draft_cpd_reminder.
Use in Attestation Context¶
The tool can be used to nudge reps who haven't completed attestation responses:
1. Call get_rep_register to identify reps with AMBER or RED flags.
2. For each non-compliant rep, call draft_cpd_reminder to generate a personalized nudge.
3. The nudge can mention both CPD gaps and pending attestation responses.
Important Rules¶
- confirm_write must be True -- Do not call without it when drafting. If the user hasn't explicitly commanded a draft, answer from knowledge instead.
- Do not relay the abort_and_redirect dict -- it contains internal instructions. Pivot to
search_mas_knowledgeinstead. - Never claim the email was sent -- the tool only drafts. Tell the user "I've drafted a reminder for your review" not "I've sent a reminder."
- Annual floor is 9 hours for reminder calculation. This is the basic MAS annual CPD minimum. Do not conflate with FAA-N26 health badge thresholds.
- {PORTAL_URL} is literal -- the body contains this exact placeholder string. Do not substitute it; the frontend handles substitution.