0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AI導入の保留案件をGASで管理する:再開条件・停止理由・確認期限を残す

0
Posted at

AI導入を試していると、「いったん保留」という判断が出ます。保留自体は失敗ではありません。契約条件の正本が決まっていない、確認者が不在、入力項目が足りないなど、今は安全に進められない理由を残す状態です。

この記事では、GoogleスプレッドシートとGoogle Apps Script(GAS)でAI導入候補の保留台帳を作ります。保留の自動解除やAIへの自動再投入は行わず、再開候補を人間が確認できる状態にするところまでを扱います。

作るもの

AI導入候補を人間確認へ戻すゲート

ADOPTION_HOLD シートの1行目を次の列にします。

hold_id,work_id,created_at,reason_code,impact_level,owner_role,due_at,restart_condition,status,last_reviewed_at,source_ref,next_action

status は自由入力にせず、次の値に限定します。

HOLD -> REVIEW_READY -> HUMAN_REVIEW
  └──────────────────────> STOPPED

REVIEW_READY は再開許可ではなく、条件を確認する人のキューへ入った状態です。

保留理由を設定ファイルで固定する

const ADOPTION_HOLD_CONFIG = Object.freeze({
  sheetName: 'ADOPTION_HOLD',
  statuses: ['HOLD', 'REVIEW_READY', 'HUMAN_REVIEW', 'STOPPED'],
  reasonCodes: [
    'SOURCE_NOT_DECIDED',
    'REVIEWER_NOT_ASSIGNED',
    'REQUIRED_INPUT_MISSING',
    'SENSITIVE_SCOPE',
    'ROLLBACK_NOT_READY',
  ],
  reviewWindowDays: 7,
});

契約、料金、返金、苦情、個人情報などを含む SENSITIVE_SCOPE は、資料がそろっただけでは再開しません。対象範囲と確認者を人が決めます。

列名から行を読み込む

列番号を直接参照すると、列追加時に別の値を更新する危険があります。

function readHoldRows_() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet()
    .getSheetByName(ADOPTION_HOLD_CONFIG.sheetName);
  if (!sheet) throw new Error('ADOPTION_HOLD sheet is missing');

  const values = sheet.getDataRange().getValues();
  if (values.length === 0) return { sheet, headers: [], rows: [] };
  const headers = values[0].map(String);
  const rows = values.slice(1).map((rowValues, index) => {
    const row = { _rowNumber: index + 2 };
    headers.forEach((header, column) => {
      row[header] = rowValues[column];
    });
    return row;
  });
  return { sheet, headers, rows };
}

function assertHoldHeaders_(headers) {
  const required = [
    'hold_id', 'work_id', 'reason_code', 'owner_role',
    'due_at', 'restart_condition', 'status', 'next_action',
  ];
  const missing = required.filter((name) => !headers.includes(name));
  if (missing.length > 0) {
    throw new Error('missing hold columns: ' + missing.join(', '));
  }
}

期限切れを自動解除せず、確認キューへ送る

期限が来たからといって保留案件を再開すると、古い根拠や未確認の入力を処理する可能性があります。GASは期限を検出し、再確認が必要な行だけを REVIEW_READY にします。

function markReviewReadyHolds() {
  const { sheet, headers, rows } = readHoldRows_();
  assertHoldHeaders_(headers);
  const column = Object.fromEntries(
    headers.map((name, index) => [name, index + 1])
  );
  const now = new Date();
  rows
    .filter((row) => row.status === 'HOLD')
    .filter((row) => row.due_at && new Date(row.due_at) <= now)
    .forEach((row) => {
      sheet.getRange(row._rowNumber, column.status).setValue('REVIEW_READY');
      sheet.getRange(row._rowNumber, column.last_reviewed_at).setValue(now);
      sheet.getRange(row._rowNumber, column.next_action)
        .setValue('再開条件・正本・確認者を人が確認する');
    });
}

owner_role、restart_condition、source_ref が空欄なら、確認キューに置いたまま進めません。

保留を新規登録する

原文や顧客情報を保留台帳へ複製せず、業務IDと正本への参照だけを持たせます。

function addHold_(input) {
  const { sheet, headers } = readHoldRows_();
  assertHoldHeaders_(headers);
  if (!ADOPTION_HOLD_CONFIG.reasonCodes.includes(input.reasonCode)) {
    throw new Error('unknown reason code');
  }
  if (!input.workId || !input.ownerRole || !input.restartCondition) {
    throw new Error('workId, ownerRole, restartCondition are required');
  }
  const holdId = ['HOLD-', Utilities.getUuid()].join('');
  const row = {
    hold_id: holdId,
    work_id: input.workId,
    created_at: new Date(),
    reason_code: input.reasonCode,
    impact_level: input.impactLevel || 'unknown',
    owner_role: input.ownerRole,
    due_at: input.dueAt || addDays_(new Date(), ADOPTION_HOLD_CONFIG.reviewWindowDays),
    restart_condition: input.restartCondition,
    status: 'HOLD',
    last_reviewed_at: '',
    source_ref: input.sourceRef || '',
    next_action: input.nextAction || '保留理由を確認する',
  };
  sheet.appendRow(headers.map((name) => row[name] ?? ''));
  return holdId;
}

function addDays_(date, days) {
  const result = new Date(date);
  result.setDate(result.getDate() + days);
  return result;
}

work_id と source_ref は別に残します。台帳にメールアドレス、電話番号、認証情報、顧客本文を入れないでください。

人間が再開可否を記録する

再開の判定は、GASの条件一致だけで完了させません。確認者が正本・範囲・戻し先を見た上で状態を更新します。

function recordHumanReview(holdId, decision, note) {
  const allowed = new Set(['RESTART_APPROVED', 'CONTINUE_HOLD', 'STOPPED']);
  if (!allowed.has(decision)) throw new Error('invalid human decision');
  if (!note || !note.trim()) throw new Error('review note is required');
  const { sheet, headers, rows } = readHoldRows_();
  const row = rows.find((item) => item.hold_id === holdId);
  if (!row) throw new Error('hold record not found');
  const column = Object.fromEntries(
    headers.map((name, index) => [name, index + 1])
  );
  const nextStatus = decision === 'STOPPED' ? 'STOPPED' : 'HUMAN_REVIEW';
  sheet.getRange(row._rowNumber, column.status).setValue(nextStatus);
  sheet.getRange(row._rowNumber, column.last_reviewed_at).setValue(new Date());
  sheet.getRange(row._rowNumber, column.next_action)
    .setValue(decision + ': ' + note);
}

RESTART_APPROVED を受けても、外部返信やAI処理を自動実行しません。人が対象範囲、入力境界、承認者、ロールバック先を確認してから、別の手順で一件ずつ再開します。

運用チェックリスト

  • 保留理由を固定コードで記録している
  • 保留期限と次の確認日がある
  • 再開条件が具体的になっている
  • REVIEW_READY を再開許可と解釈していない
  • 正本への参照と業務IDを残している
  • 顧客本文、個人情報、認証情報を台帳へ複製していない
  • 契約、料金、返金、苦情、個人情報は人間確認へ戻している
  • 再開後も外部送信は人が承認する
  • 停止・保留を解除した人、理由、日時を記録している

まとめ

AI導入の保留は、進めない判断を隠す箱ではありません。なぜ止めたのか、何がそろえば再確認できるのか、誰が次に見るのかを残す業務状態です。

まずは過去の保留案件を10件ほど選び、reason_code、owner_role、restart_condition、due_at を埋めてください。再開できない案件も STOPPED と理由を残せば、次の担当者が同じ判断をやり直さずに済みます。

Miraigentでは、AIを使えるかどうかだけでなく、保留した時に次の人が安全に戻れるかを、導入前の確認項目として扱います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?