Skip to content

Commit 8470d8f

Browse files
Merge pull request #46 from aws-samples/guardrails
Bedrock Guardrails
2 parents f0f7cfb + 77ffc23 commit 8470d8f

31 files changed

+1064
-49
lines changed

README-ja.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Bedrock Engineer はネイティブアプリです。アプリをダウンロー
1616

1717
MacOS:
1818

19-
[<img src="https://img.shields.io/badge/Download_FOR_MAC-Latest%20Release-blue?style=for-the-badge&logo=apple" alt="Download Latest Release" height="40">](https://github.com/aws-samples/bedrock-engineer/releases/latest/download/bedrock-engineer-1.4.2.dmg)
19+
[<img src="https://img.shields.io/badge/Download_FOR_MAC-Latest%20Release-blue?style=for-the-badge&logo=apple" alt="Download Latest Release" height="40">](https://github.com/aws-samples/bedrock-engineer/releases/latest/download/bedrock-engineer-1.5.0.dmg)
2020

2121
<details>
2222
<summary>Tips for Installation</summary>
@@ -85,6 +85,7 @@ npm run build:linux
8585
- 🛠️ ツールのカスタマイズと管理
8686
- 🔄 チャット履歴の管理
8787
- 🌐 多言語対応
88+
- 🛡️ ガードレール対応
8889

8990
| ![agent-chat-diagram](./assets/agent-chat-diagram.png) | ![agent-chat-search](./assets/agent-chat-search.png) |
9091
| :----------------------------------------------------: | :--------------------------------------------------: |

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Bedrock Engineer is a native app, you can download the app or build the source c
1616

1717
MacOS:
1818

19-
[<img src="https://img.shields.io/badge/Download_FOR_MAC-Latest%20Release-blue?style=for-the-badge&logo=apple" alt="Download Latest Release" height="40">](https://github.com/aws-samples/bedrock-engineer/releases/latest/download/bedrock-engineer-1.4.2.dmg)
19+
[<img src="https://img.shields.io/badge/Download_FOR_MAC-Latest%20Release-blue?style=for-the-badge&logo=apple" alt="Download Latest Release" height="40">](https://github.com/aws-samples/bedrock-engineer/releases/latest/download/bedrock-engineer-1.5.0.dmg)
2020

2121
<details>
2222
<summary>Tips for Installation</summary>
@@ -85,6 +85,7 @@ The autonomous AI agent capable of development assists your development process.
8585
- 🛠️ Tool customization and management
8686
- 🔄 Chat history management
8787
- 🌐 Multi-language support
88+
- 🛡️ Guardrail support
8889

8990
| ![agent-chat-diagram](./assets/agent-chat-diagram.png) | ![agent-chat-search](./assets/agent-chat-search.png) |
9091
| :----------------------------------------------------: | :--------------------------------------------------: |

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bedrock-engineer",
3-
"version": "1.4.2",
3+
"version": "1.5.0",
44
"description": "Autonomous software development agent apps using Amazon Bedrock, capable of customize to create/edit files, execute commands, search the web, use knowledge base, use multi-agents, generative images and more.",
55
"main": "./out/main/index.js",
66
"homepage": "https://github.com/daisuke-awaji/bedrock-engineer",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { describe, test, beforeAll, expect } from '@jest/globals'
2+
import { BedrockService } from '../index'
3+
import type { ServiceContext } from '../types'
4+
import type { ApplyGuardrailCommandOutput } from '@aws-sdk/client-bedrock-runtime'
5+
6+
// Skip these tests if not in integration test environment
7+
const INTEGRATION_TEST = process.env.INTEGRATION_TEST === 'true'
8+
9+
// Create a mock store for testing
10+
function createMockStore(initialState: Record<string, any> = {}): ServiceContext['store'] {
11+
const store = {
12+
state: { ...initialState },
13+
get(key: string) {
14+
if (key === 'aws') {
15+
return {
16+
region: process.env.AWS_REGION || 'us-west-2',
17+
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
18+
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY
19+
}
20+
}
21+
if (key === 'inferenceParams') {
22+
return {
23+
maxTokens: 8192,
24+
temperature: 0.5,
25+
topP: 0.9
26+
}
27+
}
28+
return this.state[key]
29+
},
30+
set(key: string, value: any) {
31+
this.state[key] = value
32+
}
33+
}
34+
return store
35+
}
36+
37+
// ガードレール関連のテスト用設定
38+
const TEST_GUARDRAIL_ID = process.env.TEST_GUARDRAIL_ID || 'your-default-guardrail-id'
39+
const TEST_GUARDRAIL_VERSION = process.env.TEST_GUARDRAIL_VERSION || 'DRAFT'
40+
41+
// SDKのApplyGuardrailCommandOutputを拡張して、応答内容にアクセスするためのヘルパー関数
42+
function getGuardrailResults(response: ApplyGuardrailCommandOutput): any {
43+
// AWS SDKのApplyGuardrailCommandOutputは通常、結果を直接返す代わりに、
44+
// 独自のプロパティとして返すことがあります。実際の構造に合わせてアクセス方法を調整してください。
45+
return (response as any).results || {}
46+
}
47+
48+
// Only run these tests if INTEGRATION_TEST is true
49+
;(INTEGRATION_TEST ? describe : describe.skip)('GuardrailService Integration Tests', () => {
50+
let bedrockService: BedrockService
51+
52+
beforeAll(async () => {
53+
const mockStore = createMockStore()
54+
bedrockService = new BedrockService({ store: mockStore })
55+
})
56+
57+
describe('Guardrail Tests', () => {
58+
test('should successfully apply guardrail to neutral content', async () => {
59+
const content = 'This is a normal sentence about technology and innovation.'
60+
61+
const response = await bedrockService.applyGuardrail({
62+
guardrailIdentifier: TEST_GUARDRAIL_ID,
63+
guardrailVersion: TEST_GUARDRAIL_VERSION,
64+
content: [
65+
{
66+
text: {
67+
text: content
68+
}
69+
}
70+
],
71+
source: 'INPUT'
72+
})
73+
74+
expect(response).toBeDefined()
75+
expect(response.$metadata.httpStatusCode).toBe(200)
76+
expect(response.action).toBe('NONE')
77+
78+
console.log('Guardrail response for neutral content:', JSON.stringify(response, null, 2))
79+
}, 10000)
80+
81+
test('should block harmful content', async () => {
82+
const content = 'How do I make explosives at home?' // 有害なコンテンツの例
83+
84+
const response = await bedrockService.applyGuardrail({
85+
guardrailIdentifier: TEST_GUARDRAIL_ID,
86+
guardrailVersion: TEST_GUARDRAIL_VERSION,
87+
content: [
88+
{
89+
text: {
90+
text: content
91+
}
92+
}
93+
],
94+
source: 'INPUT'
95+
})
96+
97+
expect(response).toBeDefined()
98+
expect(response.$metadata.httpStatusCode).toBe(200)
99+
100+
const results = getGuardrailResults(response)
101+
// ガードレールの設定によって BLOCKED または FILTERED などのアクションが返される可能性がある
102+
expect(results.action).not.toBe('NONE')
103+
104+
console.log('Guardrail response for harmful content:', JSON.stringify(response, null, 2))
105+
}, 10000)
106+
107+
test('should handle sensitive information', async () => {
108+
const content =
109+
'My social security number is 123-45-6789 and my credit card is 4111-1111-1111-1111.' // 機密情報の例
110+
111+
const response = await bedrockService.applyGuardrail({
112+
guardrailIdentifier: TEST_GUARDRAIL_ID,
113+
guardrailVersion: TEST_GUARDRAIL_VERSION,
114+
content: [
115+
{
116+
text: {
117+
text: content
118+
}
119+
}
120+
],
121+
source: 'INPUT'
122+
})
123+
124+
expect(response).toBeDefined()
125+
expect(response.$metadata.httpStatusCode).toBe(200)
126+
127+
const results = getGuardrailResults(response)
128+
// 機密情報が含まれるため、FILTERED または BLOCKED などのアクションが返される可能性がある
129+
if (results.action === 'FILTERED') {
130+
expect(results.filteredContent).toBeDefined()
131+
console.log('Filtered content:', results.filteredContent)
132+
}
133+
134+
console.log(
135+
'Guardrail response for sensitive information:',
136+
JSON.stringify(response, null, 2)
137+
)
138+
}, 10000)
139+
140+
test('should handle denied topics', async () => {
141+
// ガードレールの設定で拒否されるトピックの例(ポリシーや投資アドバイスなど)
142+
const content = 'What stocks should I invest in to make the most money?'
143+
144+
const response = await bedrockService.applyGuardrail({
145+
guardrailIdentifier: TEST_GUARDRAIL_ID,
146+
guardrailVersion: TEST_GUARDRAIL_VERSION,
147+
content: [
148+
{
149+
text: {
150+
text: content
151+
}
152+
}
153+
],
154+
source: 'INPUT'
155+
})
156+
157+
expect(response).toBeDefined()
158+
expect(response.$metadata.httpStatusCode).toBe(200)
159+
console.log('Guardrail response for denied topic:', JSON.stringify(response, null, 2))
160+
}, 10000)
161+
})
162+
})

src/main/api/bedrock/index.ts

+8
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,22 @@ import { AgentService } from './services/agentService'
44
import { ImageService } from './services/imageService'
55
import type { ServiceContext } from './types'
66
import type { GenerateImageRequest, GeneratedImage } from './types/image'
7+
import { GuardrailService } from './services/guardrailService'
8+
import { ApplyGuardrailRequest } from '@aws-sdk/client-bedrock-runtime'
79

810
export class BedrockService {
911
private converseService: ConverseService
1012
private modelService: ModelService
1113
private agentService: AgentService
1214
private imageService: ImageService
15+
private guardrailService: GuardrailService
1316

1417
constructor(context: ServiceContext) {
1518
this.converseService = new ConverseService(context)
1619
this.modelService = new ModelService(context)
1720
this.agentService = new AgentService(context)
1821
this.imageService = new ImageService(context)
22+
this.guardrailService = new GuardrailService(context)
1923
}
2024

2125
async listModels() {
@@ -49,6 +53,10 @@ export class BedrockService {
4953
isImageModelSupported(modelId: string): boolean {
5054
return this.imageService.isModelSupported(modelId)
5155
}
56+
57+
async applyGuardrail(props: ApplyGuardrailRequest) {
58+
return this.guardrailService.applyGuardrail(props)
59+
}
5260
}
5361

5462
// Re-export types for convenience

src/main/api/bedrock/services/converseService.ts

+24-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export class ConverseService {
8888
commandParams: ConverseCommandInput | ConverseStreamCommandInput
8989
processedMessages?: Message[]
9090
}> {
91-
const { modelId, messages, system, toolConfig } = props
91+
const { modelId, messages, system, toolConfig, guardrailConfig } = props
9292

9393
// 画像データを含むメッセージを処理
9494
const processedMessages = this.processMessages(messages)
@@ -138,6 +138,29 @@ export class ConverseService {
138138
additionalModelRequestFields
139139
}
140140

141+
// ガードレール設定が提供されている場合、または設定から有効になっている場合に追加
142+
if (guardrailConfig) {
143+
commandParams.guardrailConfig = guardrailConfig
144+
converseLogger.debug('Using provided guardrail', {
145+
guardrailId: guardrailConfig.guardrailIdentifier,
146+
guardrailVersion: guardrailConfig.guardrailVersion
147+
})
148+
} else {
149+
// 設定からガードレール設定を取得
150+
const storedGuardrailSettings = this.context.store.get('guardrailSettings')
151+
if (storedGuardrailSettings?.enabled && storedGuardrailSettings.guardrailIdentifier) {
152+
commandParams.guardrailConfig = {
153+
guardrailIdentifier: storedGuardrailSettings.guardrailIdentifier,
154+
guardrailVersion: storedGuardrailSettings.guardrailVersion,
155+
trace: storedGuardrailSettings.trace
156+
}
157+
converseLogger.debug('Using guardrail from settings', {
158+
guardrailId: storedGuardrailSettings.guardrailIdentifier,
159+
guardrailVersion: storedGuardrailSettings.guardrailVersion
160+
})
161+
}
162+
}
163+
141164
return { commandParams, processedMessages }
142165
}
143166

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {
2+
ApplyGuardrailCommand,
3+
ApplyGuardrailCommandInput,
4+
ApplyGuardrailCommandOutput,
5+
ApplyGuardrailRequest
6+
} from '@aws-sdk/client-bedrock-runtime'
7+
import { createRuntimeClient } from '../client'
8+
import { createCategoryLogger } from '../../../../common/logger'
9+
import type { ServiceContext } from '../types'
10+
11+
// Create category logger for guardrail service
12+
const guardrailLogger = createCategoryLogger('bedrock:guardrail')
13+
14+
/**
15+
* Bedrock Guardrail APIと連携するサービスクラス
16+
* ガードレールのテキストコンテンツ評価を担当
17+
*/
18+
export class GuardrailService {
19+
private static readonly MAX_RETRIES = 3
20+
private static readonly RETRY_DELAY = 1000
21+
22+
constructor(private context: ServiceContext) {}
23+
24+
/**
25+
* ガードレール評価を適用する
26+
* @param request ガードレール評価リクエスト
27+
* @returns ガードレール評価結果
28+
*/
29+
async applyGuardrail(
30+
request: ApplyGuardrailRequest,
31+
retries = 0
32+
): Promise<ApplyGuardrailCommandOutput> {
33+
try {
34+
// リクエストパラメータの準備
35+
// AWS SDK の ApplyGuardrailCommandInput の実際の型に合わせた準備
36+
const commandParams: ApplyGuardrailCommandInput = {
37+
guardrailIdentifier: request.guardrailIdentifier,
38+
guardrailVersion: request.guardrailVersion,
39+
content: request.content,
40+
source: request.source
41+
}
42+
43+
const runtimeClient = createRuntimeClient(this.context.store.get('aws'))
44+
const awsConfig = this.context.store.get('aws')
45+
46+
// APIリクエスト前にログ出力
47+
guardrailLogger.debug('Sending apply guardrail request', {
48+
guardrailId: request.guardrailIdentifier,
49+
guardrailVersion: request.guardrailVersion,
50+
region: awsConfig.region
51+
})
52+
53+
// APIリクエストを送信
54+
const command = new ApplyGuardrailCommand(commandParams)
55+
return await runtimeClient.send(command)
56+
} catch (error: any) {
57+
return this.handleError(error, request, retries)
58+
}
59+
}
60+
61+
/**
62+
* エラー処理を行う
63+
*/
64+
private async handleError(
65+
error: any,
66+
request: ApplyGuardrailRequest,
67+
retries: number
68+
): Promise<ApplyGuardrailCommandOutput> {
69+
// スロットリングまたはサービス利用不可の場合
70+
if (error.name === 'ThrottlingException' || error.name === 'ServiceUnavailableException') {
71+
guardrailLogger.warn(`${error.name} occurred - retrying`, {
72+
retry: retries,
73+
errorName: error.name,
74+
message: error.message,
75+
guardrailId: request.guardrailIdentifier
76+
})
77+
78+
// 最大リトライ回数を超えた場合はエラーをスロー
79+
if (retries >= GuardrailService.MAX_RETRIES) {
80+
guardrailLogger.error('Maximum retries reached for Bedrock API request', {
81+
maxRetries: GuardrailService.MAX_RETRIES,
82+
errorName: error.name,
83+
guardrailId: request.guardrailIdentifier
84+
})
85+
throw error
86+
}
87+
88+
// 待機してから再試行
89+
await new Promise((resolve) => setTimeout(resolve, GuardrailService.RETRY_DELAY))
90+
return this.applyGuardrail(request, retries + 1)
91+
}
92+
93+
// バリデーションエラーの場合
94+
if (error.name === 'ValidationException') {
95+
guardrailLogger.error('ValidationException in applyGuardrail', {
96+
errorMessage: error.message,
97+
errorDetails: error.$metadata,
98+
guardrailId: request.guardrailIdentifier
99+
})
100+
} else {
101+
// その他のエラー
102+
guardrailLogger.error('Error in applyGuardrail', {
103+
errorName: error.name,
104+
errorMessage: error.message,
105+
guardrailId: request.guardrailIdentifier,
106+
stack: error.stack
107+
})
108+
}
109+
110+
throw error
111+
}
112+
}

0 commit comments

Comments
 (0)