GenUに、ただオウム返しするだけのチャット機能を追加してみる

# 当記事について

下記画面のような入力テキストをオウム返しするだけの「SimpleEchoチャット」と称した無駄機能をAWSの生成AIデモアプリである[GenU](https://github.com/aws-samples/generative-ai-use-cases-jp/tree/main)のフロントエンドとバックエンドに追加してみる、という記事です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2531623/2bdb3960-195c-48d8-9a2b-c19c4cde0e45.png)

オウム返しするだけなので機能自体に価値はありませんが、
- 極力オリジナルのGenUのソースに変更を加えずに
- 極力オリジナルのGenUの実装を利用しつつ
- オリジナルのGenUがサポートしていない方法での回答機能を追加してみる

にはどのような対応が必要か？ということを整理したいがために作成しました。
必要最小限の修正ながら、会話の履歴管理機能などGenUが元々実装している機能をそのまま利用しつつ独自実装の回答生成を行う機能追加を行っています。

# 変更内容（全容）

GitHubのプルリク差分画面で全体の変更内容を確認可能です。
[プルリクの差分内容](https://github.com/chs-k-kinoshita/generative-ai-use-cases-jp/pull/1/files)

オリジナルのGenUからforkした自分のリポジトリのdevelopブランチへのセルフプルリクです(念の為)

※ "predict.test.ts"ファイルはテストコードなので無視して下さい。

# バックエンド側対応

下記2つのファイルの対応のみです。

| ファイル | 概要 | 更新区分 | 
| ---- | ---- | ---- |
| cdk/lambda/utils/mycustomApi.ts | 回答生成処理を実装します | 追加 |
| cdk/lambda/utils/api.ts | フロントエンド側から指定されるmodelのtypeと、実装モジュールとの紐づけを行います | 変更 |

- packages/cdk/lambda/utils/mycustomApi.ts
  - predictとpredictStreamのLambdaハンドラからCallされる"invoke"と"invokeStream"関数を実装します
  - "invokeStream"ではServer-Sent events（SSE）でStreamingレスポンスを返す処理を実装します
  
```typescript:cdk/lambda/utils/mycustomApi
import {
  InvokeInterface,
  InvokeStreamInterface,
} from 'generative-ai-use-cases-jp';
import { streamingChunk } from './streamingChunk';

export type ApiInterface = {
  invoke: InvokeInterface;
  invokeStream: InvokeStreamInterface;
};

const mycustomApi: ApiInterface = {
  invoke: async (model, messages, id) => {
    console.log('mycustomApi invoke', model, messages, id);
    return `simple echo: ${messages[messages.length - 1].content}`;
  },
  invokeStream: async function* (model, messages, id) {
    console.log('mycustomApi invokeStream', model, messages, id);
    yield streamingChunk({ text: "streaming\n"});
    yield streamingChunk({ text: "simple echo\n"});
    yield streamingChunk({ text: messages[messages.length - 1].content});
  },
};

export default mycustomApi;
```

- packages/cdk/lambda/utils/api.ts
  - 前述のmycustomApi.tsとmodelのtype（今回の場合は"mycustom"）との紐づけを追加します
  - predictとpredictStreamから呼ばれるApi処理は、typeに何を指定されるかによって決定されます

```typescript:cdk/lambda/utils/api.ts
import bedrockApi from './bedrockApi';
import bedrockAgentApi from './bedrockAgentApi';
import bedrockKbApi from './bedrockKbApi';
import sagemakerApi from './sagemakerApi';
import mycustomApi from './mycustomApi';

const api = {
  bedrock: bedrockApi,
  bedrockAgent: bedrockAgentApi,
  bedrockKb: bedrockKbApi,
  sagemaker: sagemakerApi,
  mycustom: mycustomApi, // 追加！
};

export default api;
```

# フロントエンド側対応

下記４つのファイルの対応のみです。

| ファイル | 概要 | 更新区分 | 
| ---- | ---- | ---- |
| web/src/pages/SimpleEchoChatPage.tsx | 追加機能のPageコンポーネントです | 追加 |
| web/src/App.tsx | メニューに「SimpleEchoチャット」を追加します | 変更 |
| web/src/main.tsx | PageコンポーネントとURLのパスとのマッピングを追加します | 変更 |
| web/src/hooks/useModel.ts | modelIdとmodelのtypeとの紐づけを追加します | 変更 |

SimpleEchoChatPageコンポーネントは、オリジナルのChatPage.tsxをベースに極力シンプルな内容に削ぎ落としたものです。  
初期化処理(useEffect)で"simpleEcho"というidのモデルを固定で設定しています。

```typescript:pages/SimpleEchoChatPage.tsx
  // 抜粋
  const {
    loading,
    loadingMessages,
    isEmpty,
    messages,
    clear,
    postChat,
    setModelId,
  } = useChat(pathname);
  const { scrollableContainer, setFollowing } = useFollow();

  const title = 'SimpleEchoチャット';

  useEffect(() => {
    setModelId('simpleEcho');
  }, []);
```

useModel.tsでは、modelのIDとtypeのマッピングを追加しています。
ここで指定されたtypeの値は、バックエンド側での回答生成ロジック(util/XxxxApi.ts に実装するinvokeあるいはinvokeStream関数)と紐付きます。

```typescript:hooks/useModel.ts
// モデルオブジェクトの定義
const textModels = [
  ...bedrockModelConfigs.map(
    (model) =>
      ({
        modelId: model.modelId,
        type: 'bedrock',
        region: model.region,
      }) as Model
  ),
  ...endpointNames.map(
    (name) => ({ modelId: name, type: 'sagemaker' }) as Model
  ),
  // My Custom Model
  {
    modelId: 'simpleEcho',
    type: 'mycustom',
  }
];
```


# 最後に

今回追加したメニュー「SimpleEchoチャット」は、機能自体には意味はありませんがGenUがサポートしていない機能(※)を追加したい場合の最小限の変更箇所まとめ情報としては意味があるかと思っています。

※例として、GenUは「Flow チャット」としてBedrock Flowsを利用したタスクフローの実行機能を提供してくれていますが、同じくタスクフローを構成できるサービスである「AWS Step Functions」でフローを構成したくなることもあるかと思います。
このような場合、今回のサンプルでいうと"mycutom"タイプで"sfn-stateMachine"というidのmodelをフロント側から呼び出すようにして、"mycustomApi.ts"でStep Functionsのステートマシンの処理を同期で呼び出すような実装を行うことで、Step Functionsのフローで生成した出力結果をアシスタントの応答として返す、という処理も実現できるかと思われます。

以上、なにかのご参考になれば幸いです。