🦅

はやぶさノーティング③ ノートのフレームワーク化と自動化の第一歩

に公開

前回のおさらい

前回の記事では、はやぶさノーティングの基本環境構築と解説までを実施した。


TL;DR

今回は、まだもう少し、今後色んな便利機能を追加していく前にこのノート術の考え方の整理とフレームワーク化をしておくことで、後々の機能追加をラクにしていく。料理をする前には、キッチンを使いやすくしておいた方がいいよね🍎

  • GTDの考え方を参考にしながら、タスク管理術としてノートのフォルダ管理をきめ細かく考えておく
  • 今後のために、プロンプトを構造化しておくことでメンテナンスしやすくする
  • タスクの自動追加までをいったん仕組み化する。

GTDを意識したノートのディレクトリ構造と役割

ライフハック野郎たちが、人生を賭してロジックを研ぎ澄まし、絶対に誰にもバカにさせるものかと一厘の隙すらない記事をGetting Things Done - Wikipediaに書いてくれている。

今回のはやぶさノート術も参考にするところはしていきつつ、ちょっとアレンジをしてノーツのディレクトリ構造を決める。

  • 0_inbox/: 未整理の情報を一時的に置く場所。すべての新しい情報はまずここに集約。
  • 1_tasks/: タスク管理用フォルダ。タスクはここに分類され、進捗状況を管理。
  • 2_projects/: プロジェクト単位の情報を格納。各プロジェクトごとにフォルダを作成して管理。
  • 3_knowledges/: 知識や参照情報を保存。再利用可能な情報をここに集約。
  • 4_archive/: 完了したプロジェクトや参照頻度が下がった情報をアーカイブ。
  • 5_prompts/: LLMへの指示(プロンプト)を管理。再利用可能なプロンプトをここに保存。
  • _templates/: Obsidianのテンプレート用フォルダ。日次ノートやタスクノートの雛形を格納。

まあまあこれで使いやすそうなんですが、もう少し細かい運用も、今ここで決める。

0_inbox/ フォルダ

基本的にはフラットな構造で、下は階層化せずなんでも放り込めばよいということにする。

  • LLMの自動化機能もここに色々なものを放り込む。
  • ユーザも雑に、ミーティングのメモやら人に受け取ったファイル、アイデアの断片、ChatGPTなどのAIチャットと会話したログなどを雑に放り込む。

そして、逆にこのフォルダの下では、人間もLLMもここではノートに追記したり更新するという動作は基本的にしない。そういう行為は他のタスクやプロジェクトのフォルダでやる。

あくまでこのフォルダは、後で整理するかと思ったファイルをバンバン放り込むだけで、ノートへの未反映の情報しかない。そしてLLMを使った自動化スクリプトは後ほど、ここに何かが入っていたら、これどうしようかな?と考えてノートへの更新や、タスクとしての把握、適切な場所への移動などを考えて実行する。ユーザの意図を読み取ってやってくれる。

処理済みのファイルを一応残しておくのは 4_archive/ フォルダの下に inbox/ ってフォルダを作って、そこに移動すr。

1_tasks/ フォルダ

ここはユーザが日常的に見たり触ったりするフォルダ。普通のタスク管理フォルダになっているが、GTDと私の好みをもとに、サブフォルダをこう作る。

  • 1_tasks/
    • 0_inbox/: 未整理のタスクを一時的に置く場所。すべての新しいタスクはまずここに集約されます。
    • 10_next/: 今週または今日中に着手したいタスク。常時 1〜3件程度を上限に集中。毎朝 Obsidian またはタスクパネルで見返す対象。
    • 20_todo/: 急ぎではないが、期限や優先度が見えているタスク。10_nextが空いたら上から移動。定期レビュー(週次)で内容を確認・調整。
    • 30_waiting/: 他者にボールがある状態。依頼中、レビュー待ち、会議待ちなど。ノート内に「誰が・何を・いつまでに」明記すること。
    • 40_someday/: 明確な期限も依頼主も存在しない「将来の種」。改善アイデア、やりたい目標、調査・試したい技術など。月1回の見直しで内容の棚卸しを行う。
    • 99_done/: 完了したタスクを格納。定期的にアーカイブ(4_archive/)へ移動。

ここには一つ一つの一定のタスクの単位で1ノートが格納されている。なお、ノートの名前はタスクの内容をそのまま書く。タスクの内容は、LLMに自動生成させることもできるし、ユーザが手動で書くこともできる。

一つ大事なのが、GTDでは「するのに2分とかからない仕事は、今すぐ行う」という「2分ルール」がある。これはノート術として理にかなっていて、どうでもいい一瞬で終わる仕事をやるにおいてノートを取っている時間は無駄で、そんなことする前に終わらせてすっきり忘れればいい。私の場合はここは10分くらいかな。人から何か質問されて答えるとか、誰かに対してメール一本書いて何かを依頼するとかでもう頭から消去してしまえばいい仕事は追わない。他の人が責任を持っている仕事に一瞬巻き込まれただけで、対応したら忘れればいいようなものも。

ただし、所要時間は短くても、結果を見届けなければ安心できない仕事や、後で自分にもアクションが色々来るかもなと思うものはタスク化して30_waitingに入れておくとよい。あ、あれどうなったかな、って思い出した方がいいもの。一定期間過ぎてどうでもよくなったら、99_doneに放り投げればいいだけ。

なお、ひとつひとつのタスクについてのノートは、以下くらいのフォーマットになっていればよい。毎回書いたり更新したりするのは面倒なので、以下にそこを、画面のスクショとるだけ、とか、Teamsのチャットや会議の文字お越しをコピペするだけとか、生成によって作るかをこだわることになる。

# 🚧 <タスク名>

## 🎯 目的
このタスクで何を達成したいのか? なぜやるのか?

## 📎 関連プロジェクト
- [[2_projects/project-name]]
- タスク起点:[[0_inbox/xxx]]

## 🧾 インプット情報
- テキストメモ
- [[3_knowledges/xxxxx]]
- ![[screenshot_2025-06-01.png]]
- [会議録メモ →](../../2_project/Someproject/Meetings/2025-05-30_team-checkin.md)

## 🔹 サブアクション
- [ ] ○○について調査する
- [ ] 関係者に確認
- [ ] ドラフト資料を作成
- [ ] 上司に提出・レビュー依頼

2_projects/ フォルダ

GTDの定義と似ているが少し変えて、**「1か月以上期間が継続し、多くの関係者が関与し、発生するタスクや情報の量が多い業務上の活動」**というような定義に当てはまるものをプロジェクトと呼びここで管理することにする。

そもそもの「プロジェクト」という言葉の定義には「ゴールと期限が存在する」とされるが、それはここでは外す。例えば○○という組織のマネジャーというロールについて回る作業をこなす、というものもプロジェクト化してここでノートをとる。

ここから派生するタスクは、1_tasks/フォルダに随時作るが、共通情報の蓄積をこのフォルダで行う。

2_projects/ フォルダの下は、フラットにプロジェクトごとの名前を付けたフォルダを作成し、その下は自由に階層化やノートを作ればよいとする。プロジェクトをクローズしたら、そのフォルダを 4_archive/projects フォルダ配下にプロジェクトごと移動してアーカイブする。

3_knowledges/ フォルダ

ここはGTDと関係なく、LLMの知識ベースとして食わせる情報を格納する。例えば、すごく役立つドキュメントとかマニュアルを見つけて、なんのプロジェクトにも紐つかないし、タスクでもないけど、自分のアシスタントがこういうことを知っておいてくれて、いつかポンと必要な時にその知識をインプットしてくれたらいいな、みたいな情報を入れる。

できれば、大量の情報(例えば、大量のトレーニング資料とか、プロジェクトの成果物とか)を一気に取り込めるような仕組みもどこかで作れると素敵。

4_archive/ フォルダ

他のフォルダで、参照頻度が下がったりして完了して時間がたった情報を放り込む。LLMに対しては、参照の優先度を下げさせる(これによりこのノートを長年使っても、フレッシュなトピックに関してを中心に仕事をさせることができる)。

5_prompts/ フォルダ

これはノートとは別で、今後LLMに対してこのノートの記述、参照、操作を色々と頼むことになるが、その際にプロンプト調整をしながら仕組みを整えることが予想されるので、これをまたいちいちどこかで管理するのも面倒なので、ここに置く。

.templates/ フォルダ

新しいノートを生成する際のテンプレートを置く。Obsidianの機能で使うものだが、細かい使い方は色々やりながら調整する。

グランドプロンプトの準備

これからAPIを使ったLLMに仕事をさせる準備もしていく。プロンプトを作ってメンテナンスするのって結構面倒なので、入れ込みたいにしておければ便利かなと思うんですよね。ちょっと言葉でうまく説明できないので、実物を見てほしいのですが。

5_prompts/ にまずは、以下のような4ファイルを作る。

5_prompts/
├── system_prompt.md
├── ground_prompt.md
├── user_information.md
└── note_structure.md

で、それぞれの中身は以下のようにする。

system_prompt.md

あなたは、IT技術者であるユーザが業務を実行するうえで必要な情報管理を行うためのアシスタントです。
主にユーザのノートの管理、更新、読み取りなどを、タスク管理や知識管理も含めて行います。
ユーザの役に立つために様々な要求を受けて実行します。
ユーザの指示には忠実に従うようにし、アウトプットは常に充実したものを出力してください。

ground_prompt.md

以下の依頼を実行してください。

# 現在の日時
{{DATETIME}}

# ユーザの情報
{{./user_information.md}}

# ユーザのノートの構成
{{./note_structure.md}}

# 前提事項・アウトプットの形式の指示
{{instruction}}

# 依頼内容
{{request}}

# 依頼を達成するために必要な情報、他のノートファイルの情報
{{reference}}

user_information.md

# ユーザの情報
- 名前: 若草 奈良助
- 所属: ダイブツシステム株式会社
- ID: narasuke.wakakusa@daibutsu-system.com
- 役割: 念仏詠唱システムの運用保守を実施
- 通常実施する業務に関しての補足情報
  - 念仏詠唱システムは、Node.jsで実装された日本の近畿地方の宗教法人に採用されているシステムで、ユーザはこれの開発と保守エンジニアとして働いている。
  - 直属の上司は、同社第一事業部長の袈裟川 道弘(Michihiro Kesagawa)。彼がユーザに対して作業の指示を実施する。

note_structure.md

あなたが管理するノートは以下のような構成になっています。

/0_inbox/ -- 未整理の情報を一時的に置く場所
/1_tasks/
  - 10_next/ -- ユーザがすぐに取り掛かるタスク
  - 20_todo/ -- ユーザが今後取り掛かる予定のタスク
  - 30_waiting/ -- ユーザが他者の対応を待っているタスク
  - 40_someday/ -- ユーザが将来的に取り掛かりたいタスク
  - 99_done/ -- 完了したタスク
/2_projects/
  - プロジェクト名/
    - プロジェクトに関するノート群
/3_knowledges/ -- 知識や参照情報を保存
/4_archive/ -- 完了したプロジェクトや参照頻度が下がった情報をアーカイブ
/5_prompts/ -- LLMへの指示(プロンプト)を管理
/_templates/ -- Obsidianのテンプレート用フォルダ

こういう感じで、プロンプトを入れ子構造にしておいて、メンテナンスしやすくする。注入とかをさせる。オブジェクト指向プログラミングでいうInheritanceなのかCompositionなのか、そういうとこでしょうか。

例えば、0_inbox/ フォルダに何かが入ったら、それがタスクの追加だと分かった場合にタスクノートを 20_todo フォルダに生成するというアクションを自動化する場合には、そういうプロンプトが必要になるが、以下のように作る。

5_prompts/
├── task_add/
│   ├── instruction.md
│   └── request.md
├── system_prompt.md
├── ground_prompt.md
├── user_information.md
└── note_structure.md

task_add/instruction.md

- 関連情報を参照して、ユーザがノートとして記録すべきタスクのノートを作成する
- 出力結果は必ずこの形式にする

# 🚧 <タスク名>

## 🎯 目的
このタスクで何を達成したいのか? なぜやるのか?

## 📎 関連プロジェクト
- [[2_projects/project-name]]
- タスク起点:[[0_inbox/xxx]]

## 🧾 インプット情報
- テキストメモ
- [[3_knowledges/xxxxx]]
- ![[screenshot_2025-06-01.png]]
- [会議録メモ →](../meetings/2025-05-30_team-checkin.md)

## 🔹 サブアクション
- [ ] ○○について調査する
- [ ] 関係者に確認
- [ ] ドラフト資料を作成
- [ ] 上司に提出・レビュー依頼

task_add/request.md

以下の情報をもとに、ユーザがノートとして記録すべきタスクのノートを作成してください。
{{input}}

これで、0_inbox/ フォルダに何かが入ったら、プロンプトが複数積みあがって一つになり、後は結果を保存すればよい、という仕掛けを作っていける。


タスク自動追加スクリプト

まずは最低限の仕様で実装する。

0_inbox/ フォルダに、以下のようなpngファイルが格納されたら、それっぽいタスクを自動的に生成して、1_tasks/20_todo/ フォルダに追加されるまでをゴールとして、スクリプトを作る。いったんは単発実行で、うまくいったら後でI/O部分を切り離してコアプロセス部分はMCP化して使いまわししやすくする。

alt text

これ、PowerPointで手作りしたサンプルのスクショなんですが、ありそうですよね。そしてこれを見ただけでそこそこ情報はあるので、AIであれば一定は自分のノートにタスク情報として書き起こしはてくれそう。

マルチモーダル対応のAIチャットにそういう指示をかけて返ってきた結果をコピペするっていうのとやることは同じなんですが、そこを自動化して、スクショをとるだけでいいにすることで余計な手間を省く発想。

では、先日のQdrantへの同期のプログラムと同じように別コンテナで稼働させることにする。Docker周りは同じなので省略して、スクリプト本体を以下に示す。

# -*- coding: utf-8 -*-
"""
スクリーンショットの画像ファイルが格納されたことは、ユーザが新しいタスクが発生したと認識した前提として
ToDo管理のためのノートを生成する
"""
import os
import json
import time
import qdrant_client
from qdrant_client import QdrantClient
from qdrant_client.http import models as rest
import base64
import openai
import hashlib
import sys
import re
import uuid

AzureOpenAI_API_ENDPOINT = os.environ.get("AZURE_OPENAI_API_ENDPOINT")
AzureOpenAI_API_KEY = os.environ.get("AZURE_OPENAI_API_KEY")
AzureOpenAI_COMP_API_VERSION = os.environ.get("AZURE_OPENAI_COMP_API_VERSION")
AzureOpenAI_COMP_MODEL = os.environ.get("AZURE_OPENAI_COMP_MODEL")
AzureOpenAI_COMP_DEPLOYMENT = os.environ.get('AZURE_OPENAI_COMP_DEPLOYMENT')

NOTE_DIR = "/note"  # ノートのフォルダパス

# マルチモーダルに対応したCompletionクライアント設定
compclient = openai.AzureOpenAI(
    azure_endpoint=AzureOpenAI_API_ENDPOINT,
    azure_deployment=AzureOpenAI_COMP_DEPLOYMENT,
    api_key=AzureOpenAI_API_KEY,
    api_version=AzureOpenAI_COMP_API_VERSION
)

# 監視対象のフォルダ
WATCH_DIR = os.path.join(NOTE_DIR, "0_inbox")
PROMPT_DIR = os.path.join(NOTE_DIR, "5_prompts")

# 監視対象のファイル拡張子
WATCH_EXTENSIONS = [".png"]

def get_image_caption(image_path):
    """
    画像をGPT-4oで言語化してキャプションを取得する関数
    """
    with open(image_path, "rb") as f:
        image_data = base64.b64encode(f.read()).decode('utf-8')
    response = compclient.chat.completions.create(
        model=AzureOpenAI_COMP_MODEL,
        messages=[
            {"role": "user", "content": [
                {
                    "type": "text", "text": "与えられた画像がどのようなものか、そこから読み取れる情報をすべて言語化してください。"
                }, 
                {
                    "type": "image_url",
                    "image_url": {
                        "url": f"data:image/png;base64,{image_data}"
                    }
                }],
            }
        ],
        temperature=0.2,
    )

    if not response.choices or not response.choices[0].message.content:
        print("Error: No content in response")
        sys.exit(1)  # エラー終了

    caption = response.choices[0].message.content
    return caption

# グランドプロンプト系のファイルを読み込んでこの機能で使うプロンプトを作成
def load_prompt():
    """
    プロンプトを読み込む関数
    """
    system_prompt_path = os.path.join(PROMPT_DIR, "system_prompt.md")
    ground_prompt_path = os.path.join(PROMPT_DIR, "ground_prompt.md")
    instruction_path = os.path.join(PROMPT_DIR, "task_add", "instruction.md")
    request_path = os.path.join(PROMPT_DIR, "task_add", "request.md")

    with open(system_prompt_path, "r", encoding="utf-8") as f:
        system_prompt = f.read()

    with open(ground_prompt_path, "r", encoding="utf-8") as f:
        ground_prompt = f.read()
    
        current_datetime = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        ground_prompt = ground_prompt.replace("{{DATETIME}}", current_datetime)

        ground_prompt = ground_prompt.replace("{{./user_information.md}}", 
                                              os.path.join(PROMPT_DIR, "user_information.md"))
        ground_prompt = ground_prompt.replace("{{./note_structure.md}}", 
                                              os.path.join(NOTE_DIR, "note_structure.md"))

    with open(instruction_path, "r", encoding="utf-8") as f:
        instruction = f.read()
        ground_prompt = ground_prompt.replace("{{instruction}}", instruction)

    with open(request_path, "r", encoding="utf-8") as f:
        request = f.read()
        ground_prompt = ground_prompt.replace("{{request}}", request)

    return system_prompt, ground_prompt

def process_screenshot(file_path):
    caption = get_image_caption(file_path)
    system_prompt, ground_prompt = load_prompt()
    ground_prompt = ground_prompt.replace("{{input}}", caption)
    # ここでキャプションを使ってプロンプトを構築し、OpenAIにリクエストを送信する
    response = compclient.chat.completions.create(
        model=AzureOpenAI_COMP_MODEL,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": ground_prompt}
        ]
    )
    # ノートを保存
    note_title = f"{time.strftime('%Y%m%d_%H%M%S')}_{uuid.uuid4().hex[:8]}.md"
    note_path = os.path.join(NOTE_DIR, "1_tasks", "20_todo", note_title)
    with open(note_path, "w", encoding="utf-8") as f:
        f.write(response.choices[0].message.content)
    # スクリーンショットをアーカイブフォルダに移動
    archive_path = os.path.join(NOTE_DIR, "4_archive", "inbox", os.path.basename(file_path))
    os.rename(file_path, archive_path)

def watch_screenshots():
    """
    スクリーンショットを監視して新しいタスクを検出する関数
    """
    while True:
        for filename in os.listdir(WATCH_DIR):
            if any(filename.endswith(ext) for ext in WATCH_EXTENSIONS):
                file_path = os.path.join(WATCH_DIR, filename)
                if os.path.isfile(file_path):
                    try:
                        process_screenshot(file_path)
                    except Exception as e:
                        print(f"Error processing {file_path}: {e}")
        time.sleep(15)  # 15秒ごとに監視

if __name__ == "__main__":

    # スクリーンショットの監視を開始
    print("Watching for new screenshots...")
    watch_screenshots()

これも動くまでだいぶ苦労して、画像読み込みのところをググってたら普通に原さんのノートに行きついたりして驚いたりした。

何とか、先ほどのサンプルのスクリーンショットを0_inboxに格納したら以下のようなノートが勝手にできるところまでたどり着いた。

alt text

いいぞぉぉぉ!!!いいぞいいぞぉぉ❗❗❗❗❗🤪

ゼロから書くことを思うと全然ラクで、まだまだこれから工夫してどんどん便利にできるな。こうやって 20_ToDo フォルダに自分のタスクがどんどんたまってくるが、

  • 今からこれやろうと思ったものは、 10_next フォルダに移動して取り掛かる
  • その後で順番にこなしていくつもりのタスクは 20_todo フォルダに残す
  • 他の人の対応待ちだったりするタスクは、30_waiting フォルダに移動しておく

という運用をする。

まだまだプロンプトの工夫もできるし、関連資料へのリンクとか変なもの作っては来ているが、他のノートが蓄積してQdrantのベクトル検索と組み合わせると間違いなく精度は向上するので今はこれで問題ない。

エピローグ

今回はノート術としてのフレームワークを定義することと、プロンプトの管理フォルダも準備、そこでまず第一歩の「Teamsのやり取りをスクショとるだけでToDoノートに変換」というユースケースのプロトタイプを作成した。

しかし、前回のQdrantの同期の件も今回も、単一で完結するプログラムで暫定対応したのでコードが冗長な状況。かつMCPサーバ化をしての呼び出しが今後を見据えるときっと便利で、機能追加が楽になっていくはず。

いったんこの状態で普段使いしていきつつ、リファクタリングも考えながら再構築かな。ここは急がば回れだな。

次回はちょっとお勉強しながら、機能のMCPサーバ化にトライする予定。真のはやぶさノーティングの完成はたぶん半年後くらいだろうか。

Accenture Japan (有志)

Discussion