【Azure OpenAI Service利用事例】新卒の技術研修にGPT-4を導入して日報の内容から復習問題を作成してもらった

# Azure OpenAI Serviceを何に利用したか
技術研修中は新卒に毎日日報を書いてもらってます。
日報にはその日どんなことを学んだのか、気づきや分からなかったこと、何に何時間使ったのかを記載してもらってます。
そこで日報に書かれている内容を取得して、GPT-4に投げて学んだ内容から問題を作成してもらうようにしました。
目的は新卒が学んだ内容を理解できているかのチェックと、ちゃんと言語化して説明できるようにしてもらうため、また社内でのChatGPT・GPT-4の利用事例を増やすためです。


# 使うサービス・ツール
#### 1. Azure OpenAI Service

https://azure.microsoft.com/ja-jp/products/cognitive-services/openai-service

ChatGPT・GPT-4は通常のOpen AI社からのではなく、Azure OpenAI Seriviceのを利用します。
Open AI社のChatGPTは質問・リクエスト内容がAIの学習に利用されたりと、会社の機密情報などを入力することができないのですが、
Azure OpenAI Serviceを利用することで、セキュリティ・プライバシー的に安全に使うことができます。
弊社では社内で利用できる専用のリソースが作成されてますので、そちらを利用していきます。
利用可能なモデルはGPT-3.5やGPT-4、またcodexなどがあります。

こちらの記事で詳しく記載されてます。

https://zenn.dev/microsoft/articles/1a15305021cd01


#### 2. Power Automate

https://powerautomate.microsoft.com/ja-jp/

One Noteから新卒の日報情報の取得や、Azure OpenAI Serviceへのリクエスト、Teamsへの投稿など裏側の処理に利用します。
ローコードツールでMicrosoft社のサービスとの連携がとても簡単です。

#### 3. One Note

https://www.microsoft.com/ja-jp/microsoft-365/onenote/digital-note-taking-app

新卒が毎日日報を書いているツールです。
こちらに書かれている内容を取得して、GPT-4にリクエストを送ります。

#### 4. Microsoft Teams

https://www.microsoft.com/ja-jp/microsoft-teams/group-chat-software

GPT-4から出題された問題を投稿するために利用します。

# 実装
## 1. Power Automateで新しいフローの作成、スケジュール設定

平日の17時に実行されるようにします。
タイムゾーンを日本時間に設定することを忘れずに。

![スクリーンショット 2023-06-25 19.02.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/9d08bf73-dea8-2012-4cde-492e8a1f033e.png)


## 2. One Noteの日報データを取得

ここが若干大変です。

One Noteの日報は以下のようになってます。
新卒ごとにセクションを作成して、その中に毎日新しいページを作成します。
ページタイトルは日付「yyyy/MM/dd」になるようにしてもらってます。
![スクリーンショット 2023-06-25 19.14.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/ff5fd42c-69f2-8aa8-7248-1f5cd05b03a2.png)

やることとしては

>1. 現在時刻からページタイトルを作成
>2. ノートブック（新卒技術研修の日報）のセクション一覧を取得
>3. セクション一覧をループ処理して、各セクション内のページ一覧を取得
>4. さらにページ一覧をループしてページタイトルがマッチする、現在日時と同じのページを取得
>5. 「**4.**」　でマッチしたページのコンテンツを取得
>6. ページコンテンツ内のHTMLを行ごとに分割して配列データにする
>7. 変数配列に追加

上記1~7を順にやっていきます。

#### 2-1. 現在時刻からページタイトルを作成
現在時刻を取得して、9時間追加した時間を「yyyy/MM/dd」にフォーマットします。
`formatDateTime(addHours(body('現在の時刻'),9), 'yyyy/MM/dd')`
![スクリーンショット 2023-06-25 19.42.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/96b9a61b-6a4a-7c3b-5ca8-215197899d2d.png)

#### 2-2. ノートブック（新卒技術研修の日報）のセクション一覧を取得
以下のフローで一覧取得できます。
![スクリーンショット 2023-06-25 19.43.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/d55ac308-c765-9060-3d6e-4550f95d6541.png)

#### 2-3. セクション一覧をループ処理して、各セクション内のページ一覧を取得
Apply to eachを使って新卒一人一人のセクション情報を取得します。
ここはセクションの数（つまり新卒の数）だけループされることになります。
・`value`には「2-2」で取得したセクション一覧情報の**value**という値を選択
・`ノートブックのセクション`には**セクションキー**を選択
![スクリーンショット 2023-06-25 19.47.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/17c23c07-0720-f562-32cd-d0eb566f5ea2.png)

#### 2-4. さらにページ一覧をループしてページタイトルがマッチする、現在日時と同じのページを取得
Apply to eachの中でさらにApply to eachを行います。
ちょっと複雑なので、こまめにテストしながらどんなデータが取得できるか確認することをお勧めします。

ここはページの数だけループ処理が走ることになります。
そしてページタイトルが「**2-1**」で作成したページタイトルと等しいかをチェックしてます。

今回はApply to eachでページの数だけループしてますが、本来はDo untilを使った方が良いかなと思います。
※ Do untilというのは特定の条件になるまで何回もループしてくれます。今回だとページタイトルが見つかったらループを抜ければ良いので無駄な処理が省かれます。

・`以前の手順から出力を選択`には**セクション内ページの値オブジェクト**を選択
・`ページタイトル`には**セクション内ページの値オブジェクト ページタイトル**を選択
![スクリーンショット 2023-06-25 19.48.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/c72d0e93-f65f-8526-1350-b9602e2e43f8.png)


#### 2-5. 「2-4」　でマッチしたページのコンテンツを取得
・`ページID`には**セクション内ページの値オブジェクト　応答の一意識別子**を選択
![スクリーンショット 2023-06-25 19.59.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/dcb135c6-3347-810b-6708-4de8d8f8902e.png)

#### 2-6. ページコンテンツ内のHTMLを行ごとに分割して配列データにする
Apply to eachを実行する前の段階で改行コードを変数で定義しておいてください。
ちなみに改行コードは「\n」とかではなく、シンプルに値の中で1行改行するだけで大丈夫です。
![スクリーンショット 2023-06-25 20.04.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/3b50f2ef-22e1-d292-b7c1-7bd955096dc1.png)

splitを使うことで指定の文字列ごとに分割して配列に変換してくれます。
`split(outputs('ページ_コンテンツを取得_2')?['body'],variables('改行'))`
![スクリーンショット 2023-06-25 19.59.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/b5b0860f-e557-fe7e-49d7-a70dc1265a1d.png)

#### 2-7. 変数配列に追加
ここまで来たら一度変数配列に格納しておきます。
こちらもApply to eachを実行しているフローの前にまず、変数配列を定義しておいてください。
![スクリーンショット 2023-06-25 20.16.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/5452baff-418c-e901-f507-814317fa0dc3.png)


・nameにはセクション名なのでループ処理中の新卒の名前が入ります
・htmlには直前でHTMLを行ごとに分割された配列データを選択
![スクリーンショット 2023-06-25 19.59.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/62e04db1-a82d-9edd-23ac-8ba31d8af5ce.png)

一連の処理後このようなデータが入ってきます。
nameには新卒の名前が、htmlには配列でhtmlが1行ごとに文字列となって入っているのが確認できます。
html内のところはこのあとさらに整形していきます。
![スクリーンショット 2023-06-25 20.12.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/2e6000bf-8a40-bc4c-fa93-7f46f6dee635.png)


## 3. Azure OpenAI ServiceのGPT-4にリクエストを投げる
「**2.**」で新卒一人一人のその日の日報データをHTML形式で取得できました。
ここでようやくそのデータをGPT-4にリクエスト処理するフローを書いていきます。

やることとしては、
>1. Azure OpenAI ServiceのGPT-4にリクエスト投げるための設定
>2. 「**2-*.**」で取得したhtml形式の日報データをさらに整形
>3. リクエスト文を作成
>4. GPT-4のAPIのエンドポイントにリクエスト（POST）
>5. 返ってきた値を取得

上記1~5をやっていきます。

#### 3-1. Azure OpenAI ServiceのGPT-4にリクエスト投げるための設定
こちらはセキュリティの問題上中身は見せられませんが、以下の情報を変数で定義してください。
![スクリーンショット 2023-06-25 20.18.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/9ee94180-0906-ed5e-b65a-0d4731a8947d.png)

#### 3-2. 「2-*.」で取得したhtml形式の日報データをさらに整形
ここでは無駄なhtmlタグの削除と、htmlを全て連結して一つの長い文字列とします。

まず先ほど追加した「ユーザー名と本文HTML」の配列をループ処理します。
ここだと新卒は4人いるので4回ループしてます。

そしてkeyのhtmlには配列で行ごとのhtmlが文字列として入ってるので、ループ処理して不要なhtmlタグを削除していきます。
![スクリーンショット 2023-06-25 20.23.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/872fbf5b-3b03-b233-2e0d-6316fb7e43fc.png)

条件式には不要なhtmlタグが含まれているかをジャッジしています。
![スクリーンショット 2023-06-25 20.26.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/dde2c7f9-205b-f473-c4c3-9148ce5d1078.png)

全ての条件をクリアしたら文字列に追加していきます。
ここもApply to eachをする前に文字列を追加していく変数を定義しておいてください。
![スクリーンショット 2023-06-25 20.30.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/2e037387-4439-8f54-a2bb-13db2fba1f95.png)

Trueの時は以下のように文字列に追加されていきます。
![スクリーンショット 2023-06-25 20.29.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/a42cb75f-1158-f8d3-b917-99f8259ff56b.png)

最終的に以下のような長い文字列のhtmlが作成されました。
![スクリーンショット 2023-06-25 20.31.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/767e194d-c8be-7bd1-ea22-146c9aa7a6f0.png)


#### 3-3. リクエスト文を作成
ここがとても大事で実際にGPT-4に投げるリクエスト文を作成していきます。

今回は以下のようにしました。
>1. 利用するモデルはGPT-4なのでroleをつけます。
>2. GPT-4側はベテランのエンジニアであることを伝え、自分が新卒技術研修中で日報をhtml形式で書いた旨を記載します。
>3. そして日報の内容から問題を作って欲しいことと、レスポンスの条件をいくつか記載します。
　・ Teamで投稿するために文末には改行コードの`<br>`を入れてもらうこと
　・ 答えは出さないこと(これを伝えないと答えまでついてくる時があります)
　・ 問題だけでなくプログラミングに関する雑学も一つ添えること
　・ 日報のレビューもしてあげること
　・ 新卒は日々頑張って学習してるので方弁で応援してあげること
　・ レスポンスのフォーマットを固定していること
>4. 最後に「**3-4**」で作成した日報データを加えています。


これで準備が整いました。

↓systemロール
![スクリーンショット 2023-08-01 0.39.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/c8b5904c-5e39-053e-4270-5801d5163486.png)
↓userロール
![スクリーンショット 2023-08-01 0.39.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/ac59b1c9-51b7-ac9c-e7e9-bb1aa8996b7a.png)



#### 3-4. GPT-4モデルのAPIのエンドポイントにリクエスト（POST）
青色で隠されているところにはモデル名を入れましょう。
![スクリーンショット 2023-08-01 0.39.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/637a0f64-a867-0a89-67e8-8aa5666d2efc.png)

エンドポイントの設定はAzureのドキュメントを見るとわかります。

https://learn.microsoft.com/ja-jp/azure/ai-services/openai/chatgpt-quickstart?tabs=command-line&pivots=rest-api

#### 3-5. 帰ってきた値を取得
実行すると数秒後にレスポンスが返ってきました！
choices配列のmessageのcontent内に文字列として入ってきています。
![スクリーンショット 2023-06-25 20.40.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/ae2eb6ce-d8e6-fd03-20c0-0dbf1000635c.png)

ここまできたらあとはTeamsに投稿するだけです。


## 4. Teamsに投稿
Teamsに投稿するために対象の新卒にメンションをつけてあげたいです。
Apply to eachで実行する前に新卒一覧の変数配列を定義してください。
![スクリーンショット 2023-06-25 20.43.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/0c424fbd-b000-4090-22e5-10501632293f.png)

現在ループ処理中の新卒の名前に紐づくメールアドレスを取得してメンションを生成します。
`variables('新卒一覧')[items('Apply_to_each_4')['name']]`
![スクリーンショット 2023-06-25 20.44.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/81112242-763d-d669-d51e-3f817f8d8a16.png)

投稿したいchanelにメンションとGPT-4のレスポンスをつけて投稿します。
投稿者は融通が利く「ユーザー」にしてます。
※ボットとかだとGPT-4から投稿されて感があって良いのですが。。。
![スクリーンショット 2023-06-25 20.46.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/e85464a3-c1fd-91be-dbf0-5d0ecb4aa8af.png)

こんな感じで投稿されます。
![スクリーンショット 2023-06-25 20.51.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/d39b0407-cfd9-fbf2-4b9a-f30065bd910c.png)

これでTeamsに投稿するところまでできました。

## 最終的なフロー一覧
![スクリーンショット 2023-06-25 20.54.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/b0e5efe2-c5f6-319d-fc75-d76ac68114ca.png)
![スクリーンショット 2023-06-25 20.54.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/1ab5df12-cc1d-b9e3-b239-822cf805617b.png)
![スクリーンショット 2023-06-25 20.54.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/23510456-a60a-5885-a61f-32fc70903ffb.png)
![スクリーンショット 2023-06-25 20.55.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/5c9e3b7a-17a8-695f-8bed-d783436239d1.png)
![スクリーンショット 2023-06-25 20.55.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/75615a40-2125-e189-52d8-a26821bdf351.png)
![スクリーンショット 2023-06-25 20.56.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/a639cc3d-5247-6fa0-9a87-f65a0f3f3d31.png)
![スクリーンショット 2023-06-25 20.56.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1812049/8927642d-fc8b-46c8-6c64-58f8c1cfa2d5.png)


## 最後に
今回は新卒向けに問題を作成するフローを紹介しましたが、日報データを取得できているので日報のレビューとかフィードバックもGPT-4にしてもらってます。
GPT-4に送るリクエスト文はもう少し精査が必要かなと思います。

現在会社で社内専用のGPT-4・ChatGPTを利用できる状態ですので色んなケースに導入していければと思います。