Introduction#
Video demo
This repo shows how to get started with Amazon Bedrock in golang through basic examples.
- simple chat and prompt
- query vector database (opensearch)
- simple image analyzing
- bedrock knowledge base
For learning purpose, it implement these features using only basic concepts and without relying on framework like LangChain, Streamlit, or React.
- basic stream response
- basic css and javascript
WebServer#
Project structure
|--image|--demo.jpeg|--static|--claude-haiku.html|--claude2.html|--image.html|--opensearch.html|--aoss.go|--bedrock.go|--constants.go|--go.mod|--main.go
main.go implement a http server and route request to handlers. bedrock.go and aoss.go are functions to invoke Amazon Bedrock and Amazon OpenSearch Serverless (AOSS), respecitively. static folder contains simple frontend with javascript.
[!IMPORTANT]
To use AOSS, you need create a OpenSearch collection and provide its URL endpoint in constants.go. In addition, you need to setup data access in the AOSS for the running time environment (EC2 profile, ECS taks role, Lambda role, .etc)
Stream Response#
First it is good to create some data structs according to Amazon Bedrock Claude3 API format
type Content struct {Type string `json:"type"`Text string `json:"text"`}type Message struct {Role string `json:"role"`Content []Content `json:"content"`}type Body struct {MaxTokensToSample int `json:"max_tokens"`Temperature float64 `json:"temperature,omitempty"`AnthropicVersion string `json:"anthropic_version"`Messages []Message `json:"messages"`}// list of messagesmessages := []Message{{Role: "user",Content: []Content{{Type: "text", Text: promt}},}}// form request bodypayload := Body{MaxTokensToSample: 2048,Temperature: 0.9,AnthropicVersion: "bedrock-2023-05-31",Messages: messages,}
Then convert the payload to bytes and invoke Bedrock client
payload := Body{MaxTokensToSample: 2048,Temperature: 0.9,AnthropicVersion: "bedrock-2023-05-31",Messages: messages,}// marshal payload to bytespayloadBytes, err := json.Marshal(payload)if err != nil {fmt.Println(err)return}// create request to bedrockoutput, error := BedrockClient.InvokeModelWithResponseStream(context.Background(),&bedrockruntime.InvokeModelWithResponseStreamInput{Body: payloadBytes,ModelId: aws.String("anthropic.claude-3-haiku-20240307-v1:0"),ContentType: aws.String("application/json"),Accept: aws.String("application/json"),},)if error != nil {fmt.Println(error)return}
Finally, parse the streaming reponse and decode to text. When deploy on a http server, we need to modify the code a bit to stream each chunk of response to client. For example HERE
output, error := BedrockClient.InvokeModelWithResponseStream(context.Background(),&bedrockruntime.InvokeModelWithResponseStreamInput{Body: payloadBytes,ModelId: aws.String("anthropic.claude-3-haiku-20240307-v1:0"),ContentType: aws.String("application/json"),Accept: aws.String("application/json"),},)if error != nil {fmt.Println(error)return}// parse response streamfor event := range output.GetStream().Events() {switch v := event.(type) {case *types.ResponseStreamMemberChunk://fmt.Println("payload", string(v.Value.Bytes))var resp ResponseClaude3err := json.NewDecoder(bytes.NewReader(v.Value.Bytes)).Decode(&resp)if err != nil {fmt.Println(err)}fmt.Println(resp.Delta.Text)case *types.UnknownUnionMember:fmt.Println("unknown tag:", v.Tag)default:fmt.Println("union is nil or unknown type")}}
Image Analyze#
Similarly, for image analyzing using Amazon Bedrock Claude3, we need to create a correct request format. It is possible without explicitly define structs as above and using interface
// read image from local fileimageData, error := ioutil.ReadFile("demo.jpeg")if error != nil {fmt.Println(error)}// encode image to base64base64Image := base64.StdEncoding.EncodeToString(imageData)source := map[string]interface{}{"type": "base64","media_type": "image/jpeg","data": base64Image,}messages := []map[string]interface{}{{"role": "user","content": []map[string]interface{}{{"type": "image", "source": source}, {"type": "text", "text": "what is in this image?"}},}}payload := map[string]interface{}{"max_tokens": 2048,"anthropic_version": "bedrock-2023-05-31","temperature": 0.9,"messages": messages,}
Then invoke Amazon Bedrock Client like below, and similar for streaming reponse as previous example.
// convert payload struct to bytespayloadBytes, error := json.Marshal(payload)if error != nil {fmt.Println(error)}// invoke bedrock claude3 haikuoutput, error := BedrockClient.InvokeModel(context.Background(),&bedrockruntime.InvokeModelInput{Body: payloadBytes,ModelId: aws.String("anthropic.claude-3-haiku-20240307-v1:0"),ContentType: aws.String("application/json"),Accept: aws.String("application/json"),},)if error != nil {fmt.Println(error)}// responsefmt.Println(string(output.Body))
OpenSearch#
- create OpenSearch client
- convert user question to embedding vector
- send query or request to OpenSearch
A OpenSearch and Bedrock client can be initialized as below
InitOpenSearchBedrockClient
// opensearch severless clientvar AOSSClient *opensearch.Client// bedrock clientvar BedrockClient *bedrockruntime.Client// create an init function to initializing opensearch clientfunc init() {//fmt.Println("init and create an opensearch client")// load aws credentials from profile demo using configawsCfg, err := config.LoadDefaultConfig(context.Background(),config.WithRegion("us-east-1"),)if err != nil {log.Fatal(err)}// create bedorck runtime clientBedrockClient = bedrockruntime.NewFromConfig(awsCfg)// create a aws request signer using requestsignersigner, err := requestsigner.NewSignerWithService(awsCfg, "aoss")if err != nil {log.Fatal(err)}uncommen for opensearch clientcreate an opensearch client using opensearch packageAOSSClient, err = opensearch.NewClient(opensearch.Config{Addresses: []string{AOSS_ENDPOINT},Signer: signer,})if err != nil {log.Fatal(err)}}
Create a function to convert text to vector by invoking Amazon Bedrock Titan model.
GetEmbedVector
func GetEmbedVector(qustion string) ([]float64, error) {// create request body to titan modelbody := map[string]interface{}{"inputText": qustion,}bodyJson, err := json.Marshal(body)if err != nil {fmt.Println(err)return nil, err}// invoke bedrock titan model to convert string to embedding vectorresponse, error := BedrockClient.InvokeModel(context.Background(),&bedrockruntime.InvokeModelInput{Body: []byte(bodyJson),ModelId: aws.String("amazon.titan-embed-text-v1"),ContentType: aws.String("application/json"),},)if error != nil {fmt.Println(error)return nil, error}// assert response to mapvar embedResponse map[string]interface{}error = json.Unmarshal(response.Body, &embedResponse)if error != nil {fmt.Println(error)return nil, error}// assert response to arrayslice, ok := embedResponse["embedding"].([]interface{})if !ok {fmt.Println(ok)}// assert to array of float64values := make([]float64, len(slice))for k, v := range slice {values[k] = float64(v.(float64))}return values, nil}
Then send request or query to AOSS
QueryOpenSearch
func QueryAOSS(vec []float64) ([]string, error) {// let query get all item in an index// content := strings.NewReader(`{// "size": 10,// "query": {// "match_all": {}// }// }`)vecStr := make([]string, len(vec))// convert array float to stringfor k, v := range vec {if k < len(vec)-1 {vecStr[k] = fmt.Sprint(v) + ","} else {vecStr[k] = fmt.Sprint(v)}}// create request body to titan modelcontent := strings.NewReader(fmt.Sprintf(`{"size": 5,"query": {"knn": {"vector_field": {"vector": %s,"k": 5}}}}`, vecStr))// fmt.Println(content)search := opensearchapi.SearchRequest{Index: []string{"demo"},Body: content,}searchResponse, err := search.Do(context.Background(), AOSSClient)if err != nil {log.Fatal(err)}// fmt.Println(searchResponse)var answer AossResponsejson.NewDecoder(searchResponse.Body).Decode(&answer)// first := answer.Hits.Hits[0]// fmt.Printf("id: %s\n, index: %s\n, text: %s", first["_id"], first["_index"], first["_source"].(map[string]interface{})["text"])// fmt.Println(answer.Hits.Hits[0]["_id"])queryResult := answer.Hits.Hits[0]["_source"].(map[string]interface{})["text"]if queryResult == nil {return []string{"nil"}, nil}// extract hint text onlyhits := []string{}for k, v := range answer.Hits.Hits {if k > 0 {hits = append(hits, v["_source"].(map[string]interface{})["text"].(string))}}return hits, nil// return fmt.Sprint(queryResult), nil}
Knowledge Based#
- create knowledge based
- retrieve
- retrieve and generate
- iam permissions for ECS task
First, go to bedrock console to create a new Knowledge Based and select Amazon OpenSearch Serverless (AOSS) as a vector database. Then let write a simple Handler to query the AOSS collection.
output, error := client.Retrieve(context.TODO(),&bedrockagentruntime.RetrieveInput{KnowledgeBaseId: aws.String(KNOWLEDGE_BASE_ID),RetrievalQuery: &types.KnowledgeBaseQuery{Text: aws.String(userQuestion),},RetrievalConfiguration: &types.KnowledgeBaseRetrievalConfiguration{VectorSearchConfiguration: &types.KnowledgeBaseVectorSearchConfiguration{NumberOfResults: aws.Int32(KNOWLEDGE_BASE_NUMBER_OF_RESULT),},},},)
Second, let write a function to retrieve and generate
output, error := client.RetrieveAndGenerate(context.TODO(),&bedrockagentruntime.RetrieveAndGenerateInput{Input: &types.RetrieveAndGenerateInput{Text: aws.String(userQuestion),},RetrieveAndGenerateConfiguration: &types.RetrieveAndGenerateConfiguration{Type: types.RetrieveAndGenerateTypeKnowledgeBase,KnowledgeBaseConfiguration: &types.KnowledgeBaseRetrieveAndGenerateConfiguration{KnowledgeBaseId: aws.String(KNOWLEDGE_BASE_ID),ModelArn: aws.String(KNOWLEDGE_BASE_MODEL_ID),RetrievalConfiguration: &types.KnowledgeBaseRetrievalConfiguration{VectorSearchConfiguration: &types.KnowledgeBaseVectorSearchConfiguration{NumberOfResults: aws.Int32(KNOWLEDGE_BASE_NUMBER_OF_RESULT),},},},},},)
Then response should look like this
POST /knowledgebases/knowledgeBaseId/retrieve HTTP/1.1Content-type: application/json{"nextToken": "string","retrievalConfiguration": {"vectorSearchConfiguration": {"filter": { ... },"numberOfResults": number,"overrideSearchType": "string"}},"retrievalQuery": {"text": "string"}}
The response format should look like this
POST /retrieveAndGenerate HTTP/1.1Content-type: application/json{"input": {"text": "string"},"retrieveAndGenerateConfiguration": {"knowledgeBaseConfiguration": {"generationConfiguration": {"promptTemplate": {"textPromptTemplate": "string"}},"knowledgeBaseId": "string","modelArn": "string","retrievalConfiguration": {"vectorSearchConfiguration": {"filter": { ... },"numberOfResults": number,"overrideSearchType": "string"}}},"type": "string"},"sessionConfiguration": {"kmsKeyArn": "string"},"sessionId": "string"}
knowledge-based.go
package bedrockimport ("context""encoding/json""fmt""net/http""github.com/aws/aws-sdk-go-v2/aws""github.com/aws/aws-sdk-go-v2/service/bedrockagentruntime""github.com/aws/aws-sdk-go-v2/service/bedrockagentruntime/types")func HandleRetrieve(w http.ResponseWriter, r *http.Request, client *bedrockagentruntime.Client) {// parse user messagestype Content struct {Type string `json:"type"`Text string `json:"text"`}type Message struct {Role string `json:"role"`Content []Content `json:"content"`}var request struct {Messages []Message `json:"messages"`}error := json.NewDecoder(r.Body).Decode(&request)if error != nil {fmt.Println(error)}messages := request.Messagesfmt.Println(messages)// pop the last message as user questionuserQuestion := messages[len(messages)-1].Content[0].Text// invoke bedrock agent runtime to retreive opensearchoutput, error := client.Retrieve(context.TODO(),&bedrockagentruntime.RetrieveInput{KnowledgeBaseId: aws.String(KNOWLEDGE_BASE_ID),RetrievalQuery: &types.KnowledgeBaseQuery{Text: aws.String(userQuestion),},RetrievalConfiguration: &types.KnowledgeBaseRetrievalConfiguration{VectorSearchConfiguration: &types.KnowledgeBaseVectorSearchConfiguration{NumberOfResults: aws.Int32(KNOWLEDGE_BASE_NUMBER_OF_RESULT),},},},)if error != nil {fmt.Println(error)}// for k, v := range output.RetrievalResults {// fmt.Println(k)// fmt.Println("=======================================")// fmt.Println(*v.Content.Text)// }// parse output to []byte and return clientoutputJson, error := json.Marshal(output)if error != nil {fmt.Println(error)}w.Write(outputJson)}func HandleRetrieveAndGenerate(w http.ResponseWriter, r *http.Request, client *bedrockagentruntime.Client) {// parse user messagestype Content struct {Type string `json:"type"`Text string `json:"text"`}type Message struct {Role string `json:"role"`Content []Content `json:"content"`}var request struct {Messages []Message `json:"messages"`}error := json.NewDecoder(r.Body).Decode(&request)if error != nil {fmt.Println(error)}messages := request.Messagesfmt.Println(messages)// pop the last message as user questionuserQuestion := messages[len(messages)-1].Content[0].Text// invoke bedrock agent runtime to retrieve and generateoutput, error := client.RetrieveAndGenerate(context.TODO(),&bedrockagentruntime.RetrieveAndGenerateInput{Input: &types.RetrieveAndGenerateInput{Text: aws.String(userQuestion),},RetrieveAndGenerateConfiguration: &types.RetrieveAndGenerateConfiguration{Type: types.RetrieveAndGenerateTypeKnowledgeBase,KnowledgeBaseConfiguration: &types.KnowledgeBaseRetrieveAndGenerateConfiguration{KnowledgeBaseId: aws.String(KNOWLEDGE_BASE_ID),ModelArn: aws.String(KNOWLEDGE_BASE_MODEL_ID),RetrievalConfiguration: &types.KnowledgeBaseRetrievalConfiguration{VectorSearchConfiguration: &types.KnowledgeBaseVectorSearchConfiguration{NumberOfResults: aws.Int32(KNOWLEDGE_BASE_NUMBER_OF_RESULT),},},},},},)if error != nil {fmt.Println(error)}fmt.Println(*output.Output.Text)// parse output to []byteoutputJson, error := json.Marshal(output)if error != nil {fmt.Println(error)}// write output to clientw.Write(outputJson)}
retrieve.html
<html><head><meta name="viewport" content="width=device-width" /><style>:root {box-sizing: border-box;}*,::before,::after {box-sizing: inherit;}body {/* background-color: antiquewhite; */}.container {width: 100%;max-width: 500px;margin: auto;/* background-color: antiquewhite; */}.button {background-color: #43a047;padding: 8px 20px;border-radius: 5px;border: none;cursor: pointer;position: absolute;transform: translateY(-50%);top: 50%;right: 10px;opacity: 0.8;}.button:hover {background-color: orange;}.text-input {padding: 10px 15px;width: 100%;outline: none;border: solid black 1px;background-color: #e0e0e0;box-shadow: 0 10px 15px -3px #e0e0e0;font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;font-size: medium;font-weight: 400;letter-spacing: normal;line-height: 25px;}.text-input:focus {border: solid #4caf50 1.5px;outline: none;}.container-input {position: relative;}.form {margin-top: 20px;}.text-model {/* color: #4caf50; */font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;font-size: medium;font-weight: 400;letter-spacing: normal;line-height: 25px;}</style></head><body><div class="container"><form id="form" onkeydown="return event.key != 'Enter';" class="form"><div class="container-input"><input class="text-input" type="text" id="text-input" /><button id="submit" class="button">retrieve</button></div></form><div id="list" class="text-model"></div></div><script>const callBedrockStream = async () => {// Get the list container elementvar listContainer = document.getElementById('list')// clear content before querylistContainer.innerHTML = ''// conversation turnslet messages = []// get user questionconst userQuestion = document.getElementById('text-input').value// push user question to messagesmessages.push({role: 'user',content: [{ type: 'text', text: userQuestion }]})if (userQuestion) {try {const response = await fetch('/knowledge-base-retrieve', {method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ messages: messages })})console.log(response)const decoder = new TextDecoder()// batch processingconst json = await response.json()const items = json['RetrievalResults']// update frontendfor (var i = 0; i < items.length; i++) {var listItem = document.createElement('div')listItem.style.marginBottom = '15px'listItem.style.borderBottom = '1px solid #0000FF'var header = document.createElement('h4')header.textContent = `Chunk ${i}`var itemText = document.createTextNode(items[i]['Content']['Text'])listItem.appendChild(header)listItem.appendChild(itemText)listContainer.appendChild(listItem)}// push model answer to converstion turn// messages.push({// role: "assistant",// content: [{ type: "text", text: modelAnswer.innerText }],// });} catch (error) {console.log(error)}} else {console.log('Please enter question ...')}}document.getElementById('submit').addEventListener('click', async event => {event.preventDefault()await callBedrockStream()})document.getElementById('text-input').addEventListener('keydown', async event => {if (event.code === 'Enter') {await callBedrockStream()}})</script></body></html>
retrieve-generate.html
<html><head><meta name="viewport" content="width=device-width" /><style>:root {box-sizing: border-box;}*,::before,::after {box-sizing: inherit;}body {/* background-color: antiquewhite; */}.container {width: 100%;max-width: 500px;margin: auto;/* background-color: antiquewhite; */}.button {background-color: #43a047;padding: 8px 20px;border-radius: 5px;border: none;cursor: pointer;position: absolute;transform: translateY(-50%);top: 50%;right: 10px;opacity: 0.8;}.button:hover {background-color: orange;}.text-input {padding: 10px 15px;width: 100%;outline: none;border: solid black 1px;background-color: #e0e0e0;box-shadow: 0 10px 15px -3px #e0e0e0;font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;font-size: medium;font-weight: 400;letter-spacing: normal;line-height: 25px;}.text-input:focus {border: solid #4caf50 1.5px;outline: none;}.container-input {position: relative;}.form {margin-top: 20px;}.text-model {/* color: #4caf50; */font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;font-size: medium;font-weight: 400;letter-spacing: normal;line-height: 25px;}</style></head><body><div class="container"><form id="form" onkeydown="return event.key != 'Enter';" class="form"><div class="container-input"><input class="text-input" type="text" id="text-input" /><button id="submit" class="button">retrieve</button></div></form><div><p id="model-answer" class="text-model"></p></div><div id="list" class="text-model"></div></div><script>// get html component for model answerconst modelAnswer = document.getElementById('model-answer')// Get the list container elementvar listContainer = document.getElementById('list')// conversation turnslet messages = []const callBedrockStream = async () => {// present model answer to frontendmodelAnswer.innerText = ''// clear content before querylistContainer.innerHTML = ''// get user questionconst userQuestion = document.getElementById('text-input').value// push user question to messagesmessages.push({role: 'user',content: [{ type: 'text', text: userQuestion }]})if (userQuestion) {try {const response = await fetch('/knowledge-base-retrieve-and-generate',{method: 'POST',headers: {'Content-Type': 'application/json'},body: JSON.stringify({ messages: messages })})console.log(response)const decoder = new TextDecoder()// batch processingconst json = await response.json()console.log(json)// update frontendmodelAnswer.innerText += json['Output']['Text']// citationscitations = json['Citations']console.log(citations)// update frontendfor (var i = 0; i < citations.length; i++) {// generted partvar genertedPart =citations[i]['GeneratedResponsePart']['TextResponsePart']['Text']// referencesvar retrievedReferences = citations[i]['RetrievedReferences']// citation i and generated ivar listItemC = document.createElement('div')listItemC.style.marginBottom = '15px'listItemC.style.borderBottom = '1px solid #0000FF'listItemC.style.color = 'blue'var headerC = document.createElement('h4')headerC.textContent = `Citation ${i} Generated Part ${i}`var itemTextC = document.createTextNode(genertedPart)//listItemC.appendChild(headerC)listItemC.appendChild(itemTextC)listContainer.appendChild(listItemC)for (var j = 0; j < retrievedReferences.length; j++) {console.log(`citation ${i} and reference ${j}`)// reference textvar refText = retrievedReferences[j]['Content']['Text']// reference uri s3var refUri =retrievedReferences[j]['Location']['S3Location']['Uri']// citation i and reference jvar listItem = document.createElement('div')listItem.style.marginBottom = '15px'listItem.style.borderBottom = '1px solid #0000FF'var header = document.createElement('h4')header.textContent = `Citation ${i} Reference ${j}`var itemText = document.createTextNode(refText + refUri)// citation i and reference jlistItem.appendChild(header)listItem.appendChild(itemText)listContainer.appendChild(listItem)}}// push model answer to converstion turnmessages.push({role: 'assistant',content: [{ type: 'text', text: modelAnswer.innerText }]})} catch (error) {console.log(error)}} else {console.log('Please enter question ...')}}document.getElementById('submit').addEventListener('click', async event => {event.preventDefault()await callBedrockStream()})document.getElementById('text-input').addEventListener('keydown', async event => {if (event.code === 'Enter') {await callBedrockStream()}})</script></body></html>
IAM Permissions#
To allow the ECS task to invoke bedrock runtime agent, and retreive AOSS we need to add permissions to task role by below policy
{"Version": "2012-10-17","Statement": [{"Action": ["bedrock:InvokeModel","bedrock:InvokeModelWithResponseStream","bedrock:InvokeAgent","bedrock:Retrieve","bedrock:RetrieveAndGenerate"],"Resource": ["arn:aws:bedrock:us-east-1::foundation-model/*","arn:aws:bedrock:us-west-2::foundation-model/*","arn:aws:bedrock:us-west-2:<ACCOUNT_ID>:knowledge-base/<KNOWLEDGE_BASE_ID>"],"Effect": "Allow"},{"Action": ["s3:GetObject", "s3:PutObject"],"Resource": ["arn:aws:s3:::<BUCKET_NAME>/*"],"Effect": "Allow"},{"Action": "aoss:APIAccessAll","Resource": ["arn:aws:aoss:us-east-1:<ACCOUNT_ID>:collection/<AOSS_ID>","arn:aws:aoss:us-west-2:<ACCOUNT_ID>:collection/<AOSS_ID>","arn:aws:aoss:us-west-2:<ACCOUNT_ID>:collection/<AOSS_ID>"],"Effect": "Allow"}]}
And we also need to add the task role ARN to data collection of the AOSS collection
UserData#
#!/bin/bashcd /home/ec2-user/wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gztar -xvf go1.21.5.linux-amd64.tar.gzexport 'PATH=/home/ec2-user/go/bin:$PATH' >> ~/.bashrcwget https://github.com/cdk-entest/golang-bedrock-demo/archive/refs/heads/main.zipunzip maincd golang-bedrock-demo-maingo mod tidygo run .