Skip to content

Commit faeaa0d

Browse files
Add a LLM assistant using GraalPy demo
1 parent 0e2f9d3 commit faeaa0d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+37432
-0
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
OLLAMA_HOST=http://ollama:11434
2+
GROQ_API_KEY=PUT_YOUR_GROQ_API_KEY_HERE
3+
COHERE_API_KEY=PUT_YOUR_COHERE_API_KEY_HERE
4+
USER=rachida
5+
PASSWORD=MySecurePass123
6+
DSN=oracle-db:1521/free
Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
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+
![img_1.png](img_1.png)
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

Comments
 (0)