Skip to contents

In this tutorial, you’ll make your first structured LLM call with dsprrr. By the end, you’ll be able to ask questions, get typed responses, and understand why signatures are powerful.

Time: 10-15 minutes

What You’ll Build

A working question-answering system that returns structured data—not just raw text.

Prerequisites

  • R installed
  • An OpenAI API key (set as OPENAI_API_KEY environment variable)
  • Install the packages:
install.packages("pak")
pak::pak("JamesHWade/dsprrr")
pak::pak("tidyverse/ellmer")

Step 1: Load the Packages

library(dsprrr)
#> 
#> Attaching package: 'dsprrr'
#> The following object is masked from 'package:methods':
#> 
#>     signature
library(ellmer)

You should see no errors. If you do, check that your API key is set correctly.

Step 2: Create a Chat Connection

Connect to OpenAI:

chat <- chat_openai()
#> Using model = "gpt-4.1".

This creates a chat object you’ll use for all your LLM calls.

Step 3: Your First Structured Call

Let’s ask a simple question using dsp():

chat |> dsp("question -> answer", question = "What is the capital of France?")
#> [1] "The capital of France is Paris."

You should see "Paris" returned. Let’s break down what happened:

  • "question -> answer" is a signature—it declares one input (question) and one output (answer)
  • dsprrr handled all the prompt engineering for you
  • The result came back as structured data, not raw text

Step 4: Try Different Questions

The same signature works for any question:

chat |> dsp("question -> answer", question = "What is 7 * 8?")
#> [1] "7 multiplied by 8 is 56."

chat |> dsp("question -> answer", question = "Who wrote Romeo and Juliet?")
#> [1] "Romeo and Juliet was written by William Shakespeare."

Notice how you get clean, direct answers—no extra prose.

Step 5: Add Output Types

So far, answers have been strings. But what if you want a number or a specific choice?

Getting a Number

chat |> dsp(
  "math_problem -> result: number",
  math_problem = "What is 15% of 200?"
)
#> [1] 30

The : number after result tells dsprrr you want a numeric value, not a string.

Getting a Choice (Enum)

chat |> dsp(
  "text -> sentiment: enum('positive', 'negative', 'neutral')",
  text = "I absolutely loved this movie!"
)
#> [1] "positive"

The LLM must pick from exactly those three options. Try changing the text to see different results:

chat |> dsp(
  "text -> sentiment: enum('positive', 'negative', 'neutral')",
  text = "This was a complete waste of time."
)
#> [1] "negative"

chat |> dsp(
  "text -> sentiment: enum('positive', 'negative', 'neutral')",
  text = "It was okay, I guess."
)
#> [1] "neutral"

Getting True/False

chat |> dsp(
  "statement -> is_true: bool",
  statement = "The Earth orbits the Sun."
)
#> [1] TRUE

chat |> dsp(
  "statement -> is_true: bool",
  statement = "Cats are larger than elephants."
)
#> [1] FALSE

Step 6: Multiple Inputs

Signatures can have multiple inputs. Separate them with commas:

chat |> dsp(
  "context, question -> answer",
  context = "R was created in 1993 by Ross Ihaka and Robert Gentleman at the University of Auckland.",
  question = "When was R created?"
)
#> [1] "R was created in 1993."

Now the LLM uses your context to answer the question:

chat |> dsp(
  "context, question -> answer",
  context = "The bakery opens at 7am and closes at 6pm. They sell croissants for $3 each.",
  question = "How much do croissants cost?"
)
#> [1] "Croissants cost $3 each."

Step 7: Adding Instructions

You can guide the LLM’s behavior with instructions:

chat |> dsp(
  signature("question -> answer", instructions = "Answer in exactly one word."),
  question = "What color is the sky on a clear day?"
)
#> [1] "Blue"

chat |> dsp(
  signature("question -> answer", instructions = "Answer like a pirate."),
  question = "What is the capital of France?"
)
#> [1] "Arrr, matey! The capital of France be Paris!"

What You Learned

In this tutorial, you:

  1. Made your first structured LLM call with dsp()
  2. Used signatures to declare inputs and outputs
  3. Added output types: string, number, bool, enum()
  4. Combined multiple inputs
  5. Added instructions to guide behavior

What’s Different from Raw LLM Calls?

Without dsprrr, you’d write prompts like:

You are a helpful assistant. The user will ask a question.
Respond with just the answer, nothing else.
User: What is the capital of France?

With dsprrr, you just declare "question -> answer" and the framework handles the rest. This becomes powerful when you need to:

  • Optimize prompts automatically
  • Chain multiple LLM calls together
  • Get consistent, typed outputs

Next Steps

Ready to build something reusable? Continue to: