Skip to content

Commit

Permalink
Add example for vector store observability
Browse files Browse the repository at this point in the history
  • Loading branch information
ThomasVitale committed Aug 15, 2024
1 parent 71a0aa0 commit 37778c9
Show file tree
Hide file tree
Showing 15 changed files with 436 additions and 3 deletions.
1 change: 1 addition & 0 deletions 10-observability/observability-models-openai/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ dependencies {
implementation 'io.micrometer:micrometer-tracing-bridge-otel'
implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
implementation 'io.micrometer:micrometer-registry-otlp'
implementation 'net.ttddyy.observation:datasource-micrometer-spring-boot:1.0.5'

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

Expand Down
61 changes: 61 additions & 0 deletions 10-observability/observability-vector-stores-pgvector/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Vector Store Observability: PGVector

Vector Store Observability for PGVector.

## 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 and a PGVector database.

### 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.

```shell
http --raw "What is Iorek's biggest dream?" :8080/chat/doc
```

```shell
http --raw "Who is Lucio?" :8080/chat/doc
```
46 changes: 46 additions & 0 deletions 10-observability/observability-vector-stores-pgvector/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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 'org.springframework.ai:spring-ai-pgvector-store-spring-boot-starter'

implementation 'io.micrometer:micrometer-tracing-bridge-otel'
implementation 'io.opentelemetry:opentelemetry-exporter-otlp'
implementation 'io.micrometer:micrometer-registry-otlp'
implementation 'net.ttddyy.observation:datasource-micrometer-spring-boot:1.0.5'

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

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-webflux'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.springframework.ai:spring-ai-spring-boot-testcontainers'
testImplementation 'org.testcontainers:postgresql'
}

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

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
class ChatController {

private final ChatService chatService;

ChatController(ChatService chatService) {
this.chatService = chatService;
}

@PostMapping("/chat/doc")
String chatWithDocument(@RequestBody String input) {
return chatService.chatWithDocument(input);
}

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

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.QuestionAnswerAdvisor;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service;

@Service
class ChatService {

private final ChatClient chatClient;
private final VectorStore vectorStore;

ChatService(ChatClient.Builder chatClientBuilder, VectorStore vectorStore) {
this.chatClient = chatClientBuilder.build();
this.vectorStore = vectorStore;
}

String chatWithDocument(String message) {
return chatClient.prompt()
.advisors(new QuestionAnswerAdvisor(vectorStore))
.user(message)
.call()
.content();
}

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

import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.reader.TextReader;
import org.springframework.ai.transformer.splitter.TokenTextSplitter;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.List;

@Component
public class IngestionPipeline {

private static final Logger logger = LoggerFactory.getLogger(IngestionPipeline.class);
private final VectorStore vectorStore;

@Value("classpath:documents/story1.md")
Resource textFile1;

@Value("classpath:documents/story2.txt")
Resource textFile2;

public IngestionPipeline(VectorStore vectorStore) {
this.vectorStore = vectorStore;
}

@PostConstruct
public void run() {
List<Document> documents = new ArrayList<>();

logger.info("Loading .md files as Documents");
var textReader1 = new TextReader(textFile1);
textReader1.getCustomMetadata().put("location", "North Pole");
textReader1.setCharset(Charset.defaultCharset());
documents.addAll(textReader1.get());

logger.info("Loading .txt files as Documents");
var textReader2 = new TextReader(textFile2);
textReader2.getCustomMetadata().put("location", "Italy");
textReader2.setCharset(Charset.defaultCharset());
documents.addAll(textReader2.get());

logger.info("Creating and storing Embeddings from Documents");
vectorStore.add(new TokenTextSplitter().split(documents));
}

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

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class ObservabilityVectorStoresPgVector {

public static void main(String[] args) {
SpringApplication.run(ObservabilityVectorStoresPgVector.class, args);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
spring:
application:
name: observability-vector-stores-pgvector
ai:
chat:
client:
observations:
include-input: true
observations:
include-completion: true
include-prompt: true
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o-mini
temperature: 0.7
embedding:
options:
model: text-embedding-3-small
vectorstore:
pgvector:
initialize-schema: true
dimensions: 1536
index-type: hnsw

management:
endpoints:
web:
exposure:
include: "*"
metrics:
tags:
service.name: ${spring.application.name}
tracing:
sampling:
probability: 1.0
otlp:
tracing:
endpoint: http://localhost:4318/v1/traces

---

spring:
config:
activate:
on-profile: ollama
ai:
openai:
base-url: http://localhost:11434
chat:
options:
model: mistral
temperature: 0.7
embedding:
options:
model: nomic-embed-text
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# The Adventures of Iorek and Pingu

Iorek was a little polar bear who lived in the Arctic circle. He loved to explore the snowy landscape and
dreamt of one day going on an adventure around the North Pole. One day, he met a penguin named Pingu who
was on a similar quest. They quickly became friends and decided to embark on their journey together.

Iorek and Pingu set off early in the morning, eager to cover as much ground as possible before nightfall.
The air was crisp and cold, and the snow crunched under their paws as they walked. They chatted excitedly
about their dreams and aspirations, and Iorek told Pingu about his desire to see the Northern Lights.

As they journeyed onward, they encountered a group of playful seals who were sliding and jumping in the
snow. Iorek and Pingu watched in delight as the seals frolicked and splashed in the water. They even tried
to join in, but their paws kept slipping and they ended up sliding on their stomachs instead.

After a few hours of walking, Iorek and Pingu came across a cave hidden behind a wall of snow. They
cautiously entered the darkness, their eyes adjusting to the dim light inside. The cave was filled with
glittering ice formations that sparkled like diamonds in the flickering torchlight.

As they continued their journey, Iorek and Pingu encountered a group of walruses who were lounging on the
ice. They watched in amazement as the walruses lazily rolled over and exposed their tusks for a good
scratch. Pingu even tried to imitate them, but ended up looking more like a clumsy seal than a walrus.

As the sun began to set, Iorek and Pingu found themselves at the edge of a vast, frozen lake. They gazed
out across the glassy surface, mesmerized by the way the ice glinted in the fading light. They could see
the faint outline of a creature moving beneath the surface, and their hearts raced with excitement.

Suddenly, a massive narwhal burst through the ice and into the air, its ivory tusk glistening in the
sunset. Iorek and Pingu watched in awe as it soared overhead, its cries echoing across the lake. They felt
as though they were witnessing a magical moment, one that would stay with them forever.

As the night drew in, Iorek and Pingu settled down to rest in their makeshift camp. They huddled together
for warmth, gazing up at the starry sky above. They chatted about all they had seen and experienced during
their adventure, and Iorek couldn't help but feel grateful for the new friend he had made.

The next morning, Iorek and Pingu set off once again, determined to explore every inch of the North Pole.
They stumbled upon a hidden cave filled with glittering crystals that sparkled like diamonds in the
sunlight. They marveled at their beauty before continuing on their way.

As they journeyed onward, Iorek and Pingu encountered many more wonders and adventures. They met a group
of playful reindeer who showed them how to pull sledges across the snow, and even caught a glimpse of the
mythical Loch Ness Monster lurking beneath the icy waters. In the end, their adventure around the North
Pole had been an unforgettable experience, one that they would treasure forever.
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
Lucio and Bolosso explore the Alps

Lucio was a little wolf who lived in the Italian Alps. He loved to explore the rugged landscape and dreamt
of one day going on an adventure around the mountains. One day, he met a brown bear named Balosso who was
on a similar quest. They quickly became friends and decided to embark on their journey together.

Lucio and Balosso set off early in the morning, eager to cover as much ground as possible before
nightfall. The air was crisp and cool, and the sun shone brightly overhead. They chatted excitedly about
their dreams and aspirations, and Lucio told Balosso about his desire to climb to the top of the highest
peak in the Alps.

As they journeyed onward, they encountered a group of playful marmots who were scampering across the rocky
terrain. Lucio and Balosso watched in delight as the marmots frolicked and chased each other, their paws
pattering on the stone. They even tried to join in, but their paws kept slipping and they ended up
tumbling onto their backsides.

After a few hours of walking, Lucio and Balosso came across a hidden glacier nestled between two towering
peaks. They cautiously approached the icy surface, their breath misting in the cold air. The glacier was
covered in intricate patterns and colors, shimmering like a shimmering jewel in the sunlight.

As they continued their journey, Lucio and Balosso encountered a group of majestic eagles soaring
overhead. They watched in awe as the eagles swooped and dived, their wings spread wide against the blue
sky. Lucio even tried to imitate them, but ended up flapping his ears instead of wings.

As the sun began to set, Lucio and Balosso found themselves at the foot of the highest peak in the Alps.
They gazed upwards in awe, their hearts pounding with excitement. They could see the faint outline of a
summit visible through the misty veil that surrounded the mountain.

Lucio and Balosso carefully climbed the steep slope, their claws digging into the rocky surface. The air
grew colder and thinner as they ascended, but they pressed onward, determined to reach the top. Finally,
they reached the summit, where they found a stunning view of the Italian Alps stretching out before them.

As they gazed out across the landscape, Lucio and Balosso felt an overwhelming sense of pride and
accomplishment. They had faced many challenges along the way, but their friendship had carried them
through. They even spotted a group of rare alpine ibex grazing on the distant slopes, adding to the
adventure's magic.

As night began to fall, Lucio and Balosso made their way back down the mountain, their paws sore but their
spirits high. They couldn't wait to tell their friends and family about their amazing adventure around the
Italian Alps. Even as they drifted off to sleep, visions of the stunning landscape danced in their minds.

The next morning, Lucio and Balosso set off once again, eager to explore every inch of the mountain range.
They stumbled upon a hidden valley filled with sparkling streams and towering trees, and even caught a
glimpse of a rare, elusive snow leopard lurking in the shadows. In the end, their adventure around the
Italian Alps had been an unforgettable experience, one that they would treasure forever.
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.thomasvitale.ai.spring;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
@Disabled
class ObservabilityVectorStoresPgVectorTests {

@Test
void contextLoads() {
}

}
Loading

0 comments on commit 37778c9

Please sign in to comment.