Anthropic in the Terminal with Elixir
Intro
If you've been around the internet lately, you've probably seen a bunch of people talking about Anthropic's Claude. Either as a consumer or a developer, it's hard to ignore the hype.
So what is Claude?
Claude is an AI assistant developed by Anthropic, a company that specializes in conversational AI. It's designed to understand and generate human-like text, making it a powerful tool for tasks like content creation, coding, and more.
As an engineer, you may have already started utilizing Claude via Cursor or the Claude API, but what about the terminal?
Because We Can
Some of us still prefer the terminal. (NeoVim... btw) It has some deep rooted nostalgia that can't quite be replicated in a GUI.
The issue is that most of the AI bots around are Web Based apps, or in the case of Cursor an entirely new IDE altogether!
While Cursor is *REALLY nice... Sometimes I simply want to stay in my flow state inside of tmux.
Like any true Linux, Terminal, Vim loving nerd... I can just build my own gosh darn Claude in the terminal!
Overview
This could be in most any language, but I chose Elixir here as it is a language I've used for almost a decade now... I'm comfortable with it, and I enjoy it. Elixir also has some really nice IO capabilities that make it easy to stream data to the console.
Brew your own CLI however you see fit.(Rust, Python, Node, etc)
The point here is that you CAN build something that streams responses from Claude into your terminal the same as you would with their web app.
Grab yourself an API key and crack open their docs.
Showcase
In my example below I'm able to simply run the program using iex -S mix
or compiling and running the script elixir claude.ex
and be dropped into a REPL where I can chat with Claude.
The Code
Below is a simple IO / REST example of how to interact with Claude via their API. This example doesn't cover everything (streaming, error handling, etc) but it's a good starting point.
defmodule Claude do
@moduledoc """
Chat with Claude CLI
"""
import IO.ANSI
@api_url "https://api.anthropic.com/v1/messages"
def start do
IO.puts(green() <> "Chatbot initialized. Type 'exit' to end the conversation.")
chat_loop()
end
defp chat_loop do
input = IO.gets(cyan() <> "You: " <> reset()) |> String.trim()
if input == "exit" do
IO.puts(yellow() <> "Chatbot: " <> reset() <> "Goodbye!")
else
case request_completion(input) do
{:ok, response} ->
IO.puts(yellow() <> "Claude: " <> reset() <> "#{response}")
{:error, response} ->
IO.puts(red() <> "Error: #{inspect(response)}")
end
chat_loop()
end
end
defp api_key do
Application.get_env(:anthropic, :api_key) || raise("Anthropic API key is not set")
end
defp request_completion(input) do
headers = [
{"Content-Type", "application/json"},
{"x-api-key", api_key()},
{"anthropic-version", "2023-06-01"}
]
body =
Jason.encode!(%{
"model" => "claude-3-5-sonnet-20240620",
"max_tokens" => 1024,
"messages" => [
%{"role" => "user", "content" => input}
]
})
case HTTPoison.post(@api_url, body, headers, recv_timeout: 120_000) do
{:ok, %{status_code: 200, body: response_body}} ->
{:ok, extract_response(response_body)}
{:ok, %{status_code: status_code, body: response_body}} ->
{:error, "#{status_code} #{extract_error(response_body)}"}
{:error, %{status_code: status_code, body: response_body}} ->
{:error, "#{status_code} #{extract_error(response_body)}"}
{:error, reason} ->
{:error, inspect(reason)}
_ ->
{:error, %{reason: "Unknown error"}}
end
end
defp extract_error(body) do
body
|> Jason.decode!()
|> Map.get("error")
|> Map.get("message")
end
defp extract_response(body) do
body
|> Jason.decode!()
|> Map.get("content")
|> List.first()
|> Map.get("text")
end
end
Conclusion
Is it overkill? No way! We are engineers after all, and it's fun to build things!
Get inspired! Take a look at the cookbook for more examples of what you can do with Claude.
Related Tags
Blog Archive