Rifqi Hafizuddin Claude Fable 5 commited on
Commit ·
089b69d
1
Parent(s): b9dfc76
[KM-644] planner: infeasible plan outcome + entity-ranking few-shots
Browse filesTaskList.infeasible_reason lets the planner decline questions no catalog
column can answer (was force-mapping, e.g. pa AS revenue); coordinator
short-circuits to a deterministic EN/ID data-gap reply and a
non-substantive record. Few-shots add top-N entity ranking (G) and an
infeasible example (H); planner.md gains hard rule 6 + 'When the catalog
cannot answer'. Live-verified 2026-07-08 on the PA analysis.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- src/agents/planner/examples.py +90 -0
- src/agents/planner/schemas.py +15 -0
- src/agents/planner/validator.py +9 -0
- src/agents/refusals.py +33 -0
- src/agents/slow_path/coordinator.py +33 -1
- src/config/prompts/planner.md +37 -3
src/agents/planner/examples.py
CHANGED
|
@@ -524,6 +524,94 @@ _EXAMPLE_F = TaskList(
|
|
| 524 |
)
|
| 525 |
|
| 526 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 527 |
EXAMPLES: list[tuple[str, TaskList]] = [
|
| 528 |
("Which product categories drove last quarter's revenue?", _EXAMPLE_A),
|
| 529 |
("How has monthly revenue trended by region this year, and what's unusual?", _EXAMPLE_B),
|
|
@@ -531,6 +619,8 @@ EXAMPLES: list[tuple[str, TaskList]] = [
|
|
| 531 |
("What is the average and total order value per region?", _EXAMPLE_D),
|
| 532 |
("Total revenue for the East and West regions, counting orders of at least 100.", _EXAMPLE_E),
|
| 533 |
("Give me the summary statistics for order revenue and quantity.", _EXAMPLE_F),
|
|
|
|
|
|
|
| 534 |
]
|
| 535 |
|
| 536 |
|
|
|
|
| 524 |
)
|
| 525 |
|
| 526 |
|
| 527 |
+
# --------------------------------------------------------------------------- #
|
| 528 |
+
# Example G — top-N ranking.
|
| 529 |
+
# "Top 3 product categories by total revenue."
|
| 530 |
+
# Shows: top-N is ONE retrieve_data query — group by the entity, aggregate the
|
| 531 |
+
# measure with an alias, order by that alias, limit N. NEVER a bare
|
| 532 |
+
# order-by-measure + limit (that ranks raw rows, so the same entity can appear
|
| 533 |
+
# twice — observed in production: "top 3 models" returned one model twice).
|
| 534 |
+
# --------------------------------------------------------------------------- #
|
| 535 |
+
|
| 536 |
+
_EXAMPLE_G = TaskList(
|
| 537 |
+
plan_id="example_g",
|
| 538 |
+
goal_restated="Rank product categories by total revenue and return the top 3.",
|
| 539 |
+
assumptions=[],
|
| 540 |
+
open_questions=[],
|
| 541 |
+
tasks=[
|
| 542 |
+
Task(
|
| 543 |
+
id="t1",
|
| 544 |
+
stage="data_understanding",
|
| 545 |
+
objective="Confirm the sales source exposes category and revenue.",
|
| 546 |
+
tool_calls=[ToolCall(tool="check_data", args={"source_id": "src_sales"})],
|
| 547 |
+
expected_output="source_shape",
|
| 548 |
+
success_criteria=(
|
| 549 |
+
"Produced the orders table schema; category and revenue columns "
|
| 550 |
+
"are present."
|
| 551 |
+
),
|
| 552 |
+
depends_on=[],
|
| 553 |
+
estimated_cost="low",
|
| 554 |
+
),
|
| 555 |
+
Task(
|
| 556 |
+
id="t2",
|
| 557 |
+
stage="data_preparation",
|
| 558 |
+
objective="Aggregate revenue per category, rank descending, keep the top 3.",
|
| 559 |
+
tool_calls=[
|
| 560 |
+
ToolCall(
|
| 561 |
+
tool="retrieve_data",
|
| 562 |
+
args={
|
| 563 |
+
"ir": {
|
| 564 |
+
"source_id": "src_sales",
|
| 565 |
+
"table_id": "t_orders",
|
| 566 |
+
"select": [
|
| 567 |
+
{"kind": "column", "column_id": "c_category", "alias": "category"},
|
| 568 |
+
{
|
| 569 |
+
"kind": "agg",
|
| 570 |
+
"fn": "sum",
|
| 571 |
+
"column_id": "c_revenue",
|
| 572 |
+
"alias": "total_revenue",
|
| 573 |
+
},
|
| 574 |
+
],
|
| 575 |
+
"group_by": ["c_category"],
|
| 576 |
+
"order_by": [{"column_id": "total_revenue", "dir": "desc"}],
|
| 577 |
+
"limit": 3,
|
| 578 |
+
}
|
| 579 |
+
},
|
| 580 |
+
)
|
| 581 |
+
],
|
| 582 |
+
expected_output="top3_categories",
|
| 583 |
+
success_criteria=(
|
| 584 |
+
"Produced at most 3 rows, one distinct category each, ranked by "
|
| 585 |
+
"total revenue."
|
| 586 |
+
),
|
| 587 |
+
depends_on=["t1"],
|
| 588 |
+
estimated_cost="low",
|
| 589 |
+
),
|
| 590 |
+
],
|
| 591 |
+
)
|
| 592 |
+
|
| 593 |
+
# --------------------------------------------------------------------------- #
|
| 594 |
+
# Example H — infeasible question (see planner.md "When the catalog cannot
|
| 595 |
+
# answer"). "What is our customer churn rate?" against a sales catalog with no
|
| 596 |
+
# subscription/churn data: no task list is forced onto unrelated columns;
|
| 597 |
+
# instead `infeasible_reason` states the gap + the nearest available data.
|
| 598 |
+
# --------------------------------------------------------------------------- #
|
| 599 |
+
|
| 600 |
+
_EXAMPLE_H = TaskList(
|
| 601 |
+
plan_id="example_h",
|
| 602 |
+
goal_restated="Measure the customer churn rate.",
|
| 603 |
+
assumptions=[],
|
| 604 |
+
open_questions=[],
|
| 605 |
+
tasks=[],
|
| 606 |
+
infeasible_reason=(
|
| 607 |
+
"The connected source has no churn or subscription-status data — the "
|
| 608 |
+
"orders table only carries order-level category, revenue, quantity, and "
|
| 609 |
+
"dates. Nearest available analyses: repeat-purchase behaviour or revenue "
|
| 610 |
+
"per customer over time."
|
| 611 |
+
),
|
| 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),
|
|
|
|
| 619 |
("What is the average and total order value per region?", _EXAMPLE_D),
|
| 620 |
("Total revenue for the East and West regions, counting orders of at least 100.", _EXAMPLE_E),
|
| 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 |
|
src/agents/planner/schemas.py
CHANGED
|
@@ -58,3 +58,18 @@ class TaskList(BaseModel):
|
|
| 58 |
assumptions: list[str] = Field(default_factory=list)
|
| 59 |
open_questions: list[str] = Field(default_factory=list)
|
| 60 |
tasks: list[Task] = Field(default_factory=list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
assumptions: list[str] = Field(default_factory=list)
|
| 59 |
open_questions: list[str] = Field(default_factory=list)
|
| 60 |
tasks: list[Task] = Field(default_factory=list)
|
| 61 |
+
# Infeasible sentinel (planner.md "When the catalog cannot answer"): set with
|
| 62 |
+
# an EMPTY `tasks` list when no catalog column plausibly holds the requested
|
| 63 |
+
# measure/entity. Explains what is missing and names the nearest available
|
| 64 |
+
# data. The coordinator renders it as an honest data-gap answer instead of
|
| 65 |
+
# running the pipeline — the alternative was the planner force-mapping
|
| 66 |
+
# unrelated columns (observed: `pa` aliased as "revenue").
|
| 67 |
+
infeasible_reason: str | None = Field(
|
| 68 |
+
None,
|
| 69 |
+
description=(
|
| 70 |
+
"Set ONLY when the question cannot be answered from the catalog: no "
|
| 71 |
+
"column plausibly holds the requested measure or entity. State what "
|
| 72 |
+
"is missing and the nearest data that IS available. Leave tasks "
|
| 73 |
+
"empty when set."
|
| 74 |
+
),
|
| 75 |
+
)
|
src/agents/planner/validator.py
CHANGED
|
@@ -61,6 +61,15 @@ class PlannerValidator:
|
|
| 61 |
) -> None:
|
| 62 |
tasks = task_list.tasks
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
# Check 6 — plan non-empty and within the task cap.
|
| 65 |
if not tasks:
|
| 66 |
raise PlannerValidationError("plan is empty: at least one task is required")
|
|
|
|
| 61 |
) -> None:
|
| 62 |
tasks = task_list.tasks
|
| 63 |
|
| 64 |
+
# Infeasible sentinel (planner.md "When the catalog cannot answer"): an
|
| 65 |
+
# empty plan carrying `infeasible_reason` is a VALID outcome — the
|
| 66 |
+
# coordinator renders it as an honest data-gap answer instead of the
|
| 67 |
+
# planner force-mapping the question onto unrelated columns. A non-empty
|
| 68 |
+
# plan keeps normal validation and the reason is ignored (a real plan
|
| 69 |
+
# wins over a hedge).
|
| 70 |
+
if task_list.infeasible_reason and not tasks:
|
| 71 |
+
return
|
| 72 |
+
|
| 73 |
# Check 6 — plan non-empty and within the task cap.
|
| 74 |
if not tasks:
|
| 75 |
raise PlannerValidationError("plan is empty: at least one task is required")
|
src/agents/refusals.py
CHANGED
|
@@ -56,6 +56,39 @@ _BLOCKED = {
|
|
| 56 |
}
|
| 57 |
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
def out_of_scope_message(message: str) -> str:
|
| 60 |
"""Refusal for a benign but out-of-scope request (the `out_of_scope` intent)."""
|
| 61 |
return _OUT_OF_SCOPE["id" if _is_indonesian(message) else "en"]
|
|
|
|
| 56 |
}
|
| 57 |
|
| 58 |
|
| 59 |
+
# Data-gap: the planner judged the bound sources cannot answer the question
|
| 60 |
+
# (planner.md "When the catalog cannot answer"). Deterministic wrapper on
|
| 61 |
+
# purpose — the model that declined to plan is not re-asked to prose it up.
|
| 62 |
+
# Keyed on the pipeline's reply_language ("Indonesian"/"English"), not marker
|
| 63 |
+
# detection: the upstream language decision is authoritative here.
|
| 64 |
+
_DATA_GAP = {
|
| 65 |
+
"en": (
|
| 66 |
+
"I can't answer that from the data sources connected to this analysis. "
|
| 67 |
+
"{reason}You can bind a source that holds this data, or ask me what's "
|
| 68 |
+
"available (try /help or \"what data do I have?\")."
|
| 69 |
+
),
|
| 70 |
+
"id": (
|
| 71 |
+
"Saya tidak bisa menjawab itu dari sumber data yang terhubung ke "
|
| 72 |
+
"analisis ini. {reason}Anda bisa menambahkan sumber yang memuat data "
|
| 73 |
+
"tersebut, atau tanyakan data apa yang tersedia (coba /help atau "
|
| 74 |
+
"\"data apa yang saya punya?\")."
|
| 75 |
+
),
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def data_gap_message(reason: str | None, reply_language: str | None = None) -> str:
|
| 80 |
+
"""Answer for an infeasible analysis: the bound sources lack the asked-for data.
|
| 81 |
+
|
| 82 |
+
`reason` is the planner's `infeasible_reason` (may be None/empty);
|
| 83 |
+
`reply_language` is the pipeline's detected language ("Indonesian"/"English").
|
| 84 |
+
"""
|
| 85 |
+
detail = (reason or "").strip()
|
| 86 |
+
if detail and not detail.endswith((".", "!", "?")):
|
| 87 |
+
detail += "."
|
| 88 |
+
lang = "id" if reply_language == "Indonesian" else "en"
|
| 89 |
+
return _DATA_GAP[lang].format(reason=f"{detail} " if detail else "")
|
| 90 |
+
|
| 91 |
+
|
| 92 |
def out_of_scope_message(message: str) -> str:
|
| 93 |
"""Refusal for a benign but out-of-scope request (the `out_of_scope` intent)."""
|
| 94 |
return _OUT_OF_SCOPE["id" if _is_indonesian(message) else "en"]
|
src/agents/slow_path/coordinator.py
CHANGED
|
@@ -11,13 +11,16 @@ See AGENT_ARCHITECTURE_CONTEXT_new.md §5.2 / §6.1.
|
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
from collections.abc import Awaitable, Callable
|
|
|
|
| 14 |
|
| 15 |
from ...catalog.models import Catalog
|
| 16 |
from ..planner.contracts import BusinessContext, ToolRegistry
|
| 17 |
from ..planner.inputs import Constraints
|
|
|
|
| 18 |
from ..planner.service import PlannerService
|
|
|
|
| 19 |
from .assembler import Assembler
|
| 20 |
-
from .schemas import AssembledOutput
|
| 21 |
from .task_runner import TaskRunner
|
| 22 |
|
| 23 |
|
|
@@ -54,6 +57,13 @@ class SlowPathCoordinator:
|
|
| 54 |
task_list = await self._planner.plan(
|
| 55 |
context, catalog, self._registry, query, constraints, **plan_kw
|
| 56 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
if progress:
|
| 58 |
await progress(f"Running {len(task_list.tasks)} analysis steps…")
|
| 59 |
run_state = await self._task_runner.run(
|
|
@@ -65,3 +75,25 @@ class SlowPathCoordinator:
|
|
| 65 |
return await self._assembler.assemble(
|
| 66 |
run_state, context, question=query, reply_language=reply_language, **asm_kw
|
| 67 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
from collections.abc import Awaitable, Callable
|
| 14 |
+
from datetime import UTC, datetime
|
| 15 |
|
| 16 |
from ...catalog.models import Catalog
|
| 17 |
from ..planner.contracts import BusinessContext, ToolRegistry
|
| 18 |
from ..planner.inputs import Constraints
|
| 19 |
+
from ..planner.schemas import TaskList
|
| 20 |
from ..planner.service import PlannerService
|
| 21 |
+
from ..refusals import data_gap_message
|
| 22 |
from .assembler import Assembler
|
| 23 |
+
from .schemas import AnalysisRecord, AssembledOutput
|
| 24 |
from .task_runner import TaskRunner
|
| 25 |
|
| 26 |
|
|
|
|
| 57 |
task_list = await self._planner.plan(
|
| 58 |
context, catalog, self._registry, query, constraints, **plan_kw
|
| 59 |
)
|
| 60 |
+
if task_list.infeasible_reason and not task_list.tasks:
|
| 61 |
+
# Honest data-gap outcome (planner.md "When the catalog cannot
|
| 62 |
+
# answer"): nothing to execute, and the refusal is deliberately
|
| 63 |
+
# deterministic — not LLM-prosed. The record carries no tasks, so it
|
| 64 |
+
# is non-substantive: it can never satisfy the report floor or leak
|
| 65 |
+
# into a report.
|
| 66 |
+
return _infeasible_output(task_list, context, reply_language)
|
| 67 |
if progress:
|
| 68 |
await progress(f"Running {len(task_list.tasks)} analysis steps…")
|
| 69 |
run_state = await self._task_runner.run(
|
|
|
|
| 75 |
return await self._assembler.assemble(
|
| 76 |
run_state, context, question=query, reply_language=reply_language, **asm_kw
|
| 77 |
)
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _infeasible_output(
|
| 81 |
+
task_list: TaskList, context: BusinessContext, reply_language: str | None
|
| 82 |
+
) -> AssembledOutput:
|
| 83 |
+
"""Build the data-gap answer + a faithful (non-substantive) record."""
|
| 84 |
+
reason = task_list.infeasible_reason or ""
|
| 85 |
+
return AssembledOutput(
|
| 86 |
+
chat_answer=data_gap_message(reason, reply_language),
|
| 87 |
+
analysis_record=AnalysisRecord(
|
| 88 |
+
goal_restated=task_list.goal_restated,
|
| 89 |
+
findings=[],
|
| 90 |
+
caveats=[reason] if reason else [],
|
| 91 |
+
data_used=[],
|
| 92 |
+
open_questions=list(task_list.open_questions),
|
| 93 |
+
tasks_run=[],
|
| 94 |
+
results_snapshot={},
|
| 95 |
+
plan_id=task_list.plan_id,
|
| 96 |
+
business_context_id=context.project_id,
|
| 97 |
+
created_at=datetime.now(UTC),
|
| 98 |
+
),
|
| 99 |
+
)
|
src/config/prompts/planner.md
CHANGED
|
@@ -21,6 +21,12 @@ only a `TaskList` object that conforms to the provided schema.
|
|
| 21 |
id lookup, so a paraphrased name fails.
|
| 22 |
5. **No modeling in v1.** There are no modeling tools. Do not emit `modeling`
|
| 23 |
tasks. The product is descriptive/diagnostic only — no predictions, no charts.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
# How to plan
|
| 26 |
|
|
@@ -68,9 +74,29 @@ only a `TaskList` object that conforms to the provided schema.
|
|
| 68 |
- **success_criteria is a reporting signal**, not a control trigger. State, in
|
| 69 |
checkable terms (counts, rates, "produced", "above"/"below"), what a good
|
| 70 |
result looks like. It never causes a retry.
|
| 71 |
-
- **Surface uncertainty, don't guess.** If the question is ambiguous
|
| 72 |
-
catalog can
|
| 73 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
# Writing a retrieve_data QueryIR
|
| 76 |
|
|
@@ -109,6 +135,14 @@ only a `TaskList` object that conforms to the provided schema.
|
|
| 109 |
select the product column + `sum(revenue)` aliased `total_revenue`, with
|
| 110 |
`group_by: ["<product_col_id>"]`,
|
| 111 |
`order_by: [{"column_id": "total_revenue", "dir": "desc"}]`, `limit: 3`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
# Output
|
| 114 |
|
|
|
|
| 21 |
id lookup, so a paraphrased name fails.
|
| 22 |
5. **No modeling in v1.** There are no modeling tools. Do not emit `modeling`
|
| 23 |
tasks. The product is descriptive/diagnostic only — no predictions, no charts.
|
| 24 |
+
6. **Never re-purpose a column as a different business measure.** A column means
|
| 25 |
+
what the catalog says it means — do not alias one concept as another to force
|
| 26 |
+
an answer (e.g. selecting an availability percentage AS "revenue", or a
|
| 27 |
+
0-1 ratio AS a percentage metric). If no column plausibly holds the measure
|
| 28 |
+
or entity the question asks about, the plan is **infeasible** — see "When the
|
| 29 |
+
catalog cannot answer".
|
| 30 |
|
| 31 |
# How to plan
|
| 32 |
|
|
|
|
| 74 |
- **success_criteria is a reporting signal**, not a control trigger. State, in
|
| 75 |
checkable terms (counts, rates, "produced", "above"/"below"), what a good
|
| 76 |
result looks like. It never causes a retry.
|
| 77 |
+
- **Surface uncertainty, don't guess.** If the question is *ambiguous* — the
|
| 78 |
+
catalog can answer it but a term needs interpreting (which period, which
|
| 79 |
+
metric variant) — record the interpretation in `assumptions`, anything
|
| 80 |
+
unresolved in `open_questions`, and plan the best defensible analysis. This
|
| 81 |
+
never licenses re-purposing columns: when the requested measure itself is
|
| 82 |
+
absent from the catalog, the question is not ambiguous, it is **infeasible**
|
| 83 |
+
(next section).
|
| 84 |
+
|
| 85 |
+
# When the catalog cannot answer
|
| 86 |
+
|
| 87 |
+
Some questions ask for a measure or entity the connected sources simply do not
|
| 88 |
+
hold (e.g. "sales revenue" against a maintenance database, "churn rate" with no
|
| 89 |
+
subscription data). For those:
|
| 90 |
+
|
| 91 |
+
- Return `tasks: []` and set **`infeasible_reason`**: one short paragraph naming
|
| 92 |
+
(a) what the question needs that no column provides, and (b) the nearest
|
| 93 |
+
analyses the catalog CAN support, so the user knows what to ask instead.
|
| 94 |
+
- Do NOT emit a plan that maps the question onto semantically unrelated columns
|
| 95 |
+
just because their types fit — a confidently wrong number is worse than an
|
| 96 |
+
honest gap.
|
| 97 |
+
- The test: could you point at a specific catalog column whose *meaning* (name,
|
| 98 |
+
sample values, table context) matches the requested measure? If not,
|
| 99 |
+
it is infeasible.
|
| 100 |
|
| 101 |
# Writing a retrieve_data QueryIR
|
| 102 |
|
|
|
|
| 135 |
select the product column + `sum(revenue)` aliased `total_revenue`, with
|
| 136 |
`group_by: ["<product_col_id>"]`,
|
| 137 |
`order_by: [{"column_id": "total_revenue", "dir": "desc"}]`, `limit: 3`.
|
| 138 |
+
This applies to EVERY entity-ranking phrasing — "top/best/worst/highest/lowest
|
| 139 |
+
N <entities>", "<entities> with the best <measure> performance", "which
|
| 140 |
+
<entities> perform best" — the unit being ranked is the ENTITY, so the measure
|
| 141 |
+
MUST be aggregated per entity first (`group_by` the entity column). Ranking
|
| 142 |
+
raw rows can return the same entity twice, which is never a valid entity
|
| 143 |
+
ranking. Choose the aggregate by measure type: additive measures (revenue,
|
| 144 |
+
counts, backlog) → `sum`; ratio/percentage/rate metrics (availability,
|
| 145 |
+
utilization, scores) → `avg`. Record the choice in `assumptions`.
|
| 146 |
|
| 147 |
# Output
|
| 148 |
|