Skip to content

Commit 270eea8

Browse files
authored
Add error handling, logging, and an example policy (#22)
Co-authored-by: Dennis Traub <[email protected]>
1 parent f024c31 commit 270eea8

File tree

7 files changed

+176
-54
lines changed

7 files changed

+176
-54
lines changed

README.md

+40-10
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,26 @@ Welcome to the Java Foundation Model (FM) Playground, a sandbox for exploring Ja
88

99
This repository includes a **Spring Boot** application and a **Next.js** frontend, both executable locally. Below is a screenshot of the app in action.
1010

11-
![Screenshot of the Java FM Playground](screenshot.png)
11+
![Screenshot of the Java FM Playground](resources/screenshot.png)
12+
13+
## ⚠ Important
14+
15+
- Running this application might result in charges to your AWS account. For more details, see [Amazon Bedrock Pricing](https://aws.amazon.com/bedrock/pricing/).
16+
- This app is configured to run in `us-east-1` and has not been tested in every AWS Region. For more information see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services).
17+
- We recommend that you grant your code least privilege, i.e. only the minimum permissions required by the application. You can find an IAM Policy document with the required permissions in this repository at `resources/bedrock-access-policy.json` ([display policy](./resources/bedrock-access-policy.json)).
1218

1319
## Prerequisites
1420

1521
Ensure you have the following installed:
1622

1723
- Java JDK 17+ (e.g. [Amazon Corretto](https://aws.amazon.com/corretto), a free distro of the JDK)
18-
- [Apache Maven](https://maven.apache.org/install.html)
19-
- [Node.js (v18.17+)](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with npm (for Next.js frontend)
20-
- An [AWS account](https://aws.amazon.com/free/) with permissions to access Amazon Bedrock
21-
- To use Bedrock, you must [enable access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html#add-model-access) to at least the following models in `us-east-1`:
22-
1. Anthropic: Claude
23-
2. Stability AI: Stable Diffusion XL
24+
- [Apache Maven](https://maven.apache.org/install.html) for the backend server
25+
- [Node.js (v18.17+)](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) for the frontend application
26+
- You must have an [AWS account](https://aws.amazon.com/free/), and have your default credentials and AWS Region configured as described in the [AWS Tools and SDKs Shared Configuration and Credentials Reference Guide](https://docs.aws.amazon.com/credref/latest/refdocs/creds-config-files.html).
27+
- You must request access to the models before you can use them. For more information, see [Model access](https://docs.aws.amazon.com/bedrock/latest/userguide/model-access.html). To run the app, you need access to the following models in `us-east-1`:
28+
- Anthropic Claude
29+
- AI21 Labs Jurassic-2 Mid
30+
- Stability AI Stable Diffusion XL
2431

2532
## Running the Application
2633

@@ -42,18 +49,41 @@ In the `java-fm-playground/backend` directory, run:
4249
mvn spring-boot:run
4350
```
4451

52+
Once the server is running, you will see the following output:
53+
54+
```shell
55+
...
56+
2023-11-13T14:19:02.862+01:00 INFO 34848 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 55500 (http) with context path ''
57+
2023-11-13T14:19:02.868+01:00 INFO 34848 --- [main] a.c.e.b.SpringFmPlaygroundApplication : Started SpringFmPlaygroundApplication in 1.415 seconds (process running for 1.659)
58+
```
59+
4560
> 🛠 The backend runs on port 55500 by default. See below for port changes.
4661
47-
### Frontent Setup
62+
### Frontend Setup
4863

49-
In a new terminal window, navigate to `java-fm-playground/frontend` and execute:
64+
In a **new terminal window**, navigate to the `frontend` directory and install the packages required by running the following command:
5065

5166
```shell
5267
npm install
68+
```
69+
70+
After successful installation you can start the frontend application by executing the following command:
71+
72+
```shell
5373
npm run dev
5474
```
5575

56-
> 🛠 The frontend runs on port 3000 by default. See below for port changes.
76+
When the application is running, you will see the following output:
77+
78+
```shell
79+
80+
> next dev
81+
82+
▲ Next.js 14.0.0
83+
- Local: http://localhost:3000
84+
85+
✓ Ready in 3.3s
86+
```
5787

5888
## Accessing the Application
5989

Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
package aws.community.examples.bedrock.controller;
22

33
import aws.community.examples.bedrock.aimodels.Claude;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
46
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.http.HttpStatus;
58
import org.springframework.web.bind.annotation.PostMapping;
69
import org.springframework.web.bind.annotation.RequestBody;
710
import org.springframework.web.bind.annotation.RestController;
11+
import org.springframework.web.server.ResponseStatusException;
812
import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
13+
import software.amazon.awssdk.services.bedrockruntime.model.AccessDeniedException;
914

1015
import static aws.community.examples.bedrock.aimodels.LLM.Request;
1116
import static aws.community.examples.bedrock.aimodels.LLM.Response;
1217

1318
@RestController
1419
public class ChatPlayground {
1520

21+
private static final Logger logger = LoggerFactory.getLogger(ChatPlayground.class);
22+
1623
private final BedrockRuntimeClient client;
1724

1825
@Autowired
@@ -22,16 +29,26 @@ public ChatPlayground(final BedrockRuntimeClient client) {
2229

2330
@PostMapping("/foundation-models/model/chat/anthropic.claude-v2/invoke")
2431
public Response invoke(@RequestBody Request body) {
25-
String systemPrompt =
26-
"""
27-
Take the role of a friendly chat bot. Your responses are brief.
28-
You sometimes use emojis where appropriate, but you don't overdo it.
29-
You engage human in a dialog by regularly asking questions,
30-
except when Human indicates that the conversation is over.
31-
""";
32-
33-
String prompt = systemPrompt + "\n\n" + body.prompt();
34-
35-
return new Response(Claude.invoke(client, prompt, 0.8, 300));
32+
try {
33+
34+
String systemPrompt =
35+
"""
36+
Take the role of a friendly chat bot. Your responses are brief.
37+
You sometimes use emojis where appropriate, but you don't overdo it.
38+
You engage human in a dialog by regularly asking questions,
39+
except when Human indicates that the conversation is over.
40+
""";
41+
42+
String prompt = systemPrompt + "\n\n" + body.prompt();
43+
44+
return new Response(Claude.invoke(client, prompt, 0.8, 300));
45+
46+
} catch (AccessDeniedException e) {
47+
logger.error("Access Denied: %s".formatted(e.getMessage()));
48+
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
49+
} catch (Exception e) {
50+
logger.error("Exception: %s".formatted(e.getMessage()));
51+
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
52+
}
3653
}
3754
}

backend/src/main/java/aws/community/examples/bedrock/controller/FoundationModels.java

+43-18
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,24 @@
11
package aws.community.examples.bedrock.controller;
22

3+
import org.slf4j.Logger;
4+
import org.slf4j.LoggerFactory;
35
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.http.HttpStatus;
47
import org.springframework.web.bind.annotation.GetMapping;
58
import org.springframework.web.bind.annotation.PathVariable;
69
import org.springframework.web.bind.annotation.RestController;
10+
import org.springframework.web.server.ResponseStatusException;
711
import software.amazon.awssdk.services.bedrock.BedrockClient;
812
import software.amazon.awssdk.services.bedrock.model.*;
13+
import software.amazon.awssdk.services.bedrock.model.AccessDeniedException;
914

1015
import java.util.List;
1116

1217
@RestController
1318
public class FoundationModels {
1419

20+
private static final Logger logger = LoggerFactory.getLogger(FoundationModels.class);
21+
1522
private final BedrockClient client;
1623

1724
@Autowired
@@ -21,6 +28,7 @@ public FoundationModels(final BedrockClient client) {
2128

2229
@GetMapping("/foundation-models")
2330
public List<FoundationModelListItem> listFoundationModels() {
31+
try {
2432

2533
ListFoundationModelsRequest request = ListFoundationModelsRequest.builder().build();
2634
ListFoundationModelsResponse response = client.listFoundationModels(request);
@@ -32,31 +40,48 @@ public List<FoundationModelListItem> listFoundationModels() {
3240
model.providerName()
3341
)
3442
).toList();
43+
44+
} catch (AccessDeniedException e) {
45+
logger.error("Access Denied: %s".formatted(e.getMessage()));
46+
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
47+
} catch (Exception e) {
48+
logger.error("Exception: %s".formatted(e.getMessage()));
49+
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
50+
}
3551
}
3652

3753
public record FoundationModelListItem(String modelId, String modelName, String providerName) { }
3854

3955
@GetMapping("/foundation-models/model/{modelId}")
40-
public getFoundationModel getFoundationModel(@PathVariable String modelId) {
41-
42-
GetFoundationModelRequest request = GetFoundationModelRequest.builder()
43-
.modelIdentifier(modelId)
44-
.build();
45-
GetFoundationModelResponse response = client.getFoundationModel(request);
46-
47-
FoundationModelDetails model = response.modelDetails();
48-
49-
return new getFoundationModel(
50-
model.modelArn(),
51-
model.modelId(),
52-
model.modelName(),
53-
model.providerName(),
54-
String.join(", ", model.customizationsSupportedAsStrings()),
55-
String.join(", ", model.outputModalitiesAsStrings())
56-
);
56+
public GetFoundationModel getFoundationModel(@PathVariable String modelId) {
57+
try {
58+
59+
GetFoundationModelRequest request = GetFoundationModelRequest.builder()
60+
.modelIdentifier(modelId)
61+
.build();
62+
GetFoundationModelResponse response = client.getFoundationModel(request);
63+
64+
FoundationModelDetails model = response.modelDetails();
65+
66+
return new GetFoundationModel(
67+
model.modelArn(),
68+
model.modelId(),
69+
model.modelName(),
70+
model.providerName(),
71+
String.join(", ", model.customizationsSupportedAsStrings()),
72+
String.join(", ", model.outputModalitiesAsStrings())
73+
);
74+
75+
} catch (AccessDeniedException e) {
76+
logger.error("Access Denied: %s".formatted(e.getMessage()));
77+
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
78+
} catch (Exception e) {
79+
logger.error("Exception: %s".formatted(e.getMessage()));
80+
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
81+
}
5782
}
5883

59-
public record getFoundationModel(
84+
public record GetFoundationModel(
6085
String modelArn,
6186
String modelId,
6287
String modelName,

backend/src/main/java/aws/community/examples/bedrock/controller/ImagePlayground.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
package aws.community.examples.bedrock.controller;
22

33
import aws.community.examples.bedrock.aimodels.StableDiffusion;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
46
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.http.HttpStatus;
58
import org.springframework.web.bind.annotation.*;
9+
import org.springframework.web.server.ResponseStatusException;
610
import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
11+
import software.amazon.awssdk.services.bedrockruntime.model.AccessDeniedException;
712

813
@RestController
914
public class ImagePlayground {
1015

16+
private static final Logger logger = LoggerFactory.getLogger(ImagePlayground.class);
17+
1118
private final BedrockRuntimeClient client;
1219

1320
@Autowired
@@ -17,8 +24,16 @@ public ImagePlayground(final BedrockRuntimeClient client) {
1724

1825
@PostMapping ("/foundation-models/model/image/stability.stable-diffusion-xl/invoke")
1926
public StableDiffusion.Response invoke(@RequestBody StableDiffusion.Request body) {
27+
try {
2028

21-
return StableDiffusion.invoke(client, body.prompt(), body.stylePreset());
29+
return StableDiffusion.invoke(client, body.prompt(), body.stylePreset());
2230

31+
} catch (AccessDeniedException e) {
32+
logger.error("Access Denied: %s".formatted(e.getMessage()));
33+
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
34+
} catch (Exception e) {
35+
logger.error("Exception: %s".formatted(e.getMessage()));
36+
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
37+
}
2338
}
2439
}

backend/src/main/java/aws/community/examples/bedrock/controller/TextPlayground.java

+31-14
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,26 @@
22

33
import aws.community.examples.bedrock.aimodels.Claude;
44
import aws.community.examples.bedrock.aimodels.Jurassic2;
5+
import org.slf4j.Logger;
6+
import org.slf4j.LoggerFactory;
57
import org.springframework.beans.factory.annotation.Autowired;
8+
import org.springframework.http.HttpStatus;
69
import org.springframework.web.bind.annotation.PathVariable;
710
import org.springframework.web.bind.annotation.PostMapping;
811
import org.springframework.web.bind.annotation.RequestBody;
912
import org.springframework.web.bind.annotation.RestController;
13+
import org.springframework.web.server.ResponseStatusException;
1014
import software.amazon.awssdk.services.bedrockruntime.BedrockRuntimeClient;
15+
import software.amazon.awssdk.services.bedrockruntime.model.AccessDeniedException;
1116

1217
import static aws.community.examples.bedrock.aimodels.LLM.Request;
1318
import static aws.community.examples.bedrock.aimodels.LLM.Response;
1419

1520
@RestController
1621
public class TextPlayground {
1722

23+
private static final Logger logger = LoggerFactory.getLogger(TextPlayground.class);
24+
1825
private final BedrockRuntimeClient client;
1926

2027
@Autowired
@@ -24,19 +31,29 @@ public TextPlayground(final BedrockRuntimeClient client) {
2431

2532
@PostMapping("/foundation-models/model/text/{modelId}/invoke")
2633
public Response invoke(@PathVariable String modelId, @RequestBody Request request) {
27-
return switch (modelId) {
28-
29-
case Claude.MODEL_ID -> {
30-
String completion = Claude.invoke(client, request.prompt(), request.temperature(), request.maxTokens());
31-
yield new Response(completion);
32-
}
33-
34-
case Jurassic2.MODEL_ID -> {
35-
String completion = Jurassic2.invoke(client, request.prompt(), request.temperature(), request.maxTokens());
36-
yield new Response(completion);
37-
}
38-
39-
default -> throw new IllegalArgumentException("Unsupported model ID");
40-
};
34+
try {
35+
36+
return switch (modelId) {
37+
38+
case Claude.MODEL_ID -> {
39+
String completion = Claude.invoke(client, request.prompt(), request.temperature(), request.maxTokens());
40+
yield new Response(completion);
41+
}
42+
43+
case Jurassic2.MODEL_ID -> {
44+
String completion = Jurassic2.invoke(client, request.prompt(), request.temperature(), request.maxTokens());
45+
yield new Response(completion);
46+
}
47+
48+
default -> throw new IllegalArgumentException("Unsupported model ID");
49+
};
50+
51+
} catch (AccessDeniedException e) {
52+
logger.error("Access Denied: %s".formatted(e.getMessage()));
53+
throw new ResponseStatusException(HttpStatus.FORBIDDEN);
54+
} catch (Exception e) {
55+
logger.error("Exception: %s".formatted(e.getMessage()));
56+
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR);
57+
}
4158
}
4259
}

resources/bedrock-access-policy.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"Version": "2012-10-17",
3+
"Statement": [
4+
{
5+
"Effect": "Allow",
6+
"Action": "bedrock:ListFoundationModels",
7+
"Resource": "*"
8+
},
9+
{
10+
"Effect": "Allow",
11+
"Action": [
12+
"bedrock:GetFoundationModel",
13+
"bedrock:InvokeModel"
14+
],
15+
"Resource": "*"
16+
}
17+
]
18+
}
File renamed without changes.

0 commit comments

Comments
 (0)