Skip to content

Commit

Permalink
Add MCP and Tools examples
Browse files Browse the repository at this point in the history
  • Loading branch information
ThomasVitale committed Dec 30, 2024
1 parent 08e2abb commit bb47bb1
Show file tree
Hide file tree
Showing 32 changed files with 965 additions and 0 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,12 @@ Vector Store Observability for different vector stores:

* **[PGVector](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/observability/observability-vector-stores-pgvector)**

## ⚙️ Model Context Protocol

Integrations with MCP Servers for providing contexts to LLMs.

* **[Brave Search API](https://github.com/ThomasVitale/llm-apps-java-spring-ai/tree/main/model-context-protocol/mcp-brave)**

## 📋 Evaluation

_Coming soon_
Expand Down
1 change: 1 addition & 0 deletions gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
springAiVersion=1.0.0-SNAPSHOT
springAiMcpVersion=0.2.0
otelInstrumentationVersion=2.10.0-alpha
63 changes: 63 additions & 0 deletions labs/tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Labs: Tools

Integrating with Tools, including @Tools-annotated methods and MCP Servers.

## Brave

The application consumes the [Brave Search API](https://api.search.brave.com).

### Create an account

Visit [api.search.brave.com](https://api.search.brave.com) and sign up for a new account.
Then, in the Brave Search API console, navigate to _Subscriptions_ and choose a subscription plan.
You can choose the "Free AI" plan to get started.

### Configure API Key

In the Brave Search API console, navigate to _API Keys_ and generate a new API key.
Copy and securely store your API key on your machine as an environment variable.
The application will use it to access the Brave Search API.

```shell
export BRAVE_API_KEY=<YOUR-API-KEY>
```

## Ollama

The application consumes models from an [Ollama](https://ollama.ai) inference server. You can either run Ollama locally on your laptop,
or rely on the Testcontainers support in Spring Boot to spin up an Ollama service automatically.
If you choose the first option, make sure you have Ollama installed and running on your laptop.
Either way, Spring AI will take care of pulling the needed Ollama models when the application starts,
if they are not available yet on your machine.

## Running the application

If you're using the native Ollama application, run the application as follows.

```shell
./gradlew bootRun
```

If you want to rely on the native Testcontainers support in Spring Boot to spin up an Ollama service at startup time,
run the application as follows.

```shell
./gradlew bootTestRun
```

## Calling the application

> [!NOTE]
> These examples use the [httpie](https://httpie.io) CLI to send HTTP requests.
Call the application that will use a @Tool-annotated method to retrieve the context to answer your question.

```shell
http :8080/chat/method authorName=="J.R.R. Tolkien" -b
```

Call the application that will use an MCP Server to retrieve the context to answer your question.

```shell
http :8080/chat/mcp question=="Does Spring AI supports a Modular RAG architecture? Please provide some references."
```
41 changes: 41 additions & 0 deletions labs/tools/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
plugins {
id 'java'
id 'org.springframework.boot'
id 'io.spring.dependency-management'
id 'org.graalvm.buildtools.native'
}

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

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

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-web'
implementation "org.springframework.ai:spring-ai-ollama-spring-boot-starter"

implementation "org.springframework.experimental:spring-ai-mcp:${springAiMcpVersion}"

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

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

tasks.named('test') {
useJUnitPlatform()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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"));
}

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

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,54 @@
package com.thomasvitale.ai.spring;

import com.thomasvitale.ai.spring.api.tools.mcp.McpToolCallbackResolver;
import com.thomasvitale.ai.spring.api.tools.method.MethodToolCallbackResolver;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.mcp.client.McpSyncClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* Chat examples using the high-level ChatClient API.
*/
@RestController
class ChatController {

private final ChatClient chatClient;
private final McpSyncClient mcpClient;
private final Tools tools;

ChatController(ChatClient.Builder chatClientBuilder, McpSyncClient mcpClient, Tools tools) {
this.chatClient = chatClientBuilder.build();
this.mcpClient = mcpClient;
this.tools = tools;
}

@GetMapping("/chat/method")
String chatMethod(String authorName) {
var userPromptTemplate = "What books written by {author} are available in the library?";
return chatClient.prompt()
.user(userSpec -> userSpec
.text(userPromptTemplate)
.param("author", authorName)
)
.functions(MethodToolCallbackResolver.builder()
.target(tools)
.build()
.getToolCallbacks())
.call()
.content();
}

@GetMapping("/chat/mcp")
String chatMcp(String question) {
return chatClient.prompt()
.user(question)
.functions(McpToolCallbackResolver.builder()
.mcpClients(mcpClient)
.build()
.getToolCallbacks())
.call()
.content();
}

}
19 changes: 19 additions & 0 deletions labs/tools/src/main/java/com/thomasvitale/ai/spring/Functions.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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)
class Functions {

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

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.mcp.client.McpClient;
import org.springframework.ai.mcp.client.McpSyncClient;
import org.springframework.ai.mcp.client.stdio.ServerParameters;
import org.springframework.ai.mcp.client.stdio.StdioClientTransport;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class LabsToolsApplication {

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

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

@Bean
McpSyncClient mcpClient() {
var serverParameters = ServerParameters.builder("npx")
.args("-y", "@modelcontextprotocol/server-brave-search")
.addEnvVar("BRAVE_API_KEY", System.getenv("BRAVE_API_KEY"))
.build();

var mcpClient = McpClient.using(new StdioClientTransport(serverParameters)).sync();

var initializeResult = mcpClient.initialize();
logger.info("MCP Initialized: {}", initializeResult);

return mcpClient;
}

}
22 changes: 22 additions & 0 deletions labs/tools/src/main/java/com/thomasvitale/ai/spring/Tools.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.thomasvitale.ai.spring;

import com.thomasvitale.ai.spring.api.tools.Tool;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class Tools {

private final BookService bookService;

Tools(BookService bookService) {
this.bookService = bookService;
}

@Tool("Get the list of books written by the given author available in the library")
public List<BookService.Book> booksByAuthor(String author) {
return bookService.getBooksByAuthor(new BookService.Author(author));
}

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

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Tool {

String value() default "";

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

import org.springframework.ai.model.function.FunctionCallback;

public interface ToolCallback extends FunctionCallback {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.thomasvitale.ai.spring.api.tools;

import org.springframework.ai.model.function.FunctionCallback;

public interface ToolCallbackResolver {

FunctionCallback[] getToolCallbacks();

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

import com.thomasvitale.ai.spring.api.tools.ToolCallback;
import org.springframework.ai.mcp.client.McpSyncClient;
import org.springframework.ai.mcp.spec.McpSchema;
import org.springframework.ai.mcp.spring.McpFunctionCallback;

public class McpToolCallback extends McpFunctionCallback implements ToolCallback {

public McpToolCallback(McpSyncClient clientSession, McpSchema.Tool tool) {
super(clientSession, tool);
}

}
Loading

0 comments on commit bb47bb1

Please sign in to comment.