IBM Cloudからの通知を生成AIを使ってわかりやすく - (2)Webhookアプリコーディング編

# はじめに
※こちらはシリーズとなっており、下記ラインナップの1つ目になります。
「IBM Cloudからの通知を生成AIを使ってわかりやすく」シリーズ

- [(1)通知の仕様編](https://qiita.com/katahiro/items/b9bd400f93d328613cff)
- [(2)Webhookアプリコーディング編](https://qiita.com/katahiro/items/d8048393ce42cf9c5b3b) ← 今ココ
- [(3)デプロイ＆設定編](https://qiita.com/katahiro/items/53443fabe2980c2fba03)


こちらは、前回記事に続いて、IBM Cloudから送られる通知の配信先として設定するWebhookアプリをコーディングする話になります。
注意点として、watsonx.aiを利用するため、費用が発生します。無償ではない点にご注意ください。とはいえ、通知数に依存しますが、$1もかからない程度ではないかと思います。

# ソースコード
こちらで用意しました。Slackに通知する形で実装しているので、メール送信にしたい、Teamsで通知したい場合はSlackに通知する部分の実装を変更頂ければと思います。

https://github.com/katahiro-github/cloud-notification.git

```
│  .ceignore
│  .env.sample
│  .gitignore
│  driver.js
│  LICENSE
│  main.js
│  package-lock.json
│  package.json
│  README.md
│
├─util
│      commonMethod.js
│      requestCheck.js
│      sendToSlack.js // Slackに通知を送信する部分
│
└─wx
        dictionary.js
        translate-to-ja.js
        what-implement.js
        what-should-do.js
        wx-client.js
```

こちらは、下記で紹介しているIBM CloudのCode Engineのサービスにある `functions`を利用する形で実装しています。

https://qiita.com/katahiro/items/b2866df4225bbdd8334d

`README`に記載しているデプロイ方法もCode EngineのFunctionsを作成する手順としています。
ローカルPCで動作させてみる方法は、ぜひGithubのREADMEを参照してください。

# 処理の流れ
次の手続きで処理を行っています。コードの `main.js` を参照してください。
1. 環境変数に `BEARER_VALUE` が存在する場合、Bearer認証が含まれたリクエストかチェック
1. IBM Cloudから渡された通知内容のパラメータをコード内で処理する形に変換
1. 通知本文から、その内容をwatsonx.aiで要約するように指示
1. 要約された通知内容を日本語に翻訳するようにwatsonx.aiに指示
1. 通知本文から、アクションすべき内容を抽出するようにwatsonx.aiに指示
1. アクションすべき内容を日本語に翻訳するように指示
1. 日本語化された要約、アクションすべき内容をSlackに通知

# watsonx.aiを呼び出すためのAPIキーやProject IDの取得方法
コードの中ではwatsonx.aiを呼び出すためのAPIキーやProject IDを環境変数に登録するように指示していますが、その取得方法の記載はしておりません。次の記事を参考にして登録、取得頂ければと思います。

https://qiita.com/katahiro/items/3258cd42226ed82268ac

# 実装に関するいくつかの注意点
## 時間情報に関する取扱い
IBM Cloudの通知に含まれるメンテナンスの開始時間といった情報はunixtimeで渡されます。こちらUTCの時間情報で渡されるのと、Code Engineの実行環境自体はそのタイムゾーンがUTCであるため、時間表記の変換の際には +9:00 することで日本時間で表示するようにしています。UTCで取り扱いたい場合は、こちらの時差調整している部分を削除ください。
```javascript:main.js
  const startTimeUnix = args.startTime;
  const startDate = `${commonMethod.dateToString('YYYY/MM/DD H:mm:SS(DDD)', new Date((startTimeUnix + 3600*9)*1000))}`;
  const endTimeUnix = args.endTime;
  const endDate = `${commonMethod.dateToString('YYYY/MM/DD H:mm:SS(DDD)', new Date((endTimeUnix + 3600*9)*1000))}`;
  const updateTimeUnix = args.updateTime;
  const updateDate = `${commonMethod.dateToString('YYYY/MM/DD H:mm:SS(DDD)', new Date((updateTimeUnix + 3600*9)*1000))}`;
```
## Bearer認証
IBM CloudのCode Engineにデプロイする予定ですが、アプリのエンドポイントはPublicに開放されます。リクエストに投げるパラメータを正しく設定すれば誰でもこのツールを動かすことが出来てしまいます。今回はwatsonx.aiを利用するため、生成AIのモデル利用費用が若干発生することを考えると、予期しない利用は避けたいところです。
このツールの中では簡易にBearer認証を実装しています。
環境変数で `BEARER_VALUE` で認証に必要なトークン(適当な文字列)を指定している場合、リクエストヘッダーの `Authorization`でそのトークンと同じ文字列が指定されているかをチェックする仕組みにしています。
```javascript:main.js
const bearerValue = process.env.BEARER_VALUE; 

const main = async (args) => {
  console.log(JSON.stringify(args));
  // request check
  if(bearerValue){
    const requestCheckResult = reqCheck.bearerCheck(args);
    if(!requestCheckResult.ok) return requestCheckResult.returnInfo;
  }
```
環境変数に `BEARER_VALUE` が指定されているにも関わらず、`Authorization`のヘッダーが無い、もしくは誤ったトークンが指定されている場合は、そこで処理を停止させることが出来るので、不用意に生成AIのモデルが利用されることは無くなるでしょう。

## 日本語翻訳方法について
単純に今回利用している生成AIのモデル `meta-llama/llama-2-70b-chat` に日本語翻訳するように指示すると、特に製品や技術用語の翻訳がうまくいかないことが多いです。従って、以前書いたような `逆ルー大柴対応` で翻訳させています。

https://qiita.com/katahiro/items/7c1994c33f4df426554f

これは、あらかじめ下記のように英文内の英語を部分的に日本語に置き換える辞書を作成して、日本語が入り混じる英文を翻訳させるというものです。不足があれば利用者の方でどんどん追加頂ければと思います。正規表現で変換するようにしている点にご注意ください。
```javascript:dictionary.js
const dictionaryJsonArray = [
  // 契約文言
  {en:"Data Processing Addendum",jp:"データ処理補足契約書"},
  {en:"Service Description",jp:"サービス記述"},
  {en:"Service Level Agreement",jp:"サービス・レベル・アグリーメント"},
  {en:"pay-as-you-go",jp:"従量課金"},
  {en:"material change",jp:"重大な変更"},
  {en:"terminating your account",jp:"アカウント削除"},
  {en:"3rd-party offerings?",jp:"サードパーティの製品"},
  {en:"The wording",jp:"文言"},
  {en:"simplified language",jp:"簡略化された文言"},
  {en:"late payment fee",jp:"損害遅延金"},

  // 保守関連用語
  {en:"[mM]aintenance window",jp:"メンテナンスウインドウ"},
  {en:"[sS]ecurity bulletin",jp:"セキュリティ速報"},
  {en:"IBM [rR]epresentative",jp:"IBM担当者"},
  {en:"IBM Support [rR]epresentative",jp:"IBMサポート担当"},
  {en:"client support",jp:"IBMサポート担当"},
  {en:"unavailability",jp:"利用不可"},
  {en:"[iI]nfrastructure replacement",jp:"インフラストラクチャ交換"},
  {en:"[nN]ew accounts",jp:"新規アカウント"},
  {en:"[eE]xisting accounts",jp:"既存アカウント"},
  {en:"potential downtime",jp:"潜在的なダウンタイム"},
  {en:"potential disruptions?",jp:"潜在的なダウンタイム"},
  {en:"outage",jp:"停止"},
  {en:"customer advocate",jp:"IBMサポート担当"},
  {en:"production",jp:"本番環境"},

  // 製品用語
  {en:"IBM Cloud",jp:"IBM Cloud"},
  {en:"[cC]lassic infrastructure",jp:"クラシックインフラストラクチャ"},
  {en:"[nN]etwork infrastructure",jp:"ネットワークインフラストラクチャ"},
  {en:"[pP]ublic service endpoint",jp:"パブリックサービスエンドポイント"},
  {en:"[pP]rivate service endpoint",jp:"プライベートサービスエンドポイント"},
  {en:"the tier",jp:"ティア"},
  {en:"[bB]are metal servers",jp:"ベアメタルサーバ"},
  {en:"Continuous Delivery Service",jp:"Continuous Delivery"},
  {en:"[eE]ndurance",jp:"エンデュランス"},

  // 技術用語
  {en:"[pP]enetration testing",jp:"侵入テスト"},
  {en:"[rR]eserved instance",jp:"リザーブドインスタンス"},
  {en:"[iI]ncompatibilities",jp:"非互換性"},
  {en:"[iI]ncompatibility",jp:"非互換性"},
  {en:"database deployments",jp:"データベース導入環境"},
  {en:"[cC]onvergence",jp:"コンバージェンス"},
  {en:"redundant configurations?",jp:"冗長構成"},
  {en:"redundant paths?",jp:"冗長パス"},
  {en:"discrete",jp:"個別"},
  {en:"multi-pathing software",jp:"マルチパスソフトウェア"},

  // その他
  {en:"[iI]f you have any questions or concerns",jp:"ご質問や懸念がある場合は"},
  {en:"[iI]f you have any questions",jp:"ご質問がある場合は"},
  {en:"[fF]amiliarize yourself with",jp:"よく理解してください"},  
  {en:"[cC]oncerns",jp:"ご懸念"},
  {en:"Osaka",jp:"大阪"},
  {en:"remain",jp:"継続"},

  // 英英
  {en:"environment(s)",jp:"environment"},

]
```
## 翻訳語の日本語調整
残念ながらモデルに指示した結果生成された日本語文が変な日本語になってしまうケースや、どうしても一般的な名称に変換されてしまい、IBM Cloudからの通知ということを考えると、別の日本語での表現がより伝わりやすいケースがあります。
例えば、 `Customer`を翻訳すると、 `顧客`と翻訳されますが、Cloudからの通知と考えると `利用者` のほうが伝わりやすいと思います。
この状況に対して、先ほど同じ `dictionary.js` には日本語から日本語に変換するための辞書配列を用意しています。これには変な日本語の調整や、誤った翻訳を後からまとめて修正する形で登録しています。余計なワードを消すにも使えます。
```javascript:dictionary.js
const jpToJpJsonArray = [
  {jp1:"実装の詳細",jp2:"内容"},
  {jp1:"開いておください",jp2:"開いてください"},
  {jp1:"IBM ?クラウド",jp2:"IBM Cloud"},
  {jp1:"顧客",jp2:"利用者"},
  {jp1:"（s）",jp2:""},
  {jp1:"働きましています",jp2:"動き出しています"},
]
```

## Slack通知のデフォルト設定
`sendToSlack.js`では下記の通り環境変数から設定を取得するようにしていますが、この中で必須の環境変数は `SLACK_WEBHOOK_URL`のみです。
```javascript:sendToSlack.js
const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL;
const slackChannel = process.env.SLACK_CHANNEL;
const slackIcon = process.env.SLACK_ICON_EMOJI || ':loudspeaker:';
const slackUserName =  process.env.SLACK_USER_NAME || 'IBM Cloud Notification with watsonx' ;
const slackAddMessage = process.env.SLACK_ADD_MESSAGE ;
```
次の表に従って設定を入れるか検討してもらえればと思います。
|環境変数|利用用途|
|:---|:---|
|SLACK_CHANNEL|SlackのWebhookURLは作成する時に投稿先のチャンネルを指定して作りますが、POSTする際に同じワークスペース内のチャンネルIDを指定することで、別のチャンネルに投稿することが可能です。作成したWebhook URLのデフォルトの投稿先と異なるチャンネルに投稿させたい場合は、こちらでそのチャンネルIDを指定してください。|
|SLACK_ICON_EMOJI|Slackに通知する際の投稿メッセージの投稿者アイコンで利用する絵文字の指定です。デフォルトでは`:loudspeaker:`(スピーカーのアイコン)にしているので、変更したい場合は指定してください。|
|SLACK_USER_NAME|Slackに通知する際の投稿メッセージの投稿者名で利用する名前の指定です。デフォルトでは `IBM Cloud Notification with watsonx` としているので、変更したい場合は指定してください。|
|SLACK_ADD_MESSAGE|Slackに通知するメッセージの冒頭に指定されたメッセージを埋め込みます。例えば `<!channel>`と入れることで、投稿されたチャンネル全体にメンションを行うことが可能です。|

- 通知イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/651598/101222f7-bfb4-c874-c7ac-5692fa0cee12.png)

## ローカル環境でのテスト
`driver.js`を含めており、 `npm test` でテスト実行が可能です。
実際にCode EngineのFunctionにIBM Cloudから送られてきた過去のIncidentの通知内容を設定して、 `main.js`に渡しています。
Bearer認証を設定している場合は、ヘッダーの `Authorizaiton`の設定変更を忘れないようにしてください。`account_id`を正しく設定すると、Slackに送られてくる通知に、設定されたアカウントの通知一覧へ飛ぶリンクが正常に作成されます。
```javascript:driver.js
const mainJs = require('./main');
const main = async () => {
  const returnMain = await mainJs.main(
    {
      "__ce_headers":{
        "Accept":"application/json, text/plain, */*",
        "Accept-Encoding":"gzip, compress, deflate, br",
        "Authorization":"Bearer xxxxx",
        "Content-Length":"1087",
        "Content-Type":"application/json",
        "Ibm-Notifications-Api-Version":"v2.0.0",
        "User-Agent":"axios/1.6.1",
        "X-Request-Id":"04522fb7-7a0c-4a9d-871a-a2f00d1fc2d6"
      },
      "__ce_method":"POST",
      "__ce_path":"/",
      "account_id":"xxxxx",
      "body":[
        {
          "content-type":"text/html",
          "language":"en",
          "text":"Description:<br />On 28 February 2024 at 11:54 UTC, an issue occurred on the data center Aggregate Router (dar01) and datacenter OSA22 (dar01.osa22). On 28 February 2024 at 13:47 UTC, the device war was removed from production, and the issue was resolved. Some customers in the OSA22 data center may have experienced latency or packet loss disruption.  This incident may have affected Storage Area Network (SAN) connectivity. Customers utilizing SAN may find that their environments went read-only and will need to perform a file system check. Network specialists are working on restoring the service ASAP and to determine whether additional action may be necessary to prevent a recurrence of this issue. We apologize for any issues which this incident may have caused.<br /><br /><br />"
        }
      ],
      "category":"Incident",
      "severity":"Severity 2",
      "sourceID":"159522420",
      "startTime":1709121240,
      "state":"Investigating",
      "title":[
        {
          "language":"en",
          "text":"Routing issue on | dar01.osa22 | "
        }
      ]
    }
  )
  console.log(returnMain);
}

main()
```

# さいごに
この記事では、IBM Cloudから送られてくる通知を生成AIを用いて要約、翻訳するコードを紹介させて頂きました。
利用頂く環境によってコードの改修が必要と思いますし、また現時点でも十分な辞書登録状況ではありません。より良い通知になるようにご利用頂く方で、好きにカスタマイズして頂けると幸いです。
次の記事では、用意したコードをIBM CloudのCode Engineにデプロイして、IBM Cloudの通知設定にデプロイしたアプリを指定することで、無事に通知が受け取れる部分を紹介したいと思います。

# シリーズのリンク

https://qiita.com/katahiro/items/b9bd400f93d328613cff

https://qiita.com/katahiro/items/d8048393ce42cf9c5b3b

https://qiita.com/katahiro/items/53443fabe2980c2fba03