Skip to content

Public safety: FDA recall monitor ​

examples/72-recall-monitor.halo policies |! people

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.

Open in the playground →

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)

curl -fsSL https://halolang.com/install.sh | sh