Commit ·
db217d6
1
Parent(s): 726faa2
[KM-625] Prompt-style Tool Descriptions
Browse filesAdd a DESCRIPTION constant to each of the 8 analytics compute tools,
written prompt-style (Summary / USE WHEN / DON'T USE WHEN -> other tool /
example questions) for the Planner to decide which tool to pick. English
body with Indonesian trigger-word equivalents in parentheses. comparison
also documents the group_a baseline / comparison-direction rule.
Final destination is ToolSpec.description once the wrapper layer is built.
No behavior change; 72 tests, ruff, and mypy strict all green.
- src/tools/analytics/aggregation.py +25 -0
- src/tools/analytics/comparison.py +29 -0
- src/tools/analytics/decomposition.py +25 -0
- src/tools/analytics/descriptive.py +27 -0
- src/tools/analytics/quality.py +25 -0
- src/tools/analytics/relationship.py +24 -0
- src/tools/analytics/segmentation.py +25 -0
- src/tools/analytics/temporal.py +24 -0
src/tools/analytics/aggregation.py
CHANGED
|
@@ -34,6 +34,31 @@ def _clean(value: object) -> object:
|
|
| 34 |
return value
|
| 35 |
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
def analyze_aggregate(
|
| 38 |
df: pd.DataFrame,
|
| 39 |
aggregations: dict[str, list[str]],
|
|
|
|
| 34 |
return value
|
| 35 |
|
| 36 |
|
| 37 |
+
# Prompt-style description read by the Planner to decide WHEN to pick this tool.
|
| 38 |
+
# Final destination is ToolSpec.description once the wrapper layer is built.
|
| 39 |
+
DESCRIPTION = """\
|
| 40 |
+
Summary: Group-by aggregation. Splits rows by one or more key columns and \
|
| 41 |
+
computes aggregates per group (sum, mean, count, min, max, median, nunique). \
|
| 42 |
+
Returns one row per group.
|
| 43 |
+
|
| 44 |
+
USE WHEN the question groups a metric by a category — the tell-tale sign is \
|
| 45 |
+
"per"/"each"/"by" a dimension. Trigger words: "per/each" (per/tiap), "by" \
|
| 46 |
+
(berdasarkan), "breakdown", "total/average ... per ...".
|
| 47 |
+
|
| 48 |
+
DON'T USE WHEN:
|
| 49 |
+
- it summarizes a column with no grouping -> analyze_descriptive
|
| 50 |
+
- it compares two specific groups (A vs B) -> analyze_comparison
|
| 51 |
+
- it splits a single total into shares -> analyze_contribution
|
| 52 |
+
- the grouping is over time periods -> analyze_trend
|
| 53 |
+
|
| 54 |
+
Example questions:
|
| 55 |
+
- "total revenue per region"
|
| 56 |
+
- "average order value by customer segment"
|
| 57 |
+
- "how many distinct products were sold per store?"
|
| 58 |
+
- "count of orders for each status"
|
| 59 |
+
"""
|
| 60 |
+
|
| 61 |
+
|
| 62 |
def analyze_aggregate(
|
| 63 |
df: pd.DataFrame,
|
| 64 |
aggregations: dict[str, list[str]],
|
src/tools/analytics/comparison.py
CHANGED
|
@@ -33,6 +33,35 @@ class GroupNotFoundError(ValueError):
|
|
| 33 |
|
| 34 |
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
def analyze_comparison(
|
| 37 |
df: pd.DataFrame,
|
| 38 |
dimension: str,
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
|
| 36 |
+
# Prompt-style description read by the Planner to decide WHEN to pick this tool.
|
| 37 |
+
# Final destination is ToolSpec.description once the wrapper layer is built.
|
| 38 |
+
DESCRIPTION = """\
|
| 39 |
+
Summary: Head-to-head comparison of one aggregated metric between TWO specific \
|
| 40 |
+
groups of a dimension (group_a is the baseline). Reports each group's value, \
|
| 41 |
+
the absolute and percent difference, and which side is higher.
|
| 42 |
+
|
| 43 |
+
USE WHEN the question pits two named groups against each other. Trigger words: \
|
| 44 |
+
"vs"/"versus", "compare" (bandingkan), "A or B", "difference between" \
|
| 45 |
+
(selisih/beda antara), "higher/lower than".
|
| 46 |
+
|
| 47 |
+
SETTING GROUPS: group_a is the BASELINE (the reference). The "comparison" field \
|
| 48 |
+
reads as "group_b is {higher/lower/equal} than group_a", and diff = value_b - \
|
| 49 |
+
value_a. Put the reference/older/expected side in group_a. E.g. "is this year \
|
| 50 |
+
higher than last year" -> group_a=last year, group_b=this year.
|
| 51 |
+
|
| 52 |
+
DON'T USE WHEN:
|
| 53 |
+
- it aggregates across many groups at once -> analyze_aggregate
|
| 54 |
+
- it splits a single total into shares -> analyze_contribution
|
| 55 |
+
- it tracks change over time -> analyze_trend
|
| 56 |
+
|
| 57 |
+
Example questions:
|
| 58 |
+
- "compare revenue between Jakarta and Surabaya"
|
| 59 |
+
- "is the average order value higher for members or non-members?"
|
| 60 |
+
- "difference in churn between plan A and plan B"
|
| 61 |
+
- "male vs female average spend"
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
|
| 65 |
def analyze_comparison(
|
| 66 |
df: pd.DataFrame,
|
| 67 |
dimension: str,
|
src/tools/analytics/decomposition.py
CHANGED
|
@@ -36,6 +36,31 @@ def _clean(value: object) -> object:
|
|
| 36 |
return value
|
| 37 |
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
def analyze_contribution(
|
| 40 |
df: pd.DataFrame,
|
| 41 |
dimension: str,
|
|
|
|
| 36 |
return value
|
| 37 |
|
| 38 |
|
| 39 |
+
# Prompt-style description read by the Planner to decide WHEN to pick this tool.
|
| 40 |
+
# Final destination is ToolSpec.description once the wrapper layer is built.
|
| 41 |
+
DESCRIPTION = """\
|
| 42 |
+
Summary: Breaks a single total into per-category contributions (share of total) \
|
| 43 |
+
in one snapshot, largest first, with cumulative share. Supports Pareto (80/20) \
|
| 44 |
+
reasoning. Optional top_n folds the long tail into "Others".
|
| 45 |
+
|
| 46 |
+
USE WHEN the question is about how a whole splits into parts, or which \
|
| 47 |
+
categories dominate. Trigger words: "contribution" (kontribusi), "share" \
|
| 48 |
+
(porsi/proporsi), "% of total" (persen dari total), "Pareto/80-20", "top \
|
| 49 |
+
contributors", "which ... make up most".
|
| 50 |
+
|
| 51 |
+
DON'T USE WHEN:
|
| 52 |
+
- it pits two specific groups against each other -> analyze_comparison
|
| 53 |
+
- it tracks change over time -> analyze_trend
|
| 54 |
+
- it just aggregates per group without share-of-total -> analyze_aggregate
|
| 55 |
+
|
| 56 |
+
Example questions:
|
| 57 |
+
- "which products contribute most to total sales?"
|
| 58 |
+
- "what share of revenue comes from each region?"
|
| 59 |
+
- "top 5 customers by contribution to profit"
|
| 60 |
+
- "do 20% of items make up 80% of revenue?"
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
|
| 64 |
def analyze_contribution(
|
| 65 |
df: pd.DataFrame,
|
| 66 |
dimension: str,
|
src/tools/analytics/descriptive.py
CHANGED
|
@@ -83,6 +83,33 @@ def _describe_one(series: pd.Series, metrics: tuple[str, ...]) -> dict[str, obje
|
|
| 83 |
return out
|
| 84 |
|
| 85 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
def analyze_descriptive(
|
| 87 |
df: pd.DataFrame,
|
| 88 |
column_ids: list[str],
|
|
|
|
| 83 |
return out
|
| 84 |
|
| 85 |
|
| 86 |
+
# Prompt-style description read by the Planner to decide WHEN to pick this tool.
|
| 87 |
+
# Final destination is ToolSpec.description once the wrapper layer is built.
|
| 88 |
+
DESCRIPTION = """\
|
| 89 |
+
Summary: Descriptive statistics (EDA) for one or several columns in a single \
|
| 90 |
+
call — center (mean, median, mode), spread (std, variance, min, max, Q1/Q3 \
|
| 91 |
+
quartiles), distribution shape (skew), and completeness (null count & rate).
|
| 92 |
+
|
| 93 |
+
USE WHEN the user asks for an overview, summary, or single-column statistics \
|
| 94 |
+
of ONE or SEVERAL columns as a whole, with NO grouping and NO comparison \
|
| 95 |
+
between groups. Trigger words: "overview/summary" (ringkasan), "average" \
|
| 96 |
+
(rata-rata), "median", "spread/distribution" (sebaran), "how many nulls" \
|
| 97 |
+
(berapa nilai kosong).
|
| 98 |
+
|
| 99 |
+
DON'T USE WHEN:
|
| 100 |
+
- the question groups by something ("per"/"each"/"by") -> analyze_aggregate
|
| 101 |
+
- it compares two specific groups (A vs B) -> analyze_comparison
|
| 102 |
+
- it tracks a metric over time -> analyze_trend
|
| 103 |
+
- it checks data type, quality, duplicates, outliers, constants -> analyze_profile
|
| 104 |
+
|
| 105 |
+
Example questions:
|
| 106 |
+
- "what's the average and median customer age?"
|
| 107 |
+
- "summarize the income column"
|
| 108 |
+
- "how is product price distributed?"
|
| 109 |
+
- "how many nulls are in the email column?"
|
| 110 |
+
"""
|
| 111 |
+
|
| 112 |
+
|
| 113 |
def analyze_descriptive(
|
| 114 |
df: pd.DataFrame,
|
| 115 |
column_ids: list[str],
|
src/tools/analytics/quality.py
CHANGED
|
@@ -82,6 +82,31 @@ def _profile_one(series: pd.Series) -> dict[str, object]:
|
|
| 82 |
return out
|
| 83 |
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
def analyze_profile(
|
| 86 |
df: pd.DataFrame,
|
| 87 |
column_ids: list[str] | None = None,
|
|
|
|
| 82 |
return out
|
| 83 |
|
| 84 |
|
| 85 |
+
# Prompt-style description read by the Planner to decide WHEN to pick this tool.
|
| 86 |
+
# Final destination is ToolSpec.description once the wrapper layer is built.
|
| 87 |
+
DESCRIPTION = """\
|
| 88 |
+
Summary: Per-column data-quality profile. For each column reports dtype, \
|
| 89 |
+
inferred type, completeness (null count/rate), cardinality (distinct count/rate, \
|
| 90 |
+
constant flag), and — for numeric columns — min/max/mean plus an IQR-based \
|
| 91 |
+
outlier count; for non-numeric columns the most frequent value.
|
| 92 |
+
|
| 93 |
+
USE WHEN the question is about the HEALTH of the data, not its statistics: \
|
| 94 |
+
missing values, duplicates, data types, outliers, "is this clean enough to \
|
| 95 |
+
analyze". Trigger words: "quality" (kualitas), "missing/nulls" (data kosong), \
|
| 96 |
+
"data type" (tipe data), "duplicates/unique" (duplikat/unik), "outliers".
|
| 97 |
+
|
| 98 |
+
DON'T USE WHEN:
|
| 99 |
+
- the user wants statistics like mean/median/std/skew -> analyze_descriptive
|
| 100 |
+
- it groups or compares -> analyze_aggregate / analyze_comparison
|
| 101 |
+
|
| 102 |
+
Example questions:
|
| 103 |
+
- "is this dataset clean enough to analyze?"
|
| 104 |
+
- "which columns have a lot of missing values?"
|
| 105 |
+
- "what are the data types and unique counts per column?"
|
| 106 |
+
- "are there outliers in the amount column?"
|
| 107 |
+
"""
|
| 108 |
+
|
| 109 |
+
|
| 110 |
def analyze_profile(
|
| 111 |
df: pd.DataFrame,
|
| 112 |
column_ids: list[str] | None = None,
|
src/tools/analytics/relationship.py
CHANGED
|
@@ -44,6 +44,30 @@ def _clean(value: object) -> float | None:
|
|
| 44 |
return None if math.isnan(f) else f
|
| 45 |
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
def analyze_correlation(
|
| 48 |
df: pd.DataFrame,
|
| 49 |
column_ids: list[str] | None = None,
|
|
|
|
| 44 |
return None if math.isnan(f) else f
|
| 45 |
|
| 46 |
|
| 47 |
+
# Prompt-style description read by the Planner to decide WHEN to pick this tool.
|
| 48 |
+
# Final destination is ToolSpec.description once the wrapper layer is built.
|
| 49 |
+
DESCRIPTION = """\
|
| 50 |
+
Summary: Pairwise correlation across numeric columns (pearson, spearman, or \
|
| 51 |
+
kendall). Returns a correlation matrix plus the strongest pairs ranked by \
|
| 52 |
+
absolute strength.
|
| 53 |
+
|
| 54 |
+
USE WHEN the question is about relationship or association between numeric \
|
| 55 |
+
variables. Trigger words: "correlation" (korelasi), "related/relationship" \
|
| 56 |
+
(hubungan/keterkaitan), "does X affect Y", "move together".
|
| 57 |
+
|
| 58 |
+
DON'T USE WHEN:
|
| 59 |
+
- it implies causation — correlation is not causality; stay descriptive
|
| 60 |
+
- it compares two groups of one metric -> analyze_comparison
|
| 61 |
+
- it summarizes a single column -> analyze_descriptive
|
| 62 |
+
|
| 63 |
+
Example questions:
|
| 64 |
+
- "is there a correlation between price and quantity sold?"
|
| 65 |
+
- "which variables are most related to revenue?"
|
| 66 |
+
- "do age and spending move together?"
|
| 67 |
+
- "show the correlation matrix for the numeric columns"
|
| 68 |
+
"""
|
| 69 |
+
|
| 70 |
+
|
| 71 |
def analyze_correlation(
|
| 72 |
df: pd.DataFrame,
|
| 73 |
column_ids: list[str] | None = None,
|
src/tools/analytics/segmentation.py
CHANGED
|
@@ -52,6 +52,31 @@ def _clean(value: object) -> object:
|
|
| 52 |
return value
|
| 53 |
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
def analyze_segment(
|
| 56 |
df: pd.DataFrame,
|
| 57 |
column: str,
|
|
|
|
| 52 |
return value
|
| 53 |
|
| 54 |
|
| 55 |
+
# Prompt-style description read by the Planner to decide WHEN to pick this tool.
|
| 56 |
+
# Final destination is ToolSpec.description once the wrapper layer is built.
|
| 57 |
+
DESCRIPTION = """\
|
| 58 |
+
Summary: Bins a NUMERIC column into segments and counts how rows distribute \
|
| 59 |
+
across them (optionally aggregating another column per segment). Two modes: \
|
| 60 |
+
explicit cut edges (e.g. age 0-18-35-60) or equal-frequency quantile buckets \
|
| 61 |
+
(quartiles, deciles).
|
| 62 |
+
|
| 63 |
+
USE WHEN the question asks to bucket/bracket a continuous number into ranges. \
|
| 64 |
+
Trigger words: "segment" (segmen), "bucket/bracket" (kelompokkan ke rentang), \
|
| 65 |
+
"age groups/tiers" (kelompok umur/tingkatan), "quartiles/deciles", "bins".
|
| 66 |
+
|
| 67 |
+
DON'T USE WHEN:
|
| 68 |
+
- the category already exists (no binning needed) -> analyze_contribution
|
| 69 |
+
- it aggregates by an existing key -> analyze_aggregate
|
| 70 |
+
- it compares two named groups -> analyze_comparison
|
| 71 |
+
|
| 72 |
+
Example questions:
|
| 73 |
+
- "split customers into age brackets 0-18, 18-35, 35-60"
|
| 74 |
+
- "bucket orders into value tiers"
|
| 75 |
+
- "divide users into spending quartiles"
|
| 76 |
+
- "how many customers fall in each income band?"
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
+
|
| 80 |
def analyze_segment(
|
| 81 |
df: pd.DataFrame,
|
| 82 |
column: str,
|
src/tools/analytics/temporal.py
CHANGED
|
@@ -64,6 +64,30 @@ def _period_label(ts: pd.Timestamp, freq: str) -> str:
|
|
| 64 |
return str(ts.strftime("%Y-%m-%d")) # day / week
|
| 65 |
|
| 66 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
def analyze_trend(
|
| 68 |
df: pd.DataFrame,
|
| 69 |
date_column: str,
|
|
|
|
| 64 |
return str(ts.strftime("%Y-%m-%d")) # day / week
|
| 65 |
|
| 66 |
|
| 67 |
+
# Prompt-style description read by the Planner to decide WHEN to pick this tool.
|
| 68 |
+
# Final destination is ToolSpec.description once the wrapper layer is built.
|
| 69 |
+
DESCRIPTION = """\
|
| 70 |
+
Summary: Time-series trend of one metric over evenly-spaced periods (day, week, \
|
| 71 |
+
month, quarter, year). Reports per-period points plus direction, absolute and \
|
| 72 |
+
percent change, and a linear slope.
|
| 73 |
+
|
| 74 |
+
USE WHEN the question is about movement over time — growth, decline, trend, \
|
| 75 |
+
seasonality. Trigger words: "over time" (dari waktu ke waktu), "trend" (tren), \
|
| 76 |
+
"monthly/yearly" (bulanan/tahunan), "growth" (pertumbuhan), "since/last N months".
|
| 77 |
+
|
| 78 |
+
DON'T USE WHEN:
|
| 79 |
+
- it groups by a non-time category -> analyze_aggregate
|
| 80 |
+
- it compares two specific groups (A vs B) -> analyze_comparison
|
| 81 |
+
- it summarizes a column with no time axis -> analyze_descriptive
|
| 82 |
+
|
| 83 |
+
Example questions:
|
| 84 |
+
- "how did monthly revenue change this year?"
|
| 85 |
+
- "show the sales trend over the last 12 months"
|
| 86 |
+
- "is the number of signups growing quarter over quarter?"
|
| 87 |
+
- "yearly profit from 2019 to 2024"
|
| 88 |
+
"""
|
| 89 |
+
|
| 90 |
+
|
| 91 |
def analyze_trend(
|
| 92 |
df: pd.DataFrame,
|
| 93 |
date_column: str,
|