My latest video is about accessing the Claude AI API in a Spring Boot application:
The title is a little click-baity for my taste, especially because the LangChain4J project is even easier. Still, the input and output request types for Claude AI are so incredibly simple, that this really did make the resulting Spring and Java code as simple as I’ve ever seen, and I’m comparing to a whole playlist of AI integration with Java.
As you are probably aware, Claude AI is the AI tool from Anthropic, a company that was founded by some former OpenAI employees. Claude AI is free for most uses, accepts a range of input files if you want to upload them, and has a very large context window (like memory for an AI tool). Getting access to its API is a bit more complicated. You have to fill in a form at their early access page and then wait to be approved. That took about a week for me, but then I was able to access their Console page and create an API key and read the documentation.
Here’s the good part: There’s only one endpoint, which is for completions, and the input and output JSON blocks are simple. Here’s an example input:

You need to specify the Accept and Content-Type headers, which is normal, and add their own custom anthropic-version and x-api-key headers. Then you pick a model (either “claude-2” or “claude-instant-1“), add your prompt, pick an integer for the max tokens to sample, put in a temperature (between 0 and 1, the degree of randomness you’re allowing for the output), and submit the whole thing as a POST request. That’s it.
The response is even simpler:

All you get back is a JSON response with a completion element that has the answer you want, a stop reason which is always stop_sequence unless you customize it, and the model that was used.
I decided to access this using a Spring Boot 3.2.0 app, because it includes the sweet RestClient class, which is a synchronous version of the WebClient class they’ve used for the last few years. That also allows me to use the HTTP Exchange Interfaces capability.
First I mapped the JSON request and response to Java records.

The @JsonNaming annotation is included because one of the properties used camel case but is snake case (lowercase with underscores) in the JSON request. The response is even simpler:

In my Spring app, I can now include my ClaudeInterface, which only needs a single abstract method:

Spring’s HTTP interfaces require me to configure a bean to implement this interface automatically. I configured two beans in my main class. One was for the RestClient itself, to ensure that the proper headers were transmitted with each request:

Note that the @Value annotation in Spring automatically reads my ANTHROPIC_API_KEY environment variable, even though I specified it in lowercase with dots in the code.
Then, here’s the boilerplate code I needed to implement the bean, which is pretty much just copy-and-paste from the Spring documentation:

All that remains is to autowire the ClaudeInterface into a service class so I could properly prepare each request and post-process any responses. I added a class called ClaudeService for that purpose. The class contains several convenience methods, but here’s the one that does the work:

All it does is instantiate the request, call the interface method getCompletion, and extract the completion property from the response. The quirky part is that Claude requires you to format your request as a series of “Human:” and “Assistant:” calls in the following form, which is why I made the formatWithSystemPrompt method:

As the comment says, starting in Claude 2.1 you can add context information as a “system prompt” before the first call to “Human:”. I’ll come back to that.
That’s the whole app, pretty much. Here’s a simple test to see it in action:

Claude 2 returns about three paragraphs of information about The Hitchhiker’s Guide to the Galaxy, including that the Ultimate Answer to the Ultimate Question of Life, the Universe, and Everything is, of course, 42. I did the same test with the claude-Instant-1 model, and that worked too, and returned much more quickly.
I could have left it at that, but in general I don’t ask AI tools questions I could just Google. After all, Google is free, so this is a relatively expensive, probably less reliable replacement. Instead, I wanted to show a different example based on one in the LangChain4J examples project. For that, I needed a Java record representing a Person:

Now I want to extract a Person instance from a paragraph description of one, and in order to add it to my system I want the AI tool to also give me a JSON representation of it. So here’s my extractPerson method in the service:

In case that’s not clear, here’s the system prompt:
Here is a Java record representing a person:
record Person(String firstName, String lastName, LocalDate dob) {}
Please extract the relevant fields from the tags in the next
message into a JSON representation of a Person object.
The funny part is that the prompt I’m submitting goes between XML tags. Seriously, the Claude documentation has a whole section about that, where they claim there’s no schema or DTD or anything. Just make up your own XML tags and Claude will figure them out. 🙂
Given that, here’s the test case, including the prompt I submitted between the <person></person> tags:

The actual prompt says:
Captain Picard was born on the 13th of juillet, %d years from now,
in La Barre, France, Earth. His given name, Jean-Luc, is of French
origin and translates to “John Luke”.
That’s full of extraneous information, and I’m asking Claude to extract the relevant parts. The most difficult part for Claude is that I computed the number of years until 2305 (currently 282), the year Jean-Luc Picard was born, and added that as simply “282 years from now”.
Claude 2 did a decent job on this. The overall response I got back included the following block:
{
"firstName": "Jean-Luc",
"lastName": "Picard",
"dob": "2307-07-13"
}
That’s off, but not by too much. As the test shows, I allowed for five years either way, because you can’t rely on AI tools to do math. To really drive that point home, here’s the same result from claude-instant-1:
{
"firstName": "Jean-Luc",
"lastName": "Picard",
"dob": "2807-07-13"
}
That’s off by a whopping 502 years. Yikes, but at least it returned quickly. 🙂
Anyway, that’s most everything included in the video. The source code can be found in this GitHub repository.
I hope you enjoyed that. If so, and you’re interested in seeing future videos/posts in related areas, please consider subscribing to the Tales from the jar side YouTube channel. I publish a free weekly newsletter of the same name, hosted on Substack, every Sunday, and a new video version on the YouTube channel every Monday.

Leave a Reply