-
Notifications
You must be signed in to change notification settings - Fork 74
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add LLM Observability example with OpenAI
- Loading branch information
1 parent
2ca40cd
commit 3228023
Showing
15 changed files
with
525 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
40 changes: 40 additions & 0 deletions
40
...ity/observability-models-openai/src/main/java/com/thomasvitale/ai/spring/BookService.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} | ||
|
||
} |
65 changes: 65 additions & 0 deletions
65
.../observability-models-openai/src/main/java/com/thomasvitale/ai/spring/ChatController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
|
||
} |
37 changes: 37 additions & 0 deletions
37
...rvability-models-openai/src/main/java/com/thomasvitale/ai/spring/EmbeddingController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
|
||
} |
25 changes: 25 additions & 0 deletions
25
...ility/observability-models-openai/src/main/java/com/thomasvitale/ai/spring/Functions.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
27 changes: 27 additions & 0 deletions
27
...bservability-models-openai/src/main/java/com/thomasvitale/ai/spring/HttpClientConfig.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | ||
))); | ||
}; | ||
} | ||
|
||
} |
Oops, something went wrong.