はじめに
こんにちは小松です。
プレックスでは日々のタスク管理ツールとしてNotionを利用しています。 美しいUI、その印象とは裏腹に機能は骨太、ここ1〜2年でユースケースも劇的に増えて「こんな使い方もありなのか〜」と思う日々が続いております。 (そう、私はNotionが大好きです)
そして世は生成AI時代ですがNotionでも今年の春にAI機能がリリースされました。 めっちゃ便利ですよね。テキストのちょっとした言い換えであったり、フォーマットの修正であったり、当然翻訳もしてくれるし。 本当に良いところばかりです。ただ少しお値段が、可愛くないんですよね。ChatGPTのAPIと比べちゃうと。もちろん利用頻度によっても変わってくるので、参考までに、記事の最後に今回のユースケースでの費用比較を掲載しています。
全社的に使うのであれば良いのですが、やりたかったのは1プロパティに本文の要約を入れてほしいだけだったので今回はChatGPTに働いてもらうことにしました。
今回やりたいこと
今回はNotionのAIプロパティ編集機能("AIによる要約","AI:重要情報","AI:カスタム自動入力"みたいなやつ)をChatGPT APIを利用して実現したいと思います。
現在プレックスジョブのプロダクト開発では毎月60〜70程度のタスクが起票〜クローズされています。プロダクト開発を運用する上で、例えば今月何をやったか一覧で振り返りたいな、という時があり一覧表示にしたりするのですがタイトルを一覧表示してもそれが何を成したタスクなのかって思い出せる事もあれば思い出せない事もかなりあります。そこで私たちはそのタスクで何を成したかを要約するプロパティがほしいなと思いました。ただし、運用上今まで書いていなかった項目を新しく各タスクの担当者が書いていくというのも難しい話なので、ここでAIに要約してもらおうとした次第です。
なので今回はプレックスジョブの開発タスクを管理するデータベースにタスクページの'目的/ゴール'プロパティを本文から抽出して登録する。
これを今回のゴールとします。
今回登場する予定の人たち
- Notion API
- 本文の提供
- プロパティの更新
- ChatGPT API
- Notionの本文を要約したテキストを生成
- node.js(+TypeScript)の実行環境(今回作るスクリプト)(何でも良い)
- Notionから更新対象のページを取得する
- Notionから本文を取得してChatGPTへ渡す
- ChatGPTから返ってきた値をNotionのプロパティに戻す
今回触れないこと
のあたりは今回触れません。特にプロンプト周りで書いてある事は参考情報で、これで良い応答が得られることを約束するものではありません。
実装!
今回はNotion周りのライブラリが充実していたのでnode.js(TypeScript)でスクリプトを書きます。
詳細はステップ毎に説明しますが、今回は以下のようなステップでNotionのページプロパティを更新します。
①Notionから更新対象のページリスト(ページID)を取得する
②本文を取得してマークダウンに変換する
③マークダウンに変換した本文をChatGPTに投げ込む
④ ③で出力した情報をNotion構造に変換してNotionプロパティを更新する
事前にページIDが明らかになっており、単ページを更新するだけであれば②〜④のステップで実現できます。
①Notionから更新対象のページリスト(ページID)を取得する
実用を考えると、まずは更新対象のレコード(ページ)を抽出する必要はあります。コード見本にあるfilter
オブジェクトに関しては公式のこのあたりに説明がありますが、Notionの画面から設定する要領とほとんど一緒になります。
- 前提
- 今回NotionAPIへのアクセスは全てNotionSDKを使う
- 対象のレコード(ページ)は'目的/ゴール'プロパティが空欄のものとする
- ここでやること
- 対象のデータベースから今回更新対象のページリスト(ページID)を取得する
import { Client } from "@notionhq/client"; import { QueryDatabaseResponse } from "@notionhq/client/build/src/api-endpoints"; // 各環境に合わせて設定してください const notionToken = "xxxxxxxxxxxx"; const databaseId = "xxxxxxxxxxxx"; const notion = new Client({ auth: notionToken, }); const pages = async (): Promise<QueryDatabaseResponse> => { const response = await notion.databases.query({ database_id: databaseId, // 更新対象のプロパティが未記入のものが対象 filter: { property: "目的/ゴール", rich_text: { is_empty: true, }, }, }); return response; };
この後の処理はここで取得したpages
に対して1ページずつ処理を行っていくイメージになります。
②本文を取得してマークダウンに変換する
Notion APIを使うとまず驚くのですが、notionのpageオブジェクトはかなり複雑な構造体になっています。notionのリッチなテキストエディタを実装する上で必要な情報なのでしょうが、今回の用途では必要のない要素も多いので本ステップでは本文をマークダウン形式へ変換してChatGPTのトークンを節約しつつ理解のし易い形に変換します。
- 前提
- 本文のマークダウンへの変換はライブラリNotion-to-MDを使う
- ここでやること
- ①で取得したページリストの本文を取得する
- 本文をマークダウン形式のテキストに変換する
import { Client } from "@notionhq/client"; import { NotionToMarkdown } from "notion-to-md"; // 各環境に合わせて設定してください const notionToken = "xxxxxxxxxxxx"; const notion = new Client({ auth: notionToken, }); const pageId: string = "xxxxxxxxxxxx"; // ステップ①で取得したpageId const n2m = new NotionToMarkdown({ notionClient: notion }); const mdblocks = await n2m.pageToMarkdown(pageId); const mdString = n2m.toMarkdownString(mdblocks); const mdPageBody = mdString.parent;
③マークダウンに変換した本文をChatGPTに投げ込む
これでようやくChatGPTが食べやすい文書になったのでこれを渡します。
import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai"; const gptModel = "gpt-3.5-turbo"; // or "gpt-4" const mdPageBody: string = 'xxx'; // ステップ②で作成したマークダウン形式の本文、実際の運用では1000字切り捨てとしている const openAiApiKey = "xxxxxxxxx"; const openAiOrganization = "xxxxxxxxx"; const openAiConfiguration = new Configuration({ apiKey: openAiApiKey, organization: openAiOrganization, }); const openai = new OpenAIApi(openAiConfiguration); const messages: ChatCompletionRequestMessage[] = [ { role: "system", // 全ページの処理に共通するプロンプト content: "以下のテキストで目指しているゴール、目的を抽出して1〜6個程度の箇条書きで提出してください。箇条書きの始まりは'- 'から始まるマークダウンの形式で出力してください。", }, { role: "user", content: mdPageBody.slice(0, 1000), // 1000字まで渡す }, ]; const chat_completion = await openai.createChatCompletion({ model: gptModel, messages: messages, temperature: 0.5, // お好みで max_tokens: 2000, // 運用に合わせて設定してください }); const contents: string = `${ chat_completion.data.choices[0].message?.content // 生成された本文 } \n\n- Created by ${chat_completion.data.model} Usage: ${JSON.stringify( chat_completion.data.usage // この処理で利用したトークン数が取得できるので残している )}`;
プロンプトはこの記事では重点を置きませんが、とりあえず'マークダウンぽいリストで返して'ってお願いするとGPT3.5でも4でも返却フォーマットをコントロールしやすかったです。
プロンプト実行時に設定できるパラメータは以下のページに説明があります。temperatureは数字が大きい(~2)ほど揺れの大きい回答になるそうですが、今回はあまり調整していません。
④ ③で出力した情報をNotion構造に変換してNotionプロパティを更新する
まず、今回データを投入したいプロパティですが、Notionの画面上はテキスト(Text)
で作成されています。これはNotion APIの世界ではrich_text
タイプになるようです。
rich_text
についてはNotionの公式に説明はあります。上手く使えれば書式をいじったり色々遊べるのですが、なかなか仕様も複雑なので今回は極力シンプルな実装としています。
- ここでやること
- ChatGPTから取得したテキストを
rech_text
のブロックに入れる - Notionプロパティを更新する
- ChatGPTから取得したテキストを
import { Client } from "@notionhq/client"; // 各環境に合わせて設定してください const notionToken = "xxxxxxxxxxxx"; const contents: string = 'xxxxxx'; //前ステップでChatGPTが生成した文字列 const pageId: string = "xxxxxxxxxxxx"; // ステップ①で取得したpageId const richText: any = []; // notionに返すrich_text構造体 (any...) // プロパティの内容を作成 richText.push({ type: "text", text: { content: contents, link: null, }, plain_text: contents, href: null, }); const notion = new Client({ auth: notionToken, }); // Notionのページプロパティを更新 await notion.pages.update({ page_id: pageId, properties: { "目的/ゴール": { rich_text: richText, }, }, });
これでNotionのプロパティが更新されているはずです。お疲れ様でした!
ちなみに私のプロンプト力だとGPT-4の力を持ってしてもNotionAI程気の利いた要約を作れませんでした・・・。NotionAIではそこまでプロンプト作成に気を遣わずにイケちゃったので、Notion側でどのようなベースプロンプトが実装されているのかはかなり気になります。
おわりに
今回、NotionAIの費用にびびってなんちゃってNotionAIをChatGPTを利用してDIYしてみましたが、結果的に本家NotionAIの便利さを再確認することになりました。やはり組み込みで機能が提供されているという点での完成度の高さに敵いません。ただし、この後付録としてつけますが、利用用途によってはDIYした方が遥かに安価に運用できる可能性が高いので、まずはAIの運用を試してみるというのは全然ありじゃないかなーと思いました。
また、プレックスでは一緒に働いて頂けるエンジニア、デザイナーをまだまだ募集しております!気になるところがあれば、こちらに採用情報まとまっておりますので、ご覧いただければと思います。
以上、長くなってしまいましたがここまでお付き合い頂きありがとうございました!
付録:NotionAIとChatGPT APIの費用比較
以下全ての利用料は2023年7月19日時点の情報です。
notionはワークスペースの参加メンバーに対する課金、ChatGPTは処理量に対する課金なのでケース・バイ・ケースではあります。今回以下のような条件で比較します。
- 処理が必要なページは月100ページ
- Notionワークスペースのメンバーは10名、年払い
サービス | 基本情報 | 計算方法 | コスト(ドル) | コスト(円) 1ドル140円で計算 |
---|---|---|---|---|
Notion API | 基本人数課金(処理量に関係なく人数に対して定額) | 10名 × 8ドル | 80ドル | 約11,200円 |
GPT3.5 Turbo | input:$0.0015 / 1K tokens output: $0.002 / 1K tokens |
[input] 750 * 100 / 1000 * 0.0015 + [output] 150 * 100 / 1000 * 0.002 | 約0.15ドル | 約21円 |
GPT4 | input:$0.03 / 1K tokens output: $0.06 / 1K tokens |
[input] 750 * 100 / 1000 * 0.03 + [output] 150 * 100 / 1000 * 0.06 | 約3.0ドル | 約420円 |
今回スクリプトの実行環境は考慮していないので、
- 何をトリガーとして実行するか
- 実行基盤を何にするか
によってもChatGPT API実装の方は費用感変わってくると思います。
が、処理する件数が多くない場合はGPT-4を使ってもChatGPTの方が圧倒的に安いです。