これははてなエンジニア Advent Calendar 2023の2024 年 1 月 13 日の記事です。
昨日 id:mangano-itoさんのWear OS スマウォでスマホと連携する単語帳アプリをつくってみよう でした。
Wear OS! 全体感の図が分かりやすくて必見ですね。
私は自分のNotionメモに
- WearOSで、タップしたら出退勤してくれるボタン
- WearOSで、音声入れたらよしなに家計簿入力してくれる
- WearOSで、心拍数が一定を超えたらもっと頑張れって煽ってくれる
...
とかやりたいことだけ書いてありますが、途中で飽きて逃亡してます。
本編
日々の情報収集、日記、TODOやちょっとした思いつきなどあらゆるメモをNotionに書いてます。
今回、そのメモから記事を作って投稿する ということをやってみました。
投稿した記事 👇
AIが書く最初と最後の枕詞的な文章は消して、あとは導入だけ自分で書いてます。文章感がガラッと変わるので面白みありつつこの辺も調整したい。
使ったサービス
Notion内には、IntergrationというNotion内での特定のアクションをトリガーにプログラムを動かすってことができるっぽいのですが、今回は普通にAPI使って記事を取得しただけです。
Assistans API beta を使ってます。Code Interpreterが使えて、事前に与えるプロンプトにファイルが使えます。
使用言語はKotlinです。
(ReadMe後ほど書きます。。)
何をやったか
シンプルですね。とにかくワンショットで投稿までいってるように見えます
Notion周り
NotionはMarkDownではない
Notionは MarkDownの記法以外にも独自の属性を持った文字があって、ここ のtype 分に属性があります。
Qiitaに投稿するにあたってMarkDownがあれば十分だけど、NotionにMarKDonw形式で返してくれる機能はないので、NotionAPIをいい感じにラップしたライブラリを使ってます。
サンプル
const { Client } = require("@notionhq/client");
const { NotionToMarkdown } = require("notion-to-md");
const fs = require('fs');
// or
// import {NotionToMarkdown} from "notion-to-md";
const notion = new Client({
auth: "your integration token",
});
// passing notion client to the option
const n2m = new NotionToMarkdown({ notionClient: notion });
(async () => {
const mdblocks = await n2m.pageToMarkdown("target_page_id");
const mdString = n2m.toMarkdownString(mdblocks);
console.log(mdString.parent);
})();
Notion側のページのIDだけ指定すればよしなにとってきてくれるので便利。JSをkotlinでラップする暇はなかったのでローカルにサーバー立ててそこと通信する形をとりました。
どうやってページを持ってくるか
(人によるかも)リレーションで作ったタグをメモにつけて、1つのデータベースでそのメモ群を管理してるので
そのタグがくっついてるページをとってきてます。
// client/src/main/kotlin/processorImpl
val pageData = notion.retrievePageIds().bind()
val pageId = pageData.firstPage().bind()
val byteMarkDown = notion.getMarkDownContent(pageId).bind()
// ... OpenAIとかQiitaとか
notion.deleteTag(qiitaTagRelation).bind()
- データベースIDからタグ付きページIDとってくる
- ページIDから中身の文章取ってくる
- 諸々終わったらタグを消す
1つのデータベースに全てのページをつっこむ管理方法だから割とシンプルに行けてます。(逆に複数のデータベースがあると大変そう)
前述でちょっと書いてますが、Notion上にアプリを乗っけるやり方が良さそうではあるのですが、今回はそこまで行けず。。
OpenAI周り
プロンプト
このアシスタントの目的は、Markdown形式で書かれたメモを技術記事に変換することです。以下を守りつつ、再構成した記事を書いてください。形式はマークダウンとします。
- 入力はマークダウンファイルとして渡されます。
- メモの核となるアイデアを把握し、わかりやすさのために内容を再構成します。
- 技術的な詳細とコードスニペットは正確に保ち、理解可能で実用的なものに焦点を当てます。
- 組織化のために見出しを使用しますが、多用は避けます。シンプルで魅力的な導入部だけで読者を引き込むのに十分です。
- メモの疑問や問題に対しては、簡潔な説明や迅速なリサーチを行ってください。
- 最終的な記事は、技術的に正確でありながら親しみやすく、メモの元のキャラクターとアプローチャブルさを保持するものであるべきです。
- 再構成した記事のみを返却してください。
- 使用言語は日本語としてください。
(まあ、これもGPTに作ってもらったんですが)
これ + 今まで書いた記事をいくつかファイルで渡してます。<- こっちはあまり意味を感じてない
あとは、OpenAIのクライアントはKotlin製のライブラリがあったりします
val fileId = client.uploadFile(byteString).bind()
client.createMessage(fileId).bind()
val runId = client.run().bind()
client.retrieveRun(runId).orElseIntervalActionAsync(
predicate = { error -> error is OpenAiError.RunningStatusNotComplete },
action = { client.retrieveRun(runId) },
interval = 30000L, // 最大3min
retryCount = 5
).bind()
val message = client.retrieveMessage().bind()
message.text.value
AIが生成するまで1~2分かかるので、インターバルを設けてステータスを確認しにいってます。
Qiita
これはPOSTだけ使ってます。特に書くことはなさそう
久々に昔作ったBotで動かしたら、private フラグ立てるの忘れててtestが公開されたミスをしました
private
限定共有状態かどうかを表すフラグ (Qiita Teamでは無効)
Example: false
Type: boolean
Kotlin-Resultが便利 という話
👇のkotlin-result。最近使ってなかったですがやっぱり便利。
いわゆるモナドな文脈 が作れるのでエラー気にしなくてよかったり、ネストがなくなったり
val sum: Result<Int, DomainError> = binding {
val x = functionX().bind()
val y = functionY().bind()
val z = functionZ().bind()
x + y + z
}
インターバル付きのリカバリー処理が orElseの連鎖だけで 書きやすい。
suspend fun <T, E> MResult<T, E>.orElseIntervalActionAsync(
action: suspend () -> MResult<T, E>,
retryCount: Int = 2,
interval: Long = 1000L,
predicate: (E) -> Boolean = { true }
): MResult<T, E> {
return this.orElse { error ->
if (retryCount > 0 && predicate(error)) {
delay(interval)
action().orElseIntervalActionAsync(action, retryCount - 1, interval, predicate)
} else {
this
}
}
}
OpenAIの文章生成待つのに、インターバルかけて何度かリクエスト送るのに使ってました。
そのうちやりたいという意気込みだけある
- NotionのIntergrationでNotion上にアプリ乗っける
- Fine-tuningでもっと自分っぽい言葉遣いな記事にする
- 画像に対応しなければ、、
- open interpreter で野良のローカルなモデルで動かす
- KMPのJSで、NotionAPIをラップしてるJSライブラリをラップする
- (そもそもNotion側にもAIがいるので最初から記事にしちゃえばいいのでは)
明日は、id:papix さんです。