3MW (Chatting with LLMs With R & {elmer})

3 Minute Wednesdays is brought to you by

 Extended Black Friday Sale – Last Call!

Missed out? Don’t worry—Athlyticz Black Friday sale is extended! Get 50% off all Athlyticz courses and bundles until Sunday, December 8th. This is your last chance to master data science with top-notch Bayesian modeling, Shiny app development, machine learning, and more at our best prices since our April launch!

Guten Tag!

Many greetings from Munich, Germany. In today’s newsletter I want to teach you how you can easily chat with LLMs using the brand-new {elmer} package.

This package has a whole lot of features so we’ll explore this package quite a bit over the next few weeks. For now, let's start with the basics.

Install {elmer}

It seems like {elmer} isn’t yet available on CRAN. That’s why you can’t install it with install.packages(). Instead, you’ll have to use the development version from GitHub.

Normally, I’d use devtools::install_github() but the docs suggest an install via the {pak} package. Looks like this is the preferred install method, so let’s just go with that.

Chat with OpenAI

There are many LLM providers out there. There’s openAI, Anthropic, Bedrock, Azure, etc. Some of them even correspond to the same LLM model. They’re just being served from different cloud providers.

For example, Azure mostly hosts openAI’s LLMs so you’ll not find Claude there. Similarly, AWS Bedrock hosts many Anthropic models but no openAI models like gpt-4o.

The good news is that you can chat with any of these models via a very similar interface. The function for chatting is always chat_*(). For example, there’s chat_openai() and there’s chat_claude().

So let’s call gpt-4o with a system prompt that “initializes” the LLM to its task in this chat window. This is the perfect chance to do something silly. Let’s say that gpt-4o should only reply with “BANANA” at all costs.

Setting environment variables

Oh no! That was a bust. But it was kind of expected.

We can’t chat with ChatGPT yet because we’ve never specified an API key. This key is necessary to authenticate with ChatGPT (and get billed for using tokens.) So from the error message it looks like the chat_openai() function assumes that an API key is set via the environment variable OPENAI_API_KEY.

We can do so by modifying the .Rprofile file of our RStudio project. Just open the file .Rprofile at the root of your project (create that file if not already present) and put in the following code:

Of course, you’ll actually need an API key to do that. If you’re registered with openAI, you can get one here. And if you don’t want to pay to play around with LLMs programatically, you can try to use chat_ollama() and use a local model like we did last week.

Let’s try this again

So if you’ve set the API key and restarted your R session, then this should work now.

Alright, cool. This worked and we see our system prompt here. But no reply. To get that, we need to actually save the chat object and interact with it using its chat() method.

Nice! So the output of the chat() method seems to be simply the reply of the LLM. Let’s ask other questions.

Alright, so we have an LLM that will only reply with BANANA. Even if you prompt it to do something else. Goes to show the power of the system prompt. That’s pretty bananas if you ask me. (Sorry couldn’t resist 🤣.)

Get the chat history

Now, what if we wanted to get the full chat history? In that case, we can use the get_turns() method.

This is a list of 7 (one for every chat turn) but I've cropped the most for legibility

Oh wow. That’s a lot of information. But let me break it down for you.

What you see here is a list of “Turn” objects. That’s something that the {elmer} package uses to describe the chat. Basically, just like in a real convo, participants take turns to “speak”.

In our LLM setting, the participants can be either “system”, “user” or “assistant”. The first one is the system prompt which typically appears only once. The “user” is us and the “assistant” is the LLM. That’s why each Turn object has a property called “role” filled with one of these three options.

Similarly, there are many more properties:

  • the contents property is the content of that Turn’s message. Right now, it’s only ever text but we might also use stuff like images.

  • the json property is the technical stuff that the LLM returns (including the reply)

  • the tokens property contains the number of input tokens (what we prompt) and output tokens (what we get back). This is relevant for billing. As you can probably guess: The more tokens you use, the more you have to pay.

  • the text property is the actual text of the reply.

And we can actually create a Turn ourselves using the Turn function. This happens in conjunction with the contentText() function. For example, we can create a new system prompt turn. You’ll see why I want to do that in a second.

Looks exactly like our original system Turn (but with a modified text). Now, let’s mess with our chat a bit. Let’s replace the initial system Turn from our chat history and then ask the LLM why it writes BANANAs all the time.

Notice that I flattened the list here. Otherwise we’d have a list of length two instead of a list of length 7. See:

Rewrite history

So with our new turns we can rewrite the chat history. And once that is done, we can prompt our LLM again.

Fascinating, isn´t it?

Hope you enjoyed this little intro to the {elmer} package. I know that we’ve skimmed over the Contents() function quite quickly. So next week, we’ll take a closer look and use it with other content types (like images) as well.

As always, if you have any questions, or just want to reach out, feel free to contact me by replying to this mail or finding me on LinkedIn or on Bluesky.

See you next week,
Albert 👋

Enjoyed this newsletter? Here are other ways I can help you:

Reply

or to participate.