Public safety: FDA recall monitor
Watches openFDA's enforcement reports (free, no key) for recalls matching a keyword (an allergen, a pathogen, a brand), explains new ones in plain language, and keeps a record. Publishing to Slack waits for a person to approve, because a wrong alert about food or drug safety does real harm.
bash
halo run examples/72-recall-monitor.halo \
--arg product=food --arg keyword=peanut --arg slack_channel=""
(product is food, drug, or device)halo
// ============================================================
// Public safety: FDA recall monitor
// ============================================================
// Watches openFDA's enforcement reports (free, no key) for recalls
// matching a keyword (an allergen, a pathogen, a brand), explains new
// ones in plain language, and keeps a record. Publishing to Slack waits
// for a person to approve, because a wrong alert about food or drug
// safety does real harm.
//
// RUN: halo run examples/72-recall-monitor.halo \
// --arg product=food --arg keyword=peanut --arg slack_channel=""
// (product is food, drug, or device)
// ============================================================
policy Careful : MaxWords(600) & Require(JSON) & Block(PromptInjection)
policy Budget : MaxSpend(0.10)
// openFDA rejects unknown query parameters, so send only the named ones.
tool FDA : REST("api.fda.gov", allowNotFound: true, sendInput: false)
tool Chat : Slack(Env.SLACK_BOT_TOKEN)
store Log : VectorStore(namespace: "fda-recalls")
// A summary that breaks a policy (too long, not JSON) is sent back to fix.
@correct(2)
agent Explainer [anthropic/claude-haiku-4-5, temp: 0.0]
system """
You explain FDA recalls to the public in plain language. For each recall:
the product and firm, what's wrong, where it was distributed, and the
FDA class (Class I: could cause serious harm or death; Class II: could
cause temporary or reversible harm; Class III: unlikely to cause harm).
Mark a recall NEW if its recall_number isn't in the previous summary.
Keep each recall to two short lines.
State only what the reports say: no medical advice, no speculation.
Reply with JSON: {"new": <count of NEW recalls>, "summary": "<Markdown, newest first, with each recall_number>"}
"""
requires Careful, Budget
@retry(2, 5s)
pipeline RecallMonitor(product: String, keyword: String, slack_channel: String) -> Result:
product
// 1. The newest matching enforcement reports (null when there are none)
|> FDA.enforcement(
path: "/{{product}}/enforcement.json",
search: "reason_for_recall:{{keyword}}",
sort: "report_date:desc",
limit: 10
) as reports
|? (context.data.reports != null)
// 2. The fields that matter
|> System.Pick(
context.data.reports.results,
fields: ["recall_number", "report_date", "classification", "status", "recalling_firm", "product_description", "reason_for_recall", "distribution_pattern"],
maxChars: 400
) as recalls
// 3. The previous summary for this search, to mark what's new
|> Log.search(query: "recalls", tags: ["{{product}}:{{keyword}}"], order: "recent", limit: 1) as previous
|> Explainer.summarize(
"Recalls of {{product}} products mentioning {{keyword}}.",
context.data.recalls,
previous_summary: context.data.previous.content
) as summary
|> Log.log(content: context.data.summary.summary, tags: ["{{product}}:{{keyword}}"])
|> System.Value(context.data.summary.summary)
// 4. Publish only when there's something new, a channel, and a person's approval
|? (context.data.summary.new > 0 && slack_channel != "")
|! yield(msg: "Post these recalls to {{slack_channel}}?", [context.data.summary.summary]) as review
|? (context.data.review.approved == true)
|> Chat.post(channel: slack_channel, text: "{{summary.summary}}")
|> System.Value(context.data.summary.summary)