with one click
business-logic-demo-pipeline
// 複雑な事務処理ロジック(料金算出マトリックス等)をPythonで実装し、 PptxGenJSでプレゼン化、DockerでPDF変換、Slackへ配信する end-to-end パイプライン。 ビジネスロジックの自動化能力デモ資料作成に使う。 v2: 2軸マトリックス×セル内複数条件パターン(CellRuleオブジェクト設計)を含む。
// 複雑な事務処理ロジック(料金算出マトリックス等)をPythonで実装し、 PptxGenJSでプレゼン化、DockerでPDF変換、Slackへ配信する end-to-end パイプライン。 ビジネスロジックの自動化能力デモ資料作成に使う。 v2: 2軸マトリックス×セル内複数条件パターン(CellRuleオブジェクト設計)を含む。
[HINT] Download the complete skill directory including SKILL.md and all related files
| name | business-logic-demo-pipeline |
| description | 複雑な事務処理ロジック(料金算出マトリックス等)をPythonで実装し、 PptxGenJSでプレゼン化、DockerでPDF変換、Slackへ配信する end-to-end パイプライン。 ビジネスロジックの自動化能力デモ資料作成に使う。 v2: 2軸マトリックス×セル内複数条件パターン(CellRuleオブジェクト設計)を含む。 |
| triggers | ["複雑な事務処理ロジックをプログラム化","料金算出マトリックス","申請処理の自動化","条件分岐ロジックのデモ","ビジネスロジックをスライドで説明","テーブル駆動設計の実装","2軸マトリックス","セル内複数条件"] |
複雑な条件分岐ロジック(申請・料金算出等)を Python で実装し、 PptxGenJS でプレゼンテーション化、Docker+LibreOffice で PDF 変換し、Slack 配信するフルパイプライン。
1軸(申請種別のみ)で条件を dict で外部化する基本パターン。
縦軸(申請種別)× 横軸(申請者区分)の各セルが CellRule オブジェクトを保持。
同一セルでも「優先度」「複雑度」「フラグ」等によって料金が大きく異なる。
推奨: パターン B(よりリアルな事務処理に近い)
from dataclasses import dataclass, field
from typing import Dict
@dataclass
class CellRule:
base_fee: int # セル固有の基本料金
cell_surcharge: int = 0 # セル固有の固定追加料金
cell_surcharge_label: str = "" # 追加料金の名称
max_priority_multiplier: float = 2.20 # 優先度係数の上限(セルごとに異なる)
max_complexity_multiplier: float = 2.10 # 複雑度係数の上限
allow_environmental: bool = True # 環境評価フラグ適用可否
allow_joint: bool = True # 共同申請フラグ適用可否
allow_translation: bool = True # 翻訳フラグ適用可否
max_repeat_discount: float = 0.20 # リピーター割引の上限率
max_package_discount: float = 0.20 # パッケージ割引の上限率
# 2軸マトリックス定義: MATRIX[申請種別][申請者区分]
MATRIX: Dict[str, Dict[str, CellRule]] = {
"A": { # 建設工事許可
"個人": CellRule(base_fee=80_000, max_priority_multiplier=1.50,
allow_environmental=False, allow_joint=False,
max_repeat_discount=0.15),
"法人": CellRule(base_fee=150_000, cell_surcharge=20_000,
cell_surcharge_label="法人登記確認料"),
"行政": CellRule(base_fee=60_000, max_priority_multiplier=1.50,
max_complexity_multiplier=1.45, allow_joint=False,
max_package_discount=0.15),
},
"F": { # 産廃業許可(最高料金帯)
"個人": CellRule(base_fee=120_000, cell_surcharge=15_000,
cell_surcharge_label="現地施設確認料(個人)",
max_priority_multiplier=1.50, allow_joint=False,
max_repeat_discount=0.15),
"法人": CellRule(base_fee=220_000, cell_surcharge=35_000,
cell_surcharge_label="現地施設確認料(法人)"),
"行政": CellRule(base_fee=80_000, cell_surcharge=10_000,
max_priority_multiplier=1.50,
max_complexity_multiplier=1.45, allow_joint=False,
max_package_discount=0.15),
},
# ... B/C/D/E も同様に定義
}
セル内複数条件のポイント:
priority=urgent vs priority=standard で最大2.2倍の差allow_environmental=False のセルでは ¥100,000の環境評価料が発生しないmax_priority=1.00, max_complexity=1.00 → 係数がフラット(係数なし)max_repeat_discount=0.15(法人の0.20より低い)| アーキテクチャ | 説明 |
|---|---|
| テーブル駆動設計 | 条件を dict/dataclass で外部化。if 分岐を最小化 |
| 段階パイプライン | BASE → 地域 → 優先度(上限clip) → 複雑度(上限clip) → 規模 → 季節 → フラグ → 割引 の順 |
| セル上限クリップ | min(requested_multiplier, cell.max_multiplier) でセル固有の制約を実現 |
| ルール集約 | 組み合わせ爆発(1億通り超)を 50〜60 ルールで対応 |
# 申請種別ごとの基本料金
BASE_FEES = {
"A": 50000, # 建設工事許可
"B": 30000, # 営業許可
"C": 20000, # 補助金申請
"D": 80000, # 輸出入許可
"E": 60000, # 宅建免許
"F": 70000, # 産廃業許可
}
# 係数テーブル(段階パイプライン)
APPLICANT_MULTIPLIERS = {"individual": 1.0, "corporation": 1.3, "government": 0.7}
REGION_MULTIPLIERS = {"special_ward": 1.2, "city": 1.0, "town": 0.95, "rural": 0.9}
PRIORITY_MULTIPLIERS = {"normal": 1.0, "urgent": 1.5, "emergency": 2.0}
COMPLEXITY_MULTIPLIERS = {"simple": 1.0, "standard": 1.3, "complex": 1.8, "ultra": 2.5}
def calculate_fee(app: dict) -> int:
fee = BASE_FEES[app["type"]]
fee *= APPLICANT_MULTIPLIERS[app["applicant_type"]]
fee *= REGION_MULTIPLIERS[app["region"]]
fee *= PRIORITY_MULTIPLIERS[app["priority"]]
fee *= COMPLEXITY_MULTIPLIERS[app["complexity"]]
# バイナリフラグ(翻訳・現地調査・電子申請等)
if app.get("translation"): fee += 50000
if app.get("field_survey"): fee *= 1.4
if app.get("electronic"): fee *= 0.95
# 季節係数
season_map = {1:1.1, 2:1.15, 3:1.2, 4:1.0, ..., 12:1.05}
fee *= season_map.get(app.get("month", 6), 1.0)
return int(fee)
申請種別(6) × 申請者区分(3) × 地域(4) × 優先度(3) × 複雑度(4) × 事業規模(4) × 季節(12)
= 6 × 3 × 4 × 3 × 4 × 4 × 12 = 41,472
× バイナリフラグ 2^5 = 32 → 1,327,104
× 書類枚数段階(4) × 補正回数(4) × リピーター段階(5) × パッケージ段階(4)
合計 ≈ 1.05 億通り → 18 CellRule × 係数テーブルで対応
mkdir -p /tmp/demo_project
cd /tmp/demo_project
npm init -y
npm install pptxgenjs
| # | スライド内容 |
|---|---|
| 01 | タイトル(能力デモ + バッジ) |
| 02 | 組み合わせ爆発の実態(横棒グラフ) |
| 03 | テーブル駆動設計とは(図解) |
| 04 | 段階パイプライン詳細(フロー図) |
| 05 | 申請種別マトリックス(6 種) |
| 06 | 料金算出フロー(フローチャート) |
| 07 | コードサンプル(Python) |
| 08 | 簡単な別例(飲食店ランチ料金) |
| 09 | 実際の計算結果(5 ケース比較) |
| 10 | 最大/最小倍率の比較 |
| 11 | 拡張性・保守性のメリット |
| 12 | アーキテクチャ全体図 |
| 13 | まとめ |
const THEME = {
bg: "1B2A3B", // ダークネイビー背景
text: "FFFFFF", // 白テキスト
accent: "00BFFF", // シアンアクセント
green: "2E7D32",
orange: "F57C00",
red: "C62828",
};
docker run --rm \
-v /tmp/demo_project:/work \
debian:bookworm-slim \
bash -c "apt-get update -qq && \
apt-get install -y -qq libreoffice fonts-noto-cjk --no-install-recommends 2>/dev/null && \
soffice --headless --convert-to pdf /work/presentation.pptx --outdir /work && \
echo 'PDF_DONE'"
注意: fonts-noto-cjk 必須。日本語が □ になる。
docker run --rm \
-v /tmp/demo_project:/work \
debian:bookworm-slim \
bash -c "apt-get update -qq && \
apt-get install -y -qq poppler-utils --no-install-recommends 2>/dev/null && \
pdftoppm -jpeg -r 150 /work/presentation.pdf /work/slide && \
echo 'IMAGES_DONE'"
生成ファイル: slide-01.jpg, slide-02.jpg, ...
各スライド画像を vision_analyze でチェック:
確認項目:
- 日本語テキストの文字化けなし
- レイアウト崩れなし
- 数値・計算式の正確性
- カラーの一貫性
slack-file-upload スキルを参照。
# 手順: getUploadURLExternal → POST bytes → completeUploadExternal
# PPTX と PDF を別々にアップロード
# thread_ts を指定するとスレッド内に投稿できる
# PAT が環境変数に入っているか確認(値はマスク)
env | grep -i 'github\|git_token\|pat\|ghp_' | sed 's/=.*/=***/'
# → GITHUB_HERMESSHIFT_PAT=*** のように見えることがある
# 1. PAT をリモートURLに埋め込んでクローン
TOKEN=$GITHUB_HERMESSHIFT_PAT # 実際の変数名に合わせて変更
git clone "https://x-access-token:${TOKEN}@github.com/<org>/<repo>.git" /tmp/repo
# 2. Git ユーザー設定(コミットに必要)
cd /tmp/repo
git config user.name "Hermes Agent"
git config user.email "hermes@hermes-dev"
# 3. トークンを remote URL にも設定(push 用)
git remote set-url origin "https://x-access-token:${TOKEN}@github.com/<org>/<repo>.git"
# 4. サブフォルダ作成・成果物コピー
mkdir -p /tmp/repo/my_subdir
cp /tmp/project/output.py /tmp/repo/my_subdir/
cp /tmp/project/output.pptx /tmp/repo/my_subdir/
cp /tmp/project/output.pdf /tmp/repo/my_subdir/
# 5. README 作成(write_file ツールで /tmp/repo/my_subdir/README.md に書く)
# 6. コミット&プッシュ
git add my_subdir/
git commit -m "feat: <サブフォルダ名>追加\n\n<変更内容の要約>"
git push origin main
gh コマンドが使えない環境では x-access-token:<PAT>@github.com 形式の HTTPS URL が最も安定GITHUB_TOKEN, GITHUB_HERMESSHIFT_PAT など)。env | grep -i github で確認git add で問題なく追加できるfontFace: "Noto Sans CJK JP" を使うと LibreOffice PDF 変換時の日本語が正確Warning: failed to launch javaldx は無視してよい# テーブル駆動の分かりやすい別例
BASE = {"set_A": 800, "set_B": 1000, "set_C": 1200}
TIME_MULTI = {"11:00-12:00": 1.0, "12:00-13:00": 1.2, "13:00-14:00": 0.9}
DAY_MULTI = {"weekday": 1.0, "weekend": 1.3, "holiday": 1.5}
OPTION_ADD = {"drink": 200, "dessert": 300, "soup": 150}
def calc_lunch(menu, time, day, options):
price = BASE[menu]
price *= TIME_MULTI[time]
price *= DAY_MULTI[day]
price += sum(OPTION_ADD[o] for o in options)
return int(price)
組み合わせ数: 3 × 3 × 3 × 2^3 = 216 通り → 1 関数で対応
2つのパターンを比較説明するよう求められた場合、以下の形式で整理する。
| 比較項目 | パターンA(フラット型) | パターンB(2軸マトリックス型) |
|---|---|---|
| ファイル規模 | 502行・関数6個 | 726行・関数12個 |
| 係数テーブル | 全種別共通(1セット) | セルごとに個別設定 |
| セル固有追加料金 | なし | あり(例:法人登記確認料) |
| セル固有の係数上限 | なし | あり(min(実値, max_multiplier)) |
| フラグ適用制御 | 全セル一律 | allow_* フラグでセルごとに制御 |
| 割引上限 | 一律 | セルごとに異なる |
| 可読性・メンテ性 | 高い | 中(セル数に比例) |
| 現実業務への適合度 | 中 | 高 |
| 監査ログの詳細度 | 中 | 高(全ステップをトレース) |
条件: 建設工事許可 × 法人 × 東京特別区 × 急ぎ × 複雑 × 中規模 × 法務確認あり × リピーター3件
パターンA 最終(税込): ¥714,450
パターンB 最終(税込): ¥734,250 (差額 +¥19,800)
差額の原因: パターンBのみ「法人登記確認料 ¥20,000」がセル固有追加料金として加算されるため。
| 判断軸 | パターンA | パターンB |
|---|---|---|
| 係数変更頻度が高い | ✅ | ⚠️(セルごとに修正) |
| 申請種別ごとに異なる制約 | ⚠️ | ✅ |
| プロトタイプ・PoC段階 | ✅ | ⚠️(実装量多い) |
| 本番実装・監査対応 | ⚠️ | ✅ |
| 新しいエンジン追加の際 | 先にパターンAを作り、それをベースにパターンBへ段階的に進化させるとよい |
「パターンAは料金体系の骨格を素早く作るプロトタイプ・基礎設計向きです。 パターンBはパターンAを『セルごとに制御できるよう進化させた上位互換版』で、 実際の業務ルール・制約・例外を忠実に反映する本番実装向きです。 両者の係数値は意図的に揃えてあるため、基本ロジックは共通です。」
マトリックス仕様書を「セルに条件詳細を内包しつつ着色」したExcelとして出力する手法。 Markdownでは表現できない「条件分岐の視覚的強調」に有効。
cd /tmp && npm install exceljs
| 色 | 条件 | argb |
|---|---|---|
| 薄黄 | 標準(制約なし・追加料金なし) | FFFFFDE7 |
| 薄緑 | セル固有追加料金あり(制約なし) | FFE8F5E9 |
| 薄ピンク | 一部制約あり(フラグ不可・割引上限など) | FFFCE4EC |
| 橙 | 追加料金 + 制約の複合 | FFFFE0B2 |
| 赤 | 強制約:係数が1.0固定(最急・超複雑が効かない) | FFFF1744 |
function getCellColor(cell) {
const hasStrong = cell.maxPriority <= 1.00 || cell.maxComplexity <= 1.00;
const hasMedium = cell.maxPriority < 2.20 || cell.maxComplexity < 2.10
|| !cell.allowEnv || !cell.allowJoint || !cell.allowTrans
|| cell.maxRepeat < 20 || cell.maxPkg < 20;
const hasSurcharge = cell.surcharge > 0;
if (hasStrong) return 'FFFF1744'; // 赤:最強制約
if (hasSurcharge && hasMedium) return 'FFFFE0B2'; // 橙:複合
if (hasSurcharge) return 'FFE8F5E9'; // 薄緑:追加料金のみ
if (hasMedium) return 'FFFCE4EC'; // 薄ピンク:制約のみ
return 'FFFFFDE7'; // 薄黄:標準
}
// 標準から外れる条件のみ列挙して返す(空なら条件分岐なし)
function diff(cell, STANDARD) {
const lines = [];
if (cell.surcharge > 0) lines.push(`+¥${cell.surcharge.toLocaleString()} ${cell.surchargeLabel}`);
if (cell.maxPriority < STANDARD.maxPriority) lines.push(`優先度上限 ×${cell.maxPriority.toFixed(2)}`);
if (cell.maxComplexity < STANDARD.maxComplexity) lines.push(`複雑度上限 ×${cell.maxComplexity.toFixed(2)}`);
if (!cell.allowEnv) lines.push('環境評価フラグ: 不可');
if (!cell.allowJoint) lines.push('共同申請フラグ: 不可');
if (!cell.allowTrans) lines.push('翻訳フラグ: 不可');
if (cell.maxRepeat < STANDARD.maxRepeat) lines.push(`リピーター割引 上限${cell.maxRepeat}%`);
if (cell.maxPkg < STANDARD.maxPkg) lines.push(`パッケージ割引 上限${cell.maxPkg}%`);
return lines;
}
// セルに書き込む複数行テキスト
const lines = [];
lines.push(`基本料金: ¥${cell.base.toLocaleString()}`);
if (cell.surcharge > 0) lines.push(`+¥${cell.surcharge.toLocaleString()} [${cell.surchargeLabel}]`);
const cond = diff(cell, STANDARD);
if (cond.length > 0) {
lines.push('─── 条件分岐 ───');
cond.forEach(l => lines.push(' ' + l));
}
excelCell.value = lines.join('\n');
excelCell.alignment = { vertical: 'top', horizontal: 'left', wrapText: true };
| シート名 | 内容 |
|---|---|
| 全条件マトリックス | 全セルに条件詳細テキスト内包+着色。行高105px。frozen pane(A列/1行) |
| 基本料金サマリー | 数値中心。基本料金+セル固有追加料金を一覧 |
| 係数・フラグ制約 | 優先度上限/複雑度上限/フラグ可否を申請者区分別に横展開 |
| 算出フロー | パイプラインの各Stepと「セル固有条件が発動するStep」を色分け明示 |
row.height はピクセル単位。条件詳細テキストが多い行は 105 以上推奨views: [{ state: 'frozen', xSplit: 1, ySplit: 2 }]argb: 'FFFFFFFF'(6桁ではなく8桁 ARGB 形式)wrapText: true を設定しないと改行が見えないmergeCells(row1, col1, row2, col2) は数値指定が確実(文字列 'A1:C1' も可)/tmp/fee_matrix