Skip to content

Commit b8d2beb

Browse files
feat: adding conditional gleaning (#375)
* fix: improve caching and don't raise error for bad gather configs * fix: improve caching and don't raise error for bad gather configs * feat: adding conditional gleaning
1 parent 7071ade commit b8d2beb

File tree

3 files changed

+63
-10
lines changed

3 files changed

+63
-10
lines changed

docetl/operations/utils/api.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ def _cached_call_llm(
239239
)
240240

241241
for rnd in range(num_gleaning_rounds):
242+
# Break early if gleaning condition is not met
243+
if not self.should_glean(gleaning_config, parsed_output):
244+
break
242245
# Prepare validator prompt
243246
validator_prompt = strict_render(
244247
gleaning_config["validation_prompt"],
@@ -963,3 +966,32 @@ def validate_output(self, operation: Dict, output: Dict, console: Console) -> bo
963966
console.log(f"[yellow]Output:[/yellow] {output}")
964967
return False
965968
return True
969+
970+
def should_glean(self, gleaning_config: Optional[Dict[str, Any]], output: Dict[str, Any]) -> bool:
971+
"""Determine whether to execute a gleaning round based on an optional conditional expression.
972+
973+
If ``gleaning_config`` contains an ``"if"`` key, its value is treated as a Python
974+
boolean expression that will be evaluated with the current ``output`` bound to the
975+
name ``output`` using :pyfunc:`safe_eval`. When the expression evaluates to
976+
``True`` the gleaning round proceeds. If it evaluates to ``False`` (or raises an
977+
exception) the gleaning loop should terminate early.
978+
979+
If no ``"if"`` key is present the method defaults to returning ``True`` so that
980+
gleaning proceeds normally.
981+
"""
982+
# No gleaning_config or no conditional -> always glean
983+
if not gleaning_config or "if" not in gleaning_config:
984+
return True
985+
986+
condition = gleaning_config.get("if")
987+
if not isinstance(condition, str):
988+
raise ValueError(f"Invalid gleaning condition (should be a string): {condition}")
989+
990+
try:
991+
return safe_eval(condition, output)
992+
except Exception as exc:
993+
# If evaluation fails, default to not glean and log for visibility
994+
self.runner.console.log(
995+
f"[bold red]Error evaluating gleaning condition '{condition}': {exc}; executing gleaning round anyway[/bold red]"
996+
)
997+
return False

docs/concepts/operators.md

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,11 +155,8 @@ To enable gleaning, specify:
155155

156156
- `validation_prompt`: Instructions for the LLM to evaluate and improve the output.
157157
- `num_rounds`: The maximum number of refinement iterations.
158-
<<<<<<< HEAD
159158
- `model` (optional): The model to use for the LLM executing the validation prompt. Defaults to the model specified for this operation. **Note that if the validator LLM determines the output needs to be improved, the final output will be generated by the model specified for this operation.**
160-
=======
161-
- `model` (optional): The model to use for the LLM executing the validation prompt. Defaults to the model specified for that operation.
162-
>>>>>>> 070110d (docs: improve gleaning description)
159+
- `if` (optional): A Python boolean expression (evaluated with `safe_eval`) that refers to **fields in the current `output`**. If the expression evaluates to `False`, DocETL skips gleaning entirely.
163160

164161
Example:
165162

@@ -189,19 +186,27 @@ Example map operation (with a different model for the validation prompt):
189186
schema:
190187
insights_summary: "string"
191188
gleaning:
192-
num_rounds: 2 # Will refine the output up to 2 times, if the judge LLM (gpt-4o-mini) suggests improvements
189+
if: "len(output['insights_summary']) < 10" # Only refine if summary is too short
190+
num_rounds: 2 # Will refine up to 2 times if needed
193191
model: gpt-4o-mini
194192
validation_prompt: |
195193
There should be at least 2 insights, and each insight should have at least 1 supporting action.
196194
```
197195

198196
!!! tip "Choosing a Different Model for Validation"
199197

200-
<<<<<<< HEAD
201-
In the example above, the `gpt-4o` model is used to generate the main outputs, while the `gpt-4o-mini` model is used only for the validation and refinement steps. This means the more powerful (and expensive) model produces the final output, but a less expensive model handles the iterative validation, helping to reduce costs without sacrificing output quality.
202-
=======
203198
You may want to use a different model for the validation prompt. For example, you can use a more powerful (and expensive) model for generating outputs, but a cheaper model for validation—especially if the validation only checks a single aspect. This approach helps reduce costs while still ensuring quality, since the final output is always produced by the more capable model.
204-
>>>>>>> 070110d (docs: improve gleaning description)
199+
200+
!!! tip "Conditional Gleaning"
201+
202+
You can also use the `if` field to conditionally skip gleaning. For example, if you only want to glean if the output is too short, you can use:
203+
```yaml
204+
gleaning:
205+
if: "len(output['insights_summary']) < 10"
206+
num_rounds: 2
207+
```
208+
209+
If the `if` field evaluates to `False`, DocETL skips gleaning entirely. Or, if the `if` field does not exist, DocETL will always glean.
205210

206211
### How Gleaning Works
207212

tests/basic/test_basic_map.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,4 +505,20 @@ def test_map_operation_calibration_with_larger_sample(simple_map_config, map_sam
505505
)
506506

507507
# Verify that cost is greater than 0
508-
assert cost > 0
508+
assert cost > 0
509+
510+
def test_should_glean_condition(api_wrapper):
511+
"""Unit-test the conditional gleaning logic on DSLRunner.api.should_glean."""
512+
513+
wrapper = api_wrapper.api # APIWrapper instance attached to the runner
514+
515+
# Case 1: condition evaluates to True
516+
gleaning_config = {"if": "output['flag'] == True"}
517+
assert wrapper.should_glean(gleaning_config, {"flag": True}) is True
518+
519+
# Case 2: condition evaluates to False
520+
assert wrapper.should_glean(gleaning_config, {"flag": False}) is False
521+
522+
# Case 3: No condition key -> default to True
523+
assert wrapper.should_glean({}, {"flag": False}) is True
524+
assert wrapper.should_glean(None, {"flag": False}) is True

0 commit comments

Comments
 (0)