|
| 1 | +## GraalPy AI Assistant Guide |
| 2 | + |
| 3 | +## 1. Getting Started |
| 4 | + |
| 5 | +In this guide, we will demonstrate how to use Python libraries and framework (LangChain) within a Micronaut application written in Java to build a GraalPy AI Assistant. |
| 6 | + |
| 7 | +## 2. What you will need |
| 8 | + |
| 9 | +To complete this guide, you will need the following: |
| 10 | + |
| 11 | +* Some time on your hands |
| 12 | +* A decent text editor or IDE |
| 13 | +* A supported JDK[^1], preferably the latest [GraalVM JDK](https://graalvm.org/downloads/) |
| 14 | + |
| 15 | + [^1]: Oracle JDK 17 and OpenJDK 17 are supported with interpreter only. |
| 16 | + GraalVM JDK 21, Oracle JDK 21, OpenJDK 21 and newer with [JIT compilation](https://www.graalvm.org/latest/reference-manual/embed-languages/#runtime-optimization-support). |
| 17 | + Note: GraalVM for JDK 17 is **not supported**. |
| 18 | + |
| 19 | +* An API key from [Groq](https://console.groq.com/keys) |
| 20 | +* An API key from [Cohere](https://dashboard.cohere.com/api-keys) |
| 21 | + |
| 22 | +## 3. Writing the application |
| 23 | + |
| 24 | +Create an application using the [Micronaut Command Line Interface](https://docs.micronaut.io/latest/guide/#cli) or with [Micronaut Launch](https://micronaut.io/launch/). |
| 25 | +To make copying of the code snippets in this guide as smooth as possible, the application should have the base package `com.example`. |
| 26 | +We also recommend to use Micronaut version 4.7.6 or newer. |
| 27 | + |
| 28 | + |
| 29 | +```bash |
| 30 | +mn create-app com.example.demo \ |
| 31 | +--build=maven \ |
| 32 | +--lang=java \ |
| 33 | +--test=junit |
| 34 | +``` |
| 35 | + |
| 36 | + |
| 37 | +### 3.1. Application |
| 38 | + |
| 39 | +The generated Micronaut application will already contain the file _Application.java_, which is used when running the application via Maven or via deployment. |
| 40 | +You can also run the main class directly within your IDE if it is configured correctly. |
| 41 | + |
| 42 | +`src/main/java/org/example/Application.java` |
| 43 | +```java |
| 44 | +package com.example; |
| 45 | +import io.micronaut.runtime.Micronaut; |
| 46 | + |
| 47 | +public class Application { |
| 48 | + |
| 49 | + public static void main(String[] args) { |
| 50 | + Micronaut.run(Application.class, args); |
| 51 | + } |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +### 3.2 Dependency configuration |
| 56 | + |
| 57 | +Add the required dependencies for GraalPy in the dependency section of the POM build script. |
| 58 | + |
| 59 | +`pom.xml` |
| 60 | +```xml |
| 61 | +<dependency> |
| 62 | + <groupId>org.graalvm.python</groupId> |
| 63 | + <artifactId>python</artifactId> <!-- ① --> |
| 64 | + <version>24.2.0</version> |
| 65 | + <type>pom</type> <!-- ② --> |
| 66 | +</dependency> |
| 67 | +<dependency> |
| 68 | + <groupId>org.graalvm.python</groupId> |
| 69 | + <artifactId>python-embedding</artifactId> <!-- ③ --> |
| 70 | + <version>24.2.0</version> |
| 71 | +</dependency> |
| 72 | +``` |
| 73 | + |
| 74 | + |
| 75 | +❶ The `python` dependency is a meta-package that transitively depends on all resources and libraries to run GraalPy. |
| 76 | + |
| 77 | +❷ Note that the `python` package is not a JAR - it is simply a `pom` that declares more dependencies. |
| 78 | + |
| 79 | +❸ The `python-embedding` dependency provides the APIs to manage and use GraalPy from Java. |
| 80 | + |
| 81 | +### 3.3 Adding packages - GraalPy build plugin configuration |
| 82 | + |
| 83 | +Most Python packages are hosted on [PyPI](https://pypi.org) and can be installed via the `pip` tool. |
| 84 | +The Python ecosystem has conventions about the filesystem layout of installed packages that need to be kept in mind when embedding into Java. |
| 85 | +You can use the GraalPy plugins for Maven to manage Python packages for you. |
| 86 | + |
| 87 | +Add the `graalpy-maven-plugin` configuration into the plugins section of the POM or the `org.graalvm.python` plugin dependency and a `graalPy` block to your Gradle build: |
| 88 | + |
| 89 | +`pom.xml` |
| 90 | +```xml |
| 91 | + <plugin> |
| 92 | + <groupId>org.graalvm.python</groupId> |
| 93 | + <artifactId>graalpy-maven-plugin</artifactId> |
| 94 | + <version>${python.version}</version> |
| 95 | + <configuration> |
| 96 | + <packages> <!-- ① --> |
| 97 | + <package>cohere==5.14.2</package> |
| 98 | + <package>beautifulsoup4==4.13.4</package> |
| 99 | + <package>oracledb==3.1.0</package> |
| 100 | + <package>jellyfish==0.8.0</package> |
| 101 | + <package>langchain==0.3.23</package> |
| 102 | + <package>langchain-cohere==0.4.3</package> |
| 103 | + <package>langchain-community==0.3.21</package> |
| 104 | + <package>langchain-core==0.3.51</package> |
| 105 | + <package>langchain-groq==0.3.2</package> |
| 106 | + <package>langchain-text-splitters==0.3.8</package> |
| 107 | + <package>langchain-ollama==0.2.3</package> |
| 108 | + <package>pydantic==2.11.0a2</package> |
| 109 | + <package>pydantic_core==2.29.0</package> |
| 110 | + <package>tokenizers==0.15.0</package> |
| 111 | + <package>huggingface-hub==0.16.4</package> |
| 112 | + <package>yake==0.4.8</package> |
| 113 | + <package>numpy==1.26.4</package> |
| 114 | + <package>cx-Oracle</package> |
| 115 | + </packages> |
| 116 | + </configuration> |
| 117 | + <executions> |
| 118 | + <execution> |
| 119 | + <goals> |
| 120 | + <goal>process-graalpy-resources</goal> |
| 121 | + </goals> |
| 122 | + </execution> |
| 123 | + </executions> |
| 124 | +</plugin> |
| 125 | +``` |
| 126 | + |
| 127 | + |
| 128 | + |
| 129 | +❶ The `packages` section lists all Python packages optionally with [requirement specifiers](https://pip.pypa.io/en/stable/reference/requirement-specifiers/). |
| 130 | + |
| 131 | +Python packages and their versions can be specified as if used with pip. |
| 132 | + |
| 133 | +### 3.4 Creating a Python context |
| 134 | + |
| 135 | +GraalPy provides APIs to make setting up a context to load Python packages from Java as easy as possible. |
| 136 | + |
| 137 | +Create a Java class which will serve as a wrapper bean for the GraalPy context: |
| 138 | + |
| 139 | +`src/main/java/com/example/config/GraalPyContext.java` |
| 140 | +```java |
| 141 | +package com.example.config; |
| 142 | + |
| 143 | +import io.micronaut.context.annotation.Context; |
| 144 | +import jakarta.annotation.PreDestroy; |
| 145 | +import org.graalvm.polyglot.EnvironmentAccess; |
| 146 | +import org.graalvm.python.embedding.GraalPyResources; |
| 147 | +import java.io.IOException; |
| 148 | + |
| 149 | +@Context // ① |
| 150 | +public class GraalPyContext { |
| 151 | + |
| 152 | + public static final String PYTHON="python"; |
| 153 | + private final org.graalvm.polyglot.Context context; |
| 154 | + |
| 155 | + public GraalPyContext() throws IOException { |
| 156 | + |
| 157 | + |
| 158 | + context= GraalPyResources.contextBuilder() |
| 159 | + .allowEnvironmentAccess(EnvironmentAccess.INHERIT) // ② |
| 160 | + .option("python.WarnExperimentalFeatures", "false") // ③ |
| 161 | + .build(); |
| 162 | + |
| 163 | + |
| 164 | + context.initialize(PYTHON); // ④ |
| 165 | + } |
| 166 | + |
| 167 | + public org.graalvm.polyglot.Context getContext() { |
| 168 | + |
| 169 | + return context; // ⑤ |
| 170 | + } |
| 171 | + |
| 172 | + @PreDestroy |
| 173 | + void close(){ |
| 174 | + try { |
| 175 | + context.close(true); // ⑥ |
| 176 | + } catch (Exception e) { |
| 177 | + //ignore |
| 178 | + } |
| 179 | + } |
| 180 | +} |
| 181 | +``` |
| 182 | +❶ Eagerly initialize as a singleton bean. |
| 183 | + |
| 184 | +❷ Allow environment access. |
| 185 | + |
| 186 | +❸ Set GraalPy option to not log a warning every time a native extension is loaded. |
| 187 | + |
| 188 | +❹ Initializing a GraalPy context isn't cheap, so we do so already at creation time to avoid delayed response time. |
| 189 | + |
| 190 | +❺ Return the GraalPy context. |
| 191 | + |
| 192 | +❻ Close the GraalPy context. |
| 193 | + |
| 194 | + |
| 195 | +### 3.5 Binding Java interface with Python code |
| 196 | +Define a Java interface with the methods we want to bind. |
| 197 | + |
| 198 | +`GenerateAnswerModule.java` |
| 199 | + |
| 200 | +```java |
| 201 | +package com.example.services; |
| 202 | + |
| 203 | +import org.graalvm.polyglot.Value; |
| 204 | + |
| 205 | +public interface GenerateAnswerModule { |
| 206 | + |
| 207 | + String process_question(String question, Value documents); |
| 208 | +} |
| 209 | + |
| 210 | +``` |
| 211 | + |
| 212 | +Define a Micronaut service `RagPipelineService.java`. |
| 213 | + |
| 214 | +```java |
| 215 | +package com.example.services; |
| 216 | + |
| 217 | +import com.example.config.GraalPyContext; |
| 218 | +import jakarta.inject.Singleton; |
| 219 | +import org.graalvm.polyglot.Value; |
| 220 | + |
| 221 | +import static com.example.config.GraalPyContext.PYTHON; |
| 222 | + |
| 223 | + |
| 224 | +@Singleton |
| 225 | +public class RagPipelineService { |
| 226 | + |
| 227 | + |
| 228 | + private final GenerateAnswerModule generateAnswerModule; |
| 229 | + private final InitialDataModule initialDataModule; |
| 230 | + private final RetrievalModule retrievalModule; |
| 231 | + private final ExternalDataModule externalDataModule; |
| 232 | + public static final String TABLE_NAME = "VECTOR_STORE"; |
| 233 | + |
| 234 | + public RagPipelineService(GraalPyContext graalPyContext) { |
| 235 | + |
| 236 | + |
| 237 | + graalPyContext.getContext().eval(PYTHON, "import generation, prepare_initial_data, external_data_processing, retrieval"); // ① |
| 238 | + Value generation_module = graalPyContext.getContext().getBindings(PYTHON).getMember("generation"); // ② |
| 239 | + Value retrieval_module = graalPyContext.getContext().getBindings(PYTHON).getMember("retrieval"); |
| 240 | + Value initial_data_module = graalPyContext.getContext().getBindings(PYTHON).getMember("prepare_initial_data"); |
| 241 | + Value external_data_module = graalPyContext.getContext().getBindings(PYTHON).getMember("external_data_processing"); |
| 242 | + |
| 243 | + Value generateAnswerClass = generation_module.getMember("GenerateAnswer"); // ③ |
| 244 | + Value retrievalClass = retrieval_module.getMember("Retrieval"); |
| 245 | + Value initialDataClass = initial_data_module.getMember("InitialData"); |
| 246 | + Value externalDataClass = external_data_module.getMember("ExternalData"); |
| 247 | + |
| 248 | + |
| 249 | + generateAnswerModule = generateAnswerClass.newInstance().as(GenerateAnswerModule.class); // ④ |
| 250 | + retrievalModule = retrievalClass.newInstance(TABLE_NAME).as(RetrievalModule.class); |
| 251 | + initialDataModule = initialDataClass.newInstance(TABLE_NAME).as(InitialDataModule.class); |
| 252 | + externalDataModule = externalDataClass.newInstance(TABLE_NAME).as(ExternalDataModule.class); |
| 253 | + |
| 254 | + } |
| 255 | + |
| 256 | + public String generateAnswer(String question, Value retrievedDocuments) { |
| 257 | + return generateAnswerModule.process_question(question, retrievedDocuments); // ⑤ |
| 258 | + } |
| 259 | + |
| 260 | + public Value hybridSearch(String question, int numResults) { |
| 261 | + return retrievalModule.hybrid_search(question, numResults); |
| 262 | + } |
| 263 | + |
| 264 | + public AddUrlResultType addURL(String url){ |
| 265 | + if(!externalDataModule.is_graalpy_related(url)){ |
| 266 | + return AddUrlResultType.INVALID_URL; |
| 267 | + } |
| 268 | + if(!externalDataModule.add_url(url)){ |
| 269 | + return AddUrlResultType.DUPLICATE; |
| 270 | + } |
| 271 | + return AddUrlResultType.SUCCESS; |
| 272 | + } |
| 273 | + |
| 274 | + public String addText(String text){ |
| 275 | + externalDataModule.add_new_text(text); |
| 276 | + return "The text has been successfully added"; |
| 277 | + } |
| 278 | + |
| 279 | + public Boolean checkDbInit(){ |
| 280 | + return initialDataModule.check_db_init(); |
| 281 | + } |
| 282 | + |
| 283 | + public void loadDataFromWebSite(String url, String className){ |
| 284 | + initialDataModule.load_data_from_url_process(url, className); |
| 285 | + } |
| 286 | + public void loadDataFromFile(String fileName){ |
| 287 | + initialDataModule.load_data_from_file_process(fileName); |
| 288 | + |
| 289 | + } |
| 290 | + |
| 291 | + public void CreateTextIndex(){ |
| 292 | + initialDataModule.create_text_index(); |
| 293 | + } |
| 294 | + |
| 295 | +} |
| 296 | +``` |
| 297 | + |
| 298 | +❶ This imports Python modules, which should be located at `org.graalvm.python.vfs/src`. making them available in the global namespace. |
| 299 | + |
| 300 | +❷ Retrieve the Python "bindings". |
| 301 | +This is an object that provides access to Python global namespace. One can read, add, or remove global variables using this object. |
| 302 | + |
| 303 | +❸ Get the `GenerateAnswer` Class from the imported Python module. |
| 304 | + |
| 305 | +❹ Instantiate the Python class and binds it to the corresponding Java interface `GenerateAnswerModule`. |
| 306 | + |
| 307 | +❺ Call the Python method through the Java interface. |
| 308 | + |
| 309 | +### 3.6 Controller |
| 310 | + |
| 311 | +To create a microservice that provides LLM-powered question answering using Retrieval-Augmented Generation(RAG), you also need a controller: |
| 312 | + |
| 313 | +`src/main/java/com/example/controllers/RagController.java` |
| 314 | + |
| 315 | +```java |
| 316 | + @Post(value = "/answer") |
| 317 | + public String getAnswer(@Body Map<String, String> body) { // ① |
| 318 | + String query = body.get("query"); |
| 319 | + Value retrievedDocs = service.hybridSearch(query, 4); // ② |
| 320 | + return service.generateAnswer(query, retrievedDocs); // ③ |
| 321 | + } |
| 322 | +``` |
| 323 | + |
| 324 | +❶ expose `api/llm/answer` endpoint, you can use Postman or cURL to test it. |
| 325 | +```shell |
| 326 | +curl -X POST http://localhost:8080/api/llm/answer \ |
| 327 | +-H "Content_Type: application/json"\ |
| 328 | +-d '{"query" : "put your question here"}' |
| 329 | +``` |
| 330 | + |
| 331 | +❷ Uses the `retrievalModule` (via the `hybridSearch` method in the service) to execute the corresponding Python function and retrieve relevant documents as a `Value`. |
| 332 | + |
| 333 | +❸ Passes the retrieved documents to the `generateAnswer` method in the service, which calls the corresponding Python method to generate an answer using the LLM. |
| 334 | + |
| 335 | + |
| 336 | +### 3.7 Test |
| 337 | +Create a test to verify that when you make a POST request to you get the expected response: |
| 338 | + |
| 339 | +`src/test/java/com/example/controllers/RagControllerIntegrationTest.java` |
| 340 | + |
| 341 | +```java |
| 342 | + @Test |
| 343 | + void givenRelatedGraalPyQuestion_whenCallingGetAnswerEndpoint_thenReturnsExpectedAnswer() { |
| 344 | + |
| 345 | + //Arrange |
| 346 | + String query = "What is GraalPy?"; // ① |
| 347 | + |
| 348 | + //Act |
| 349 | + HttpRequest<?> req = HttpRequest.POST("/answer", Map.of("query", query)) |
| 350 | + .contentType(MediaType.APPLICATION_JSON); // ② |
| 351 | + HttpResponse<String> rsp = client.toBlocking().exchange(req, String.class); |
| 352 | + |
| 353 | + //Assert |
| 354 | + assertEquals(HttpStatus.OK,rsp.getStatus()); // ③ |
| 355 | + assert(rsp.body().toLowerCase().contains("graalpy")); |
| 356 | + assert (rsp.body().toLowerCase().contains("embedding")); |
| 357 | + assert (rsp.body().toLowerCase().contains("language")); |
| 358 | + |
| 359 | + } |
| 360 | +``` |
| 361 | + |
| 362 | +❶ Prepares the test input to be sent to the `/api/llm/answer` endpoint. |
| 363 | + |
| 364 | +❷ Builds an HTTP POST request, with the query included in the request body as JSON `({"query": "What is GraalPy?"})`. |
| 365 | + |
| 366 | +❸ Verifies The HTTP response status is OK and that the response body contains the expected keywords. |
| 367 | + |
| 368 | + |
| 369 | +### 3.7 Running the Application |
| 370 | + |
| 371 | +#### With docker |
| 372 | + |
| 373 | +* Add your API keys to `.env file` |
| 374 | + |
| 375 | +```shell |
| 376 | +GROQ_API_KEY=PUT_YOUR_GROQ_API_KEY_HERE |
| 377 | +COHERE_API_KEY=PUT_YOUR_COHERE_API_KEY_HERE |
| 378 | +``` |
| 379 | + |
| 380 | +* Run the following command to start the services using Docker Compose: |
| 381 | + |
| 382 | +```shell |
| 383 | +docker-compose up |
| 384 | +``` |
| 385 | + |
| 386 | +#### Without Docker |
| 387 | +To run the application without Docker, please check the `backend subdirectory` for instructions. |
| 388 | + |
| 389 | + |
| 390 | + |
| 391 | + |
0 commit comments