コンテンツにスキップ

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 つのネスト形状を(優先度順に)受け入れます。

  1. 生ペイロード — body が上記の JSON。
  2. SQS-in-SQS — body.Records[0].body を再パース。
  3. 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 ステップを順番に実行します。どこかで失敗したメッセージは エラーハンドリング のパスに進みます。

  1. パース + 検証: SqsBody で SQS body を検証。organization_id > 0、project_id > 0、design_id == f"{project_id}_{file_id}_{node_id}" を確認し、不一致なら ValueError。

  2. デザイン画像ダウンロード: S3 から取得。10 MB 超過の場合は例外。

  3. Figma JSON のダウンロード + パース。figma.extract_structural(schema, node_id) を呼び出し、下記を取得:

  4. structural_text — コンパクトな正規化ツリー文字列(例: tree=FRAME[H,gap_none]>[...] | components=... | depth=N)
  5. component_ids — ツリー内の全 INSTANCE.componentId をソートしたリスト
  6. viewport — ルートノードの absoluteBoundingBox から取得した {width, height, aspect_ratio, device_class}

失敗(キー不足、ネットワークエラーなど)した場合はレコードを失敗扱いにする。成功した design document は必ず Figma 由来の structural evidence を含む。

  1. 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)。

  2. 埋め込み入力テキストの構築:

  3. visual_text = "layout: {layout} | components: {component_types} | palette: {color_palette} | typography: {typography_style}"
  4. semantic_text = 正規化した semantic_words + component_functions
  5. structural_text(ステップ 3 で取得)
  6. query_terms = semantic words、component functions、component types、Figma node names、visible text、alias から構築して永続化する正規化済みマッチング term

  7. 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。

  8. DesignDocument を DocumentDB に upsert(_id == design_id)。

  9. 成功 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 つのレイヤを順に適用します。

  1. _id キーの upsert: update_one({"_id": design_id}, {"$set": doc}, upsert=True) — 再配送されたメッセージは前回のドキュメントをアトミックに上書き。
  2. ステートレスなワーカー: PostgreSQL への書き込みもローカルチェックポイントもなし。
  3. 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 — — ✓

関連リンク

  • 概要 — 処理フロー図とコンポーネント分解。
  • テストケース — この契約を守るテストマトリクス。
  • packages/models/documentdb/design.py — HNSW インデックス定義。
  • packages/agentic/vision.py — DesignDescription 構造化出力スキーマ。
  • apps/design-import/src/design_import/figma.py — Figma JSON パーサー(extract_structural)。