AWS Amplify + AI Kitを試してみた

# 前提

前回の記事で次回やりたいと言っていた AI Kit やってみます！
> AI Kitやりたい https://docs.amplify.aws/react/ai/

次の記載を見て実際いくらになるんだろ〜と思いながら挑戦してます。 :sweat: 
今回はちょっと試すくらいなので大した金額にはならないはず...

:::note info
大規模言語モデル (LLM) で推論を実行するとコストがかかる場合があります。Amazon Bedrock はサーバーレスサービスなので、使用した分だけ支払いますが、生成 AI アプリケーションの構築に関連するコストに注意してください。詳細については、Bedrock の料金をご覧ください。
:::

# できたこと

こんな感じでAIとやりとりできる画面を作ることができました :smiley: 
途中バージョンの関係で手こずってしまったんですがそれ以外は大変お手軽でした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/e4dbbcea-3e7e-ef82-9a29-b6e19cb83bd7.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/6e066219-d8ec-6431-8349-dd585e92eb84.png)

# 手順

sandbox(本番に影響出ないようにするやつ)を起動します

```sh
npx ampx sandbox
```

## Set up AI

### 権限周りの設定

- IAMへの権限付与
AmazonBedrockFullAccessを付与しました

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/7c4cc623-ec71-4bb7-5984-b5a551e67ada.png)


- 使用するモデルへのアクセス付与

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/bdec8866-e982-d61f-c0ef-a578cd0cf281.png)


前回の続きからなので... Build your AI backendあたりから実施します

### Build your AI backend

バージョンの関係でしょうか、[Claude 3.5 Haiku](https://www.anthropic.com/claude/haiku)がありません :thinking: 
Claude 3.5 Sonnetの方がnewっぽいしなくなったのだろうか... 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/b1518815-9fc6-33e1-c623-4b5098999be6.png)
Claude 3.5 Sonnetで進めます。

次のコードを追加しました。
```diff_typescript:amplify/data/resource.ts
const schema = a.schema({
  // 省略---
+ chat: a.conversation({
+   aiModel: a.ai.model('Claude 3.5 Sonnet'),
+   systemPrompt: 'You are a helpful assistant',
+ }),
    
+ generateRecipe: a.generation({
+   aiModel: a.ai.model('Claude 3.5 Sonnet'),
+   systemPrompt: 'You are a helpful assistant that generates recipes.',
+ })
+ .arguments({
+   description: a.string(),
+ })
+ .returns(
+   a.customType({
+     name: a.string(),
+     ingredients: a.string().array(),
+     instructions: a.string(),
+   })
+ )
+ .authorization((allow) => allow.authenticated()),
});
  // 省略---
```

### Connect your frontend

必要なパッケージを追加します。
```
npm add aws-amplify @aws-amplify/ui-react @aws-amplify/ui-react-ai
```

Configure the librariesはすでにやってあったのでGenerate the data clientからやります。

次のファイルを作成します。
```typescript:src/client.ts
import { generateClient } from "aws-amplify/api";
import { Schema } from "../amplify/data/resource";
import { createAIHooks } from "@aws-amplify/ui-react-ai";

export const client = generateClient<Schema>({ authMode: "userPool" });
export const { useAIConversation, useAIGeneration } = createAIHooks(client);
```

次のファイルを編集します。 
```tsx:src/App.tsx
import * as React from 'react';
import { Flex, TextAreaField, Loader, Text, View, Button } from "@aws-amplify/ui-react"
import { useAIGeneration } from "./client";

export default function App() {
  const [description, setDescription] = React.useState("");
  const [{ data, isLoading }, generateRecipe] =
    useAIGeneration("generateRecipe");

  const handleClick = async () => {
    generateRecipe({ description });
  };

  return (
    <Flex direction="column">
      <Flex direction="row">
        <TextAreaField
          autoResize
          value={description}
          onChange={(e) => setDescription(e.target.value)}
          label="Description"
        />
        <Button onClick={handleClick}>Generate recipe</Button>
      </Flex>
      {isLoading ? (
        <Loader variation="linear" />
      ) : (
        <>
          <Text fontWeight="bold">{data?.name}</Text>
          <View as="ul">
            {data?.ingredients?.map((ingredient) => (
              <View as="li" key={ingredient}>
                {ingredient}
              </View>
            ))}
          </View>
          <Text>{data?.instructions}</Text>
        </>
      )}
    </Flex>
  );
}
```

まだひと手順残ってますが、ここまでで何ができたのかが気になるので起動してみます。
認証を通り...
何か起動してますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/c9fd9174-2f32-8a22-83c6-3f9fa00fafbc.png)

入力してボタン押してみたけどうんともすんとも...

とりあえず次の手順もやってみます。

```tsx:src/App.tsx
import { Authenticator } from "@aws-amplify/ui-react";
import { AIConversation } from '@aws-amplify/ui-react-ai';
import { useAIConversation } from './client';

export default function App() {
  const [
    {
      data: { messages },
      isLoading,
    },
    handleSendMessage,
  ] = useAIConversation('chat');
  // 'chat' is based on the key for the conversation route in your schema.

  return (
    <Authenticator>
      <AIConversation
        messages={messages}
        isLoading={isLoading}
        handleSendMessage={handleSendMessage}
      />
    </Authenticator>
  );
}
```

こちらも起動して確認してみたんですが次の項目がミュウぐらいの感じで消えてきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/d4889de6-0ae7-15ac-251e-d7152097679a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/8f4930e2-4ee8-bc21-c3cc-c04dea4d4382.png)

ブラウザではこんなエラーが
```
chunk-PJEEZAML.js?v=1b728f99:9129 Uncaught TypeError: conversation.onStreamEvent is not a function
```

うーん？ :thinking: 


```diff_tsx:src/App.tsx
import { Authenticator } from "@aws-amplify/ui-react";
import { AIConversation } from '@aws-amplify/ui-react-ai';
- import '@aws-amplify/ui-react/styles.css';
- import { useAIConversation } from './client';

export default function App() {
-  const [
-    {
-      data: { messages },
-      isLoading,
-    },
-    handleSendMessage,
-  ] = useAIConversation('chat');
-  // 'chat' is based on the key for the conversation route in your schema.

export default function App() {
  return (
    <Authenticator>
      <AIConversation
        messages={[]}
        handleSendMessage={() => {}}
-       messages={messages}
-       isLoading={isLoading}
-       handleSendMessage={handleSendMessage}
      />
    </Authenticator>
  );
}
```

こうしたら消えなくなたったので @aws-amplify/ui-react-ai の問題かもしれません :thinking: 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/ef3f7356-7321-0730-6a42-fd1a60c42136.png)


## 何やら解決してくれそうなページを発見

https://docs.amplify.aws/javascript/ai/conversation/connect-your-frontend/

下記参考にできそうです

```ts:
import { generateClient } from 'aws-amplify/data';
import { type Schema } from '../amplify/data/resource'

const client = generateClient<Schema>();

// 1. Create a conversation
const { data: chat, errors } = await client.conversations.chat.create();

// 2. Subscribe to assistant responses
const subscription = chat.onStreamEvent({
  next: (event) => {
    // handle assistant response stream events
    console.log(event);
  },
  error: (error) => {
    // handle errors
    console.error(error);
  },
});

// 3. Send a message to the conversation
const { data: message, errors } = await chat.sendMessage('Hello, world!');
```

参考にしてちょっと直してみました :arrow_down: 

```tsx:src/App.tsx
import { Authenticator } from "@aws-amplify/ui-react";
import { AIConversation, ConversationMessage, SendMesageParameters } from '@aws-amplify/ui-react-ai';
import '@aws-amplify/ui-react/styles.css';
import { generateClient } from "aws-amplify/api";
import React from "react";
import { Schema } from "../amplify/data/resource";


export default function App() {
  const client = generateClient<Schema>({ authMode: "userPool" });
  const [chat, setChat] = React.useState<any>(null);
  const [messages, setMessages] = React.useState<ConversationMessage[]>([]);
  const [isLoading, setIsLoading] = React.useState(true);

  const handleSendMessage = async (input: SendMesageParameters) => {
    try {
      const response = await chat?.sendMessage(input);
      if (response?.data) {
        setMessages([...messages, response.data]);
      }
    } catch (error) {
      console.error(error);
    }
  };


  const createChat = async () => {
    try {
      const { data } = await client.conversations.chat.create();
      setChat(data);
    } catch (error) {
      console.error(error);
    }
  };

  const fetchMessages = async () => {
    try {
      const response = await chat?.listMessages();
      if (response?.data){
        setMessages(response.data);
      }
      setIsLoading(false);
    } catch (error) {
      console.error(error);
    }
  };

  React.useEffect(() => {
    createChat();
    fetchMessages();
  }, []);

  return (
    <Authenticator>
      <AIConversation
        messages={messages}
        isLoading={isLoading}
        handleSendMessage={handleSendMessage}
      />
    </Authenticator>
  );
}
```
(SendMesageParameters?)

メッセージが入れてボタン押せるようになりました、でもAIから返答が出ません...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/7dcea4dc-7b70-e25f-fae4-86a77de48d29.png)

### やはりバージョンの問題？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/76997711-0238-1859-5cc1-64a43de0139b.png)

ドキュメントやサンプルにあるこれがエラーになるんですよね...
https://github.com/aws-samples/amplify-ai-examples/tree/main をクローンして同じ問題が起こらなければバージョンの問題ということになる？ :thinking: 

package.jsonの記述合わせてnode_modules、package-lock.json消して再インストールしたけど解決しなかったのでコマンドでバージョン確認します

```
npm list --depth=0
```

ちょいちょいズレてるな... node_modules消してinstallしなおしてもこのままなんですよね

```:こちらのバージョン
├── @aws-amplify/backend-cli@1.4.2
├── @aws-amplify/backend@1.8.0
├── @aws-amplify/ui-react-ai@1.1.0
├── @aws-amplify/ui-react@6.7.1
├── @types/node@20.17.0
├── @types/react-dom@18.3.1
├── @types/react@18.3.12
├── @vitejs/plugin-react@4.3.3
├── aws-amplify@6.10.2
├── aws-cdk-lib@2.172.0
├── aws-cdk@2.172.0
├── constructs@10.4.2
├── esbuild@0.20.2
├── eslint@8.57.1
├── react-dom@18.3.1
├── react@18.3.1
├── tsx@4.19.1
├── typescript@5.6.3
└── vite@5.4.10
```


```:amplify-ai-examplesのバージョン
├── @aws-amplify/backend-cli@1.4.2
├── @aws-amplify/backend@1.7.0
├── @aws-amplify/ui-react-ai@1.0.0
├── @aws-amplify/ui-react@6.6.0
├── @types/node@20.17.6
├── @types/react-dom@18.3.1
├── @types/react@18.3.12
├── aws-amplify@6.8.2
├── aws-cdk-lib@2.167.2
├── aws-cdk@2.167.2
├── constructs@10.4.2
├── esbuild@0.24.0
├── eslint-config-next@15.0.2
├── eslint@8.57.1
├── next@14.2.15
├── react-dom@18.3.1
├── react-icons@5.3.0
├── react-markdown@9.0.1
├── react@18.3.1
├── tsx@4.19.2
└── typescript@5.6.3
```

キャッシュクリアして再インストールします。

```
npm cache clean --force
rm -rf node_modules package-lock.json
npm install
```

エラーでなくなりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/1876f444-099f-d790-9c3b-a8d6eff042e6.png)

もしかして一番最初の記述でいけるようになったんじゃないか？
ソース戻し...
```tsx:src/App.tsx
import { Button, Flex, Loader, Text, TextAreaField, View } from "@aws-amplify/ui-react";
import * as React from 'react';
import { useAIGeneration } from "./client";

export default function App() {
  const [description, setDescription] = React.useState("");
  const [{ data, isLoading }, generateRecipe] =
    useAIGeneration("generateRecipe");

  const handleClick = async () => {
    generateRecipe({ description });
  };

  return (
    <Flex direction="column">
      <Flex direction="row">
        <TextAreaField
          autoResize
          value={description}
          onChange={(e) => setDescription(e.target.value)}
          label="Description"
        />
        <Button onClick={handleClick}>Generate recipe</Button>
      </Flex>
      {isLoading ? (
        <Loader variation="linear" />
      ) : (
        <>
          <Text fontWeight="bold">{data?.name}</Text>
          <View as="ul">
            {data?.ingredients?.map((ingredient) => (
              <View as="li" key={ingredient}>
                {ingredient}
              </View>
            ))}
          </View>
          <Text>{data?.instructions}</Text>
        </>
      )}
    </Flex>
  );
}
```

## う、動くようになった!!!

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/ce1c8875-96c8-2856-8276-c0593b45ef4a.png)

人魚はサバに置き換えられてますね〜
急がば回れだったか〜... 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/cca7a0eb-a556-1852-b364-ad9a6ae8d798.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/18bad62e-d992-79dc-bb34-119f9b4da250.png)

ちゃんと別の文字を入れると別のレシピになりますね！

### 会話の方も

```tsx:src/App.tsx
import { Authenticator } from "@aws-amplify/ui-react";
import { AIConversation } from '@aws-amplify/ui-react-ai';
import { useAIConversation } from './client';

export default function App() {
  const [
    {
      data: { messages },
      isLoading,
    },
    handleSendMessage,
  ] = useAIConversation('chat');
  // 'chat' is based on the key for the conversation route in your schema.

  return (
    <Authenticator>
      <AIConversation
        messages={messages}
        isLoading={isLoading}
        handleSendMessage={handleSendMessage}
      />
    </Authenticator>
  );
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/d6ab512c-c1c2-cccd-303c-b7ee8fe15f26.png)

動くようになりました〜 ...？
き、消えた... えぇ〜？ :thinking: 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/97bfc8c7-6f0f-06ff-780d-8da2ca20c542.png)

バージョンがサンプルとズレてるとこがあったので修正します

```こちらのバージョン
├── @aws-amplify/backend@1.8.0
├── @aws-amplify/ui-react@6.7.1
```

次のコマンドを実行します

```
npm install @aws-amplify/backend@1.7.0 @aws-amplify/ui-react@6.6.0
```

しかし... うまくいかず...

## サンプルのコード起動してみよう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/0c7e6fe1-76e6-1d9c-a927-cd600a7995ea.png)

あら、なんか左にいっぱい出てますね...

### 今までの問い合わせたち！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/a41874a4-c6ca-5abd-b7cf-fdbd1bd88939.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/27bad04a-52f2-c0c2-bd45-93000cd6d5bc.png)

なるほどなー... 別の連絡手段を試す、大事ですね  :smile: 

## せっかくなので

systemPromptを編集してこれの振る舞いを変更したいです。
ちょっと難しい要求すぎるかな...

```diff_ts
import { type ClientSchema, a, defineData } from "@aws-amplify/backend";

const schema = a.schema({
  chat: a.conversation({
    aiModel: a.ai.model("Claude 3.5 Sonnet"),
+   systemPrompt: `あなたは5・7・5でのみ会話ができる俳句のAIです。`,
-   systemPrompt: `You are a helpful assistant`,
  })
    .authorization((allow) => allow.owner()),

  chatNamer: a
    .generation({
      aiModel: a.ai.model("Claude 3 Sonnet"),
      systemPrompt: `You are a helpful assistant that writes descriptive names for conversations. Names should be 2-10 words long`,
    })
    .arguments({
      content: a.string(),
    })
    .returns(
      a.customType({
        name: a.string(),
      })
    )
    .authorization((allow) => [allow.authenticated()]),
});

export type Schema = ClientSchema<typeof schema>;

export const data = defineData({
  schema,
  authorizationModes: {
    defaultAuthorizationMode: "userPool",
  },
});

```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/756c1768-c290-b0de-d774-aaf5bf0423ab.png)

アレ...？

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/888a374b-c5b6-b335-7b66-1b19b7ba0734.png)

ページリロードしたら表示できました。
@aws-amplify/ui-react-aiに問題があるのかな...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/09fbd080-cb2e-3f8e-070b-19fc3683d3e1.png)

全然575じゃない

```
systemPrompt: `今から投げられる質問に Yes か No で答えてください。`,
```

ちょっと変えてみました。思ったようにはならなかった...

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/d2a53b24-dc6d-4748-daf9-bc8d290a05ba.png)

会話の方アレンジするの難しいのかもですね

### Generatorをアレンジ

細かい命名とかはちょっと直すのスキップします

```diff_typescript:amplify/data/resource.ts
  generateRecipe: a.generation({
    aiModel: a.ai.model('Claude 3.5 Haiku'),
-   systemPrompt: 'You are a helpful assistant that generates recipes.',
+   systemPrompt: 'You are a useful assistant in creating travel plans.',
  })
```

<details><summary>...あれ、うまくいってるgenerateRecipeの方Haikuだな... :thinking:</summary>
chatの方Sonnetに変えたのが悪かったのかな...
と思って変えたけど普通にさっきのエラー出ました。
</details>

ちょっとボタンの見た目だけ変えときました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/6e066219-d8ec-6431-8349-dd585e92eb84.png)

おぉ〜計画立ててくれてますね。プロンプト戻して同じこと伝えたらどうなるんだろう？

あんま影響ないみたいですね :thinking: 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/ba98bd3f-64e0-c1f0-9b63-dd538fc0220b.png)

単語(台湾)だけを与えた場合は謎のレシピを出してくれます、何のレシピだろうこれ...
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2952155/1e3f31e2-7b08-6216-4c2f-510bce0fdf1b.png)

systemPrompt:はAIChatと話すときの最初の会話みたいな感じですかね。
普段AIとのチャットでも次の会話で全然別のこと話すと、話題変わってくれるみたいな感じで、初めに与えるsystemPromptにそこまでの影響力はないかも？

# 最後に

ここまで読んでくださってありがとうございます！
Qiitaカレンダーは新しいもの試すいい機会です、まだ参加されたことがない方もぜひやってみてください :thumbsup: