Rifqi Hafizuddin Claude Fable 5 commited on
Commit ·
a029a84
1
Parent(s): 8abf635
[KM-567] planner support for analyze_merge: data_right guard + two-retrieve merge few-shot
Browse files- src/agents/planner/examples.py +123 -0
- src/agents/planner/validator.py +22 -18
- src/config/prompts/planner.md +8 -0
src/agents/planner/examples.py
CHANGED
|
@@ -612,6 +612,124 @@ _EXAMPLE_H = TaskList(
|
|
| 612 |
)
|
| 613 |
|
| 614 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
EXAMPLES: list[tuple[str, TaskList]] = [
|
| 616 |
("Which product categories drove last quarter's revenue?", _EXAMPLE_A),
|
| 617 |
("How has monthly revenue trended by region this year, and what's unusual?", _EXAMPLE_B),
|
|
@@ -621,6 +739,11 @@ EXAMPLES: list[tuple[str, TaskList]] = [
|
|
| 621 |
("Give me the summary statistics for order revenue and quantity.", _EXAMPLE_F),
|
| 622 |
("Which 3 product categories have the best revenue performance?", _EXAMPLE_G),
|
| 623 |
("What is our customer churn rate?", _EXAMPLE_H),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
]
|
| 625 |
|
| 626 |
|
|
|
|
| 612 |
)
|
| 613 |
|
| 614 |
|
| 615 |
+
# --------------------------------------------------------------------------- #
|
| 616 |
+
# Example I — combine two measures per entity (KM-703).
|
| 617 |
+
# "Which category has both the highest revenue and the highest average order
|
| 618 |
+
# quantity?" Shows: each measure is computed in its OWN grouped retrieve_data
|
| 619 |
+
# task (a "${t<id>}" placeholder resolves to a task's LAST output, so the two
|
| 620 |
+
# retrievals must be separate tasks), then analyze_merge aligns them on the
|
| 621 |
+
# shared entity alias. The merged table answers "both A and B" questions that
|
| 622 |
+
# a single query cannot express.
|
| 623 |
+
# --------------------------------------------------------------------------- #
|
| 624 |
+
|
| 625 |
+
_EXAMPLE_I = TaskList(
|
| 626 |
+
plan_id="example_i",
|
| 627 |
+
goal_restated=(
|
| 628 |
+
"Identify the product category with both the highest total revenue and the "
|
| 629 |
+
"highest average order quantity."
|
| 630 |
+
),
|
| 631 |
+
assumptions=[],
|
| 632 |
+
open_questions=[],
|
| 633 |
+
tasks=[
|
| 634 |
+
Task(
|
| 635 |
+
id="t1",
|
| 636 |
+
stage="data_understanding",
|
| 637 |
+
objective="Confirm the sales source exposes category, revenue, and quantity.",
|
| 638 |
+
tool_calls=[ToolCall(tool="check_data", args={"source_id": "src_sales"})],
|
| 639 |
+
expected_output="source_shape",
|
| 640 |
+
success_criteria=(
|
| 641 |
+
"Produced the orders table schema; category, revenue, and quantity "
|
| 642 |
+
"columns are present."
|
| 643 |
+
),
|
| 644 |
+
depends_on=[],
|
| 645 |
+
estimated_cost="low",
|
| 646 |
+
),
|
| 647 |
+
Task(
|
| 648 |
+
id="t2",
|
| 649 |
+
stage="data_preparation",
|
| 650 |
+
objective="Total revenue per category.",
|
| 651 |
+
tool_calls=[
|
| 652 |
+
ToolCall(
|
| 653 |
+
tool="retrieve_data",
|
| 654 |
+
args={
|
| 655 |
+
"ir": {
|
| 656 |
+
"source_id": "src_sales",
|
| 657 |
+
"table_id": "t_orders",
|
| 658 |
+
"select": [
|
| 659 |
+
{"kind": "column", "column_id": "c_category", "alias": "category"},
|
| 660 |
+
{
|
| 661 |
+
"kind": "agg",
|
| 662 |
+
"fn": "sum",
|
| 663 |
+
"column_id": "c_revenue",
|
| 664 |
+
"alias": "total_revenue",
|
| 665 |
+
},
|
| 666 |
+
],
|
| 667 |
+
"group_by": ["c_category"],
|
| 668 |
+
}
|
| 669 |
+
},
|
| 670 |
+
)
|
| 671 |
+
],
|
| 672 |
+
expected_output="revenue_per_category",
|
| 673 |
+
success_criteria="Produced one total-revenue row per category.",
|
| 674 |
+
depends_on=["t1"],
|
| 675 |
+
estimated_cost="low",
|
| 676 |
+
),
|
| 677 |
+
Task(
|
| 678 |
+
id="t3",
|
| 679 |
+
stage="data_preparation",
|
| 680 |
+
objective="Average order quantity per category.",
|
| 681 |
+
tool_calls=[
|
| 682 |
+
ToolCall(
|
| 683 |
+
tool="retrieve_data",
|
| 684 |
+
args={
|
| 685 |
+
"ir": {
|
| 686 |
+
"source_id": "src_sales",
|
| 687 |
+
"table_id": "t_orders",
|
| 688 |
+
"select": [
|
| 689 |
+
{"kind": "column", "column_id": "c_category", "alias": "category"},
|
| 690 |
+
{
|
| 691 |
+
"kind": "agg",
|
| 692 |
+
"fn": "avg",
|
| 693 |
+
"column_id": "c_quantity",
|
| 694 |
+
"alias": "avg_quantity",
|
| 695 |
+
},
|
| 696 |
+
],
|
| 697 |
+
"group_by": ["c_category"],
|
| 698 |
+
}
|
| 699 |
+
},
|
| 700 |
+
)
|
| 701 |
+
],
|
| 702 |
+
expected_output="quantity_per_category",
|
| 703 |
+
success_criteria="Produced one average-quantity row per category.",
|
| 704 |
+
depends_on=["t1"],
|
| 705 |
+
estimated_cost="low",
|
| 706 |
+
),
|
| 707 |
+
Task(
|
| 708 |
+
id="t4",
|
| 709 |
+
stage="evaluation",
|
| 710 |
+
objective="Align both measures per category to find the category leading on both.",
|
| 711 |
+
tool_calls=[
|
| 712 |
+
ToolCall(
|
| 713 |
+
tool="analyze_merge",
|
| 714 |
+
args={
|
| 715 |
+
"data": "${t2}",
|
| 716 |
+
"data_right": "${t3}",
|
| 717 |
+
"on": ["category"],
|
| 718 |
+
},
|
| 719 |
+
)
|
| 720 |
+
],
|
| 721 |
+
expected_output="combined_measures",
|
| 722 |
+
success_criteria=(
|
| 723 |
+
"Produced one row per category carrying both total_revenue and "
|
| 724 |
+
"avg_quantity."
|
| 725 |
+
),
|
| 726 |
+
depends_on=["t2", "t3"],
|
| 727 |
+
estimated_cost="low",
|
| 728 |
+
),
|
| 729 |
+
],
|
| 730 |
+
)
|
| 731 |
+
|
| 732 |
+
|
| 733 |
EXAMPLES: list[tuple[str, TaskList]] = [
|
| 734 |
("Which product categories drove last quarter's revenue?", _EXAMPLE_A),
|
| 735 |
("How has monthly revenue trended by region this year, and what's unusual?", _EXAMPLE_B),
|
|
|
|
| 739 |
("Give me the summary statistics for order revenue and quantity.", _EXAMPLE_F),
|
| 740 |
("Which 3 product categories have the best revenue performance?", _EXAMPLE_G),
|
| 741 |
("What is our customer churn rate?", _EXAMPLE_H),
|
| 742 |
+
(
|
| 743 |
+
"Which product category has both the highest revenue and the highest average "
|
| 744 |
+
"order quantity?",
|
| 745 |
+
_EXAMPLE_I,
|
| 746 |
+
),
|
| 747 |
]
|
| 748 |
|
| 749 |
|
src/agents/planner/validator.py
CHANGED
|
@@ -220,24 +220,28 @@ class PlannerValidator:
|
|
| 220 |
requested columns. Resolving points at the referenced task's representative
|
| 221 |
output — its last tool call (matches TaskRunner's `outputs[-1]`).
|
| 222 |
"""
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
|
| 242 |
@staticmethod
|
| 243 |
def _validate_dag(tasks_by_id: dict, id_set: set[str]) -> None:
|
|
|
|
| 220 |
requested columns. Resolving points at the referenced task's representative
|
| 221 |
output — its last tool call (matches TaskRunner's `outputs[-1]`).
|
| 222 |
"""
|
| 223 |
+
# `data_right` is analyze_merge's second table input (KM-703) — same
|
| 224 |
+
# Pattern A handoff, so it gets the same guard.
|
| 225 |
+
for arg_name in ("data", "data_right"):
|
| 226 |
+
data_arg = call.args.get(arg_name)
|
| 227 |
+
if not isinstance(data_arg, str):
|
| 228 |
+
continue
|
| 229 |
+
match = PLACEHOLDER_RE.fullmatch(data_arg.strip())
|
| 230 |
+
if not match:
|
| 231 |
+
continue
|
| 232 |
+
ref_task = tasks_by_id.get(match.group(1))
|
| 233 |
+
if ref_task is None or not ref_task.tool_calls:
|
| 234 |
+
continue # a dangling placeholder is reported by the DAG check
|
| 235 |
+
ref_tool = ref_task.tool_calls[-1].tool
|
| 236 |
+
ref_spec = registry.get(ref_tool)
|
| 237 |
+
if ref_spec is not None and ref_spec.category in _NON_DATA_SOURCE_CATEGORIES:
|
| 238 |
+
raise PlannerValidationError(
|
| 239 |
+
f"task {task_id}: tool {call.tool!r} takes its {arg_name!r} from "
|
| 240 |
+
f"task {match.group(1)} ({ref_tool!r}, category "
|
| 241 |
+
f"{ref_spec.category!r}), which produces metadata/documents — not "
|
| 242 |
+
"analyzable data rows. Feed analyze_* from a data-producing tool "
|
| 243 |
+
"(e.g. retrieve_data)."
|
| 244 |
+
)
|
| 245 |
|
| 246 |
@staticmethod
|
| 247 |
def _validate_dag(tasks_by_id: dict, id_set: set[str]) -> None:
|
src/config/prompts/planner.md
CHANGED
|
@@ -67,6 +67,14 @@ only a `TaskList` object that conforms to the provided schema.
|
|
| 67 |
`orders.total_amount`); if they genuinely aren't linked, say the data isn't
|
| 68 |
connected rather than guessing. Prefer an existing measure column over
|
| 69 |
recomputing. Joins are database-only — not available for tabular/file sources.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
- **Mixing structured + unstructured.** If qualitative context helps, add a
|
| 71 |
`retrieve_knowledge` task against an unstructured source listed in the catalog.
|
| 72 |
- **CRISP-DM stages.** Tag each task with the stage it serves:
|
|
|
|
| 67 |
`orders.total_amount`); if they genuinely aren't linked, say the data isn't
|
| 68 |
connected rather than guessing. Prefer an existing measure column over
|
| 69 |
recomputing. Joins are database-only — not available for tabular/file sources.
|
| 70 |
+
- **Two measures per entity ("which X has both the worst A and the biggest B").**
|
| 71 |
+
Compute each measure in its OWN grouped `retrieve_data` task (one aggregate per
|
| 72 |
+
entity each), then align them with `analyze_merge`:
|
| 73 |
+
`{"data": "${tA}", "data_right": "${tB}", "on": ["<entity alias>"]}`.
|
| 74 |
+
The two retrievals MUST be separate tasks — a `"${t<id>}"` placeholder resolves
|
| 75 |
+
to a task's LAST output, so two retrievals inside one task lose the first
|
| 76 |
+
table. The merged table (one row per entity, both measures) answers the
|
| 77 |
+
question, or feeds a further `analyze_*` step.
|
| 78 |
- **Mixing structured + unstructured.** If qualitative context helps, add a
|
| 79 |
`retrieve_knowledge` task against an unstructured source listed in the catalog.
|
| 80 |
- **CRISP-DM stages.** Tag each task with the stage it serves:
|