はじめに
こんにちは。
先日、エンジニア兼作家である安野たかひろさんが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つのワークフローがありました。
├── comment-review.yml # コメントレビュー
├── issue-review.yml #Issueレビュー
└── main.yml # ドキュメントのデプロイ
それぞれ中身を見ていきます。
Issue作成時レビュー(issue-review.yml)
ファイルはこちら issue-review.yml
このワークフローは、新規作成(opened)されたIssueをOpenAIのAPIを活用し、不適切な内容の検出レビューと重複チェックを行います。
ワークフローの流れ
- ワークフロー実行のために、Issueの書き込み権限
issues: write
とリポジトリ内容の読み取り権限contents: read
を許可 -
review_issue
という名のジョブをUbuntu環境で実行 - リポジトリのチェックアウト
- Python環境のセットアップ(3系)
- Pythonパッケージの依存関係のインストール
pipのアップグレード、openai、PyGithub、qdrant-client、regexパッケージをインストール - 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を叩いています。
やっていることまとめ
- ライブラリのインポート
- 環境変数の取得
- ラベルの作成(
toxic
とduplicated
)
不適切なIssueに対してはtoxic
ラベルを、重複したIssueに対してはduplicated
ラベルを作成 - Qdrantクライアント作成
Qdrantに関してはこちらの記事を参照ください
Qdrant 公式
Python Qdrant Client リポジトリ - OpenAIクライアントの作成
- Issue内テキストの内容を検証(Moderations)
OpenAIが提供している潜在的に有害なテキストかどうかをチェックするAPIを使用 Moderationsについてはこちらを参照 - 画像を検証(GPT-4oに対し画像が暴力的か、性的な画像かをチェック)
違反があれば、toxic
ラベルを追加し、警告コメントを投稿しIssueをクローズ - Qdrantで検索する
QdrantデータベースでIssueを検索 - 違反がなければIssueをQdrantデータベースに追加
- 重複している場合、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を活用して不適切な内容検出を行い、それらのコメントを非表示にするためのものです。
ワークフローの流れ
- ワークフロー実行のために、Issueの書き込み権限
issues: write
とリポジトリ内容の読み取り権限contents: read
を許可 -
review_issue
という名のジョブをUbuntu環境で実行 - リポジトリのチェックアウト
- Python環境のセットアップ(3系)
- Pythonパッケージの依存関係のインストール
pipのアップグレード、openai、PyGithub、qdrant-client、regexパッケージをインストール - コメントのレビュー(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を叩いています。
やっていることまとめ
- ライブラリのインポート
- 環境変数からトークンを取得
- GitHubリポジトリとIssueを取得
- OpenAIクライアントを作成
- Issueのコメントを取得
- コメントの内容を検証
- 画像があれば画像を検証
- 違反があればコメントを非表示にする
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を使用してデプロイをしています。
特出した内容ではないので簡単にまとめます。
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)にプッシュされたことをトリガーにして、政策リポジトリにデプロイしています。
具体的にやっていること
- リポジトリのチェックアウト
- Gitのクレデンシャルを設定
- Pythonのセットアップ
- キャッシュIDの生成
- キャッシュの利用
- MkDocs Material
- MkDocsでのデプロイ
MkDocsについて
MkDocsのリポジトリ
おわりに
以上でワークフローの解説は終わりです。
いかがでしたでしょうか。
専用のモデルを構築しているのかなぁとも思ったんですが意外とシンプルな作りとなっていました。