Reports

TL;DR 手持ちの雑な CSV / Excel / 写真 / PDF を投げると、AI が「読み取って提案」し、決定的なコードが「矯正・分類・確定・適用」して nanco に取り込む機能。 合言葉は「AI は提案だけ、確定はコード」と「データのある列を黙って消さない」。 全体は 6層(API / ハンドラ / 共有ロジック / クライアント / UI / フック)+ DB・cron で構成され、型の背骨は contract.ts 1枚。このページはその地図です。

目次 §1 これは何か §2 設計の背骨(中核思想) §3 6層アーキテクチャ §4 モジュール全体マップ §5 5つのAPIと役割 §6 データの流れ(鳥瞰) §7 用語集 §8 このドキュメントの歩き方
§1 これは何か(ひとことで)

「データを nanco の形に整える手作業」を、AI と決定的コードの分担で肩代わりする取り込みアシスタント。

これまでの取り込みは「列の対応表をユーザーが自分で埋める」形でした。列が多いと分かりにくく、特に新規ワークスペースでは AI が属性提案を出し切れず、データのある列が無言で捨てられて消える事故(実例: 40列中20列消失)が起きていました。

本ブランチはそれを作り直し、次の3つの入口を1つの会話型UIに統合しました。

入口対象主なファイル
在庫CSV/Excel初回の一括登録・移行(オンボーディングの肝)CSVImportModal → AiImportThread
入荷 / 出荷の伝票仕入先・得意先ごとの受払データReceiving/ShippingCsvImportMultipleModal
写真 / PDF納品書・出荷表をスマホ撮影 / スキャンextract API(vision)

どの入口でも、最終的にユーザーが見るのは「AI が読み取った内容を1問ずつ確認していく会話」です。確認が終わると、できあがった整形済みデータが既存の取り込みウィザードにそのまま流れ込む(=検証ロジックは作り直さず再利用)のがポイントです。

§2 設計の背骨(中核思想)

このシステムを理解する鍵は、たった2つの原則です。コードの隅々がこの2つから演繹されています。

原則1: AI は提案だけ、確定・適用は決定的コード

AI(大規模言語モデル)は「この列はたぶんアイテム名」「この日付はたぶん賞味期限」と当たりをつける役だけ。実際にデータを変換し、全行に適用し、最終的に何を取り込むかを決めるのは必ず普通のコードです。理由は2つ。

AI に全行を渡して変換させると、(1) 行数が多いと高額・低速になり、(2) AI が値を勝手に創作(ハルシネーション)して静かにデータを汚す。だから AI には「ヘッダー+サンプル数十行+列の統計」だけ渡し、変換は許可リスト化したコードで決定的に行う。

AI がやること

  • 列の意味の推測(アイテム名 / 在庫数 / 属性…)
  • 新しいアイテム情報の提案(型・選択肢つき)
  • 価格列・個人情報列の検知
  • やさしい要約・確認文の生成

コードがやること

  • AI 出力の意味的な検証・矯正(sanitizeAnalyzeResult)
  • 許可リストの変換を全行に適用(applyAnalysisToRows)
  • 列の質問計画・自動確定の判断(buildColumnPlans)
  • 既存ウィザードと同じ検証(checkCsvHeader)

原則2: データのある列を黙って消さない

旧フローの最大の事故が「列が無言で消える」ことでした。新フローの不変条件は「中身のある列は、捨てるとしても必ず一度ユーザーに見せる」。AI が取りこぼした列・捨てようとした列は、コード側が拾い直して「これは使いますか?」と確認に回します(sanitizeAnalyzeResult の救済フォールバック)。

この2原則のため、要所には「AI の出力をそのまま信じず、コードが検算する関門」が必ず置かれています。sanitize が付く関数群がその関門です。
§3 6層アーキテクチャ

コードは責務でくっきり層分けされています。上から下へ依存(上が下を呼ぶ)、下は上を知りません。ブラウザ側(上3層)とサーバー側(下4層)は HTTP で繋がります。

UI 層画面・会話
会話スレッドの本体と、見た目だけの再利用部品。状態機械はここ。
AiImportThread.tsx / components/Common/AiFlow/* / CSVImportModal.tsx / 入荷・出荷モーダル
▲ ユーザー操作 / ▼ 呼び出し
フック層進行状態
「いま何列目を聞いているか」などの進行管理。回答そのものは持たず、UIへ橋渡し。
hook/useColumnFlow.ts / tasks/shared/useAiTradeImportFlow.ts
▼
クライアント層API呼び出し
サーバーの5つのAPIを叩く薄いラッパー。中断(AbortSignal)対応。
services/client/aiImport_repository.ts
⇅ HTTP(ここでブラウザ↔サーバー)
API 層入口・検証
認証・入力検証(Zod)・ボディサイズ上限を通してハンドラへ。LLM は呼ばない。
pages/api/v1/ai-import/{analyze,commit,extract,interpret-column,lookup}.ts
▼
ハンドラ層処理本体
会社確認・権限・クォータ→LLM呼び出し→矯正→DB保存→テレメトリ。処理の中核。
services/server/aiImport/*Handler.ts / aiUsage/dailyQuota.ts / recordTelemetry.ts
▼
共有ロジック層純粋関数・型
型の背骨・プロンプト生成・矯正・変換・列分類。ブラウザとサーバーの両方から使う。
services/aiImport/{contract,applyAnalysis,columnDecision,detectTransforms,cleanTable,interpretColumn,buildAnalyzePrompt,…}.ts
▼
データ層DB・cron
セッション・テレメトリ・利用カウンタの保存と、古いテレメトリの定期削除。
prisma/schema.prisma(ImportSession / ImportTelemetry / AiUsageCounter) / cloudflare/import-telemetry-cleanup

共有ロジック層が「両側から使われる」のがこの設計の特徴です。たとえば applyAnalysisToRows はブラウザ側(確定時の整形)でも使われ、sanitizeAnalyzeResult はサーバー側(AI出力の矯正)で使われます。型(contract.ts)を1枚に集約することで、ブラウザとサーバーが「同じ言葉」で会話できます。

§4 モジュール全体マップ

本ブランチで追加・変更した本番コード(テスト除く)の主役を、層ごとに並べます。背骨 が型の中心 contract.ts。詳細な責務は ②レイヤーと責務 へ。

共有ロジック層(src/services/aiImport/)

contract.ts 背骨
入出力の型(Zodスキーマ)と、AI出力を検算する sanitizeAnalyzeResult。全ての中心。
applyAnalysis.ts
確定した方針を全行に決定的に適用し、nanco正規の表へ。変換の実装本体。
columnDecision.ts
「どの列を質問し、どれを自動確定するか」を決める(buildColumnPlans)。
detectTransforms.ts
列データが対応先の型に合っているか全行判定し、救済整形を提案。
cleanTable.ts
解析前に全空の列を落とす前処理(落とした列名を返す)。
interpretColumn.ts
列カードの自由入力(「数値にして」等)を1ショットで解釈。
buildAnalyzePrompt.ts
解析用のシステム/ユーザープロンプトを組み立てる。
extractDocument.ts
画像/PDF抽出(vision)のスキーマ・プロンプト・サニタイズ。
headerFingerprint.ts
列名の指紋と重なり率。「前回と同じ形式か」の照合。
buildReuseDecisions.ts
確定内容を「ルールだけ」に圧縮(行の値は持たない)。
applyReuseDecisions.ts
前回ルールを今回のデータに当てる(合わなければAI解析へ)。
maskTelemetry.ts
テレメトリのPIIマスク・件数化(サーバー専用)。

サーバー(API / ハンドラ / コスト)

pages/api/v1/ai-import/*.ts
5つのAPI入口。認証・Zod検証・ボディ上限。
server/aiImport/*Handler.ts
analyze / commit / extract / interpretColumn / lookup の処理本体。
server/aiImport/recordTelemetry.ts
改善用テレメトリの追記(opt-out尊重・失敗握り潰し)。
server/aiUsage/dailyQuota.ts
会社×日×機能の日次上限ゲート(コスト暴走の防波堤)。
server/aiClient/index.ts
OpenAI Responses API ラッパー。store:false 強制・tier切替。

クライアント / UI / フック

client/aiImport_repository.ts
5つのAPIを叩くラッパー。AbortSignal対応。
folder/CSV/components/AiImportThread.tsx
会話UIの本体(1,938行)。状態機械・列カード・最終レビュー。
components/Common/AiFlow/*
吹き出し・カード・型ピッカー・自由入力欄など再利用部品。
folder/CSV/hook/useColumnFlow.ts
列を1つずつ送る進行状態(reducer)。
tasks/shared/useAiTradeImportFlow.ts
入荷/出荷向けのAIライフサイクル(画像・再利用・同意)。
tasks/shared/aiTableToParsedRows.ts
AIの表を既存の受払パーサ行へ変換(無改修注入)。

データ層

ImportSessionDB
1回の取り込みセッション。会話の決定・再利用ルールを保存。
ImportTelemetryDB
改善用の記録(PIIマスク済・90日保持)。本体と別テーブル。
AiUsageCounterDB
会社×日×機能の利用回数。日次上限の原子的ゲート。
cloudflare/import-telemetry-cleanup
90日超のテレメトリを毎日バッチ削除する cron worker。
§5 5つのAPIと役割

サーバーが公開する入口は5つだけ。LLM(お金がかかる呼び出し)を使うのは3つ(analyze / extract / interpret-column)、残り2つ(commit / lookup)は DB 操作だけです。

API役割LLMモデル上限
analyze列統計+サンプルを解析して取り込み方針を出す使うgpt-4o(strong)200/日/社
extract画像/PDFから明細表+伝票ヘッダーを抽出(vision)使うgpt-4o(vision)200/日/社
interpret-column列カードの自由入力(「数値にして」等)を解釈使うgpt-4o-mini(cheap)1000/日/社
commit取り込み確定。再利用ルールを保存使わない——
lookup「前回と同じ形式」を過去セッションから探す使わない——

全APIは withAuth(Firebase認証)でラップされ、ハンドラ内で必ず会社メンバーシップ検証(越境=IDOR防止)を通します。LLMを呼ぶ3つと書き込みのcommitは assertCanWrite で権限・サブスクも確認。詳細は ⑤コスト制御。

§6 データの流れ(鳥瞰)

在庫CSVの場合の最短ルート。各段の詳細は ③在庫CSVの全行程 で1ステップずつ解説します。

AI が判断 コードが決定的に処理 DB
① ドロップ
CSVImportModal
テンプレ一致なら従来へ、不一致だけAIへ
② 前処理
dropEmptyColumns
空列を落とし、列統計を集計
③ 解析
analyze API
AIが列の意味・提案を返す
④ 矯正
sanitizeAnalyzeResult
範囲外・架空名を除去、消える列を救済
⑤ 列計画
buildColumnPlans
質問する列/自動確定する列を仕分け
⑥ 会話確認
AiImportThread
迷う列だけ1問ずつ確認
⑦ 適用
applyAnalysisToRows
全行を正規の表へ変換
⑧ 既存ウィザード
useSpreadsheetImport
既存の検証・取り込みを再利用
⑨ 確定保存
commit API
成功後に再利用ルールを保存
§7 用語集(これだけ押さえれば読める)
AnalyzeResult
AI が返す「取り込み方針」一式。列の対応(columnMapping)・変換ルール・新規属性提案・価格列・PII列・フォルダ計画・伝票ヘッダー・確認事項・要約 を内包する。型の中心は contract.ts。
sanitize〜(矯正・検算)
AI 出力をそのまま信じず、コードが意味的に検証して直す関門。範囲外の列番号や存在しない属性名を除去し、消えそうな列を救済する。sanitizeAnalyzeResult / sanitizeExtractedDocument / sanitizeInterpretResult。
ColumnPlan / buildColumnPlans
各列を「自動確定 / 新規属性 / 救済整形 / 曖昧 / 価格」に分類した計画。確認が要る列だけが会話の質問になる。
transform(変換)
許可リスト化された機械的変換だけ(nfkc 全角半角統一 / extractNumber 数字抜き出し / normalizeDate 日付整形 / cleanText 整える)。AIは「どれを使うか」選ぶだけで、実装はコード。
lossy(破壊的)
変換で意味が落ちる可能性があるもの(単位や語を削る等)。lossy な時だけ「直していい?」と確認に出し、無害な変換は黙って適用する。
documentFields(伝票ヘッダー)
取引先名・伝票番号・伝票日付の「文書レベル」のメタ情報。列ではなく1伝票=全行共通の値。入荷/出荷で使い、在庫では常に空。
ReuseDecisions(形式別再利用)
「前回この形式はこう取り込んだ」をルールだけで再現する記録。行の値は一切持たない(PIIを貯めない設計)。
tier(strong / cheap)
LLM のモデル選択。strong=gpt-4o(解析・vision)、cheap=gpt-4o-mini(列の自由入力解釈)。
クォータ(日次上限)
会社あたり1日に呼べる回数の上限。コスト暴走を止める防波堤。AiUsageCounterDB の原子的カウントで管理。