Understand the difference between LLM providers and models
Choose appropriate models for different tasks
Handle multi-modal inputs (images, PDFs)
Extract structured data from LLM responses
Perform parallel and batch LLM calls for efficiency
Application exercise
ae-17
Instructions
Go to the course GitHub org and find your ae-17 (repo name will be suffixed with your GitHub name).
Clone the repo in Positron, install required packages using renv::restore() (R) or uv sync (Python), open the Quarto document in the repo, and follow along and complete the exercises.
Render, commit, and push your edits by the AE deadline – end of the day
library(ellmer)chat <-chat("openai/gpt-4.1-nano")chat$chat(content_image_url("https://placecats.com/bella/400/400"),"What do you see in this image?")
Python
import chatlaschat = chatlas.ChatAuto("openai/gpt-4.1-nano")chat.chat( chatlas.content_image_url("https://placecats.com/bella/400/400"),"What do you see in this image?")
⌨️ 08_vision
I’ve put some images of food in the data/recipes/images folder.
Your job: show the food to the LLM and see if it gets hungry.
05:00
📑 content_pdf_file
R
library(ellmer)chat <-chat("openai/gpt-4.1-nano")chat$chat(content_pdf_file("financial-report.pdf"),"What's my tax liability for 2024?")
Python
import chatlaschat = chatlas.ChatAuto("openai/gpt-4.1-nano")chat.chat( content_pdf_file("financial-report.pdf"),"What's my tax liability for 2024?")
📑 content_pdf_url
R
library(ellmer)chat <-chat("openai/gpt-4.1-nano")chat$chat(content_pdf_url("http://pdf.secdatabase.com/1757/0001104659-25-042659.pdf"),"Describe Tesla’s executive compensation and stock award programs.")
Python
import chatlaschat = chatlas.ChatAuto("openai/gpt-4.1-nano")chat.chat( content_pdf_url("http://pdf.secdatabase.com/1757/0001104659-25-042659.pdf"),"Describe Tesla’s executive compensation and stock award programs.")
⌨️ 09_pdf
We have the actual recipes as PDFs in the data/recipes/pdf folder.
Your job: ask the LLM to convert the recipes to markdown.
05:00
Structured output
How would you extract name and age?
age_free_text <-list("I go by Alex. 42 years on this planet and counting.","Pleased to meet you! I'm Jamal, age 27.","They call me Li Wei. Nineteen years young.","Fatima here. Just celebrated my 35th birthday last week.","The name's Robert - 51 years old and proud of it.","Kwame here - just hit the big 5-0 this year.")
If you wrote R code, it might look like this…
word_to_num <-function(x) {# normalize x <-tolower(x)# direct numbersif (grepl("\\b\\d+\\b", x)) return(as.integer(regmatches(x, regexpr("\\b\\d+\\b", x))))# hyphenated like "5-0"if (grepl("\\b\\d+\\s*-\\s*\\d+\\b", x)) { parts <-as.integer(unlist(strsplit(regmatches(x, regexpr("\\b\\d+\\s*-\\s*\\d+\\b", x)), "\\s*-\\s*")))return(10* parts[1] + parts[2]) }# simple word numbers ones <-c(zero=0, one=1, two=2, three=3, four=4, five=5, six=6, seven=7, eight=8, nine=9,ten=10, eleven=11, twelve=12, thirteen=13, fourteen=14, fifteen=15, sixteen=16,seventeen=17, eighteen=18, nineteen=19 ) tens <-c(twenty=20, thirty=30, forty=40, fifty=50, sixty=60, seventy=70, eighty=80, ninety=90)# e.g., "nineteen"if (x %in%names(ones)) return(ones[[x]])# e.g., "thirty five" or "thirty-five" x2 <-gsub("-", " ", x) parts <-strsplit(x2, "\\s+")[[1]]if (length(parts) ==2&& parts[1] %in%names(tens) && parts[2] %in%names(ones)) {return(tens[[parts[1]]] + ones[[parts[2]]]) }if (length(parts) ==1&& parts[1] %in%names(tens)) return(tens[[parts[1]]])return(NA_integer_)}# Extract name candidatesextract_name <-function(s) {# patterns that introduce a name pats <-c("I go by\\s+([A-Z][a-z]+)","I'm\\s+([A-Z][a-z]+(?:\\s+[A-Z][a-z]+)?)","They call me\\s+([A-Z][a-z]+(?:\\s+[A-Z][a-z]+)?)","^([A-Z][a-z]+) here","The name's\\s+([A-Z][a-z]+)","^([A-Z][a-z]+)\\s"# fallback: leading capital word )for (p in pats) { m <-regexpr(p, s, perl =TRUE)if (m[1] !=-1) {return(sub(p, "\\1", regmatches(s, m))) } }NA_character_}# Extract age phrases and convert to numberextract_age <-function(s) {# capture common age phrases around a number m <-regexpr("(\\b\\d+\\b|\\b\\d+\\s*-\\s*\\d+\\b|\\b[Nn][a-z-]+\\b)\\s*(years|year|birthday|young|this)", s, perl =TRUE)if (m[1] !=-1) { token <-sub("(years|year|birthday|young|this)$", "", trimws(substring(s, m, m +attr(m, "match.length") -1)))return(word_to_num(token)) }# handle pure word-number without trailing keyword (e.g., "Nineteen years young." handled above) m2 <-regexpr("\\b([A-Z][a-z]+)\\b\\s+years", s, perl =TRUE)if (m2[1] !=-1) { token <-tolower(sub("\\s+years.*", "", regmatches(s, m2)))return(word_to_num(token)) }# handle hyphenated "big 5-0" m3 <-regexpr("big\\s+(\\d+\\s*-\\s*\\d+)", s, perl =TRUE)if (m3[1] !=-1) { token <-sub("big\\s+", "", regmatches(s, m3))return(word_to_num(token)) }NA_integer_}
# A tibble: 6 × 2
name age
<chr> <int>
1 Alex 42
2 Jamal NA
3 Li Wei NA
4 Fatima NA
5 Robert 51
6 Kwame 5
age_free_text
[[1]]
[1] "I go by Alex. 42 years on this planet and counting."
[[2]]
[1] "Pleased to meet you! I'm Jamal, age 27."
[[3]]
[1] "They call me Li Wei. Nineteen years young."
[[4]]
[1] "Fatima here. Just celebrated my 35th birthday last week."
[[5]]
[1] "The name's Robert - 51 years old and proud of it."
[[6]]
[1] "Kwame here - just hit the big 5-0 this year."
But if you ask an LLM…
library(ellmer)chat <-chat("openai/gpt-5-nano",system_prompt ="Extract the name and age.")chat$chat(age_free_text[[1]])#>chat$chat(age_free_text[[2]])#>
But if you ask an LLM…
library(ellmer)chat <-chat("openai/gpt-5-nano",system_prompt ="Extract the name and age.")chat$chat(age_free_text[[1]])#> Name: Alex; Age: 42chat$chat(age_free_text[[2]])#> Name: Jamal; Age: 27
Wouldn’t this be nice?
chat$chat(age_free_text[[1]])#> list(#> name = "Alex",#> age = 42#> )
Structured chat output
chat$chat_structured(age_free_text[[1]])#> list(#> name = "Alex",#> age = 42#> )
Structured chat output
type_person <-type_object(name =type_string(),age =type_integer())chat$chat_structured(age_free_text[[1]], type = type_person)#> list(#> name = "Alex",#> age = 42#> )
type_person <-type_object(name =type_string("The person's name"),age =type_integer("The person's age in years"))
In Python, use Pydantic
import chatlasfrom pydantic import BaseModelclass Person(BaseModel): name: str age: intchat = chatlas.ChatAuto("openai/gpt-5-nano")chat.chat_structured("I go by Alex. 42 years on this planet and counting.", data_model=Person)#> Person(name='Alex', age=42)
In Python, use Pydantic
import chatlasfrom pydantic import BaseModel, Fieldclass Person(BaseModel): name: str= Field(..., description="The person's name") age: int= Field(..., description="The person's age in years")chat = chatlas.ChatAuto("openai/gpt-5-nano")chat.chat_structured("I go by Alex. 42 years on this planet and counting.", data_model=Person)#> Person(name='Alex', age=42)
In Python, use Pydantic
import chatlasfrom pydantic import BaseModel, ConfigDictclass Person(BaseModel): model_config = ConfigDict(use_attribute_docstrings=True) name: str"""The person's name""" age: int"""The person's age in years"""
⌨️ 10_structured-output
We also have text versions of the recipes in data/recipes/txt.
Use ellmer::type_*() or a Pydantic model to extract structured data from the recipe you used in activity 09.
I’ve given you the expected structure, you just need to implement it.
type_person <-type_object(name =type_string(),age =type_integer())chat$chat_structured(age_free_text, type = type_person)#> ???
Structured chat output
type_person <-type_object(name =type_string(),age =type_integer())chat$chat_structured(age_free_text, type = type_person)#> Error in `FUN()`:#> ! `...` must be made up strings or <content> objects, not a list.