Notionのメモをもとに記事を作らせて投稿してみる

これは[はてなエンジニア Advent Calendar 2023](https://developer.hatenastaff.com/advent-calendar-2023)の2024 年 1 月 13 日の記事です。
昨日 [id:mangano-ito](https://mangano-ito.hatenablog.com/)さんの[Wear OS スマウォでスマホと連携する単語帳アプリをつくってみよう](https://mangano-ito.hatenablog.com/entry/2024/01/12/000229) でした。

Wear OS！ 全体感の図が分かりやすくて必見ですね。

私は自分のNotionメモに
- WearOSで、タップしたら出退勤してくれるボタン
- WearOSで、音声入れたらよしなに家計簿入力してくれる
- WearOSで、心拍数が一定を超えたらもっと頑張れって煽ってくれる


...
とかやりたいことだけ書いてありますが、途中で飽きて逃亡してます。

# 本編

日々の情報収集、日記、TODOやちょっとした思いつきなどあらゆるメモをNotionに書いてます。
今回、そのメモから記事を作って投稿する　ということをやってみました。
投稿した記事 👇

https://qiita.com/kk__777/items/1b0958d3137546df916a

AIが書く最初と最後の枕詞的な文章は消して、あとは導入だけ自分で書いてます。文章感がガラッと変わるので面白みありつつこの辺も調整したい。

# 使ったサービス

https://developers.notion.com/

Notion内には、IntergrationというNotion内での特定のアクションをトリガーにプログラムを動かすってことができるっぽいのですが、今回は普通にAPI使って記事を取得しただけです。

<br>

https://platform.openai.com/docs/overview

[Assistans API beta](https://platform.openai.com/docs/assistants/overview) を使ってます。Code Interpreterが使えて、事前に与えるプロンプトにファイルが使えます。

<br>

https://qiita.com/api/v2/docs

<br>

使用言語はKotlinです。

https://github.com/kk-777-1127/LazyWriter

（ReadMe後ほど書きます。。）


# 何をやったか

```mermaid
sequenceDiagram
    participant l as App
    participant n as Notion
    participant o as OpenAI
    participant q as Qiita

    l ->> n: 
    Note over l,n: 特定の元メモを探す
    n -->> l: 
    l ->> o: 
    Note over l,o: 記事を整形してもらう
    o -->> l: 
    l ->> q: 
    Note over l,q: 投稿
```

シンプルですね。とにかくワンショットで投稿までいってるように見えます

## Notion周り

### NotionはMarkDownではない

Notionは MarkDownの記法以外にも独自の属性を持った文字があって、[ここ](https://developers.notion.com/reference/block) のtype 分に属性があります。
<br>
Qiitaに投稿するにあたってMarkDownがあれば十分だけど、NotionにMarKDonw形式で返してくれる機能はないので、NotionAPIをいい感じにラップしたライブラリを使ってます。

https://github.com/souvikinator/notion-to-md

サンプル
```javascript
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つのデータベースでそのメモ群を管理してるので

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/700755/0d5ee206-c8dc-1404-77a2-6420622cac55.png" width=360>

そのタグがくっついてるページをとってきてます。

```kotlin
// 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製のライブラリがあったりします

https://github.com/aallam/openai-kotlin

```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分かかるので、インターバルを設けて[ステータス](https://platform.openai.com/docs/assistants/how-it-works/run-lifecycle)を確認しにいってます。

# Qiita

これはPOSTだけ使ってます。特に書くことはなさそう

https://platform.openai.com/docs/assistants/how-it-works/run-lifecycle

久々に昔作ったBotで動かしたら、private フラグ立てるの忘れててtestが公開されたミスをしました
```
private
限定共有状態かどうかを表すフラグ (Qiita Teamでは無効)
Example: false
Type: boolean
```

# Kotlin-Resultが便利 という話

👇のkotlin-result。最近使ってなかったですがやっぱり便利。

https://github.com/michaelbull/kotlin-result

いわゆるモナドな文脈　が作れるのでエラー気にしなくてよかったり、ネストがなくなったり
```kotlin
val sum: Result<Int, DomainError> = binding {
    val x = functionX().bind()
    val y = functionY().bind()
    val z = functionZ().bind()
    x + y + z
}
```

インターバル付きのリカバリー処理が orElseの連鎖だけで 書きやすい。
```kotlin
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](https://platform.openai.com/docs/guides/fine-tuning)でもっと自分っぽい言葉遣いな記事にする
- 画像に対応しなければ、、
- [open interpreter](https://github.com/KillianLucas/open-interpreter/) で野良のローカルなモデルで動かす
- KMPのJSで、NotionAPIをラップしてるJSライブラリをラップする
- （そもそもNotion側にもAIがいるので最初から記事にしちゃえばいいのでは）
---

明日は、[id:papix](https://papix.hatenablog.com/) さんです。