文書タイプ別 AI プロンプトのカスタマイズ
各 DocumentClass に設定するプロンプトを工夫することで、AI の出力 JSON に固有のフィールドを追加できます。 抽出したい項目と形式を自然言語で指示するだけで、構造化データとして取り出せます。
1プロンプト全体の構造を理解する
AI へ送られるプロンプトは、システム共通部分と各 DocumentClass ごとの追記部分が結合されたものです。 管理画面で DocumentClass に設定するテキストは、後者の「追記部分」にあたります。
# システム共通プロンプト(変更不可) PDFまたは画像ファイルの内容を読み取り、文書の種類を判定してください。 JSONのみを返答してください。... fax_properties: { ... FAXヘッダー情報 } content_properties: { ... 本文の共通項目 } typed_properties: { ... ★ 各クラスの指示で追加するフィールド } -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=ypl ← DocumentClass の区切り文字 ### Invoice(管理画面で設定した DocumentClassID) DocumentClassID Invoice ↑ここから下が管理画面の「Prompt」フィールドに書いたテキスト 請求書と判定するための条件と、抽出すべき項目… -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=ypl ### OrderForm(次の DocumentClass) DocumentClassID OrderForm …
管理画面の「Prompt」欄に書いたテキストは、そのままの形で AI へ渡されます。Markdown 形式で箇条書きや見出しを使うと AI が読みやすくなります。
2基本の出力 JSON スキーマ
すべての分類結果に共通して出力されるフィールドです。DocumentClass のプロンプトで指示した内容は typed_properties に格納されます。
{
"documentClassId": "Invoice", // 判定された DocumentClassID(null = 不明)
"confidence": 0.95, // 分類確信度(0.0 〜 1.0)
"fax_properties": { // FAX ヘッダー/フッターから読み取った伝送情報
"senderName": "...",
"senderFaxNumber": "...",
"recipientName": "...",
"recipientFaxNumber": "...",
"transmissionTimestamp": "2026-02-18T08:18:00",
"totalPages": 3,
"jobId": "..."
},
"content_properties": { // 本文から読み取った共通情報
"title": "請求書",
"senderName": "XX 商事株式会社",
"senderFaxNumber": "03-1234-5678",
"senderPhoneNumber": "03-1234-5679",
"recipientName": "担当者名",
"recipientFaxNumber": "...",
"recipientPhoneNumber": "...",
"timestamp": "2026-02-10T00:00:00"
},
"typed_properties": { // ★ DocumentClass のプロンプトで追加した固有フィールド
// (後述のカスタマイズ例を参照)
},
"notes": "" // 補足コメント(AI が任意で記入)
}
3typed_properties に独自フィールドを追加する
DocumentClass の「Prompt」フィールドに、typed_properties へ格納してほしい項目をJSON形式のサンプルとともに指示します。
AI はその指示に従って、文書画像から該当する値を読み取り出力します。
## 分類条件 次のいずれかに当てはまる場合、この文書を Invoice(請求書)と判定してください。 - 「請求書」「御請求書」「Invoice」などの見出しがある - 請求金額・振込先口座・請求日が記載されている - 発行者の印鑑または社名スタンプがある ## 抽出フィールド 以下のフィールドを typed_properties に格納してください。 読み取れない場合は null としてください。 ```json { "typed_properties": { "invoiceNumber": "請求書番号(文字列)", "invoiceDate": "請求日(ISO 日付文字列 YYYY-MM-DD)", "dueDate": "支払期限(ISO 日付文字列 YYYY-MM-DD)", "totalAmount": "請求合計金額(税込、数値)", "taxAmount": "消費税額(数値)", "currency": "通貨コード(例: JPY)", "bankName": "振込先金融機関名", "bankBranch": "振込先支店名", "accountType": "口座種別(普通/当座)", "accountNumber": "口座番号", "accountHolder": "口座名義", "lineItems": [ { "description": "品目・摘要", "quantity": "数量(数値)", "unitPrice": "単価(数値)", "amount": "金額(数値)" } ] } } ```
ポイント:JSON サンプルを使って指示する
抽出してほしい構造を JSON サンプルとしてプロンプト内に記述すると、AI が出力する JSON のキー名・型・構造を確実に制御できます。値部分を日本語の説明にしておくと、何を読み取ればよいかが AI に伝わりやすくなります。
4カスタマイズ後の出力 JSON 例
上記のプロンプトを設定した場合、AI は次のような JSON を出力します。この内容がそのまま DB の DocumentData 列に格納されます。
{
"documentClassId": "Invoice",
"confidence": 0.97,
"fax_properties": { ... },
"content_properties": {
"title": "御請求書",
"senderName": "XX 商事株式会社",
"timestamp": "2026-02-10T00:00:00"
...
},
"typed_properties": {
"invoiceNumber": "INV-2026-0042",
"invoiceDate": "2026-02-10",
"dueDate": "2026-02-28",
"totalAmount": 110000,
"taxAmount": 10000,
"currency": "JPY",
"bankName": "〇〇銀行",
"bankBranch": "渋谷支店",
"accountType": "普通",
"accountNumber": "1234567",
"accountHolder": "ヨキンソフト",
"lineItems": [
{ "description": "システム保守費", "quantity": 1, "unitPrice": 100000, "amount": 100000 }
]
},
"notes": ""
}
plugins フォルダへのカスタムハンドラの実装
plugins/ フォルダに特定の命名規則でPythonファイルを置くと、該当する文書種別のファイルが正常に処理された直後に自動で呼び出されます。
外部システムへの転送・メール通知・連携 API 呼び出しなど、業務固有の後処理を自由にプログラムできます。
1ファイルの命名規則と配置場所
YokinsPaperless/ ├── monitor/ │ └── monitor.py └── plugins/ ← ここにハンドラを配置 ├── docClassHandler_Invoice.py ← DocumentClassID が "Invoice" の場合に呼び出される ├── docClassHandler_OrderForm.py ← DocumentClassID が "OrderForm" の場合に呼び出される └── docClassHandler_Notice.py ← DocumentClassID が "Notice" の場合に呼び出される
- ファイル名は必ず
docClassHandler_<DocumentClassID>.pyとする <DocumentClassID>の部分は管理画面で設定した DocumentClassID と完全一致(大文字・小文字含む)させる- 分類不能(
documentClassId: null)な文書にはハンドラは呼び出されない - ハンドラが存在しない DocumentClass でもシステムは通常通り動作する
2handle_document(document) 関数を実装する
ハンドラファイルには handle_document(document) という関数を定義します。
引数 document は Documents テーブルの1行を dict として渡したものです。
def handle_document(document): """ document: Documents テーブルの1行を表す dict 正常処理が完了した直後に呼び出される。 戻り値は無視される。例外をスローしてもシステムに影響しない。 """ pass
3引数 document の内容
document は以下のキーを持つ辞書です。DocumentData の値は JSON 文字列なので、json.loads() でパースして使います。
| キー | 型 | 内容 |
|---|---|---|
ID | str | 文書 UUID |
Active | int | 有効フラグ(1 = 有効) |
SourcePath | str | 元の FAX ファイルの絶対パス |
DateCreated | str | DB 登録日時(ISO) |
DateReceived | str | FAX 受信日時(ISO) |
Title | str | AIが抽出したタイトル |
Sender | str | 送信者名 |
SenderOrganization | str | 送信者 FAX 番号 |
Recipient | str | 受信者名 |
RecipientOrganization | str | 受信者 FAX 番号 |
DocumentClassID | str | 文書クラス ID(このハンドラを呼んだクラス) |
DocumentData | str | AI 出力 JSON 全体の文字列(json.loads でパース) |
4実装例
例 1:ログファイルに書き出す
import json, pathlib, datetime def handle_document(document): data = json.loads(document["DocumentData"] or "{}") tp = data.get("typed_properties", {}) log_path = pathlib.Path("/var/log/paperless/invoice.log") log_path.parent.mkdir(parents=True, exist_ok=True) line = f'[{datetime.datetime.now().isoformat()}] ' \ f'id={document["ID"]} ' \ f'no={tp.get("invoiceNumber")} ' \ f'amount={tp.get("totalAmount")}\n' with log_path.open("a", encoding="utf-8") as f: f.write(line)
例 2:メール通知を送る
import json, smtplib from email.mime.text import MIMEText SMTP_HOST = "localhost" NOTIFY_TO = "[email protected]" def handle_document(document): data = json.loads(document["DocumentData"] or "{}") tp = data.get("typed_properties", {}) amt = tp.get("totalAmount", "不明") subj = f'[請求書受信] {document["Sender"]} / {amt}円' body = ( f'文書ID : {document["ID"]}\n' f'受信日時: {document["DateReceived"]}\n' f'送信者 : {document["Sender"]}\n' f'金額 : {amt}\n' f'ファイル: {document["SourcePath"]}\n' ) msg = MIMEText(body, "plain", "utf-8") msg["Subject"] = subj msg["From"] = "[email protected]" msg["To"] = NOTIFY_TO with smtplib.SMTP(SMTP_HOST) as s: s.send_message(msg)
例 3:外部 Web API へ POST する
import json, urllib.request WEBHOOK_URL = "https://erp.example.com/api/invoices" API_TOKEN = "Bearer my-secret-token" def handle_document(document): data = json.loads(document["DocumentData"] or "{}") tp = data.get("typed_properties", {}) payload = { "source": "paperless", "documentId": document["ID"], "receivedAt": document["DateReceived"], "sender": document["Sender"], "invoiceNumber": tp.get("invoiceNumber"), "totalAmount": tp.get("totalAmount"), "lineItems": tp.get("lineItems", []), } body = json.dumps(payload, ensure_ascii=False).encode() req = urllib.request.Request( WEBHOOK_URL, data=body, headers={ "Content-Type": "application/json", "Authorization": API_TOKEN, }, method="POST", ) with urllib.request.urlopen(req, timeout=10) as r: print(f"[plugin] ERP response: {r.status}")
例 4:元ファイルを別フォルダにコピーする
import json, shutil, pathlib, datetime ARCHIVE_DIR = pathlib.Path("/mnt/nas/invoice-archive") def handle_document(document): data = json.loads(document["DocumentData"] or "{}") tp = data.get("typed_properties", {}) yyyymm = datetime.datetime.now().strftime("%Y%m") dst_dir = ARCHIVE_DIR / yyyymm dst_dir.mkdir(parents=True, exist_ok=True) src = pathlib.Path(document["SourcePath"]) inv_no = tp.get("invoiceNumber") or document["ID"][:8] dst = dst_dir / f"INV_{inv_no}_{src.name}" shutil.copy2(src, dst) print(f"[plugin] copied to {dst}")
5動作の仕様と注意事項
- ハンドラはファイルが正常に分類・DB 登録された 直後に同一スレッドで呼び出される
- ハンドラの実行中、次のファイル処理は待機状態になる(長時間処理は非同期で実行すること)
- ハンドラ内で例外が発生してもシステム側でキャッチされ、エラーログに記録されるだけで 文書の登録自体には影響しない
- ハンドラファイルは呼び出しのたびに都度ロードされる(サービス再起動不要でコード変更が反映される)
- ハンドラから DB(SQLite)に直接アクセスすることも可能だが、
SourcePathのファイルは移動・削除しないこと
長時間かかる処理はスレッドに逃がす:
ハンドラは同期的に呼ばれるため、外部 API やメール送信など時間がかかる処理はハンドラ内で threading.Thread を使って非同期実行するか、タイムアウトを短めに設定してください。
環境変数の利用:
SMTP ホスト名・API エンドポイント・認証トークンなどは、プロジェクトルートの .env ファイルに書いておくと、ハンドラ内から os.getenv() で取得できます。ソースコードへの直書きは避けてください。