Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/includes/attributes.adoc
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
:project-version: 0.14.1
:langchain4j-version: 0.31.0
:examples-dir: ./../examples/
:examples-dir: ./../examples/
5 changes: 5 additions & 0 deletions model-providers/vertex-ai-gemini/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
<artifactId>quarkus-rest-client-reactive-jackson</artifactId>
</dependency>

<dependency>
<groupId>io.quarkus.security</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>

<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import java.util.regex.Pattern;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
import jakarta.ws.rs.BeanParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
Expand All @@ -27,6 +29,7 @@

import io.quarkus.arc.DefaultBean;
import io.quarkus.rest.client.reactive.jackson.ClientObjectMapper;
import io.quarkus.security.credential.TokenCredential;
import io.vertx.core.Handler;
import io.vertx.core.MultiMap;
import io.vertx.core.buffer.Buffer;
Expand Down Expand Up @@ -128,6 +131,9 @@ class TokenFilter implements ResteasyReactiveClientRequestFilter {
private final ExecutorService executorService;
private final AuthProvider authProvider;

@Inject
Instance<TokenCredential> tokenCredential;

public TokenFilter(ExecutorService executorService, AuthProvider authProvider) {
this.executorService = executorService;
this.authProvider = authProvider;
Expand All @@ -136,11 +142,13 @@ public TokenFilter(ExecutorService executorService, AuthProvider authProvider) {
@Override
public void filter(ResteasyReactiveClientRequestContext context) {
context.suspend();
final String currentToken = tokenCredential.isResolvable() ? tokenCredential.get().getToken() : null;
executorService.submit(new Runnable() {
@Override
public void run() {
try {
context.getHeaders().add("Authorization", "Bearer " + authProvider.getBearerToken());
context.getHeaders().add("Authorization", "Bearer " +
(currentToken != null ? currentToken : authProvider.getBearerToken()));
context.resume();
} catch (Exception e) {
context.resume(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ interface VertexAiGeminiConfig {
Optional<String> baseUrl();

/**
* Whether to enable the integration. Defaults to {@code true}, which means requests are made to the Anthropic
* Whether to enable the integration. Defaults to {@code true}, which means requests are made to the Vertex AI
* provider.
* Set to {@code false} to disable all requests.
*/
Expand All @@ -86,4 +86,13 @@ interface VertexAiGeminiConfig {
*/
ChatModelConfig chatModel();
}

/**
* Whether to use the current security identity's access token to access Vertex AI provider.
* If it is set to {@code true} but the security identity has no access token then default Google application credentials
* which must be setup in your environment will be used.
* Set to {@code false} to access Vertex AI provider only with the default Google application credentials.
*/
@WithDefault("false")
Boolean useSecurityIdentityToken();
}
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@
<module>samples/chatbot</module>
<module>samples/chatbot-easy-rag</module>
<module>samples/sql-chatbot</module>
<module>samples/secure-vertex-ai-gemini-poem</module>
</modules>
</profile>
</profiles>
Expand Down
113 changes: 113 additions & 0 deletions samples/secure-vertex-ai-gemini-poem/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# Secure Vertex AI Gemini Poem Demo

This demo showcases the implementation of a secure Vertex AI Gemini Poem Demo which is available only to users authenticated with Google.

## The Demo

### Setup

The demo asks Vertex AI Gemini LLM to write a short 1 paragraph poem, using the access token acquired during the OIDC authorization code flow.

### AI Service

This demo leverages the AI service abstraction, with the interaction between the LLM and the application handled through the AIService interface.

The `io.quarkiverse.langchain4j.sample.PoemAiService` interface uses specific annotations to define the LLM:

```java
package io.quarkiverse.langchain4j.sample;

import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import io.quarkiverse.langchain4j.RegisterAiService;

@RegisterAiService
public interface PoemAiService {

/**
* Ask the LLM to create a poem about Enterprise Java.
*
* @return the poem
*/
@SystemMessage("You are a professional poet")
@UserMessage("""
Write a short 1 paragraph poem about Java. Set an author name to the model name which created the poem.
""")
String writeAPoem();

}

### Using the AI service

Once defined, you can inject the AI service as a regular bean, and use it:

```java
package io.quarkiverse.langchain4j.sample;

import java.net.URISyntaxException;

import io.quarkus.security.Authenticated;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("/poem")
@Authenticated
public class PoemResource {

private final PoemAiService aiService;

public PoemResource(PoemAiService aiService) throws URISyntaxException {
this.aiService = aiService;
}

@GET
public String getPoem() {
return aiService.writeAPoem();
}
}

```

`PoemResource` can only be accessed by authenticated users.

## Google Authentication

This demo requires users to authenticate with Google.
All you need to do is to register an application with Google, follow steps listed in the [Quarkus Google](https://quarkus.io/guides/security-openid-connect-providers#google) section.
Name your Google application as `Quarkus LangChain4j AI`, and make sure an allowed callback URL is set to `http://localhost:8080/login`.
Google will generate a client id and secret, use them to set `quarkus.oidc.client-id` and `quarkus.oidc.credentials.secret` properties:

```properties
quarkus.oidc.provider=google
quarkus.oidc.client-id=${GOOGLE_CLIENT_ID}
quarkus.oidc.credentials.secret=${GOOGLE_CLIENT_SECRET}
quarkus.oidc.authentication.extra-params.scope=https://www.googleapis.com/auth/generative-language.retriever,https://www.googleapis.com/auth/cloud-platform
quarkus.oidc.authentication.redirect-path=/login

# See https://cloud.google.com/vertex-ai/docs/general/locations
vertex-ai-region=europe-west2

quarkus.langchain4j.vertexai.gemini.location=https://${vertex-ai-region}-aiplatform.googleapis.com
quarkus.langchain4j.vertexai.gemini.project-id=${GOOGLE_PROJECT_ID}
```

You must enable Vertex AI API in your Google Cloud project.

## Security Considerations

This demo makes it possible to access Google Vertex AI API enabled in the Google Cloud project only to users who:

* Authenticated to Quarkus REST PoemService with Google using OIDC authorization code flow.
* Authorized `Quarkus LangChain4j AI` application registered in the Google Cloud project to use the access token to access Google Generative API on behalf of the currently authentiicated user. This authorization is requested from users during the authentication process and is configured by adding `quarkus.oidc.authentication.extra-params.scope=https://www.googleapis.com/auth/generative-language.retriever,https://www.googleapis.com/auth/cloud-platform` in the application properties.
* Quarkus LangChain4j vertex-ai-gemini model provider uses this authorized token on behalf of the current user to access Google Vertex AI endpoint.

## Running the Demo

To run the demo, use the following commands:

```shell
mvn quarkus:dev
```

Then, access `http://localhost:8080`, login to Google, and follow a provided application link to read the poem.

122 changes: 122 additions & 0 deletions samples/secure-vertex-ai-gemini-poem/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-sample-secure-vertex-ai-gemini-poem</artifactId>
<name>Quarkus LangChain4j - Sample - Secure Vertex AI Gemini Poem</name>
<version>1.0-SNAPSHOT</version>

<properties>
<compiler-plugin.version>3.13.0</compiler-plugin.version>
<maven.compiler.parameters>true</maven.compiler.parameters>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
<quarkus.platform.version>3.9.4</quarkus.platform.version>
<skipITs>true</skipITs>
<surefire-plugin.version>3.2.5</surefire-plugin.version>
<quarkus-langchain4j.version>999-SNAPSHOT</quarkus-langchain4j.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>${quarkus.platform.group-id}</groupId>
<artifactId>${quarkus.platform.artifact-id}</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkiverse.langchain4j</groupId>
<artifactId>quarkus-langchain4j-vertex-ai-gemini</artifactId>
<version>${quarkus-langchain4j.version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive-qute</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<version>${quarkus.platform.version}</version>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>${compiler-plugin.version}</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>native</id>
<activation>
<property>
<name>native</name>
</property>
</activation>
<build>
<plugins>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>3.2.5</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<properties>
<quarkus.package.type>native</quarkus.package.type>
</properties>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.quarkiverse.langchain4j.sample;

import org.eclipse.microprofile.jwt.JsonWebToken;

import io.quarkus.oidc.IdToken;
import io.quarkus.qute.Template;
import io.quarkus.qute.TemplateInstance;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

/**
* Login resource which returns a poem welcome page to the authenticated user
*/
@Path("/login")
@Authenticated
public class LoginResource {

@Inject
@IdToken
JsonWebToken idToken;

@Inject
Template poem;

@GET
@Produces("text/html")
public TemplateInstance poem() {
return poem.data("name", idToken.getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.quarkiverse.langchain4j.sample;

import io.quarkus.oidc.OidcSession;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;

/**
* Logout resource
*/
@Path("/logout")
@Authenticated
public class LogoutResource {

@Inject
OidcSession session;

@GET
public Response logout(@Context UriInfo uriInfo) {
// remove the local session cookie
session.logout().await().indefinitely();
// redirect to the login page
return Response.seeOther(uriInfo.getBaseUriBuilder().path("login").build()).build();
}
}
Loading