logo
Menu

[Beginner] Experience RAG through a Hands-on : Part 2

I've created a hands-on article with the concept of just trying it out and experiencing it. 🎉🎉🎉

Published May 6, 2024
Last Modified May 7, 2024
✅️I am translating and posting the original text written in Japanese. Screen capture and language settings are set for Japan.
Generative AI is a very prominent technology, but it may be a bit difficult to get started with. New models and utilization methods are emerging almost daily, making it quite challenging to stay up-to-date.To help those who have missed the opportunity to get started, I've created a hands-on article with the concept of just trying it out and experiencing it. 🎉🎉🎉The content of the hands-on session is RAG. RAG is a commonly used keyword when it comes to the application of generative AI. In this hands-on, you'll be able to experience the latest "Advanced RAG", not just regular RAG.
✅️We'll assume you have some knowledge of AWS and Python.
⚠️Since this is a super-fast RAG experience, we won't cover what generative AI is or how to write prompts at all (prompts won't even be mentioned).

Part1

Application Creation

Let's create the application.
Prepare a Python 3.11 environment and install boto3.
1
pip install boto3

Step 1: Generating the Search Query

First, let's import the necessary libraries.
1
2
3
import json

import boto3
Create a client using boto3. The service name to use for invoking Bedrock models is bedrock-runtime.
1
bedrock_client = boto3.client("bedrock-runtime")
✅️While there are also bedrock clients, let's ignore those for now.
This is the ID of the Bedrock model to be used. The model ID for the Cohere Command R+ model is:
1
model_id = "cohere.command-r-plus-v1:0"
The Bedrock invocation is done using the invoke_model method.
Set the question as the message in the JSON body, and the rest can be written as is.
1
2
3
4
5
6
7
8
9
response = bedrock_client.invoke_model(
modelId=model_id,
body=json.dumps(
{
"message": question,
"search_queries_only": True,
}
),
)
The response is not a simple JSON string, but the body part is returned as a StreamingBody type. We need to convert the body to JSON.
1
response_body = json.loads(response.get("body").read())
✅️This is similar to using get_object to retrieve objects from an S3 bucket. Bedrock seems to be designed to return not only text but also images, as it has image generation models.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"chat_history": [],
"finish_reason": "COMPLETE",
"generation_id": "1e17eba2-3a45-49b4-b3b7-57247d7a0b40",
"is_search_required": true,
"response_id": "a3bee56e-b65d-4853-ab72-2455ea62e493",
"search_queries": [
{
"generation_id": "1e17eba2-3a45-49b4-b3b7-57247d7a0b40",
"text": "Amazon Kendra Crawl URL Restrictions"
}
],
"text": ""
}
Extract the text values from the search_queries array.
1
search_queries = list(map(lambda x: x["text"], response_body["search_queries"]))
It's done.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import json

import boto3

bedrock_client = boto3.client("bedrock-runtime")
kendra_client = boto3.client("kendra")

model_id = "cohere.command-r-plus-v1:0"

def generate_search_query(question: str) -> dict:
"""
Generate search queries from question.

question: str
"""


response = bedrock_client.invoke_model(
modelId=model_id,
body=json.dumps(
{
"message": question,
"search_queries_only": True,
}
),
)

response_body = json.loads(response.get("body").read())
search_queries = list(map(lambda x: x["text"], response_body["search_queries"]))

return search_queries

if __name__ == "__main__":

question = "I'm considering using Amazon Kendra to make the content of my website searchable. Is there a way to restrict the URLs that Kendra crawls?"

search_queries = generate_search_query(question=question)
print(search_queries)
Let's try running it.
The user's question is: "I'm considering using Amazon Kendra to make the content of my website searchable. Is there a way to restrict the URLs that Kendra crawls?"
1
python app.py
['Amazon Kendra Crawl URL Restrictions']
---
✅️In the example above, the array contains only one item. However, if the question was "Tell me about the regions supported by Kendra and Bedrock", the following two items would be returned:* Kendra available regions
* Bedrock coverage

Step2: Searching

We will perform a search using the generated search query in Kendra.
We will generate a Kendra client.
1
kendra_client = boto3.client("kendra")
We will call the retrieve API.
Set the IndexId to the Kendra Index ID, and the QueryText to the search keyword generated in the previous step.
The AttributeFilter is a setting to search for data indexed in Japanese.
1
2
3
4
5
6
7
response = kendra_client.retrieve(
IndexId="5a28372c-6ad3-4e51-8263-d9d271d845cf",
QueryText=search_query,
AttributeFilter={
"EqualsTo": {"Key": "_language_code", "Value": {"StringValue": "ja"}}
},
)
✅️If you don't specify the search language in the AttributeFilter, the search will be in English (en).
The ResultItems in the JSON response are the search results. They are returned in the following structure.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"Id": "94980f7c-0c1f-4337-b994-927039ab37a5-e0b3aa98-532f-4f99-9489-f22c645c75d0",
"DocumentId": "https://docs.aws.amazon.com/ja_jp/kendra/latest/dg/ds-schemas.html",
"DocumentTitle": "Data Source Template Schemas - Amazon Kendra",
"Content": "If you don't set crawlSubDomain or crawlAllDomain to true, only the domains of the web sites that Amazon Kendra crawls will be crawled. If you set honorRobots to true, the crawler will honor the robots.txt directives of the websites it crawls. These directives can be... (truncated)",
"DocumentURI": "https://docs.aws.amazon.com/ja_jp/kendra/latest/dg/ds-schemas.html",
"DocumentAttributes": [
{
"Key": "_source_uri",
"Value": {
"StringValue": "https://docs.aws.amazon.com/ja_jp/kendra/latest/dg/ds-schemas.html"
}
}
],
"ScoreAttributes": {
"ScoreConfidence": "NOT_AVAILABLE"
}
}
Since there may be multiple search queries, we will store the ResultItems values in search_results.
1
search_results.extend(response["ResultItems"])
After executing all the search queries, we will convert the data to include only the "Id", "DocumentTitle", "Content", and "DocumentURI" fields.
1
2
3
4
5
6
7
8
9
10
11
documents = list(
map(
lambda x: {
"Id": x["Id"],
"DocumentTitle": x["DocumentTitle"],
"Content": x["Content"],
"DocumentURI": x["DocumentURI"],
},
search_results,
)
)
The search process is now complete.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
kendra_client = boto3.client("kendra")

def search(search_queries: dict):
"""
Search documents from search queries.

search_queries: dict
"""


search_results = []

for search_query in search_queries:

response = kendra_client.retrieve(
IndexId="5a28372c-6ad3-4e51-8263-d9d271d845cf",
QueryText=search_query,
AttributeFilter={
"EqualsTo": {"Key": "_language_code", "Value": {"StringValue": "ja"}}
},
)
search_results.extend(response["ResultItems"])

documents = list(
map(
lambda x: {
"Id": x["Id"],
"DocumentTitle": x["DocumentTitle"],
"Content": x["Content"],
"DocumentURI": x["DocumentURI"],
},
search_results,
)
)

return documents

if __name__ == "__main__":

question = "I am considering using Amazon Kendra to make the content of my website searchable. Is there a way to limit the URLs that the crawler will target?"

search_queries = generate_search_query(question=question)

documents = search(search_queries=search_queries)
print(json.dumps(documents, indent=2, ensure_ascii=False))
Execute.
1
python app.py
Result.
[
{
"Id": "b0701a9f-3928-49e5-a98d-a48f6e7e3a58-9d3749bf-3d22-4361-add3-e90945426f1b",
"DocumentTitle": "Confluence Connector V2.0 - Amazon Kendra",
"Content": "[Additional Configuration] using the [Spaces] key...",
"DocumentURI": "https://docs.aws.amazon.com/ja_jp/kendra/latest/dg/data-source-v2-confluence.html"
},
...
{
"Id": "b0701a9f-3928-49e5-a98d-a48f6e7e3a58-89d91765-63e3-44c0-acd6-0c5d61b9353b",
"DocumentTitle": "Data Source Template Schemas - Amazon Kendra",
"Content": "The repositoryConfigurations for the data source...",
"DocumentURI": "https://docs.aws.amazon.com/ja_jp/kendra/latest/dg/ds-schemas.html"
}
]

Step3: Answer Generation

Generate the answer from the question and search results.
Call Bedrock again to generate the answer.
When generating the search queries, we specified message and search_queries_only in the body. For answer generation, we specify message and documents instead.
1
2
3
4
5
6
7
8
9
response = bedrock_client.invoke_model(
modelId=model_id,
body=json.dumps(
{
"message": question,
"documents": documents,
}
),
)
The way to receive the response is the same as before, but the structure of the response_body is different.
The text contains the answer generated by the AI based on the referenced documents.
1
response_body = json.loads(response.get("body").read())
1
2
3
4
5
6
7
8
9
{
"chat_history": [],
"citations": [],
"documents": [],
"finish_reason": "COMPLETE",
"generation_id": "71bb4862-5aa1-49a2-801b-16e3231b55f8",
"response_id": "6e2ab7ac-a74b-464d-875b-b824b9de13d5",
"text": "Yes, Amazon Kendra allows you to restrict the URLs that are crawled. When configuring the synchronization settings, you can set limits on the crawling of web pages, such as by domain, file size, links, and use regular expression patterns to filter URLs."
}
It's complete.
😀Question:
I'm considering making the content of my website searchable using Amazon Kendra. Is there a way to restrict the URLs that are crawled?
🤖Answer:
Yes, Amazon Kendra allows you to restrict the URLs that are crawled. When configuring the synchronization settings, you can set limits on the crawling of web pages, such as by domain, file size, links, and use regular expression patterns to filter URLs.
---
✅️It's complete. This is RAG (Retrieval-Augmented Generation).
[R] Retrieval: Search
[A] Augmented: Augment
[G] Generation: Generate the answer
The complete source code is here.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
import json

import boto3

bedrock_client = boto3.client("bedrock-runtime")
kendra_client = boto3.client("kendra")

model_id = "cohere.command-r-plus-v1:0"

def generate_search_query(question: str) -> dict:
"""
Generate search queries from question.

question: str
"""


response = bedrock_client.invoke_model(
modelId=model_id,
body=json.dumps(
{
"message": question,
"search_queries_only": True,
}
),
)

response_body = json.loads(response.get("body").read())
search_queries = list(map(lambda x: x["text"], response_body["search_queries"]))

return search_queries

def search(search_queries: dict):
"""
Search documents from search queries.

search_queries: dict
"""


search_results = []

for search_query in search_queries:

response = kendra_client.retrieve(
IndexId="5a28372c-6ad3-4e51-8263-d9d271d845cf",
QueryText=search_query,
AttributeFilter={
"EqualsTo": {"Key": "_language_code", "Value": {"StringValue": "ja"}}
},
)
search_results.extend(response["ResultItems"])

documents = list(
map(
lambda x: {
"Id": x["Id"],
"DocumentTitle": x["DocumentTitle"],
"Content": x["Content"],
"DocumentURI": x["DocumentURI"],
},
search_results,
)
)

return documents

def generate_answer(question: str, documents: dict):
"""
Generate answer from question and documents.

question: str
documents: dict
"""


response = bedrock_client.invoke_model(
modelId=model_id,
body=json.dumps(
{
"message": question,
"documents": documents,
}
),
)

response_body = json.loads(response.get("body").read())

return response_body

if __name__ == "__main__":

question = "I'm considering making the content of my website searchable using Amazon Kendra. Is there a way to restrict the URLs that are crawled?"

search_queries = generate_search_query(question=question)
print(search_queries)

documents = search(search_queries=search_queries)
print(json.dumps(documents, indent=2, ensure_ascii=False))

answer = generate_answer(question=question, documents=documents)
print(json.dumps(answer, indent=2, ensure_ascii=False))
We've created an RAG (Advanced RAG) application with about 100 lines of code. As you can see, each step is very simple.
Didn't you feel like you were using a generative AI at all? What do you think?
There was no mention of "prompts" or "prompt engineering" at all. We just simply:
  1. Call an API to generate search queries from the user's question
  2. Call a search API
  3. Call an API to generate the answer from the search results
This ease of use is a characteristic of Cohere Command R+. In the case of GPT-4 or Claude 3, you would need techniques like "how to write the question in the prompt" and "how to reference the documents", which can lead to difficulty in getting started.
Command R+ has a very well-designed API, and I was impressed.
If you feel like "Ah, RAG is simple after all, I completely understand it!!", then please come to the world of Bedrock and let's have fun together!!
 

Comments