Client SDK キー (
osk_ 接頭辞) で認証。非同期 (即時返却) + ポーリング構造 — POST はラウンドを開始して即座に識別子を返し、クライアントはディテールエンドポイントで状態を確認します。モデル
- chat_id = 1 つの会話。最初のラウンドで発行され、以降同じ chat_id でフォローアップを累積。
- round_id = ラウンド識別子。POST 1 回 = ラウンド 1 個。各ラウンドは prompt + 結果スナップショットを保持。
- フォローアップコンテキスト (2026-05-19~): 同じ chat の 直前の COMPLETED ラウンドすべて が
(user prompt, assistant meal JSON)のペアとして自動的に llm-demo のconversation_historyに積まれます。クライアントは{chat_id, prompt}を送るだけで OK — 「ワイン追加」「豚肉抜きで」のような短い精密化発話でも直前ラウンドの ingredients を基準点に精密化されます。meal を復元できないラウンドは静かにスキップ。 - 応答言語:
profile.locale優先、無ければテナント既定言語、それも無ければko。メッセージ系テキスト (recipe_title/intro_message/recipe_description/recommendation_message/notes等) がその言語で出力されます。サポート言語:ko/en/ja。 reasonフィールドは例外: テナント管理画面に通知として表示され、UI 言語切替時にデータ再取得なしで即座に表示言語を切り替える必要があるため、常に{ko, en, ja}オブジェクトで返ります (3 言語すべて充填済み)。- フォローアップ上限: 1 ラウンド内で refinement が 8 回を超えると
409 REFINEMENT_CAP_EXCEEDED。新ラウンド (POST) または新 chat でリセット。 - 進行中ラウンド保護 (2026-05-20~): 直前ラウンドの step2 がまだ完了していない 状態でのみ
409 PRIOR_ROUND_IN_FLIGHT。pipeline_progress.step2 == "done"(todos 充填済み) 以降は step3/step4 がバックグラウンド進行中でも follow-up を即時受付。ラウンドは step2 境界まで直列化され、step3/4 はラウンドごとに独立してバックグラウンド進行。
1. 推薦 kickoff
リクエスト
| フィールド | 型 | 必須 | 説明 |
|---|---|---|---|
chat_id | string | × | 未指定なら新規 chat 作成、指定すれば既存 chat に新ラウンド追加 |
prompt | string | ○ | ユーザーの自然言語クエリ |
🧑 persona はプロフィールから自動注入 — リクエスト body には別途フィールドなし。サーバーがGET /v1/profile/{profile_id}のdata.personaを読み取り、値があれば user_query prelude にpersona(ペルソナ: ...)として prepend。プロフィール登録/更新時にPUT /v1/profile/{profile_id}のpersonaフィールド (自由記述文字列、nullable) で更新できます。
⚡ fire-and-forget — このエンドポイントは即時 (~100ms) で応答します。LLM 呼び出しはバックグラウンドで進行するため、結果はポーリングで取得します。RefinementCapExceeded のような LLM エラーも同期 4xx ではなくround.status=error+last_errorとして永続化 → ポーリングで検知。
レスポンス 202
GET /v1/chat/{profile_id}/recommend/{chat_id} をポーリングし、最後のラウンドの pipeline_progress.step4 == "done" になったら完了。
2. チャットリスト (ページング)
| クエリ | デフォルト | 説明 |
|---|---|---|
page | 0 | 0-indexed |
limit | 100 | ページサイズ (最大 500) |
order | desc | created_at ソート、desc / asc |
レスポンス 200
chat_id + total_rounds + ラウンド識別子のミニ配列 ({round_no, round_id} のみ) + created_at。ラウンド本体 (prompt / 結果) はディテール / 単一ラウンドエンドポイントで取得。ページング情報は pageInfo に集約。
3. チャットディテール (rounds[])
KICKED_OFF) のラウンドを llm-demo に再問い合わせし、完了していれば DB にスナップショット保存。完了済みラウンドはそのまま返却。
レスポンス 200 — 進行中 (最後のラウンド未完了)
🔑 スキーマ一貫性 (2026-05-09~) —pipeline_resultがまだ空でも、スナップショットのキー類は 常に存在 します。クライアントはキー有無ではなく 値 ベースの分岐 (pipeline_progress.stepN == "done"、items.length > 0) で記述してください。
pipeline_progress.stepN == "done" の時点から):
step1(~5s): 専用キーは未充填 (LLM 応答完了 + meal 生成)step2(~10s):todos.recipe_title / intro_message / recipe_description / recommendation_message / ingredients[] / items[].product充填step3(~15s):recommended_items[]充填 (reason= カテゴリタグ、reason_detail= 分析文、reason_detail_marketing= 顧客向けコピー)step4(~20s):notifications.ad.items(サイネージ) +notifications.coupon[](アプリ内クーポン) 充填notifications.coupon_spot[]は step 非依存 — 店舗のクーポンスポットマスターを毎ポーリング store-API から取得 (LLM 推薦ではない)
レスポンス 200 — 完了 (todos / recommended_items 充填)
| ラウンドフィールド | 説明 |
|---|---|
round_no | 1-indexed の順番。何番目の質問へのラウンドか (最後の要素の round_no が総ラウンド数と一致) |
status | in_progress / completed / error |
back_list_status | llm-demo の not_started / in_progress / completed (スナップショット未取得時は null) |
todos | 買い物リスト + recipe メタ。各要素 {quantity, product}、total = Σ(product.price × quantity) |
todos.recommendation_message | step1 (meal) のパーソナライズ推薦理由 — プロフィール (name / gender / age / extra) シグナルを反映した人間向けコメント。recipe_description (メニュー一般説明) とは別物。profile locale で出力 |
todos.items | 店内ピックアップ動線順にソート (2026-05-20~) — product.section_code 基準の安定ソート。順序: 青果(PR) → デリ(DL) → ベーカリー/製菓(BK/SW/SN) → 酒類(AL) → 乳製品/飲料(DY/DR) → 食料品(PA) → 精肉(MT) → 水産(FI) → 冷凍(FR)。ピッカーがカテゴリーを一筆書きで回れるように。表に無いコード (EC/HH/CK 等) は末尾。同カテゴリー内は元の順序維持 |
recommended_items | メニューに合う cross-sell。reason = カテゴリタグ ("cross-sell" / "upsell" / "impulse")、reason_detail = 運用者/ダッシュボード向け分析文 (lift / CVR / co_count)、reason_detail_marketing = 顧客向けコピー、product = store マージ結果。reason_detail / reason_detail_marketing は常に {ko, en, ja} オブジェクト — upstream が単一 locale のみ返す間は同じ値を 3 キーにコピー (shape のみ一貫) |
notifications.ad | サイネージ ads バンドル { signage, items }。signage = サイネージ 1 台の {x, y, radius} (なければ null)、items = type=ads 推薦リスト (top 3)。priority_score 降順 1-indexed rank |
notifications.coupon | アプリ内クーポン項目配列。各項目の location {x, y, radius} は item_id から store の product → Section 座標を解決して埋め込み (未マッチ時 null) |
notifications.coupon_spot | 店舗管理側のクーポンスポットマスター (LLM 推薦ではなく、毎ポーリング store-API から取得)。coupon[] と 完全に同じ 19 フィールド schema — iOS が同じデコーダで両者を処理可能。type = "coupon"、rec_id / reason_* / applied_score / priority_score / method 等推薦シグナルはすべて null。discount_pct ← 管理画面 discount_value、rank ← spot_no ASC インデックス。location は product.location ではなく スポット自身の座標 {x, y, radius} |
notifications.*[].item_id | store カタログの product id。llm-demo が付与できない場合 null — item_name / item_name_en でカード表示は可能 |
notifications.*[].product | store DB マッチ結果のオブジェクト一式 — id / name / brand / barcode / price / price_unit / src_url(画像) / location / section_code / section_name / quantity (未マッチ時 null) |
notifications.*[].price / price_unit | product.price / product.price_unit の平坦化 — カード価格 hot path 用 (未マッチ時いずれも null)。旧 price_yen は廃止 — 店舗 DB を単一ソース化 |
notifications.*[].type | "ads" (サイネージ) / "coupon" (アプリ内)。グループキーと同じだが項目単位処理時にも使用 |
notifications.*[].rec_id | 推薦イベント追跡 id — /trigger/signage の items[].rec_id / /trigger/coupon の rec_id として返す |
notifications.*[].reason_type / reason_detail / reason_detail_marketing | step4 推薦理由 3 種 — reason_type = カテゴリタグ ("cross-sell" / "zone_efficiency" 等)、reason_detail = 運用者/ダッシュボード向け分析文、reason_detail_marketing = サイネージ/クーポンに表示する顧客向けコピー。reason_detail / reason_detail_marketing は {ko, en, ja} オブジェクト (upstream が単一 locale の間は 3 キーで同じ値) |
last_error | status: error 時のみ (例: meal_suggest failed: upstream timeout / upstream chat session expired) |
4. 単一ラウンド取得
rounds[i] と同一 (chat メタ / profile_summary なし、該当ラウンドのみ):
NOT_FOUND 404。
5. サイネージ ads トリガー送信 — POST /trigger/signage
iOS sdk が UWB によりサイネージの location radius に 進入 / 離脱 した際にそれぞれ呼び出します。action フィールドで分岐 (default "enter")。
- enter: ラウンドの
notifications.ad.itemsをサイネージへ forward → RECOGNIZED 画面遷移。サーバー側で (1) item ごとに sdk_chat_event 永続化 (2) テナント管理者 SSE 配信 (3) store-API へ forward。 - exit: サイネージ IDLE 即時復帰。永続化 / SSE はスキップ、store-API forward のみ。
サイネージ番号別の画面表示ポリシー
Gateway はitems[] 全体を store-API へ forward し、どの項目をどう表示するかはサイネージ番号ごとのクライアントが決定します。
| サイネージ | IDLE 画面 | ACTIVE (RECOGNIZED) 画面のデータソース |
|---|---|---|
| #1 (入場サイネージ) | 管理画面で登録した 1~4 スライド (クロスフェード) | items を discount_pct 降順でソートし top 1 を単一商品カードで表示。横/縦自動切替。reason_detail を訴求コピーとして使用 |
| #2 (店内サイネージ) | 9 スロット (3 スライド × 商品 3 個) | 3 セクション構成 — ① forgotten[0] (買い忘れ商品) ② items[1] || items[0] (広告 1 件、インデックス基準) ③ store-API の signage_event から enabled=true の 1 件をランダム |
forgotten 未添付ならその領域を省略)。サイネージ #1 は単一カード設計のため discount_pct の並びが意味を持ち、#2 は広告横に「買い忘れ / イベント」を同時表示する設計のため index 1 を優先 (index 0 はより強いシグナルとして他の用途に予約)。
Request — Enter (エリア進入)
最小ペイロードはchat_id + round_id のみ。action 省略時は "enter" が default。Gateway がラウンドの永続化された notifications.ad.items を lookup して ADS_TRIGGERED を発行します。
Request — Exit (エリア離脱)
{ "success": true, "data": { "event_ids": [], "accepted": true, "action": "exit" } }
Request — Enter (items override + forgotten)
items を明示的に渡すと、その値が優先されます (ライブデータ / 部分セット用)。forgotten でユーザーが店舗内で買い忘れた商品リストも添付可能 (2026-05-20~):
| フィールド | 必須 | 説明 |
|---|---|---|
chat_id | ✅ | 推薦を受けた chat |
round_id | ✅ | その round |
items | optional | 省略 / 空配列 → Gateway がラウンドの永続データから自動 lookup。指定すると優先 |
items[].rec_id | ✅ (items 送信時) | 各項目の推薦イベント追跡 id |
items[].item_id / item_name / item_name_en / brand / zone_id / zone_name / price_yen / discount_pct / applied_score / priority_score / method / reason_type / reason_detail / reason_detail_marketing | optional | FIREHOSE / サイネージ表示用。表示ポリシーは signage_no 別 — 上記「サイネージ番号別の画面表示ポリシー」表を参照 (#1 = discount_pct top 1 単一カード、#2 = items[1] || items[0] 広告 + forgotten + event の 3 セクション) |
forgotten | optional | ユーザーが店舗内で買い忘れた商品リスト。iOS sdk が該当する todos.items を そのまま添付。各要素の schema は自由 (any) — Gateway は加工せず、永続化 payload・管理者 SSE 配信・store-API forward すべてに root 階層でそのまま透過。空配列 / 省略時はキー自体不在。exit 時は無視 |
Response 202
event_ids は永続化された項目数だけ返却。呼び出し頻度 = location 進入回数 (1 ラウンド + 1 サイネージ = 1 呼び出し)。
サーバーは item ごとに step4 の store_reasoning.analyst_comment を raw pipeline から抽出し、analyst_comment キーで SSE payload に付与します — 管理画面 user-analytics のサイネージイベントの「推薦根拠」として表示。
6. クーポン発行/使用送信 — POST /trigger/coupon
iOS sdk が notifications.coupon[] の項目をアプリ内で発行/使用/拒否する際に項目 1 件ずつ呼び出します。サイネージ非関連のため (1) 永続化 (2) テナント管理者 SSE 配信 のみ。
Request
最小ペイロードはchat_id + round_id + rec_id のみで OK。Gateway がラウンドの永続化された notifications.coupon[] から rec_id で照合してメタを自動補完します。
| フィールド | 必須 | 説明 |
|---|---|---|
chat_id / round_id | ✅ | 上記と同じ |
rec_id | ✅ | 推薦イベント追跡 id (ラウンドのスナップショットとの照合キー) |
kind | optional | "issued" / "redeemed" / "dismissed" (デフォルト issued) |
| その他 | optional | 省略時はマッチした項目から自動補完。指定時はその値が優先 |
Response 202
7. 個別推薦の表示ログ — POST /trigger/recommend
iOS sdk が recommended_items[] (step3) の 1 項目をアプリ内で表示/タップした際に 1 回限りのログ として呼び出します。サイネージ / store-API 非関連 — (1) sdk_chat_event 永続化 (2) テナント管理者 SSE 配信 のみ。
Request
| フィールド | 必須 | 説明 |
|---|---|---|
chat_id / round_id | ✅ | 上記と同じ |
rec_id | ✅ | 推薦イベント追跡 id (recommended_items[].rec_id または id) |
recommended_items[] から rec_id でマッチングし、item_name / price / reason_detail / reason_detail_marketing 等のメタを自動 lookup して payload に埋め込みます。step3 の surface_reasoning.analyst_comment も raw pipeline から抽出して analyst_comment キーで付与 — 管理画面 user-analytics の個別推薦イベントの「推薦根拠」として表示。
Response 202
8. ラウンドのバーコード一括取得 — GET /recommend/{chat_id}/{round_id}/barcodes
ラウンド内の product をカテゴリ別にまとめ、EAN-13 PNG (base64) と共に返します。管理画面 / デモ用 — store-API 再呼び出しなし (ラウンドの平坦化データ + ZXing でその場 PNG 生成)。
Response 200
barcodeが 13 桁の数字でない、または checksum エラーの場合はbarcode_image: null— クライアント側は null ガードのみ実装。- 同一 product が複数カテゴリに現れることがあります。
ポーリングガイド
- 推奨間隔: ~2 秒 (フルパイプライン平均 ~46s)
rounds配列の最後の要素のstatusのみチェック — 過去ラウンドは常にcompleted(またはerror) で固定- フォローアップ POST 後にポーリングすると配列長 +1、最後の要素が
in_progressから再開 error/ TTL 期限切れ: 新規 chat 作成 (chat_id なしで POST) を推奨
エラーコード
| HTTP | code | 原因 |
|---|---|---|
| 400 | INVALID_REQUEST | body 検証失敗 (prompt 欠落等) |
| 404 | NOT_FOUND | profile / chat 不存在または所有権不一致 |
| 402 | INSUFFICIENT_CREDIT | テナントクレジット残高不足 |
| 409 | REFINEMENT_CAP_EXCEEDED | 同一ラウンドでフォローアップが 8 回を超過 — 新ラウンド/新 chat でリセット |
| 409 | PRIOR_ROUND_IN_FLIGHT | 直前ラウンドの step2 がまだ未完了 — pipeline_progress.step2 == "done" 確認後に再試行。step3/4 進行中でも step2 さえ終われば follow-up OK |
| 500 | INTERNAL_ERROR | 上流障害等 |