Skip to content

Commit

Permalink
Add LLM Observability example with OpenAI
Browse files Browse the repository at this point in the history
  • Loading branch information
ThomasVitale committed Aug 6, 2024
1 parent 2ca40cd commit 3228023
Show file tree
Hide file tree
Showing 15 changed files with 525 additions and 0 deletions.
121 changes: 121 additions & 0 deletions 10-observability/observability-models-openai/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# LLM Observability: OpenAI

LLM Observability for OpenAI.

## Running the application

The application relies on an OpenAI API for providing LLMs. This example guides you to use either the OpenAI Platform
or Ollama via the OpenAI-compatible API. The application also relies on Testcontainers to provision automatically
a Grafana LGTM observability stack.

### When using OpenAI

First, make sure you have an OpenAI account.
Then, define an environment variable with the OpenAI API Key associated to your OpenAI account as the value.

```shell
export SPRING_AI_OPENAI_API_KEY=<INSERT KEY HERE>
```

Finally, run the Spring Boot application.

```shell
./gradlew bootTestRun
```

### When using Ollama as a native application

First, make sure you have [Ollama](https://ollama.ai) installed on your laptop.
Then, use Ollama to run the _mistral_ and _nomic-embed-text_ models. Those are the ones we'll use in this example.

```shell
ollama run mistral
ollama run nomic-embed-text
```

Finally, run the Spring Boot application.

```shell
./gradlew bootTestRun --args='--spring.profiles.active=ollama'
```

## Observability Platform

Grafana is listening to port 3000. Check your container runtime to find the port to which is exposed to your localhost
and access Grafana from http://localhost:<port>. The credentials are `admin`/`admin`.

The application is automatically configured to export metrics and traces to the Grafana LGTM stack via OpenTelemetry.
In Grafana, you can query the traces from the "Explore" page, selecting the "Tempo" data source. You can also visualize metrics in "Explore > Metrics".

## Calling the application

You can now call the application to perform generative AI operations.
This example uses [httpie](https://httpie.io) to send HTTP requests.

### Chat

```shell
http :8080/chat
```

Try passing your custom prompt and check the result.

```shell
http :8080/chat message=="What is the capital of Italy?"
```

The next request is configured with a custom temperature value to obtain a more creative, yet less precise answer.

```shell
http :8080/chat/generic-options message=="Why is a raven like a writing desk? Give a short answer."
```

The next request is configured with Open AI-specific customizations.

```shell
http :8080/chat/openai-options message=="What can you see beyond what you can see? Give a short answer."
```

Finally, try a request which uses function calling.

```shell
http :8080/chat/functions authorName=="Philip Pullman"
```

### Embedding

```shell
http :8080/embed
```

Try passing your custom prompt and check the result.

```shell
http :8080/embed message=="The capital of Italy is Rome"
```

The next request is configured with OpenAI-specific customizations.

```shell
http :8080/embed/openai-options message=="The capital of Italy is Rome"
```

### Image

_If you're using the Ollama OpenAI API compatibility, the image use case is not supported._

```shell
http :8080/image
```

Try passing your custom prompt and check the result.

```shell
http :8080/image message=="Yellow Submarine"
```

The next request is configured with Open AI-specific customizations.

```shell
http :8080/image/openai-options message=="Here comes the sun"
```
43 changes: 43 additions & 0 deletions 10-observability/observability-models-openai/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
plugins {
id 'java'
id 'org.springframework.boot'
id 'io.spring.dependency-management'
}

group = 'com.thomasvitale'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(22)
}
}

repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}

dependencies {
implementation platform("org.springframework.ai:spring-ai-bom:${springAiVersion}")

implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.ai:spring-ai-openai-spring-boot-starter'

implementation 'io.micrometer:micrometer-tracing-bridge-otel'
implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
implementation 'io.micrometer:micrometer-registry-otlp'

testAndDevelopmentOnly 'org.springframework.boot:spring-boot-devtools'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:junit-jupiter'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.named('test') {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.thomasvitale.ai.spring;

import org.springframework.stereotype.Service;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class BookService {

private static final Map<Integer,Book> books = new ConcurrentHashMap<>();

static {
books.put(1, new Book("His Dark Materials", "Philip Pullman"));
books.put(2, new Book("Narnia", "C.S. Lewis"));
books.put(3, new Book("The Hobbit", "J.R.R. Tolkien"));
books.put(4, new Book("The Lord of The Rings", "J.R.R. Tolkien"));
books.put(5, new Book("The Silmarillion", "J.R.R. Tolkien"));
}

List<Book> getBooksByAuthor(Author author) {
return books.values().stream()
.filter(book -> author.name().equals(book.author()))
.toList();
}

Book getBestsellerByAuthor(Author author) {
return switch (author.name()) {
case "J.R.R. Tolkien" -> books.get(4);
case "C.S. Lewis" -> books.get(2);
case "Philip Pullman" -> books.get(1);
default -> null;
};
}

public record Book(String title, String author) {}
public record Author(String name) {}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.thomasvitale.ai.spring;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.ChatOptionsBuilder;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Set;

@RestController
class ChatController {

private final Logger logger = LoggerFactory.getLogger(ChatController.class);

private final ChatModel chatModel;

ChatController(ChatModel chatModel) {
this.chatModel = chatModel;
}

@GetMapping("/chat")
String chat(@RequestParam(defaultValue = "What did Gandalf say to the Balrog?") String message) {
logger.info(message);
return chatModel.call(message);
}

@GetMapping("/chat/generic-options")
String chatWithGenericOptions(@RequestParam(defaultValue = "What did Gandalf say to the Balrog?") String message) {
return chatModel.call(new Prompt(message, ChatOptionsBuilder.builder()
.withTemperature(1.3f)
.build()))
.getResult().getOutput().getContent();
}

@GetMapping("/chat/openai-options")
String chatWithOpenAiOptions(@RequestParam(defaultValue = "What did Gandalf say to the Balrog?") String message) {
return chatModel.call(new Prompt(message, OpenAiChatOptions.builder()
.withFrequencyPenalty(1.3f)
.withMaxTokens(1500)
.withPresencePenalty(1.0f)
.withStop(List.of("this-is-the-end", "addio"))
.withTemperature(0.7f)
.withTopP(0f)
.withUser("jon.snow")
.build()))
.getResult().getOutput().getContent();
}

@GetMapping("/chat/functions")
String chatWithFunctions(@RequestParam(defaultValue = "Philip Pullman") String author) {
return chatModel.call(new Prompt("What books written by %s are available to read and what is their bestseller?".formatted(author),
OpenAiChatOptions.builder()
.withTemperature(0.3f)
.withFunctions(Set.of("booksByAuthor", "bestsellerBookByAuthor"))
.build()))
.getResult().getOutput().getContent();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.thomasvitale.ai.spring;

import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingRequest;
import org.springframework.ai.openai.OpenAiEmbeddingOptions;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
class EmbeddingController {

private final EmbeddingModel embeddingModel;

EmbeddingController(EmbeddingModel embeddingModel) {
this.embeddingModel = embeddingModel;
}

@GetMapping("/embed")
String embed(@RequestParam(defaultValue = "And Gandalf yelled: 'You shall not pass!'") String message) {
var embeddings = embeddingModel.embed(message);
return "Size of the embedding vector: " + embeddings.size();
}

@GetMapping("/embed/openai-options")
String embedWithOpenAiOptions(@RequestParam(defaultValue = "And Gandalf yelled: 'You shall not pass!'") String message) {
var embeddings = embeddingModel.call(new EmbeddingRequest(List.of(message), OpenAiEmbeddingOptions.builder()
.withDimensions(1536)
.withEncodingFormat("float")
.build()))
.getResult().getOutput();
return "Size of the embedding vector: " + embeddings.size();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.thomasvitale.ai.spring;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;

import java.util.List;
import java.util.function.Function;

@Configuration(proxyBeanMethods = false)
public class Functions {

@Bean
@Description("Get the list of available books written by the given author")
public Function<BookService.Author, List<BookService.Book>> booksByAuthor(BookService bookService) {
return bookService::getBooksByAuthor;
}

@Bean
@Description("Get the bestseller book written by the given author")
public Function<BookService.Author, BookService.Book> bestsellerBookByAuthor(BookService bookService) {
return bookService::getBestsellerByAuthor;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.thomasvitale.ai.spring;

import org.springframework.boot.web.client.ClientHttpRequestFactories;
import org.springframework.boot.web.client.ClientHttpRequestFactorySettings;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.BufferingClientHttpRequestFactory;

import java.time.Duration;

@Configuration(proxyBeanMethods = false)
public class HttpClientConfig {

@Bean
RestClientCustomizer restClientCustomizer() {
return restClientBuilder -> {
restClientBuilder
.requestFactory(new BufferingClientHttpRequestFactory(
ClientHttpRequestFactories.get(ClientHttpRequestFactorySettings.DEFAULTS
.withConnectTimeout(Duration.ofSeconds(60))
.withReadTimeout(Duration.ofSeconds(60))
)));
};
}

}
Loading

0 comments on commit 3228023

Please sign in to comment.