見出し画像

【プロンプト公開】コードレビューコメントの語気を和らげる提案をChatGPTにやってもらうChrome拡張を作って試してみた


はじめに

こんにちは、SHIFTの開発部門に所属している Katayama です。

レトロスペクティブで、MR上の指摘コメントの語気が強すぎるときがある、という意見が出た。確かに、以下のようなコメントだと意図しない語気の強さを感じてしまう可能性はあるだろう。コミュニケーションコストへの意識も関係があるかもしれない。

// コメント例①
これ、メソッド分けるべきでは?

// コメント例②
ここ、アーリーリターンにすべきでは?

上記のコメントは、別に相手を攻めているわけではなく、「メソッド分けたほうがいいな、責務が複数になっているから」や「あ、これはアーリーリターンにしたほうが可読性いいな~」と言った考えを提示したいだけなのだが、どうしても文字だけだとなんとなく語気が強く感じられてしまう。

こうしたコメントをもう少し語気を柔らかくすることで、印象が変わり心理的安全性の向上にも役立つかも、という事で、今回はChromeの拡張機能でMRコメントをChatGPTで分析し、コメントを語気を柔らかくなるように改善するという事をやってみた。

実際に導入してみる

どのような形で利用できるようにしたか?

今回はPoCレベルでの取り組みなので、以下の手順でChromeの拡張機能を有効化した(Chromeの拡張機能の開発については別の章「Chromeの拡張機能 の開発について」で取り上げる)。

1.chrome://extensions/ から拡張機能の設定画面を開く

2.「デベロッパーモード」をオンにし、「パッケージ化されていない拡張機能を読み込む」からローカル環境の拡張機能のPJを選択する

3.以下のように表示されれば完了

上記の手順で有効化したら、後は以下の動画のようにいつも通りGitLabを利用するだけでコメントに対する分析と改善提案が行われるようになる(MRのコメントを行っている箇所は、コメントの内容と関係のない部分で、デモ動画の撮影のためにコメントを行ったものになる)。

改善後のコメント例

上記のようにMRのコメントを行うと、その内容を読み取りChatGPTで分析・フィードバックを得られるようになった。実際にどのようなフィードバックが得られたか?の例を以下で見ていく(語気の柔らかサレベルは、数値が低いほど語気が強く、数値が高いほど柔らかいの判定)。

まず、例えば「これ、メソッド分けるべきでは?」というコメントについては以下のようなChatGPTからのフィードバックが得られた。

改善例と元のコメントを読み比べると、確かに元のコメントに比べて改善例のコメントの方が語気やニュアンスが柔らかくなったように感じられる。

続いて「ここ、アーリーリターンにすべきでは?」というコメントについては、以下のようになった。

こちらも改善後のコメントの方が語気やニュアンスは柔らかくなっているように思える(少し表現が冗長なような気もするが(笑))。

チームのメンバーの所感

今回の取り組みをやってみて、チームのメンバーからは以下のような所感が得られた。

  • コメントを書く側

    • Chromeの拡張機能で仕組みとして導入することで改善後のコメントをそのまま使えて楽

    • ついつい忙しいタイミングでコメントが短文になり、語気が強く感じられるようなコメントになっていた時に有用だった

  • コメントを読む側

    • 改善前より改善後のコメントで指摘をもらった方が気持ちが楽だったり、ネガティブな感情にならないのでいい

上記の所感の内容から、少なからずコメントを書く側も受け取る側もよい面を感じていると思わる。そして、意図せずコメントの受け手側にネガティブな感情が湧くようなコメントをしてしまったりという事がなくなるので、チーム全体としてみると心理的安全性の面でいい影響がありそうだと思われる。

※その他の所感として、コメントの評価結果の数値は結構ブレがあるな~というものもあり、プロンプト自体は要改善だなと感じている。

Chromeの拡張機能の開発について

ここからは、上記でみたMRのコメントを入力すると改善例を提示するようなChromeの拡張機能の実装について取り上げる。

今回実装した拡張機能の処理フローは以下のようになる。

  1. content-scriptでブラウザ上のコンテンツ(テキストエリアの文字列)を取得する

  2. メッセージパッシングの仕組みで、バックグラウンドで実行されるサービスワーカーにメッセージをリクエストする

  3. サービスワーカーでメッセージを受け取り、受け取ったメッセージの中のコメントを取り出し、OpenAI の API を呼び出す

  4. OpenAI の API のレスポンスを非同期で処理し、メッセージパッシングのレスポンスとして返す

  5. content-scriptでサービスワーカーからの応答を受け取る

  6. content-scriptでMRのコメント欄のDOMをjQueyで操作して改善提案を表示

ポイントは、content-scriptからしかWebページのコンテンツにアクセスできないので、content-scriptでWebページのコンテンツを取得し、逆にcontent-scriptでは実行できない Web API をサービスワーカー(service-worker)で実行するという部分。そしてそれを実現するためのメッセージパッシングの仕組みになる。

これを実装したものは以下になる(今回はPoC的な活動だったので実装自体はかなり雑になっている)。

// manifest.json
{
	"manifest_version": 3,
	"name": "Feedback on comments from ChatGPT",
	"description": "ChatGPT analyzes the wording of the comment and suggests a comment with softer wording.",
	"version": "1.0.0",
	"content_scripts": [
		{
			"matches": ["https://gitlab.one-shift.net/*"],
			"css": ["style.css"],
			"js": ["jquery-3.7.0.min.js", "content-script.js"]
		}
	],
	"background": {
		"service_worker": "service-worker.js",
		"type": "module"
	},
	"permissions": ["input"],
	"host_permissions": ["https://api.openai.com/*"]
}
// content-script.js
const loadingEl = `
		<div id="chatgpt-loading">
			<a href="#" class="chatgpt-info-icon" style="margin-right: 5px"></a>
			<div class="chatgpt-loading-text" style="margin-right: 10px">ChatGPTで評価中...</div>
			<div class="chatgpt-loader"></div>
		</div>
`;

const cardEl = (options = { rating: 0, improved_comment: '', advice: '' }) => `
		<div id="chatgpt-feedback-alert">
			<div class="chatgpt-feedback-result">
				<h6>ChatGPTによるコメントの評価結果</h6>
				<p>語気の柔らかさレベル:${options.rating}</p>
			</div>
			<div>
				<h6>コメントの改善例</h6>
				<p>${options.improved_comment}</p>
			</div>
			<div>
				<h6>コメントをする際のアドバイス</h6>
				<p>${options.advice}</p>
			</div>
			<div class="chatgpt-feedback-alert-footer">
				<button id="close-chatgpt-comment" type="button">閉じる</button>
				<button id="adopt-chatgpt-comment" type="button">
					改善後のコメントをクリップボードにコピー
				</button>
			</div>
		</div>
`;

const copyToClipboard = (text) => {
	if (navigator.clipboard) return navigator.clipboard.writeText(text);
	console.error('navigator.clipboard is not supported.');
};

$(function () {
	let isRegisteredOnblur = false;

	$(document).on('input', function (inputEvent) {
		const targetEl = inputEvent.target;

		if (targetEl.tagName === 'TEXTAREA' && !isRegisteredOnblur) {
			let evaluating = false;
			let currentImprovedComment = null;

			$(targetEl).on('blur', async function (e) {
				const input = $(`#${e.target.id}`).val();
				if (evaluating || input === currentImprovedComment) return;

				evaluating = true;

				$(`#chatgpt-feedback-alert`).remove();
				$(`#${e.target.id}`).after(loadingEl);

				// https://developer.chrome.com/docs/extensions/mv3/messaging/#simple
				const response = await chrome.runtime.sendMessage({ input });
				const improvedComment = JSON.parse(response.output).improved_comment;

				$(`#chatgpt-loading`).remove();
				$(`#${e.target.id}`).after(cardEl(JSON.parse(response.output)));

				$('#adopt-chatgpt-comment').on('click', () => {
					copyToClipboard(improvedComment);
					$(`#chatgpt-feedback-alert`).remove();
					currentImprovedComment = improvedComment;
				});
				$('#close-chatgpt-comment').on('click', () => {
					$(`#chatgpt-feedback-alert`).remove();
				});

				evaluating = false;
			});
			isRegisteredOnblur = true;

			// cancelボタンを押した時に、isRegisteredOnblurを初期化する
			$(`[data-testid="cancelBatchCommentsEnabled"]`).on('click', () => {
				setTimeout(() => {
					$(`#confirmationModal___BV_modal_footer_ .js-modal-action-primary`).on('click', () => {
						isRegisteredOnblur = false;
						currentImprovedComment = null;
					});
				}, 100);
			});

			// Start a reviewボタンを押した時に、isRegisteredOnblurを初期化する
			$(`[data-qa-selector="start_review_button"]`).on('click', () => {
				isRegisteredOnblur = false;
				currentImprovedComment = null;
			});

			// Add comment nowボタンを押した時に、isRegisteredOnblurを初期化する
			$(`[data-qa-selector="comment_now_button"]`).on('click', () => {
				isRegisteredOnblur = false;
				currentImprovedComment = null;
			});
		}
	});
});
// service-worker.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
	if (request.input) {
		const apiKey = '***********************************';

		fetch('https://api.openai.com/v1/chat/completions', {
			method: 'POST',
			headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey}` },
			body: JSON.stringify({
				model: 'gpt-3.5-turbo-0613',
				messages: [
					{
						role: 'system',
						content: `ソフトウェアエンジニアのコメント分析します。`
					},
					{
						role: 'system',
						content: `与えられたコメントの語気を分析して下さい。
5段階評価で評価し、語気を柔らかくし人間が心地よく感じる改善後のコメント例と、コメントの語気を柔らかくするためのアドバイスを提示してください。
ただし、JSON形式で出力してください。
JSONのキーは、以下の通りにしてください。
・input_comment: 入力されたコメント
・improved_comment: 語気を柔らかくした改善後のコメント
・rating: 5段階評価の値
・advice: コメントの語気を柔らかくするためのアドバイス
`
					},
					{
						role: 'system',
						content: `コメント: ${request.input}`
					}
				]
			})
		})
			.then((response) => {
				if (!response.ok)
					sendResponse({
						output: null,
						error: `Failed to fetch. Status code: ${response.status}`
					});
				return response.json();
			})
			.then((data) => {
				console.log(data);
				if (data && data.choices && data.choices.length > 0)
					sendResponse({ output: data.choices[0].message.content, error: null });
			})
			.catch((e) => {
				console.log(e);
				sendResponse({ output: null, error: e.message });
			});
	}
	return true;
});

以下で上記のコードについて少し補足する。

メッセージパッシングの実装方法について

メッセージパッシングの実装方法は公式に例があるので、それが参考になるが、例えば以下のように実装することで、Webページのボタンをクリックしたときにサービスワーカー(service-worker)にメッセージ"{ greeting: 'hello' }"を送ることができる。そして、sendResponse関数を実行することでsendMessageに対する応答をする事ができる。

// content-script.js
$(function () {
	$(`button#hoge`).on('click', async (inputEvent) => {
		const response = await chrome.runtime.sendMessage({ greeting: 'hello' });
		console.log(response); // { farewell: 'goodbye' }
	});
});
// service-worker.js
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
	console.log(sender.tab ? 'from a content script:' + sender.tab.url : 'from the extension');
	if (request.greeting === "hello") sendResponse({farewell: "goodbye"});
});

runtime.onMessage のイベントハンドラで非同期処理を実装する場合の注意

メッセージパッシングの仕組み上で非同期処理を実装する場合は、以下の引用の通りイベントハンドラの関数からは true を返す必要がある。

In the above example, sendResponse() was called synchronously. If you want to asynchronously use sendResponse(), add return true; to the onMessage event handler. 上記の例では、sendResponse() は同期的に呼び出されました。非同期に sendResponse() を使用したい場合は、onMessage イベント・ハンドラに return true; を追加します。

ここで注意が必要で、非同期処理というと async・await で実装する癖がある場合、以下のような実装をしがちだと思う。

chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
	if (request.input) {
		try {
			// await fetch(...)のような非同期処理
		}
		catch (e) {
			// ...
		}
	}
	return true;
});

ただ、上記の実装は true が return されるのではなく、"Promise<boolean>" を返してしまうので意図通りにならない。そこで昔ながらのthen/catchを用いた実装にする必要がある。

chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
	if (request.input) {
		fetch(...)
			.then(response => {...})
			.catch((e) => {...})
	}
	return true;
});

上記のように実装することで、Promise ではなく true がハンドラ関数の戻りになるので、意図通りに動作させることができる。

まとめとして

今回はChromeの拡張機能で OpenAI の API を利用して、コメントの語気を柔らかくするという事をやってみた。最初はちょっとした遊び心でやってみようと思ったことだったが、チームの中でPoC的に利用をしてみて、以下のような反応が得られた事から心理的安全性に関する取り組み役に立ったのではないかと個人的には感じている。

改善前より改善後のコメントで指摘をもらった方が気持ちが楽だったり、
ネガティブな感情にならないのでいい

語気の柔らかさレベルは適当な感じがするが、改善例は確かに語気が柔らかくなっている気がするので、意図せず強い語気になるのを防げるのでいい

今後もツールなどを使った方法以外でもこうした心理的安全性を高めるような取り組みができればいいなと思っている。

ちなみに、今回のPoCの中で出た技術的な課題として、APIの応答を待つ時間が長いと3~5秒くらいあり、それを短くする部分が挙げられた。

《この公式ブロガーの記事一覧》


執筆者プロフィール:Katayama Yuta
認証認可(SHIFTアカウント)や課金決済のプラットフォーム開発に従事。リードエンジニア。 経歴としては、SaaS ERPパッケージベンダーにて開発を2年経験。 SHIFTでは、GUIテストの自動化やUnitテストの実装などテスト関係の案件に従事したり、DevOpsの一環でCICD導入支援をする案件にも従事。その後現在のプラットフォーム開発に参画。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/