@@ -10881,6 +10881,7 @@ def prompt(
1088110881 prompt: str,
1088210882 model: str,
1088310883 columns_subset: str | list[str] | None = None,
10884+ attachments: list | None = None,
1088410885 batch_size: int = 1000,
1088510886 max_concurrent: int = 3,
1088610887 pre: Callable | None = None,
@@ -10926,6 +10927,15 @@ def prompt(
1092610927 A single column or list of columns to include in the validation. If `None`, all columns
1092710928 will be included. Specifying fewer columns can improve performance and reduce API costs
1092810929 so try to include only the columns necessary for the validation.
10930+ attachments
10931+ An optional list of reference files (images or PDFs) to attach as global context for
10932+ every batch. Each item can be a local file path, a URL (`http://` / `https://`), a
10933+ `pathlib.Path`, or a pre-built chatlas `Content` object. Supported extensions are
10934+ `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, and `.pdf`. The attachments apply to the whole
10935+ validation step, not per-row, so they are well-suited for things like brand guides,
10936+ schema diagrams, or sample documents the LLM should consult when scoring each row.
10937+ **Cost note**: attachments are re-sent on every batch — see the *Multi-modal
10938+ attachments* section below for cost-management tips.
1092910939 model
1093010940 The model to be used. This should be in the form of `provider:model` (e.g.,
1093110941 `"anthropic:claude-opus-4-6"`). Supported providers are `"anthropic"`, `"openai"`,
@@ -11094,6 +11104,39 @@ def prompt(
1109411104 - "Describe the quality of each row" (asks for description, not validation)
1109511105 - "How would you improve this data?" (asks for suggestions, not pass/fail)
1109611106
11107+ Multi-modal Attachments
11108+ -----------------------
11109+ Use `attachments=` to give the LLM a reference image or PDF that applies to every row. The
11110+ attachment is sent as global context, while each row's values are still serialized as JSON
11111+ and validated against your `prompt=`. Useful patterns:
11112+
11113+ - validating descriptions against a brand-style image: `attachments=["brand_guide.pdf"]`
11114+ - cross-referencing rows with a schema diagram: `attachments=["schema.png"]`
11115+ - matching free-text fields to a sample document: `attachments=["sample_invoice.pdf"]`
11116+
11117+ Accepted values per list item:
11118+
11119+ - local path strings or `pathlib.Path` objects (e.g., `"docs/diagram.png"`)
11120+ - URLs (e.g., `"https://example.com/diagram.png"`)
11121+ - pre-built chatlas `Content` objects (e.g., `chatlas.content_image_plot()`)
11122+
11123+ Supported extensions: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.pdf`.
11124+
11125+ Note: local image paths auto-coerced through `attachments=` use `resize="low"` (chatlas's
11126+ default downscale to 512x512) to keep token costs predictable. For higher fidelity,
11127+ pre-build the content yourself with `chatlas.content_image_file(path, resize="high")`
11128+ (or `"none"`) and pass that object inside `attachments=`.
11129+
11130+ **Cost / batching note**: attachments are re-sent on *every* batch (one batch = one LLM API
11131+ call). For a table that requires N batches, each attachment's input tokens are billed N
11132+ times. To control costs:
11133+
11134+ - keep attachments small (downscale images, crop PDFs to the relevant page)
11135+ - use `columns_subset=` aggressively to maximize row-signature memoization (fewer unique
11136+ rows means fewer batches)
11137+ - raise `batch_size=` when the combined system prompt, attachment, and row JSON fit
11138+ comfortably under the model's context window
11139+
1109711140 Performance Considerations
1109811141 --------------------------
1109911142 AI validation is significantly slower than traditional validation methods due to API calls
@@ -11215,6 +11258,28 @@ def prompt(
1121511258 which exceeds all threshold levels. The validation will trigger the specified error action
1121611259 since the failure rate (40%) is above the error threshold (20%). The AI can recognize
1121711260 various phone number formats and determine whether they include area codes.
11261+
11262+ **Multi-modal example with `attachments=`:**
11263+
11264+ Suppose you have a table of product descriptions and a brand-style PDF that describes the
11265+ approved tone and vocabulary. Pass the PDF as a global attachment so the LLM can compare
11266+ each description against it.
11267+
11268+ ```python
11269+ validation = (
11270+ pb.Validate(data=products)
11271+ .prompt(
11272+ prompt="Each product description must match the tone and vocabulary in the brand guide.",
11273+ columns_subset=["description"],
11274+ attachments=["docs/brand_guide.pdf"],
11275+ model="anthropic:claude-opus-4-6",
11276+ )
11277+ .interrogate()
11278+ )
11279+ ```
11280+
11281+ The brand guide is sent alongside the row JSON on every batch, so the LLM evaluates each
11282+ description with the same reference document in view.
1121811283 """
1121911284
1122011285 assertion_type = _get_fn_name()
@@ -11241,6 +11306,13 @@ def prompt(
1124111306 if not isinstance(max_concurrent, int) or max_concurrent < 1:
1124211307 raise ValueError("max_concurrent must be a positive integer")
1124311308
11309+ # Coerce `attachments=` into a list of chatlas Content objects. Fails fast
11310+ # on unsupported extensions so users see the error at step definition time,
11311+ # not deep inside interrogation.
11312+ from pointblank._utils_ai import _prepare_attachments
11313+
11314+ prepared_attachments = _prepare_attachments(attachments)
11315+
1124411316 _check_pre(pre=pre)
1124511317 _check_thresholds(thresholds=thresholds)
1124611318 _check_active_input(param=active, param_name="active")
@@ -11264,6 +11336,7 @@ def prompt(
1126411336 "llm_model": model_name,
1126511337 "batch_size": batch_size,
1126611338 "max_concurrent": max_concurrent,
11339+ "attachments": prepared_attachments,
1126711340 }
1126811341
1126911342 val_info = _ValidationInfo(
0 commit comments