Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 101 additions & 3 deletions docs/examples/extract_ai/extract_ai_recipe_examples.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -1925,7 +1925,10 @@
"\n",
"`examples` on a field nudge the model toward a particular output style or\n",
"unit format, without being included in the strict JSON schema sent to\n",
"OpenAI (they're folded into the instructions instead)."
"OpenAI (they're folded into the instructions instead). The existing scalar\n",
"or list form remains supported. A field example can also be a paired object\n",
"with `input` and `output`, plus optional `name` and `notes`; that pair guides\n",
"only the field containing it."
]
},
{
Expand Down Expand Up @@ -2019,6 +2022,54 @@
"result"
]
},
{
"cell_type": "markdown",
"id": "b18c5f72",
"metadata": {},
"source": [
"### Whole-record examples: `name`, `notes`, `input`, and `output`\n",
"\n",
"Top-level `record_examples` teach a complete input-to-output mapping. Keep the\n",
"source under `input`, the desired record under `output`, and optionally add\n",
"a descriptive `name` and explanatory `notes`. Sparse example outputs are\n",
"completed with `null` for\n",
"fields you omit. These are different from `examples` nested under one output\n",
"field, which guide only that field."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9f3a10de",
"metadata": {},
"outputs": [],
"source": [
"recipe_record_examples = '''\n",
"wrangles:\n",
" - extract.ai:\n",
" api_key: ${OPENAI_API_KEY}\n",
" output:\n",
" best_match:\n",
" type: string\n",
" description: Best catalog match for the input item\n",
" certainty:\n",
" type: string\n",
" enum: [high, medium, low]\n",
" record_examples:\n",
" - name: exact manufacturer part match\n",
" notes: Use high certainty only when both manufacturer and part number match.\n",
" input:\n",
" description: ACME valve V-100\n",
" output:\n",
" best_match: ACME V-100 valve\n",
" certainty: high\n",
"'''\n",
"\n",
"df = pd.DataFrame({\"description\": [\"ACME valve model V-100\"]})\n",
"result = recipe.run(recipe_record_examples, variables=variables, dataframe=df)\n",
"result"
]
},
{
"cell_type": "markdown",
"id": "e6dc6eb6",
Expand Down Expand Up @@ -2887,26 +2938,73 @@
"server.shutdown()"
]
},
{
"cell_type": "markdown",
"id": "57f0ca93",
"metadata": {},
"source": [
"## 14. Web search and source provenance\n",
"\n",
"Set `web_search: true` to make the native OpenAI Responses web-search tool\n",
"available to the model. The model decides whether a row needs a search.\n",
"Every processed row receives a separate `web_search_sources` column containing\n",
"a deduplicated list of `{title, url}` objects taken from Responses metadata.\n",
"The list is empty when no search/source was used. The reserved column name is\n",
"automatic; do not also define it under `output`.\n",
"\n",
"Web results are cached with the extracted value. Use `cache: false` when the\n",
"recipe must retrieve fresh web information. Web search requires the default\n",
"`responses` protocol and is not available with legacy `chat_completions`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d8641ec7",
"metadata": {},
"outputs": [],
"source": [
"recipe_web_search = '''\n",
"wrangles:\n",
" - extract.ai:\n",
" input: company\n",
" api_key: ${OPENAI_API_KEY}\n",
" web_search: true\n",
" cache: false\n",
" output:\n",
" latest_news_title:\n",
" type: string\n",
" description: Title of the company's most recent official news post\n",
"'''\n",
"\n",
"df = pd.DataFrame({\"company\": [\"OpenAI\"]})\n",
"result = recipe.run(recipe_web_search, variables=variables, dataframe=df)\n",
"result[[\"latest_news_title\", \"web_search_sources\"]]"
]
},
{
"cell_type": "markdown",
"id": "0f2c87d0",
"metadata": {},
"source": [
"## 14. Quick reference\n",
"## 15. Quick reference\n",
"\n",
"| What you want | Recipe snippet |\n",
"|---|---|\n",
"| Single field, typed | `output: {length: {type: string, description: ...}}` |\n",
"| Multiple fields | Add more keys under `output` |\n",
"| Quick shorthand | `output: <description string>` (uses input column name) |\n",
"| Constrained values | Add `enum: [...]` to a field |\n",
"| Overall guidance | `instructions: ...` applies guidance to every input row |\n",
"| Numeric / list result | `type: integer` / `type: array` |\n",
"| Style hints | Add `examples: [...]` to a field |\n",
"| Whole-record examples | `record_examples: [{name: ..., notes: ..., input: ..., output: ...}]` |\n",
"| Web search + sources | `web_search: true` adds `web_search_sources` |\n",
"| Reasoning models | `model: gpt-5-mini`, `reasoning: {effort: low}`, `verbosity: low` |\n",
"| Parallelism | `threads: <n>` |\n",
"| Resilience | `timeout: <seconds>`, `retries: <n>` |\n",
"| Legacy endpoint | `url: https://api.openai.com/v1/chat/completions` |\n",
"| WrangleWorks model | `model_id: <id>` instead of `api_key`/`model` |"
"| Saved WrangleWorks definition | `model_id: <id>` instead of an `output` schema; `api_key` is still required |"
]
}
],
Expand Down
48 changes: 38 additions & 10 deletions docs/extract_ai_configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,28 @@ versioned replacement YAML file to override the complete configuration.
Recipes and Python calls can override these settings individually. Saved XL
models and recipe outputs are compiled through the same definition compiler.

## Instructions

Use `instructions` for guidance that applies to every input row:

```yaml
wrangles:
- extract.ai:
input: Description
api_key: ${OPENAI_API_KEY}
instructions:
- Prefer explicit evidence over inferred evidence.
- Normalize dimensions to inches.
output:
Product Type:
type: string
```

Instructions are useful for decision rules, evidence priorities,
normalization requirements, or other behavior that applies to the complete
extraction. The former `messages` parameter remains available as a compatibility
alias but is no longer advertised in the recipe schema. Do not provide both.

## Nullable output fields

Defined output keys remain required so strict Structured Outputs always return
Expand All @@ -45,7 +67,7 @@ cells as sequences; users do not need to quote every object key and value.

## Examples

Definitions support both field-specific and holistic examples. They compile to
Definitions support both field-specific and record examples. They compile to
the same stable prompt representation and precede each row's dynamic input.

For saved models, the field grid supports:
Expand All @@ -61,8 +83,9 @@ expected output may be human-friendly JSON/YAML-like syntax. Use explicit list
syntax for an array-valued paired output, such as `[Ceramic Tile, Slate]`.
An explicit `null` is valid because output fields are nullable by default.

Field-specific examples teach only the named field. Definitions may also
provide holistic examples that pair one input with a multi-field output:
Field-specific examples teach only the named field. Paired field examples may
include optional `name` and `notes` metadata. Definitions may also provide
`record_examples` that pair one input with a multi-field output:

```yaml
wrangles:
Expand All @@ -73,27 +96,32 @@ wrangles:
Power Source:
type: string
examples:
- input: 18V cordless drill
- name: cordless tool
notes: Voltage without a cord indicates a battery.
input: 18V cordless drill
output: Battery
- Corded
Voltage:
type: number
examples:
record_examples:
- name: corded saw
notes: Use both fields from this complete example.
input: 120V corded jig saw
output:
Power Source: Corded
Voltage: 120
```

The first `Power Source` item is a paired field example; `Corded` remains
output-only value guidance. Top-level `examples` are holistic. Their output may
be sparse: the compiler inserts `null` for omitted output fields so every
example demonstrates the complete required response shape. Unknown fields and
values that do not match the output schema fail during compilation.
output-only value guidance. Top-level `record_examples` demonstrate the complete
record. Their output may be sparse: the compiler inserts `null` for omitted
output fields so every example demonstrates the complete required response
shape. Unknown fields and values that do not match the output schema fail during
compilation. Optional `name` and `notes` metadata are included in model guidance
at both levels.

Saved-model content may likewise include a top-level `Examples` array of
holistic pairs. A dedicated WranglesXL interface for those examples can be
record pairs. A dedicated WranglesXL interface for those examples can be
added later without changing the compiler or runtime contract.

## Result cache
Expand Down
64 changes: 64 additions & 0 deletions tests/recipes/wrangles/test_extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,70 @@
from unittest.mock import patch


class TestExtractAIWebSearch:
@patch("wrangles.recipe_wrangles.extract._extract.ai")
def test_web_search_writes_a_dedicated_sources_column(self, extract_ai):
extract_ai.return_value = [
{
"manufacturer": "Acme",
"web_search_sources": [{
"title": "Acme product page",
"url": "https://example.com/acme",
}],
},
{
"manufacturer": "Contoso",
"web_search_sources": [],
},
]
data = pd.DataFrame({"description": ["Acme part", "Contoso part"]})
recipe = """
wrangles:
- extract.ai:
input: description
api_key: test-key
web_search: true
output:
manufacturer:
type: string
description: Manufacturer name
"""

result = wrangles.recipe.run(recipe, dataframe=data)

assert result["manufacturer"].tolist() == ["Acme", "Contoso"]
assert result["web_search_sources"].tolist() == [
[{
"title": "Acme product page",
"url": "https://example.com/acme",
}],
[],
]
assert extract_ai.call_args.kwargs["web_search"] is True

@patch("wrangles.recipe_wrangles.extract._extract.ai")
def test_web_search_rejects_an_existing_sources_column(self, extract_ai):
data = pd.DataFrame({
"description": ["Acme part"],
"web_search_sources": [[]],
})
recipe = """
wrangles:
- extract.ai:
input: description
api_key: test-key
web_search: true
output:
manufacturer:
type: string
"""

with pytest.raises(ValueError, match="is reserved"):
wrangles.recipe.run(recipe, dataframe=data)

extract_ai.assert_not_called()


class TestExtractAddress:
"""
Test extract.address
Expand Down
65 changes: 65 additions & 0 deletions tests/recipes/wrangles/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9339,6 +9339,71 @@ def test_all_wrangle_docstrings_parse_as_yaml(self):

assert not failures, 'Wrangle schema docstring YAML parse failures:\n' + '\n'.join(failures)

def test_extract_ai_schema_documents_public_parameters_and_example_shape(self):
import inspect
import yaml

method = wrangles.recipe._recipe_wrangles.extract.ai
schema = yaml.safe_load(method.__doc__)
properties = schema["properties"]
parameters = {
name
for name in inspect.signature(method).parameters
if name not in {"df", "kwargs"}
}

assert parameters <= properties.keys()
assert all(
isinstance(definition.get("description"), str)
and definition["description"].strip()
for definition in properties.values()
)
assert properties["web_search"]["type"] == "boolean"
assert "web_search_sources" in properties["web_search"]["description"]

assert "messages" not in properties
instructions = properties["instructions"]
assert instructions["title"] == "Instructions"
assert instructions["type"] == ["string", "array"]
assert "every input row" in instructions["description"]
assert "decision rules" in instructions["description"]
assert "after" not in instructions["description"].lower()

assert "examples" not in properties
examples = properties["record_examples"]
assert examples["title"] == "Record examples"
assert examples["required"] == ["input", "output"]
assert examples["items"]["required"] == ["input", "output"]
assert set(examples["items"]["properties"]) == {
"name",
"notes",
"input",
"output",
}
assert all(
definition["description"].strip()
for definition in examples["items"]["properties"].values()
)

field_schema = next(iter(properties["output"]["patternProperties"].values()))
field_examples = field_schema["properties"]["examples"]
assert field_examples["title"] == "Field examples"
assert "backward-compatible" in field_examples["description"]
assert "optional name and notes" in field_examples["description"]
paired_field_example = field_examples["items"]["anyOf"][0]
assert paired_field_example["required"] == ["input", "output"]
assert set(paired_field_example["properties"]) == {
"name",
"notes",
"input",
"output",
}
assert all(
definition["description"].strip()
for definition in field_schema["properties"].values()
)
assert "effort" in properties["reasoning"]["properties"]

def test_extract_codes_schema_matches_microservice_params(self):
import yaml

Expand Down
4 changes: 2 additions & 2 deletions tests/samples/extract ai judge example.wrgl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ wrangles:
input: Judge Input
api_key: ${OPENAI_API_KEY}
model: gpt-5.4-mini
messages: |
instructions: |
You are an expert classifier for supplier product content.

Each input contains:
Expand Down Expand Up @@ -128,7 +128,7 @@ wrangles:
description: |
True when confidence is Uncertain or Selected Classification is
"None (needs review)"; otherwise false.
examples:
record_examples:
- name: dynamic two-candidate example
input:
Input Description: 18V cordless drill with battery and charger
Expand Down
Loading
Loading