Skip to content

AI Design Import — Overview

A Lambda worker that processes design ingestion jobs from the design-import SQS queue. It downloads the Figma node image and Figma node JSON from S3, extracts visual features, semantic keywords, component functions, and structural node evidence, generates 3 512-dimensional embedding vectors (visual + semantic + structural), upserts them into the design collection in DocumentDB, and then notifies the backend of the status via Webhook.

Replaces: V1 design app (guinness-ai-develop)
Queue: design-import
Max execution time: approx. 2–5 minutes
Source: guinness-ai-v2 (new repository)


1. Tech Stack

Layer Technology
Runtime Python 3.12 on AWS Lambda
AI Framework PydanticAI (agent orchestration, provider-agnostic)
LLM DESC_MODEL environment variable (e.g., openai:gpt-5.4-nano) — visual feature extraction + semantic keywords
Embeddings EMBEDDING_MODEL environment variable (e.g., text-embedding-3-small, 512 dimensions) — direct openai SDK call
Database Amazon DocumentDB (MongoDB-compatible) — design collection, AI-dedicated
Storage Amazon S3 (design images, Figma JSON)
Queue AWS SQS (design-import queue)
Status notification Webhook POST /v1/webhooks/ai-status → guinness-backend
RDB No access — does not touch PostgreSQL / MySQL (strict database isolation)

2. Key Changes from V1

Item V1 V2
AI Framework LangGraph StateGraph PydanticAI agent (run_sync)
Embeddings per design 1 (visual style keywords) 3 (visual + semantic + structural)
Database writes Direct MySQL + DocumentDB DocumentDB only + Webhook (no RDB access)
Figma JSON Stored as-is in document Parsed into structural embedding + component IDs + viewport
Batch processing Retries entire batch on first failure batchItemFailures response (retry per record)
design_id validation Descriptive only Enforced by Pydantic model_validator
LLM client ChatOpenAI (langchain-openai) PydanticAI (provider-agnostic)
Embedding client OpenAIEmbeddings (langchain) — single openai SDK direct call — visual + semantic + structural

3. Processing Flow

flowchart TD
    A["SQS: design-import message"] --> B["Parse + SqsBody validation\n(design_id composite key enforced)"]
    B --> C["Download design image from S3\n(max 10 MB)"]
    C --> E["Download Figma JSON from S3\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["Build embedding input texts\nvisual_text, semantic_text\n+ structural_text\n+ query_terms"]
    I --> J["OpenAI Embeddings — single batch call\n3 inputs → 512-dim vectors"]
    J --> K["Build DesignDocument\nupsert into DocumentDB with _id=design_id"]
    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

The worker executes 8 steps sequentially for each record. Any message that fails along the way goes through the error handling path, which sends a failure Webhook before adding the message to batchItemFailures.


4. Embedding Strategy

Up to 3 embeddings per design cover orthogonal search signals. All are generated in a single batch call to prevent drift:

Embedding Input Text Purpose
visual "layout: {layout} \| components: {types} \| palette: {colors} \| typography: {style}" Find designs that look the same
semantics " ".join(semantic_words) Find designs about the same domain
structural Deterministic Figma tree rendering (see figma.py) Find designs with the same component structure

structural is generated for every successful import because json_schema_url is required. The document also persists query_terms and Figma node evidence for Des2Code reranking.


5. Figma JSON Parsing (figma.extract_structural)

Figma JSON is parsed deterministically without an LLM. The parser (apps/design-import/src/design_import/figma.py) traverses the node tree and produces three outputs:

structural_text — a compact single-line string encoding layout, components, and tree shape:

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

Design choices: - Layout direction and spacing are bucketed (gap_none, gap_xs, gap_sm, gap_md, gap_lg, gap_xl) - INSTANCE nodes use Figma's componentId (canonical ID) as the key; designer-assigned names are ignored - TEXT nodes are bucketed by font size (TEXT_HEADING ≥28px, TEXT_SUBHEADING ≥18px, TEXT_BODY) - Tree depth is capped at 12 levels (max_depth)

component_ids — a sorted list of all INSTANCE.componentId values in the tree. Allows des2code Stage 2 to verify component mapping coverage without re-parsing the JSON.

viewport — {width, height, aspect_ratio, device_class} derived from the root node's absoluteBoundingBox. Device classes: mobile (<600px), tablet (<1024px), desktop (<1600px), wide (≥1600px).


6. SQS Message Schema

Message enqueued to the design-import queue by the backend (apps/app/src/services/design.ts):

{
  "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 fields (validated by the SqsBody Pydantic model):

Field Type Required Notes
organization_id integer ✓ Tenant scope. Also copied into the DocumentDB document and webhook
project_id integer ✓ Project scope. Also copied into the DocumentDB document and webhook
img_url string ✓ S3 URL of the design image
node_id string ✓ Figma node id (typically <int>:<int> format)
file_id string ✓ Figma file id
design_id string ✓ Must match "{project_id}_{file_id}_{node_id}" — enforced by Pydantic model_validator. Becomes the DocumentDB _id
design_name string ✓ Figma frame name
json_schema_url string ✓ S3 URL of the Figma node JSON export. Required; missing, download failure, or parse failure makes the import fail

7. Webhook Payload Schema

After processing completes, a POST is sent to WEBHOOK_BASE_URL:

On success

{
  "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"
  }
}

On failure

{
  "type": "design-import",
  "recordId": "42_hDDA9BNori9OTXSClduXqR_40002029:37033",
  "projectId": 42,
  "organizationId": 1,
  "status": "failed"
}

On failure, there is no result block. If the failure occurs before parsing, recordId, projectId, and organizationId may be null.


8. PydanticAI — Vision Agent

Replaces V1's LangGraph StateGraph with a single PydanticAI agent. The framework is provider-agnostic — the model name is specified in provider:model format:

from pydantic_ai import Agent
from pydantic import BaseModel

class DesignDescription(BaseModel):
    layout: str                  # one of 11 archetypes
    component_types: list[str]   # 3–15 normalized snake_case names
    color_palette: list[str]     # 2–5 dominant hex colors (#RRGGBB)
    typography_style: str        # one of 8 archetypes
    semantic_words: list[str]    # 5–10 domain keywords

vision_agent = Agent(
    DESC_MODEL,  # e.g., "openai:gpt-5.4-nano"
    output_type=DesignDescription,
)

Input: design image downloaded from S3 (binary).
Output: DesignDescription — structured visual features and semantic keywords.
Execution mode: run_sync (synchronous) — once per record.


9. Embedding Generation

PydanticAI does not handle embeddings. The openai Python SDK is used directly to generate all vectors in a single batch call (to prevent model drift):

client.embeddings.create(
    model=config.embedding_model,
    input=[visual_text, semantic_text, structural_text],
    dimensions=config.embedding_dimensions,  # default 512
)
# data[0] → visual, data[1] → semantic, data[2] → structural (if present)
Embedding Input Text
Visual "layout: {layout} \| components: {component_types} \| palette: {color_palette} \| typography: {typography_style}"
Semantic " ".join(semantic_words)
Structural structural_text from required Figma JSON

10. DocumentDB Document Structure

{
  "_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 floats */]
  },

  "semantics": {
    "metadata": { "words": ["login", "authentication", "form", "submit", "credentials"] },
    "vector_embedding": [/* 512 floats */]
  },

  "structural": {
    "metadata": {
      "structural_text": "tree=FRAME[V,gap_md]>[...] | components=... | depth=2"
    },
    "vector_embedding": [/* 512 floats */]
  }
}

viewport and structural are required on new successful imports because json_schema_url is required.

HNSW Vector Indexes

packages/models/documentdb/design.py creates these idempotently at cold start:

Index Name Path Dimensions Similarity 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 operates sparsely — documents without a structural block are excluded from structural search results.


11. Error Handling

Partial batch failure support — the handler returns {"batchItemFailures": [...]}, so a single failure does not contaminate the rest of the batch. ReportBatchItemFailures must be enabled on the Lambda event source mapping.

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}
Scenario Behavior
Invalid SQS event envelope (no Records) ValueError. No Webhook. Entire invocation fails
design_id does not match composite key Pydantic validation error. Failure Webhook. Added to batchItemFailures
S3 GET failure (image) Failure Webhook. Added to batchItemFailures
Image exceeds 10 MB ValueError. Failure Webhook. Added to batchItemFailures
Missing or invalid Figma JSON Failure Webhook. Added to batchItemFailures
Vision LLM exception Failure Webhook. Added to batchItemFailures
Embedding API exception Failure Webhook. Added to batchItemFailures
DocumentDB write exception Failure Webhook. Added to batchItemFailures (up to 2 retries)
Webhook POST itself fails WARN log. Added to batchItemFailures so SQS retries
SQS retry count exceeded Message is moved to DLQ

12. Module Structure

apps/design-import/src/design_import/
  handler.py    # Lambda entry — SQS parsing, batchItemFailures, warm-start clients
  service.py    # process_record() — 8-step pipeline
  schemas.py    # SqsBody, DesignDocument, VisualBlock, SemanticsBlock, StructuralBlock,
                #   Viewport, WebhookSuccessPayload, WebhookFailurePayload
  repo.py       # DesignRepo — S3 download, 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 schema

packages/models/src/models/documentdb/
  design.py     # collection name, HNSW index creation, VECTOR_DIMENSIONS constant
File Responsibility V1 Equivalent
handler.py Lambda handler, batchItemFailures apps/design/src/main.py
service.py process_record() — 8-step pipeline Inline in main.py (V1)
schemas.py Pydantic models (SQS / DocumentDB / Webhook) apps/design/src/schema.py
repo.py DocumentDB upsert, S3 download Inline in main.py (V1)
figma.py Figma JSON parser — (new in V2)
config.py Environment variables (pydantic-settings) apps/design/src/config/env.py

Removed from V1: config/mysql.py (no MySQL access in V2).


13. Environment Variables

Variable Description Example / Default
DESC_MODEL Vision LLM model (with provider prefix) openai:gpt-5.4-nano
EMBEDDING_MODEL OpenAI embedding model text-embedding-3-small
EMBEDDING_DIMENSIONS Vector dimensions (must match HNSW index) 512
OPENAI_API_KEY OpenAI API key sk-...
OPENAI_MAX_RETRIES SDK 429/5xx retry count 3
DOCUMENTDB_CONNECTION_STRING DocumentDB connection string mongodb://user:pass@host:27017/?tls=true
DOCUMENTDB_NAME DocumentDB database name guinness_ai
DESIGN_TABLE_NAME DocumentDB collection name design
DOCUMENTDB_CA_PATH TLS CA bundle path /var/task/global-bundle.pem
WEBHOOK_BASE_URL Backend Webhook endpoint (private VPC) https://api.internal/v1/webhooks/ai-status
WEBHOOK_API_KEY Shared service key sent as X-API-Key Secrets Manager / SOPS value

All environment variables are documented in AI Infrastructure — Environment Variables.


14. Package Dependencies

For the full list of package dependencies, see AI Infrastructure — Shared Package Dependencies.