kkamegawa's weblog

Visual Studio,TFS,ALM,VSTS,DevOps関係のことについていろいろと書いていきます。Google Analyticsで解析を行っています

Azure OpenAIにブログの要約をしてもらう

GPT-4が通ったことですし、何にしようかなと思ったのですが、隔週で実施している devblog radioのブログ要約をしてもらうことにしました。

これ前からやりたかったので、Cognitive Serviceもセットアップしていたのですが、どうもなぁ…ということで二の足を踏んでいましたが、GPT-4の要約、翻訳のおかげでやってみることにしました。ローカルでテストしたらだいたいいい感じだったので、試行してみます。

最初はAzure Functionsにしようと思っていましたが、どうせPowerShellでやっているので、同じスクリプト内でやったほうがいいかということでこんな関数を作ってみました。

function Get-SummarywithOpenAI(
    [string]$blogurl
)
{
    $Uri   = $ENV:OPENAI_API_URL
    $PostBody = @{
        max_tokens = 800
        temperature = 0.7
        top_p = 0.95
        frequency_penalty = 0
        presence_penalty = 0
        stop = @('##')
    }

    $Header =@{
      "api-key" = $ENV:OPENAI_API_KEY
      "Content-Type" ="application/json"
    }

    $PostBody.messages = @(
        @{
            role = 'user'
            content = '以下のURLを要約してください。本文が日本語以外である場合、日本語で200文字以内に要約してください。'+$blogurl
        }
    )

    try {
        $response = Invoke-RestMethod -Method Post -Uri $Uri `
            -Headers $Header `
            -Body ([System.Text.Encoding]::UTF8.GetBytes(($PostBody | ConvertTo-Json -Compress)))

       $Answer = $response.choices[0].message.content
   }
    catch {
        $Answer = '要約に失敗しました。'
    }

    return $Answer
}

環境変数OPENAI_API_URLにAPIエンドポイント(https://{略}.openai.azure.com/openai/deployments/{モデル名}/chat/completions?api-version=2023-03-15-preview))、OPENAI_API_KEYにAzureポータルから取得できるAPI KEYを設定してください。

パラメータはカスタマイズできるようにenvironmentに指定してもよかったのですが、そこまではいらないかなということで固定値にしています。変わるようなら変数にしようかなと思います。

Azure PipelinesのYAMLでPATを参照する

何回も書いているにもかかわらず、そのたびに過去のパイプラインを見直すので自分用まとめ。

Azure PipelinesではLibraryに登録した値をパイプライン中で参照することができます。

learn.microsoft.com

デプロイ先とかリージョン名とかであれば平文でもいいですが、パスワードやトークンであれば🔒を設定しておくことで初回登録以降表示されなくなります。Azure Pipelinesの中でも暗号化されているので、二度と平文の表示はできません(そのはずです)。より厳密に保護したい場合、Azure KeyVaultに登録してください。シームレスに連携しているので、参照している先がどちらかというだけです。

learn.microsoft.com

KeyVaultに入れておくとLog Analyticsでアクセスの検索もできますし、そもそもKeyVaultのシークレットを管理する人とパイプラインを管理する人の分離ができます。そういうセキュリティ上の要件がある時は使ってください。一人で使うだけなら別に要らないとは思います。

パイプラインからLibraryを使う場合、Permissionから使用できるパイプラインの指定を忘れないようにしてください。ほっとくと数日間動いてないってことがあります(警告出るのですぐわかります)。

で、本題のこのPAT(個人用アクセストークン)をYAML内で参照する方法です。

YAML内で使用を宣言

variables:
- group: ReleasenoteGen

- task: PowerShell@2
  displayName: 'Check Release Notes for Azure DevOps'
  inputs:
    filePath: '$(Build.SourcesDirectory)/vsts-translate/create-task-issue.ps1'
    pwsh: true
  env:
    PAT: $(PAT)

variablesセクションのgroupにLibraryの値を指定します。これでLibraryに登録しているPATという変数の値が参照できます。これをPowerShell / Bashなどで使う場合、envに環境変数値のペアで登録しておくとスクリプト内で参照できます。例えばPowerShellの例。

$token = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$ENV:PAT"))
$header = @{authorization = "Basic $token"}
$uri = 'https://dev.azure.com/{organization}/{Project}/{Team}/_apis/wit/wiql/{QueryID}?api-version=7.1-preview.2'
$json = Invoke-RestMethod -Uri $Uri -Method Get -ContentType "application/json" -Headers $header
if($json.workItems.Count -gt 0) {
  write-host "work in progress"
  exit 
}

PowerShellでは$ENV:{環境変数名}でOSの環境変数の参照ができます。$ENV:PATでYAMLで設定した環境変数をスクリプト内で使うことが可能になります。余談ですがこのREST APIは事前に登録したQuery(WIQL)を呼び出すものです。クエリで条件に引っかかるものがあればスクリプトを終了しています。

Azure DevOps CLIから参照する

learn.microsoft.com

Azure DevOpsを制御するためのCLIがAzure CLIの拡張機能として提供されています。REST API使うよりも便利なケースがありますので、こちらを使うことがあります。最近ではREST APIでどうしてもタスク作成時に日本語渡せなかったので、挫折してaz boards work-item create使いました。このCLIも中身はPythonで同じREST API呼び出しているはずなのでソース見ればいいのですが…。

Azure DevOps CLIは基本的にインタラクティブな処理を前提にしていますが、自動化用にいくつかの方法があります。

  1. 標準入力のリダイレクト echo $PAT | az devops login --org {organization URL}
  2. AZURE_DEVOPS_EXT_PAT環境変数を使う

1に関してはストレージに格納されちゃうからちゃんとaz devops logoutしろよ、という警告が出ます。スクリプトで使うならちゃんとログアウトしましょう。

WARNING: Failed to store PAT using keyring; falling back to file storage.
WARNING: You can clear the stored credential by running az devops logout.
WARNING: Refer https://aka.ms/azure-devops-cli-auth to know more on sign in with PAT.

learn.microsoft.com

ファイルからリダイレクトする方法もありますが、PATをファイル(レポジトリ)に置いておくのはちょっと…ということで、2番目の方法を使います。これはAzure DevOps CLIのログイン処理なしでaz boardsコマンド実行するときに自動的に参照してくれるので便利です。

- task: Bash@3
    displayName: 'Create work item'
    inputs:
      targetType: 'inline'
      script: 'az boards work-item create --title "リリースノートの翻訳を行う" --type "User Story" --area "エリア名" --org https://dev.azure.com/{org名}/ -p {project} --assigned-to ID'
    env:
      AZURE_DEVOPS_EXT_PAT: $(PAT)

envに指定しておくと、上のvariablesで定義されているPATAZURE_DEVOPS_EXT_PATにマップしてくれるので、az devops loginしなくても自動的にログイン(というかREST APIのトークンとして参照)してくれます。こっちのほうがいいんじゃないかな。

YAML内での参照方法の注意点

Azure Pipelinesには複数の変数があります。

マクロ(タスク実行前)、テンプレート(YAMLをパースするとき)、ランタイム(実行時)と置換されるタイミングが違うので、気をつけてください。最初に使うのはマクロでしょう。⁠.NETであれば-c $(BuildConfiguration)とかそういった類ですね。

NAT Gatewayの削除をスクリプトで実施する

AzureのNAT GatewayをPowerShellスクリプトで削除しようとしたんですよ。

docs.microsoft.com

ご存知の通り、NAT Gatewayはこれで削除する前にPublic IPとサブネットからデタッチしないといけません。デタッチしていないと-forceをつけても失敗します。じゃあ デタッチしようとしてもスクリプトでやる方法がちょっとわからなかった。

docs.microsoft.com

最初サブネット追加する時のようにこれでいけるのかなと思ったけど、だめ。結局bicepでやらないとダメ?ということで書いてみました。

github.com

NAT GatewayのサブネットとPublic IPを消してやることでちゃんとそれぞれからデタッチされました。作る方はこっち

github.com

まぁ作る方は散々サンプルがあるからいいと思います。消す方はあまりなかったので備忘録的に。おそらく他にもLoad Balancerとかのサブネットにアタッチするものはこういう感じでやらないといけない気がします。逆にBastionやVPN gatewayなどの専用のサブネットを作るものは普通に消せますね。

あと、Azure Cloud Shellでやっていたからかどうかわかりませんが、remove-aznatgatewayremove-azpublicipaddressを連続で実行するとどちらか片方しか消せませんでした。仕方ないので、-azjobつけてバックグラウンドジョブにしてみたらどちらも消せました。

PowerShellのForeach-object parallel内でインデクサを使う

docs.microsoft.com

PowerShell 7で導入されたForeach-Object -parallelちょっと用事があってこんな感じで使おうとしました。

$outervalue = 1,2,3
$array = 10,20

$array | foreach-object -parallel {
  for($i = 0; $i -lt $using:outervalue.length;$i++){
     write-host $_ * $using:outervalue[$i]
  }
}

すると…

ParserError:
Line |
   3 |       write-host $_ * $using:outervalue[$i]
     |                                         ~~
     | Expression is not allowed in a Using expression.

こんな式は認められないと怒られました。

github.com

原因はここで書かれているのですが、parallel内の式ではオブジェクトが読み取り専用になるために、インデクサを指定するとだめなのだそうです。なので、GetValueを使って参照することになります。

$outervalue = 1,2,3
$array = 10,20

$array | foreach-object -parallel {
  for($i = 0; $i -lt $using:outervalue.length;$i++){
     write-host $_ * ($using:outervalue).GetValue($i)
  }
}

いわれてみればそうだなぁという仕様ですが、結構悩みました。