AI Design Import — I/O 定義
このページは apps/design-import/ の 公式 I/O 契約 です。フィールド、形状、ステータス値、エラーカテゴリの変更はすべて契約変更にあたります。コードを書く前に、必ずこのページと test-case-design.md を更新してください。DocumentDB ドキュメントの形状を変更したときは schemas.py の INDEX_SCHEMA_VERSION をバンプしてください。
読み順: 概要 → 入力 → 処理契約 → 出力 → エラーハンドリング → 冪等性。最後の フィールド参照 はルックアップ用です。
概要
flowchart LR
api[guinness-backend] -->|INSERT design status=0| RDB[(PostgreSQL)]
api -->|PUT image.png| S3
api -->|PUT schema.json| S3
api -->|SendMessage| Q[(SQS design-import)]
Q --> L[AI Design Import worker]
L -->|GET image.png| S3
L -->|GET schema.json| S3
L -->|UPSERT _id=design_id| Doc[(DocumentDB design)]
L -->|POST /v1/webhooks/ai-status| api
api -->|UPDATE design status=1 or 2| RDB
| 項目 | 値 |
|---|---|
| トリガー | design-import キューの SQS レコード |
| 入力 | SQS メッセージ 1 件 + S3 画像 1 つ + S3 Figma JSON 1 つ |
| 出力 | DocumentDB upsert 1 件 + Webhook POST 1 件 |
| RDB 書き込み | なし — ワーカーは PostgreSQL / MySQL にアクセスしない(データベース分離) |
| 外部呼び出し | S3 GET(画像)、S3 GET(Figma JSON)、PydanticAI 経由の Vision LLM、OpenAI Embeddings(バッチ呼び出し 1 回、入力 3 つ)、Webhook POST |
| AI フレームワーク | PydanticAI(エージェント + 構造化出力)、埋め込みは openai SDK を直接使用 |
入力
1. SQS メッセージ(トリガー)
Lambda は標準の AWS SQS イベントを受信します。Records[*].body に JSON 文字列として下記フィールドが入ります。
{
"Records": [{
"messageId": "0192a3b4-c5d6-7e8f-9a0b-1c2d3e4f5a6b",
"body": "{\"organization_id\":1,\"project_id\":42,\"img_url\":\"s3://gnss-prod-inputs/designs/42_hDDA9B..._40002029:37033.png\",\"node_id\":\"40002029:37033\",\"file_id\":\"hDDA9BNori9OTXSClduXqR\",\"design_id\":\"42_hDDA9BNori9OTXSClduXqR_40002029:37033\",\"design_name\":\"Login Screen\",\"json_schema_url\":\"s3://gnss-prod-inputs/figma_json_schema/42_hDDA9BNori9OTXSClduXqR_40002029:37033.json\"}",
"attributes": { "ApproximateReceiveCount": "1" },
"eventSource": "aws:sqs"
}]
}
Body フィールド(Records[*].body からパース、SqsBody Pydantic モデルで検証):
| フィールド | 型 | 必須 | 規約 | 備考 |
|---|---|---|---|---|
organization_id |
integer | ✓ | > 0 |
テナントスコープ。DocumentDB ドキュメントにコピーし、Webhook では organizationId として返す |
project_id |
integer | ✓ | > 0 |
プロジェクトスコープ。DocumentDB ドキュメントにもコピー |
img_url |
string | ✓ | S3 URL(s3://... または HTTPS S3) |
デザインのスクリーンショット |
node_id |
string | ✓ | Figma node id(通常 <int>:<int> 形式) |
design_id の構成要素 |
file_id |
string | ✓ | Figma file id | design_id の構成要素 |
design_id |
string | ✓ | "{project_id}_{file_id}_{node_id}" と一致しなければならない — Pydantic model_validator で強制 |
DocumentDB の _id になる |
design_name |
string | ✓ | Figma フレーム名 | name および visual.metadata.name として保存 |
json_schema_url |
string | ✓ | Figma node JSON エクスポートの S3 URL | 必須のマッチング根拠。ダウンロード、パースし、構造/ノード metadata として永続化 |
Body アンラップ。unwrap_sqs_body ユーティリティは次の 3 つのネスト形状を(優先度順に)受け入れます。
- 生ペイロード —
bodyが上記の JSON。 - SQS-in-SQS —
body.Records[0].bodyを再パース。 - API Gateway プロキシ —
body.body(任意でisBase64Encoded)。
2. S3: デザイン画像
| プロパティ | 値 |
|---|---|
| 入手元 | SQS body の img_url |
| サイズ上限 | 10 MB ハードリミット(service.py の MAX_IMAGE_BYTES) |
| MIME 検出 | mimetypes.guess_type(url)、フォールバックは image/png |
3. S3: Figma JSON
| プロパティ | 値 |
|---|---|
| 入手元 | SQS body の json_schema_url |
| SQS フィールド | 必須 — null または省略は検証エラー |
| ワーカーが取得するか? | する — 成功する import では必ずダウンロードしてパース |
| 失敗時の挙動 | failed Webhook を送信し、SQS リトライのため再送出 |
期待される Figma JSON 形状:{"nodes": {"<node_id>": {"document": <node>, ...}}}
4. RDB の行状態(別所で読み込み)
ワーカーは PostgreSQL の design 行を 直接読み込みません。SQS メッセージ発行前に、バックエンドが status = 0 で行を作成します。ワーカーは Webhook で完了報告するだけで、行のステートマシンはバックエンドが管理します。
処理契約
ワーカーは各レコードに対して下記の 8 ステップを順番に実行します。どこかで失敗したメッセージは エラーハンドリング のパスに進みます。
-
パース + 検証:
SqsBodyで SQS body を検証。organization_id > 0、project_id > 0、design_id == f"{project_id}_{file_id}_{node_id}"を確認し、不一致ならValueError。 -
デザイン画像ダウンロード: S3 から取得。10 MB 超過の場合は例外。
-
Figma JSON のダウンロード + パース。
figma.extract_structural(schema, node_id)を呼び出し、下記を取得: structural_text— コンパクトな正規化ツリー文字列(例:tree=FRAME[H,gap_none]>[...] | components=... | depth=N)component_ids— ツリー内の全INSTANCE.componentIdをソートしたリストviewport— ルートノードのabsoluteBoundingBoxから取得した{width, height, aspect_ratio, device_class}
失敗(キー不足、ネットワークエラーなど)した場合はレコードを失敗扱いにする。成功した design document は必ず Figma 由来の structural evidence を含む。
-
Vision Agent 実行(PydanticAI、
run_sync): 画像からDesignDescriptionを取得:使用モデル:class DesignDescription(BaseModel): layout: str # 11 種類のアーキタイプのうちひとつ component_types: list[str] # 3〜15 個の正規化 snake_case 名 color_palette: list[str] # 支配的な hex カラー 2〜5 個(#RRGGBB) typography_style: str # 8 種類のアーキタイプのうちひとつ semantic_words: list[str] # ドメインキーワード 5〜10 個 component_functions: list[str] # 再利用可能なコンポーネント役割/機能desc_model(環境変数で設定、例:openai:gpt-5.4-nano)。 -
埋め込み入力テキストの構築:
visual_text="layout: {layout} | components: {component_types} | palette: {color_palette} | typography: {typography_style}"semantic_text= 正規化したsemantic_words + component_functionsstructural_text(ステップ 3 で取得)-
query_terms= semantic words、component functions、component types、Figma node names、visible text、alias から構築して永続化する正規化済みマッチング term -
3 つの埋め込みを 1 回のバッチ呼び出しで生成:
client.embeddings.create( model=config.embedding_model, input=[visual_text, semantic_text, structural_text] dimensions=config.embedding_dimensions, # デフォルト 512 )data[0]→ visual、data[1]→ semantic、data[2]→ structural。 -
DesignDocumentを DocumentDB に upsert(_id == design_id)。 -
成功 Webhook を POST(
webhook_base_urlへ)。
すべての必要な埋め込みが揃った後にのみドキュメントを書き込みます — 部分的なドキュメントは存在しません。
出力
出力 1: DocumentDB design ドキュメント(upsert)
{
"_id": "42_hDDA9BNori9OTXSClduXqR_40002029:37033",
"organization_id": 1,
"project_id": 42,
"name": "Login Screen",
"type": "screen",
"based_on": "figma_import",
"file_id": "hDDA9BNori9OTXSClduXqR",
"node_id": "40002029:37033",
"json_schema_url": "s3://gnss-prod-inputs/figma_json_schema/42_hDDA9BNori9OTXSClduXqR_40002029:37033.json",
// des2code が読み込む、生成に即したテキストサマリー(vision LLM の再実行を避けるため)。
"generation_context": "Login Screen. Viewport: 375x812 (mobile). Layout: stacked_form. Typography: modern_sans. Palette: #1A1A2E, #FFFFFF. Components: input_email, input_password, button_primary. Domain: login, authentication.",
"viewport": {
"width": 375,
"height": 812,
"aspect_ratio": 0.4619,
"device_class": "mobile" // "mobile" | "tablet" | "desktop" | "wide"
},
"visual": {
"metadata": {
"name": "Login Screen",
"image_url": "s3://gnss-prod-inputs/designs/42_hDDA9B..._40002029:37033.png",
"encoder": "text-embedding-3-small@512",
"layout": "stacked_form",
"component_types": ["input_email", "input_password", "button_primary", "heading"],
"color_palette": ["#1A1A2E", "#FFFFFF", "#4A90E2"],
"typography_style": "modern_sans"
},
"vector_embedding": [/* 512 個の float */]
},
"semantics": {
"metadata": {
"words": ["login", "authentication", "form", "submit", "credentials"],
"component_functions": ["collect email", "collect password", "submit credentials"]
},
"vector_embedding": [/* 512 個の float */]
},
"structural": {
"metadata": {
"structural_text": "tree=FRAME[V,gap_md]>[FRAME[H,gap_none]>[TEXT_HEADING],INSTANCE[comp_input_email],INSTANCE[comp_input_password],INSTANCE[comp_button_primary]] | components=FRAME[H]x1 FRAME[V]x1 INSTANCE[comp_button_primary]x1 TEXT_HEADINGx1 | depth=2",
"component_ids": ["comp_button_primary", "comp_input_email", "comp_input_password"]
},
"vector_embedding": [/* 512 個の float */]
},
"query_terms": [
"login",
"authentication",
"email",
"password",
"submit",
"input_email",
"button_primary"
],
"figma_node": {
"file_id": "hDDA9BNori9OTXSClduXqR",
"node_id": "40002029:37033",
"name": "Login Screen",
"visible_text": ["Email", "Password", "Login"],
"node_names": ["Email Input", "Password Input", "Login Button"],
"component_ids": ["comp_button_primary", "comp_input_email", "comp_input_password"]
},
"localized_regions": {
"nodes": [
{
"node_id": "40002029:37101",
"name": "Login Button",
"bounds": { "x": 24, "y": 520, "width": 327, "height": 48 },
"visible_text": ["Login"],
"component_id": "comp_button_primary"
}
]
}
}
フィールド参照:
| フィールド | 型 | 入手元 |
|---|---|---|
_id |
string | SQS design_id |
organization_id |
integer | SQS organization_id |
project_id |
integer | SQS project_id |
name |
string | SQS design_name |
type |
string enum | デフォルト "screen"(DesignType.SCREEN) |
based_on |
string enum | デフォルト "figma_import"(DesignOrigin.FIGMA_IMPORT) |
file_id |
string | SQS file_id。Figma file key |
node_id |
string | SQS node_id。Figma node id |
json_schema_url |
string | SQS json_schema_url。ダウンロードしてパース |
generation_context |
string | LLM 出力から生成。200〜400 文字程度 |
visual.metadata.encoder |
string | インデックス時の "{embedding_model}@{embedding_dimensions}" |
visual.metadata.layout |
string | Vision LLM の出力 |
visual.metadata.component_types |
string[] | Vision LLM の出力(3〜15 個) |
visual.metadata.color_palette |
string[] | Vision LLM の出力、正規化 #RRGGBB(2〜5 個) |
visual.metadata.typography_style |
string | Vision LLM の出力 |
visual.vector_embedding |
float[512] | visual_text の埋め込み |
semantics.metadata.words |
string[] | Vision LLM の semantic_words(5〜10 個) |
semantics.metadata.component_functions |
string[] | Vision LLM のコンポーネント役割/機能 |
semantics.vector_embedding |
float[512] | 正規化した semantic words と component functions の埋め込み |
structural.metadata.structural_text |
string | 決定的 Figma ツリーレンダリング結果 |
structural.metadata.component_ids |
string[] | node tree 内の Figma component id をソートしたリスト |
structural.vector_embedding |
float[512] | structural_text の埋め込み |
query_terms |
string[] | Des2Code リランキングが使う正規化済みマッチング term |
figma_node |
object | Figma node identity、visible text、node names、component ids、viewport、bounds metadata |
localized_regions |
object|null | Figma node image と JSON から得た任意の node/region 単位 evidence |
書き込み操作:
collection.update_one(
{"_id": design_id},
{"$set": doc.model_dump(by_alias=True, mode="json")},
upsert=True,
)
ベクトルインデックス(3 つの HNSW インデックス、packages/models/documentdb/design.py が冪等に作成):
| インデックス名 | パス | 次元 | 類似度 | m |
efConstruction |
|---|---|---|---|---|---|
visualVectorIndex |
visual.vector_embedding |
512 | cosine | 16 | 64 |
semanticVectorIndex |
semantics.vector_embedding |
512 | cosine | 16 | 64 |
structuralVectorIndex |
structural.vector_embedding |
512 | cosine | 16 | 64 |
structural インデックスは legacy document ではスパースに動作する。新しい成功 import は必ず structural ブロックを含む。
出力 2: Webhook — success
POST {WEBHOOK_BASE_URL}
Content-Type: application/json
{
"type": "design-import",
"recordId": "42_hDDA9BNori9OTXSClduXqR_40002029:37033",
"projectId": 42,
"organizationId": 1,
"status": "success",
"result": {
"keywords": ["login", "authentication", "form", "submit", "credentials"],
"embeddingDims": 512,
"vectorsProduced": ["visual", "semantic", "structural"],
"indexSchemaVersion": "2026.05.2"
}
}
| フィールド | 型 | 備考 |
|---|---|---|
type |
string | 常に "design-import" |
recordId |
string | SQS body の design_id |
projectId |
integer | SQS project_id から |
organizationId |
integer | SQS organization_id から |
status |
string | "success" |
result.keywords |
string[] | semantics.metadata.words と同じ値 |
result.embeddingDims |
integer | 生成された全ベクトルの次元数(3 つ共通) |
result.vectorsProduced |
string[] | 成功時は常に ["visual", "semantic", "structural"] を含む |
result.indexSchemaVersion |
string | 処理時点の INDEX_SCHEMA_VERSION |
出力 3: Webhook — failure
POST {WEBHOOK_BASE_URL}
Content-Type: application/json
{
"type": "design-import",
"recordId": "42_hDDA9BNori9OTXSClduXqR_40002029:37033",
"projectId": 42,
"organizationId": 1,
"status": "failed"
}
失敗時は result ブロックはありません。パース前に失敗した場合は recordId、projectId、organizationId が null になることがあります。バックエンドはこれを UPDATE design SET status = 2 ... に変換します。
エラーハンドリング
部分バッチ失敗サポート。ハンドラは {"batchItemFailures": [...]} を返すため、1 件の失敗が残りのバッチを汚染しません。Lambda イベントソースマッピングで ReportBatchItemFailures を 有効にする必要があります。ワーカーは失敗 Webhook を送出してからアイテムを失敗としてマークします。SQS はその失敗アイテムのみ独立して再試行します。
def lambda_handler(event, context):
failures = []
for record in event["Records"]:
try:
process_record(record, ...)
except Exception:
failures.append({"itemIdentifier": record["messageId"]})
return {"batchItemFailures": failures}
| シナリオ | 挙動 |
|---|---|
無効な SQS イベントエンベロープ(Records なし) |
ValueError 送出。Webhook なし。呼び出し全体が失敗 |
design_id が複合キーと一致しない |
Pydantic バリデーションエラー。失敗 Webhook。batchItemFailures に追加 |
img_url への S3 GET 失敗 |
失敗 Webhook。batchItemFailures に追加 |
| 画像が 10 MB 超過 | ValueError。失敗 Webhook。batchItemFailures に追加 |
json_schema_url 欠落 |
検証エラー。失敗 Webhook。batchItemFailures に追加 |
json_schema_url への S3 GET 失敗 |
失敗 Webhook。batchItemFailures に追加 |
| Figma JSON パースエラー | 失敗 Webhook。batchItemFailures に追加 |
| Vision LLM 例外 | 失敗 Webhook。batchItemFailures に追加 |
| Embedding API 例外 | 失敗 Webhook。batchItemFailures に追加 |
| DocumentDB 書き込み例外 | 失敗 Webhook。batchItemFailures に追加。一時的な障害には最大 2 回リトライ |
| Webhook POST 自体が失敗 | WARN ログ。SQS retry のため batchItemFailures に追加 |
| SQS リトライ回数超過 | メッセージは DLQ に移動(ワーカー外で設定) |
冪等性
3 つのレイヤを順に適用します。
_idキーの upsert:update_one({"_id": design_id}, {"$set": doc}, upsert=True)— 再配送されたメッセージは前回のドキュメントをアトミックに上書き。- ステートレスなワーカー: PostgreSQL への書き込みもローカルチェックポイントもなし。
- At-least-once Webhook: 失敗→成功の順に Webhook が 2 回届くことがある。バックエンドは
(type, recordId)に対して冪等でなければなりません。
ロックされたパラメータ
| パラメータ | 値 / 入手元 | 備考 |
|---|---|---|
| 埋め込みモデル | EMBEDDING_MODEL 環境変数 |
例: text-embedding-3-small |
| 埋め込み次元 | EMBEDDING_DIMENSIONS 環境変数(デフォルト 512) |
HNSW インデックス設定と一致させる必要あり |
| Vision モデル | DESC_MODEL 環境変数 |
例: openai:gpt-5.4-nano |
| エージェントフレームワーク | PydanticAI | 環境変数で変更不可 |
| エージェント実行モード | run_sync(同期) |
レコードあたり 1 回 |
| レコードあたりの Embedding API 呼び出し | 1 回(バッチ、入力 3 つ) | モデルドリフトを防ぐため全ベクトルを 1 回で生成 |
| 画像サイズ上限 | 10 MB | service.py の MAX_IMAGE_BYTES としてハードコード |
| Vision タイムアウト | 60 秒 | service.py の VISION_TIMEOUT_SECONDS |
index_schema_version |
schemas.py の定数 |
DocumentDB の形状変更またはエンコーダー変更ごとにバンプ |
ロギング
レコードごとにバインドされる構造化フィールド: app=ai-design-import, message_id, prompt_version, index_schema_version, design_id, organization_id, project_id。
| イベント | レベル | タイミング |
|---|---|---|
design_import.received |
INFO | process_record の最初の行 |
design_import.parsed |
INFO | Pydantic 検証後 |
design_import.s3.image.downloaded |
INFO | 画像 GET 後(size_bytes, media_type) |
design_import.schema.parsed |
INFO | Figma JSON パース後(structural_text_len, component_id_count) |
design_import.schema.unavailable |
ERROR | Figma JSON の取得またはパースに失敗 |
design_import.schema.missing |
ERROR | json_schema_url が提供されなかった |
design_import.vision.completed |
INFO | Vision LLM 後(component_count, semantic_words_count, layout, viewport_device_class) |
design_import.embeddings.completed |
INFO | Embeddings 呼び出し後(dims, vectors=visual/semantic/structural) |
design_import.documentdb.upserted |
INFO | Upsert 後(matched, modified, upserted_id) |
design_import.webhook.sent |
INFO | Webhook POST 後(http_status) |
design_import.failed |
ERROR | process_record 内の終端的な失敗 |
design_import.webhook.failed |
WARN | Webhook POST 自体が失敗し、SQS retry 対象 |
フィールド参照(ルックアップ表)
| フィールド | SQS in | Doc out | Webhook out | 備考 |
|---|---|---|---|---|
design_id |
✓ | _id |
recordId |
複合 id(強制検証) |
organization_id |
✓ | ✓ | organizationId |
tenant scope |
project_id |
✓ | ✓ | projectId |
project scope |
design_name |
✓ | name、visual.metadata.name |
— | |
type |
— | ✓(デフォルト "screen") |
— | |
based_on |
— | ✓(デフォルト "figma_import") |
— | |
img_url |
✓ | visual.metadata.image_url |
— | |
json_schema_url |
✓ | ✓ | — | 必須 Figma node JSON。ダウンロードしてパース |
node_id, file_id |
✓ | ✓ | — | design_id の構成要素。下流 evidence 用に保存 |
generation_context |
— | ✓ | — | |
visual.metadata.* |
— | ✓ | — | encoder, layout, component_types, color_palette, typography_style |
visual.vector_embedding |
— | ✓ | — | 512 個の float |
semantics.metadata.words |
— | ✓ | result.keywords |
|
semantics.metadata.component_functions |
— | ✓ | — | |
semantics.vector_embedding |
— | ✓ | — | 512 個の float |
structural.metadata.structural_text |
— | ✓ | — | |
structural.metadata.component_ids |
— | ✓ | — | |
structural.vector_embedding |
— | ✓ | — | 512 個の float |
query_terms |
— | ✓ | — | 正規化済み matching terms |
figma_node |
— | ✓ | — | Figma node-level matching evidence |
localized_regions |
— | ✓ | — | 任意の localized evidence |
status |
— | — | "success" \| "failed" |
|
result.embeddingDims |
— | — | ✓ | |
result.vectorsProduced |
— | — | ✓ |