[This article was first published on The Jumping Rivers Blog, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.This is part one of a five-part series on putting AI agents into production with R.Each part builds on the same app, a clinical trial safety assistant. The parts are:From Chatbot to Agent (this post)Putting the Agent in a Shiny AppCan You Trust It?Audit Trails, Costs and ModelsSharing Your Tools with MCPWe asked Gemini how many people on the high dose of a drug had itching in a well-known clinical trial. It said 21, and sounded sure of it. We asked again and got 25, with different numbers for the other groups too. Both answers cited the trial’s data files, but the model hadn’t been given them. It was answering from memory, so neither answer could be checked.In this series we build an assistant whose numbers you can check. The model picks an R function, and R does the counting. We tell the model never to do the maths itself, and part 3 checks that it doesn’t. It can still ask R the wrong question, so every result says what was counted.No pharma background is needed, since the pattern works for any data your team asks about. Later parts add a Shiny app, tests, limits on who sees patient records, an audit trail, and a way to share the tools with other AI agents.What is an AI agent?A chatbot answers from what it learned in training. An AI agent can also ask your code to do things, which is called tool calling (or function calling).You give the model a list of tools, each with a name, a description and its arguments. When a question needs a tool, the model replies with a request instead of an answer, such as “run count_subjects() with arm = "Placebo"”.Your code runs the function and sends back the result. This repeats until the model has enough to answer, and that cycle is the agent loop.The model never runs your code. It can only ask. Your R session decides what runs and what goes back.The agent loop: the model asks for tools, R runs them, and the model answers from the results.{ellmer} runs this loop for you. You write ordinary R functions, describe them and register them with a chat. New to {ellmer}? Our LLMs in R & Python webinar is a good start. As the {ellmer} tool-calling guide recommends, all exact computation happens inside the tools.What you needGet this part’s folder from our blog repository, by cloning the repository or downloading it as a ZIP file, and open it as a project in R 4.5. The project uses {renv} to pin every package version, so run this once:install.packages("renv")renv::restore()If you’d rather not use {renv}, install the packages yourself, though you’ll get newer versions than we tested:install.packages(c("ellmer", "pharmaverseadam", "dplyr", "jsonlite"))You’ll also need a Gemini API key, and our alt-text post shows how to get one. To store it, run file.edit(".Renviron") in the project, add the line GEMINI_API_KEY=your-key, save the file and restart R. The code’s README has more setup tips, for Windows in particular.The free tier is fine here, because this trial’s data is public, but not for your own patient or confidential data. Google’s terms also require the paid service for any app offered to users in the UK, the EEA or Switzerland (more on this in part 4).We tested everything with R 4.5.1, {ellmer} 0.5.0 and {pharmaverseadam} 1.3.0 in September 2026. {ellmer} changes quickly, so if something behaves differently, check your versions first.The trial dataOur example is CDISC pilot study 01, a worked example published by CDISC, the body that sets clinical trial data standards. It tested a skin patch of an experimental drug, xanomeline, in people with mild to moderate Alzheimer’s disease. Participants, called subjects in trial data, wore a placebo, low-dose or high-dose patch for 26 weeks.The {pharmaverseadam} package rebuilds the study’s analysis datasets in CDISC’s ADaM format, and we need two of them: adsl, with one row per subject, and adae, with one row per adverse event. An adverse event is anything medically unwanted that happened to a subject during the trial, whether or not the drug caused it.Trial terms, in plain EnglishArm: a treatment group, here placebo, low dose (54 mg) or high dose (81 mg, after two weeks at 54 mg).Screen failure: someone assessed who didn’t join the trial.Safety population: everyone who had at least one dose. Safety results count this group.Treatment-emergent: an adverse event that started, or got worse, after the first dose and no later than a set window after the last one (14 days, in this data).Serious adverse event: one with a serious outcome, such as death, a threat to life, a hospital stay or lasting disability. Intensity is recorded separately, as severity (mild, moderate or severe), so a severe headache need not be serious. In the data, the AESER flag marks serious events, and our tools count that flag.Preferred term and body system (or system organ class): each event is coded with a standard medical dictionary, MedDRA. “Itchy skin” becomes the preferred term PRURITUS, in the body system “skin and subcutaneous tissue disorders”.Application site: where the patch was worn. Erythema means redness.Our tools count the way a safety table usually does, and two of their choices are easy to miss. Both change every count:Not everyone in adsl joined the trial. Of its 306 people, 52 are screen failures, so the safety population (SAFFL == "Y") is 254 subjects.Planned and received treatment can differ. Each subject has a planned arm (ARM) and the treatment they received (ACTARM). Twelve subjects randomised to the high dose stopped within two weeks, before their dose went up to 81 mg, so here they count as low dose.Planned arms against treatment received: 86, 84 and 84 subjects become 86, 96 and 72.If you work with trial data, the note for pharma readers near the end covers a few choices a formal analysis plan might make differently.The tools also keep only the treatment-emergent events (TRTEMFL == "Y"), which leaves 1,122 of the 1,191 event rows. One function, in R/data.R, does all of this:ARMS dplyr::select(USUBJID, ACTARM, AEDECOD, AETERM, AEBODSYS, AESER, AESEV, ASTDT) list( subjects = subjects, events = events, # Every term and body system recorded in the trial, including the few # never treatment-emergent, so asking about them gives a real zero. terms = sort(unique(pharmaverseadam::adae$AEDECOD)), body_systems = sort(unique(pharmaverseadam::adae$AEBODSYS)) )}Join us for our AI in Production conference! For more details, check out our conference website!Asking without toolsWe started by asking gemini-3.7-flash the opening question twice, with no tools:# Without tools, the model can only guess.bare