LoginSignup
35
20
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

【都知事選】マニュフェストがGithubに公開されたので、Github Actionsのワークフローで何をしているのか解説する

Last updated at Posted at 2024-06-22

はじめに

こんにちは。
先日、エンジニア兼作家である安野たかひろさんが2024年の東京都知事選挙に出馬され、参加型のマニフェストを公開しました。

安野たかひろさんが何者かは、ご本人が記事を掲載しているので下記を参照ください。
エンジニア兼作家が東京都知事選挙の出馬表明記者会見をした会見全文
東京都知事選の「参加型マニフェスト」を公開します

タイトルの通り、マニフェストがGithubに公開され、誰でも変更提案のためにIssueを作れるようになっています。
ただ、公序良俗に反した内容や・誹謗中傷等の内容のチェックをする必要があり、それをGithub Actionsのワークフロー内で行っているので中身を見て解説していきます。

安野たかひろ:都知事選マニフェストのGithub公開リポジトリ

本記事は2024/6/22時点でのリポジトリを解説しておりますが、2024/06/26時点で更新が遅れています。

執筆後の大きなワークフローの変更について簡易的に記載します。

2024/06/22

新規イシューに政策カテゴリラベルを付与するGitHub Actionsを追加
こちらは執筆中に更新されていました。
対応プルリクエストはこちら

2024/06/26

🛠️ GitHub Issue 自動レビュー機能とコードの分離
PythonスクリプトがGithub Actions内に記載されていることが指摘されており、スクリプトの切り出しの対応を行った模様です。
対応プルリクエストはこちら

本記事も追って情報更新をする予定です。
2024/06/29時点での最新状態を更新する予定です。

おことわり

本記事に政治的意図は一切ありません。
あくまでワークフローに興味を持ったので本記事を執筆しています。

.github/workflowsの内部ツリー

3つのワークフローがありました。

.github/workflows
├── comment-review.yml # コメントレビュー
├── issue-review.yml #Issueレビュー
└── main.yml # ドキュメントのデプロイ

それぞれ中身を見ていきます。

Issue作成時レビュー(issue-review.yml)

ファイルはこちら issue-review.yml
このワークフローは、新規作成(opened)されたIssueをOpenAIのAPIを活用し、不適切な内容の検出レビューと重複チェックを行います。

ワークフローの流れ

  1. ワークフロー実行のために、Issueの書き込み権限issues: writeとリポジトリ内容の読み取り権限 contents: read を許可
  2. review_issueという名のジョブをUbuntu環境で実行
  3. リポジトリのチェックアウト
  4. Python環境のセットアップ(3系)
  5. Pythonパッケージの依存関係のインストール
    pipのアップグレード、openai、PyGithub、qdrant-client、regexパッケージをインストール
  6. Issueのレビュー(Python環境でOpenAIのAPIを実行・評価)

OpenAIのAPIを用いたIssueのレビュー

name: Review comment with LLM

ここから詳しく見ていきます。

環境変数の設定

env:
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  QD_API_KEY: ${{ secrets.QD_API_KEY }}
  QD_URL: ${{ secrets.QD_URL }}

Pythonスクリプト
テキストおよび画像検証のためにスクリプトを実行し、OpenAIのAPIを叩いています。

やっていることまとめ

  1. ライブラリのインポート
  2. 環境変数の取得
  3. ラベルの作成(toxicduplicated)
    不適切なIssueに対してはtoxicラベルを、重複したIssueに対してはduplicatedラベルを作成
  4. Qdrantクライアント作成
    Qdrantに関してはこちらの記事を参照ください
    Qdrant 公式
    Python Qdrant Client リポジトリ
  5. OpenAIクライアントの作成
  6. Issue内テキストの内容を検証(Moderations)
    OpenAIが提供している潜在的に有害なテキストかどうかをチェックするAPIを使用 Moderationsについてはこちらを参照
  7. 画像を検証(GPT-4oに対し画像が暴力的か、性的な画像かをチェック)
    違反があれば、toxicラベルを追加し、警告コメントを投稿しIssueをクローズ
  8. Qdrantで検索する
    QdrantデータベースでIssueを検索
  9. 違反がなければIssueをQdrantデータベースに追加
  10. 重複している場合、Issueにdupulicatedラベルを追加
    Issueにduplicatedラベルを追加し、重複の可能性があるIssueへのリンクコメントを追加
    重複していなかったら0を返し、それ以外は重複IDを出力
      import github
      from github import Github
      import os
      import openai
      import regex as re
      from qdrant_client import QdrantClient
      from qdrant_client.models import PointStruct

      # 環境変数からトークンを取得
      token = os.getenv("GITHUB_TOKEN")
      qd_api = os.getenv("QD_API_KEY")
      qd_url = os.getenv("QD_URL")
      g = Github(token)
      repo = g.get_repo("${{ github.repository }}")
      issue = repo.get_issue(${{ github.event.issue.number }})
      issue_content = f"{issue.title}\n{issue.body}"

      # ラベルの作成
      try:
          repo.create_label(name="toxic", color="ff0000")
          repo.create_label(name="duplicated", color="708090")
      except:
          pass

      # Qdrantクライアントの作成
      qdrant_client = QdrantClient(
          url=qd_url, 
          api_key=qd_api,
      )

      # OpenAIクライアントの作成
      openai_client = openai.Client()
      embedding_model = "text-embedding-3-small"
      collection_name = "issue_collection"

      # 画像の検証を行う関数
      def validate_image(text):
          model_name = "gpt-4o"
          prompt = "この画像が暴力的、もしくは性的な画像の場合trueと返してください。"
          image_url = re.search(r"!\[[^\s]+\]\((https://[^\s]+)\)", text)
          if image_url and len(image_url) > 1:
              image_url = image_url[1]
          else:
              return False
          try:
              response = openai_client.chat.completions.create(
                model=model_name,
                messages=[
                  {
                    "role": "user",
                    "content": [
                      {"type": "text", "text": prompt},
                      {
                        "type": "image_url",
                        "image_url": {
                          "url": image_url
                        },
                      },
                    ],
                  }
                ],
                max_tokens=1200,
              )
          except:
              return True
          v = response.choices[0].message.content.lower()
          if "true" in v:
              return True
          else:
              return False

      # コメントの内容を検証する関数
      def judge_violation(text):
          # コメントの内容を検証、有害性をチェック
          response = openai_client.moderations.create(input=text)
          flag = response.results[0].flagged
          video_flag = validate_image(text)
          if flag or video_flag:
              print(response)
              issue.add_to_labels("toxic")
              if video_flag:
                  warn = "不適切な画像です。アカウントBANの危険性があります。"
              else:
                  warn = "不適切な投稿です。アカウントBANの危険性があります。"
              issue.create_comment(warn)
              issue.edit(state="closed")
              return True
          return flag

      # Issueを追加する関数
      def add_issue(text:str, iss_num:int):
          texts = [text]
          ids = [iss_num]
          result = openai_client.embeddings.create(input=texts, model=embedding_model)
          points = [
              PointStruct(
                  id=idx,
                  vector=data.embedding,
                  payload={"text": t},
              )
              for idx, data, t in zip(ids, result.data, texts)
          ]
          qdrant_client.upsert(collection_name, points)
          return text

      # Issueをマージする関数
      def merge_issue(iss:int):
          issue.add_to_labels("duplicated")
          print(f"merge to {iss}")
          issue.create_comment(f"#{iss} と重複しているかもしれません")
          return iss

      # Qdrantで検索する関数
      def qd_search(text:str):
          results = qdrant_client.search(
              collection_name=collection_name,
              query_vector=openai_client.embeddings.create(
                  input=[text],
                  model=embedding_model,
              )
              .data[0]
              .embedding,
          )
          return results

      # Qdrantに追加する関数(ただし関数は一度も呼び出しされていない)
      def qd_add(text:str, iss_num:int):
          texts = [text]
          ids = [iss_num]
          result = openai_client.embeddings.create(input=texts, model=embedding_model)
          points = [
              PointStruct(
                  id=idx,
                  vector=data.embedding,
                  payload={"text": text},
              )
              for idx, data, text in zip(ids, result.data, texts)
          ]
          qdrant_client.upsert(collection_name, points)

      # Issueの内容が不適切であるかどうかを判断
      if judge_violation(issue_content):
          quit()

      # QdrantでIssueを検索
      results = qd_search(issue_content)

      if len(results) > 2:
          results = results[:3]
      else:
          results = results
      print(results)
      res = ""
      for i in results:
          res+=f'id:{i.id}\n内容:{i.payload["text"]}\n'
      res = res.strip()

      # OpenAIに重複チェックのプロンプトを送信
      prompt= f"""
      以下は市民から寄せられた政策提案です。
      {issue_content}
      この投稿を読み、以下の過去提案の中に重複する提案があるかを判断してください。
      {res}
      重複する提案があればそのidを出力してください。
      もし存在しない場合は0と出力してください。

      [出力形式]
      id:0
      """
      print(prompt)
      completion = openai_client.chat.completions.create(
        model="gpt-4o",
        max_tokens= 1024,
        messages=[
        {"role": "system", "content": prompt},
        ]
      )
      review = completion.choices[0].message.content
      if ":" in review:
          review = review.split(":")[-1]
      if review.isdecimal():
          if review == "0":
              add_issue(issue_content, issue.number)
          else:
              merge_issue(int(review))
      print(review)

      EOF

コメントレビュー(comment-reveiw.yml)

こちらもIssue作成時と同じようなワークフローでした。
ファイルはこちら comment-review.yml

このワークフローはIssueのコメントをレビューし、OpenAIのAPIを活用して不適切な内容検出を行い、それらのコメントを非表示にするためのものです。

ワークフローの流れ

  1. ワークフロー実行のために、Issueの書き込み権限issues: writeとリポジトリ内容の読み取り権限 contents: read を許可
  2. review_issueという名のジョブをUbuntu環境で実行
  3. リポジトリのチェックアウト
  4. Python環境のセットアップ(3系)
  5. Pythonパッケージの依存関係のインストール
    pipのアップグレード、openai、PyGithub、qdrant-client、regexパッケージをインストール
  6. コメントのレビュー(Python環境でOpenAIのAPIを実行・評価)

OpenAIのAPIを用いたコメントのレビュー

name: Review comment with LLM
ここから詳しく見ていきます。

環境変数の設定

env:
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  QD_API_KEY: ${{ secrets.QD_API_KEY }}
  QD_URL: ${{ secrets.QD_URL }}

環境変数として、OpenAI APIキー、GitHubトークン、Qdrant APIキー、Qdrant URLを設定。

Pythonスクリプト
テキストおよび画像検証のためにスクリプトを実行し、OpenAIのAPIを叩いています。

やっていることまとめ

  1. ライブラリのインポート
  2. 環境変数からトークンを取得
  3. GitHubリポジトリとIssueを取得
  4. OpenAIクライアントを作成
  5. Issueのコメントを取得
  6. コメントの内容を検証
  7. 画像があれば画像を検証
  8. 違反があればコメントを非表示にする
import github
from github import Github
import os
import openai
import regex as re

# 環境変数からトークンを取得
token = os.getenv("GITHUB_TOKEN")
qd_api = os.getenv("QD_API_KEY")
qd_url = os.getenv("QD_URL")

# GitHubリポジトリとIssueを取得
g = Github(token)
repo = g.get_repo("${{ github.repository }}")
issue = repo.get_issue(${{ github.event.issue.number }})
issue_content = f"{issue.title}\n{issue.body}"

# OpenAIクライアントを作成
openai_client = openai.Client()

# 画像の検証を行う関数
def validate_image(text):
    model_name = "gpt-4o"
    prompt = "この画像が暴力的、もしくは性的な画像の場合trueと返してください。"
    image_url = re.search(r"!\[[^\s]+\]\((https://[^\s]+)\)", text)
    if image_url and len(image_url) > 1:
        image_url = image_url[1]
    else:
        return False
    try:
        response = openai_client.chat.completions.create(
            model=model_name,
            messages=[
                {
                    "role": "user",
                    "content": [
                        {"type": "text", "text": prompt},
                        {
                            "type": "image_url",
                            "image_url": {
                                "url": image_url
                            },
                        },
                    ],
                }
            ],
            max_tokens=1200,
        )
    except:
        return True
    v = response.choices[0].message.content.lower()
    if "true" in v:
        return True
    else:
        return False

# コメントの内容を検証する関数
def judge_violation(text):
    response = openai_client.moderations.create(input=text)
    print(response)
    flag = response.results[0].flagged
    video_flag = validate_image(text)
    if flag or video_flag:
        return True
    return flag

# Issueのコメントを取得
comments = issue.get_comments()

# 各コメントを検証し、違反があれば非表示にする
for comment in comments:
    if judge_violation(comment.body):
        comment.edit("このコメントは非表示にされました")
EOF

デプロイ(main.yml)

こちらのワークフローでは、MkDocsを使用してデプロイをしています。
特出した内容ではないので簡単にまとめます。

main.yml
name: mainci
on:
  push:
    branches:
      - master
      - main
permissions:
  contents: write
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Configure Git Credentials
        run: |
          git config user.name github-actions[bot]
          git config user.email 41898282+github-actions[bot]@users.noreply.github.com
      - uses: actions/setup-python@v5
        with:
          python-version: 3.x
      - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV
      - uses: actions/cache@v4
        with:
          key: mkdocs-material-${{ env.cache_id }}
          path: .cache
          restore-keys: |
            mkdocs-material-
      - run: pip install mkdocs-material
      - run: mkdocs gh-deploy --force

main.ymlはmain(or master)にプッシュされたことをトリガーにして、政策リポジトリにデプロイしています。

具体的にやっていること

  1. リポジトリのチェックアウト
  2. Gitのクレデンシャルを設定
  3. Pythonのセットアップ
  4. キャッシュIDの生成
  5. キャッシュの利用
  6. MkDocs Material
  7. MkDocsでのデプロイ
    MkDocsについて
    MkDocsのリポジトリ

おわりに

以上でワークフローの解説は終わりです。
いかがでしたでしょうか。
専用のモデルを構築しているのかなぁとも思ったんですが意外とシンプルな作りとなっていました。

35
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
20