AI Design Import — 概要
design-import SQS キューからデザイン取り込みジョブを処理する Lambda ワーカー。S3 から Figma node 画像と Figma node JSON をダウンロードし、視覚的特徴、セマンティックキーワード、component functions、構造化 node evidence を抽出、3 本の 512 次元埋め込みベクトル(visual + semantic + structural)を生成し、DocumentDB の design コレクションにアップサートした後、Webhook でバックエンドにステータスを通知する。
置き換え対象: V1 design アプリ (guinness-ai-develop)
キュー: design-import
最大実行時間: 約 2〜5 分
ソース: guinness-ai-v2(新規リポジトリ)
1. 技術スタック
| レイヤ | 採用技術 |
|---|---|
| ランタイム | Python 3.12 on AWS Lambda |
| AI フレームワーク | PydanticAI(エージェントオーケストレーション、プロバイダー非依存) |
| LLM | DESC_MODEL 環境変数(例: openai:gpt-5.4-nano)— 視覚的特徴抽出 + セマンティックキーワード |
| 埋め込み | EMBEDDING_MODEL 環境変数(例: text-embedding-3-small、512 次元)— openai SDK 直呼び出し |
| データベース | Amazon DocumentDB(MongoDB 互換)— design コレクション、AI 専用 |
| ストレージ | Amazon S3(デザイン画像、Figma JSON) |
| キュー | AWS SQS(design-import キュー) |
| ステータス通知 | Webhook POST /v1/webhooks/ai-status → guinness-backend |
| RDB | アクセスなし — PostgreSQL / MySQL には接触しない(データベース分離を徹底) |
2. V1 との主な変更点
| 項目 | V1 | V2 |
|---|---|---|
| AI フレームワーク | LangGraph StateGraph |
PydanticAI エージェント(run_sync) |
| デザインあたりの埋め込み数 | 1(視覚スタイルキーワード) | 3(visual + semantic + structural) |
| データベース書き込み | MySQL 直接 + DocumentDB | DocumentDB のみ + Webhook(RDB アクセスなし) |
| Figma JSON | ドキュメントにそのまま保存 | structural 埋め込み + コンポーネント ID + viewport にパース |
| バッチ処理 | 最初の失敗でバッチ全体を再試行 | batchItemFailures レスポンス(レコード単位で再試行) |
design_id 検証 |
説明的なのみ | Pydantic model_validator で強制 |
| LLM クライアント | ChatOpenAI(langchain-openai) |
PydanticAI(プロバイダー非依存) |
| 埋め込みクライアント | OpenAIEmbeddings(langchain)— 単一 |
openai SDK 直呼び出し — visual + semantic + structural |
3. 処理フロー
flowchart TD
A["SQS: design-import メッセージ"] --> B["パース + SqsBody 検証\n(design_id 複合キー強制)"]
B --> C["S3 からデザイン画像ダウンロード\n(最大 10 MB)"]
C --> E["S3 から Figma JSON ダウンロード\nfigma.extract_structural(schema, node_id)"]
E --> F["→ structural_text\n→ component_ids\n→ viewport"]
F --> H
H["Vision Agent — PydanticAI run_sync\nDesignDescription:\n layout, component_types, color_palette\n typography_style, semantic_words"]
H --> I["埋め込み入力テキストの構築\nvisual_text, semantic_text\n+ structural_text\n+ query_terms"]
I --> J["OpenAI Embeddings — 1 回バッチ呼び出し\n3 入力 → 512 次元ベクトル"]
J --> K["DesignDocument を構築\n_id=design_id で DocumentDB に upsert"]
K --> L["POST webhook success"]
style A fill:#f9f,stroke:#333
style H fill:#bbf,stroke:#333
style K fill:#bfb,stroke:#333
style L fill:#fdb,stroke:#333
ワーカーは各レコードに対して 8 ステップ を順番に実行します。どこかで失敗したメッセージはエラーハンドリングのパスに進み、失敗 Webhook が送出されてから batchItemFailures に追加されます。
4. 埋め込み戦略
1 デザインあたり最大 3 つの埋め込みが直交する検索シグナルをカバーします。ドリフト防止のため全て 1 回のバッチ呼び出しで生成します:
| 埋め込み | 入力テキスト | 用途 |
|---|---|---|
visual |
"layout: {layout} \| components: {types} \| palette: {colors} \| typography: {style}" |
「同じように見える」デザインを探す |
semantics |
" ".join(semantic_words) |
「同じドメインに関する」デザインを探す |
structural |
決定的 Figma ツリーレンダリング(figma.py 参照) |
「同じコンポーネント構造を持つ」デザインを探す |
structural は json_schema_url が必須のため、成功した import では必ず生成される。DocumentDB には Des2Code リランキング用の query_terms と Figma node evidence も永続化する。
5. Figma JSON パース(figma.extract_structural)
Figma JSON は LLM を使わず決定的にパースされます。パーサー(apps/design-import/src/design_import/figma.py)はノードツリーを走査し、3 つの出力を生成します:
structural_text — レイアウト、コンポーネント、ツリー形状をエンコードしたコンパクトな 1 行の文字列:
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
設計上の選択:
- レイアウト方向とスペーシングはバケット化(gap_none, gap_xs, gap_sm, gap_md, gap_lg, gap_xl)
- INSTANCE ノードは Figma の componentId(正規 ID)をキー、デザイナー付与の名前は無視
- TEXT ノードはフォントサイズでバケット化(TEXT_HEADING ≥28px、TEXT_SUBHEADING ≥18px、TEXT_BODY)
- ツリー深さは最大 12 レベル(max_depth)
component_ids — ツリー内の全 INSTANCE.componentId をソートしたリスト。des2code Stage 2 がコンポーネントマッピングのカバレッジを JSON 再パースなしに確認できます。
viewport — ルートノードの absoluteBoundingBox から取得した {width, height, aspect_ratio, device_class}。デバイスクラス: mobile(<600px)、tablet(<1024px)、desktop(<1600px)、wide(≥1600px)。
6. SQS メッセージスキーマ
バックエンド(apps/app/src/services/design.ts)が design-import キューにエンキューするメッセージ:
{
"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 フィールド(SqsBody Pydantic モデルで検証):
| フィールド | 型 | 必須 | 備考 |
|---|---|---|---|
organization_id |
integer | ✓ | tenant scope。DocumentDB document と webhook にもコピー |
project_id |
integer | ✓ | project scope。DocumentDB document と webhook にもコピー |
img_url |
string | ✓ | デザイン画像の S3 URL |
node_id |
string | ✓ | Figma node id(通常 <int>:<int> 形式) |
file_id |
string | ✓ | Figma file id |
design_id |
string | ✓ | "{project_id}_{file_id}_{node_id}" と一致必須 — Pydantic model_validator で強制。DocumentDB の _id になる |
design_name |
string | ✓ | Figma フレーム名 |
json_schema_url |
string | ✓ | Figma node JSON エクスポートの S3 URL。必須。欠落、取得失敗、パース失敗は import 失敗 |
7. Webhook ペイロードスキーマ
処理完了後、WEBHOOK_BASE_URL に POST を送信:
成功時
{
"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": "design-import",
"recordId": "42_hDDA9BNori9OTXSClduXqR_40002029:37033",
"projectId": 42,
"organizationId": 1,
"status": "failed"
}
失敗時は result ブロックはありません。パース前に失敗した場合は recordId、projectId、organizationId が null になることがあります。
8. PydanticAI — Vision エージェント
V1 の LangGraph StateGraph を PydanticAI の単一エージェントに置き換え。フレームワークはプロバイダー非依存 — モデル名は provider:model 形式で指定:
from pydantic_ai import Agent
from pydantic import BaseModel
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 個
vision_agent = Agent(
DESC_MODEL, # e.g., "openai:gpt-5.4-nano"
output_type=DesignDescription,
)
入力: S3 からダウンロードしたデザイン画像(バイナリ)。
出力: DesignDescription — 構造化された視覚特徴とセマンティックキーワード。
実行モード: run_sync(同期)— レコードあたり 1 回。
9. 埋め込み生成
PydanticAI は埋め込みを扱わない。openai Python SDK を直接使用し、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
| 埋め込み | 入力テキスト |
|---|---|
| Visual | "layout: {layout} \| components: {component_types} \| palette: {color_palette} \| typography: {typography_style}" |
| Semantic | " ".join(semantic_words) |
| Structural | 必須 Figma JSON から生成した structural_text |
10. DocumentDB ドキュメント構造
{
"_id": "42_hDDA9BNori9OTXSClduXqR_40002029:37033",
"organization_id": 1,
"project_id": 42,
"name": "Login Screen",
"type": "screen",
"based_on": "figma_import",
"index_schema_version": "2026.05.2",
"generation_context": "Login Screen. Viewport: 375x812 (mobile). Layout: stacked_form. ...",
"viewport": {
"width": 375,
"height": 812,
"aspect_ratio": 0.4619,
"device_class": "mobile"
},
"visual": {
"metadata": {
"name": "Login Screen",
"image_url": "s3://gnss-prod-inputs/designs/...",
"encoder": "text-embedding-3-small@512",
"layout": "stacked_form",
"component_types": ["input_email", "input_password", "button_primary"],
"color_palette": ["#1A1A2E", "#FFFFFF", "#4A90E2"],
"typography_style": "modern_sans"
},
"vector_embedding": [/* 512 個の float */]
},
"semantics": {
"metadata": { "words": ["login", "authentication", "form", "submit", "credentials"] },
"vector_embedding": [/* 512 個の float */]
},
"structural": {
"metadata": {
"structural_text": "tree=FRAME[V,gap_md]>[...] | components=... | depth=2"
},
"vector_embedding": [/* 512 個の float */]
}
}
新しい成功 import では json_schema_url が必須のため、viewport と structural は必須です。
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 |
structuralVectorIndex はスパース動作 — structural ブロックを持たないドキュメントは構造化検索の結果から除外されます。
11. エラーハンドリング
部分バッチ失敗サポート — ハンドラは {"batchItemFailures": [...]} を返すため、1 件の失敗が残りのバッチを汚染しません。Lambda イベントソースマッピングで ReportBatchItemFailures を有効にする必要があります。
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 に追加 |
| S3 GET 失敗(画像) | 失敗 Webhook。batchItemFailures に追加 |
| 画像が 10 MB 超過 | ValueError。失敗 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 に移動 |
12. モジュール構成
apps/design-import/src/design_import/
handler.py # Lambda エントリ — SQS パース、batchItemFailures、ウォームスタートクライアント
service.py # process_record() — 8 ステップのパイプライン
schemas.py # SqsBody, DesignDocument, VisualBlock, SemanticsBlock, StructuralBlock,
# Viewport, WebhookSuccessPayload, WebhookFailurePayload
repo.py # DesignRepo — S3 ダウンロード、DocumentDB upsert、Webhook POST
figma.py # extract_structural() — Figma JSON → structural_text + component_ids + viewport
prompts.py # VISION_INSTRUCTIONS, VISION_USER_MESSAGE, PROMPT_VERSION
config.py # Config(pydantic-settings)
packages/agentic/src/agentic/
vision.py # build_vision_agent()、DesignDescription スキーマ
packages/models/src/models/documentdb/
design.py # コレクション名、HNSW インデックス作成、VECTOR_DIMENSIONS 定数
| ファイル | 責務 | V1 の同等ファイル |
|---|---|---|
handler.py |
Lambda ハンドラー、batchItemFailures | apps/design/src/main.py |
service.py |
process_record() — 8 ステップパイプライン |
main.py 内(V1 ではインライン) |
schemas.py |
Pydantic モデル(SQS / DocumentDB / Webhook) | apps/design/src/schema.py |
repo.py |
DocumentDB upsert、S3 ダウンロード | main.py 内(V1 ではインライン) |
figma.py |
Figma JSON パーサー | ― (V2 新規) |
config.py |
環境変数(pydantic-settings) | apps/design/src/config/env.py |
V1 から削除: config/mysql.py(V2 では MySQL アクセスなし)。
13. 環境変数
| 変数 | 説明 | 例 / デフォルト |
|---|---|---|
DESC_MODEL |
Vision LLM モデル(プロバイダー付き) | openai:gpt-5.4-nano |
EMBEDDING_MODEL |
OpenAI 埋め込みモデル | text-embedding-3-small |
EMBEDDING_DIMENSIONS |
ベクトル次元数(HNSW インデックスと一致させること) | 512 |
OPENAI_API_KEY |
OpenAI API キー | sk-... |
OPENAI_MAX_RETRIES |
SDK の 429/5xx リトライ回数 | 3 |
DOCUMENTDB_CONNECTION_STRING |
DocumentDB 接続文字列 | mongodb://user:pass@host:27017/?tls=true |
DOCUMENTDB_NAME |
DocumentDB データベース名 | guinness_ai |
DESIGN_TABLE_NAME |
DocumentDB コレクション名 | design |
DOCUMENTDB_CA_PATH |
TLS CA バンドルパス | /var/task/global-bundle.pem |
WEBHOOK_BASE_URL |
バックエンド Webhook エンドポイント(プライベート VPC) | https://api.internal/v1/webhooks/ai-status |
WEBHOOK_API_KEY |
X-API-Key として送る共有サービスキー |
Secrets Manager / SOPS 値 |
すべての環境変数は AI インフラストラクチャ — 環境変数 に記載。
14. 依存パッケージ
完全な依存パッケージリストは AI インフラストラクチャ — 共有依存パッケージ を参照。