Rifqi Hafizuddin Claude Opus 4.8 commited on
Commit
83ba6b1
·
1 Parent(s): be96601

[KM-567][AI] Planner agent: prompt + few-shot examples

Browse files

- prompt.py: build_planner_prompt assembles per-call human content (business
context + condensed catalog + tool list + constraints + examples + question,
plus prior error on retry).
- config/prompts/planner.md: system prompt encoding INV-1/6/7 and the planning
principles.
- examples.py: two few-shot TaskLists (A exploratory revenue-by-category;
B descriptive monthly-trend-by-region with date_trunc), built from the real
TaskList schema so they cannot drift from the output contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

src/agents/planner/examples.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Few-shot examples for the planner prompt.
2
+
3
+ Two illustrative (question -> TaskList) pairs that teach the OUTPUT SHAPE:
4
+ stages, dependency edges, parallelism, ordered tool-call chains, inline QueryIR,
5
+ and "${t<id>}" placeholders. They reference a hypothetical sales catalog
6
+ (`src_sales` / `t_orders`); these ids are part of the illustration and are not
7
+ validated against the user's real catalog. v1 is descriptive/diagnostic — no
8
+ modeling tasks.
9
+
10
+ See AGENT_ARCHITECTURE_CONTEXT_new.md §7.3 (Examples A and B).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from .schemas import Task, TaskList, ToolCall
16
+
17
+ # --------------------------------------------------------------------------- #
18
+ # Example A — exploratory, no modeling.
19
+ # "Which product categories drove last quarter's revenue?"
20
+ # --------------------------------------------------------------------------- #
21
+
22
+ _EXAMPLE_A = TaskList(
23
+ plan_id="example_a",
24
+ goal_restated="Identify which product categories contributed most to last quarter's revenue.",
25
+ assumptions=["'last quarter' = 2026-01-01 to 2026-03-31."],
26
+ open_questions=[],
27
+ tasks=[
28
+ Task(
29
+ id="t1",
30
+ stage="data_understanding",
31
+ objective="Confirm the sales source exposes category, revenue, and order date.",
32
+ tool_calls=[ToolCall(tool="describe_source", args={"source_id": "src_sales"})],
33
+ expected_output="source_shape",
34
+ success_criteria="describe_source returns the orders table with the 3 columns.",
35
+ depends_on=[],
36
+ parallelizable_with=[],
37
+ estimated_cost="low",
38
+ ),
39
+ Task(
40
+ id="t2",
41
+ stage="evaluation",
42
+ objective="Sum last quarter's revenue per category, ranked high to low.",
43
+ tool_calls=[
44
+ ToolCall(
45
+ tool="query_structured",
46
+ args={
47
+ "ir": {
48
+ "source_id": "src_sales",
49
+ "table_id": "t_orders",
50
+ "select": [
51
+ {"kind": "column", "column_id": "c_category", "alias": "category"},
52
+ {
53
+ "kind": "agg",
54
+ "fn": "sum",
55
+ "column_id": "c_revenue",
56
+ "alias": "revenue",
57
+ },
58
+ ],
59
+ "filters": [
60
+ {
61
+ "column_id": "c_order_date",
62
+ "op": "between",
63
+ "value": ["2026-01-01", "2026-03-31"],
64
+ "value_type": "date",
65
+ }
66
+ ],
67
+ "group_by": ["c_category"],
68
+ "order_by": [{"column_id": "revenue", "dir": "desc"}],
69
+ "limit": 20,
70
+ }
71
+ },
72
+ )
73
+ ],
74
+ expected_output="revenue_by_category",
75
+ success_criteria="Produced a ranked revenue figure per category.",
76
+ depends_on=["t1"],
77
+ parallelizable_with=["t3"],
78
+ estimated_cost="low",
79
+ ),
80
+ Task(
81
+ id="t3",
82
+ stage="evaluation",
83
+ objective="Get total last-quarter revenue to contextualize each category's share.",
84
+ tool_calls=[
85
+ ToolCall(
86
+ tool="query_structured",
87
+ args={
88
+ "ir": {
89
+ "source_id": "src_sales",
90
+ "table_id": "t_orders",
91
+ "select": [
92
+ {
93
+ "kind": "agg",
94
+ "fn": "sum",
95
+ "column_id": "c_revenue",
96
+ "alias": "total_revenue",
97
+ }
98
+ ],
99
+ "filters": [
100
+ {
101
+ "column_id": "c_order_date",
102
+ "op": "between",
103
+ "value": ["2026-01-01", "2026-03-31"],
104
+ "value_type": "date",
105
+ }
106
+ ],
107
+ }
108
+ },
109
+ )
110
+ ],
111
+ expected_output="total_revenue",
112
+ success_criteria="Produced a single total revenue figure for the quarter.",
113
+ depends_on=["t1"],
114
+ parallelizable_with=["t2"],
115
+ estimated_cost="low",
116
+ ),
117
+ ],
118
+ )
119
+
120
+ # --------------------------------------------------------------------------- #
121
+ # Example B — descriptive / trend.
122
+ # "How has monthly revenue trended by region this year, and what's unusual?"
123
+ # --------------------------------------------------------------------------- #
124
+
125
+ _EXAMPLE_B = TaskList(
126
+ plan_id="example_b",
127
+ goal_restated="Describe this year's monthly revenue trend and flag unusual months.",
128
+ assumptions=["'this year' starts 2026-01-01."],
129
+ open_questions=["'Unusual' is interpreted as months far from the typical monthly revenue."],
130
+ tasks=[
131
+ Task(
132
+ id="t1",
133
+ stage="data_understanding",
134
+ objective="Confirm the sales source exposes order date, revenue, and region.",
135
+ tool_calls=[ToolCall(tool="describe_source", args={"source_id": "src_sales"})],
136
+ expected_output="source_shape",
137
+ success_criteria="describe_source returns the orders table with the needed columns.",
138
+ depends_on=[],
139
+ parallelizable_with=[],
140
+ estimated_cost="low",
141
+ ),
142
+ Task(
143
+ id="t2",
144
+ stage="data_preparation",
145
+ objective="Pull this year's order dates, revenue, and region.",
146
+ tool_calls=[
147
+ ToolCall(
148
+ tool="query_structured",
149
+ args={
150
+ "ir": {
151
+ "source_id": "src_sales",
152
+ "table_id": "t_orders",
153
+ "select": [
154
+ {
155
+ "kind": "column",
156
+ "column_id": "c_order_date",
157
+ "alias": "order_date",
158
+ },
159
+ {"kind": "column", "column_id": "c_revenue", "alias": "revenue"},
160
+ {"kind": "column", "column_id": "c_region", "alias": "region"},
161
+ ],
162
+ "filters": [
163
+ {
164
+ "column_id": "c_order_date",
165
+ "op": ">=",
166
+ "value": "2026-01-01",
167
+ "value_type": "date",
168
+ }
169
+ ],
170
+ "limit": 10000,
171
+ }
172
+ },
173
+ )
174
+ ],
175
+ expected_output="ytd_rows",
176
+ success_criteria="Produced this year's order-level rows with date, revenue, region.",
177
+ depends_on=["t1"],
178
+ parallelizable_with=[],
179
+ estimated_cost="medium",
180
+ ),
181
+ Task(
182
+ id="t3",
183
+ stage="evaluation",
184
+ objective="Bucket the order dates into months to form the monthly trend.",
185
+ tool_calls=[
186
+ ToolCall(
187
+ tool="date_trunc",
188
+ args={"values": "${t2}", "granularity": "month"},
189
+ )
190
+ ],
191
+ expected_output="monthly_series",
192
+ success_criteria="Produced a per-month revenue series.",
193
+ depends_on=["t2"],
194
+ parallelizable_with=[],
195
+ estimated_cost="low",
196
+ ),
197
+ Task(
198
+ id="t4",
199
+ stage="evaluation",
200
+ objective="Quantify month-to-month spread to flag unusual months.",
201
+ tool_calls=[ToolCall(tool="compute_stddev", args={"values": "${t3}"})],
202
+ expected_output="monthly_volatility",
203
+ success_criteria="Produced a stddev figure that flags months above the typical spread.",
204
+ depends_on=["t3"],
205
+ parallelizable_with=[],
206
+ estimated_cost="low",
207
+ ),
208
+ ],
209
+ )
210
+
211
+
212
+ EXAMPLES: list[tuple[str, TaskList]] = [
213
+ ("Which product categories drove last quarter's revenue?", _EXAMPLE_A),
214
+ ("How has monthly revenue trended by region this year, and what's unusual?", _EXAMPLE_B),
215
+ ]
216
+
217
+
218
+ def render_examples() -> str:
219
+ """Render the few-shots as text for the planner prompt."""
220
+ blocks: list[str] = []
221
+ for i, (question, plan) in enumerate(EXAMPLES, start=1):
222
+ blocks.append(
223
+ f"## Example {i}\n\n"
224
+ f"Question:\n{question}\n\n"
225
+ f"TaskList:\n{plan.model_dump_json(indent=2)}"
226
+ )
227
+ return "\n\n".join(blocks)
src/agents/planner/prompt.py ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Builds the planner LLM human-message content.
2
+
3
+ The system prompt (`config/prompts/planner.md`) carries the role, invariants,
4
+ and planning principles. This module assembles the per-call human content:
5
+ business context + condensed catalog + available tools + constraints + the
6
+ few-shot examples + the question (+ the prior error on retry).
7
+
8
+ Few-shot examples are rendered from `examples.py` (which builds them from the
9
+ real `TaskList` schema) so they cannot drift from the output contract.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from .contracts import BusinessContext, ToolRegistry
15
+ from .examples import render_examples
16
+ from .inputs import CatalogSummary, Constraints
17
+
18
+
19
+ def render_business_context(context: BusinessContext) -> str:
20
+ lines = [
21
+ f"Project: {context.project_id} (industry: {context.industry}, "
22
+ f"context completeness: {context.completeness})",
23
+ f"Business: {context.business_description}",
24
+ f"Scale & scope: {context.scale_and_scope}",
25
+ ]
26
+ if context.key_terms:
27
+ lines.append("Key terms:")
28
+ lines.extend(f" - {kt.term}: {kt.meaning}" for kt in context.key_terms)
29
+ if context.data_overview:
30
+ lines.append("Data overview:")
31
+ lines.extend(
32
+ f" - {n.table_name}: {n.row_represents}" for n in context.data_overview
33
+ )
34
+ if context.data_column_notes:
35
+ lines.append("Column notes:")
36
+ lines.extend(
37
+ f" - {n.column_name}: {n.meaning}" for n in context.data_column_notes
38
+ )
39
+ if context.whats_normal:
40
+ lines.append(f"What's normal: {context.whats_normal}")
41
+ if context.recent_events:
42
+ lines.append(f"Recent events: {context.recent_events}")
43
+ if context.things_to_watch_for:
44
+ lines.append(f"Things to watch for: {context.things_to_watch_for}")
45
+ if context.open_questions:
46
+ lines.append("Known open questions:")
47
+ lines.extend(f" - {q}" for q in context.open_questions)
48
+ return "\n".join(lines)
49
+
50
+
51
+ def render_registry(tools: ToolRegistry) -> str:
52
+ if not tools.tools:
53
+ return "(no tools available)"
54
+ blocks: list[str] = []
55
+ for spec in tools.tools:
56
+ required = spec.input_schema.get("required", [])
57
+ blocks.append(
58
+ f"- {spec.name} (category: {spec.category}, returns: {spec.output_kind})\n"
59
+ f" required args: {required}\n"
60
+ f" {spec.description}"
61
+ )
62
+ return "\n".join(blocks)
63
+
64
+
65
+ def render_constraints(constraints: Constraints) -> str:
66
+ lines = [
67
+ f"- max_tasks: {constraints.max_tasks}",
68
+ f"- modeling_allowed: {constraints.modeling_allowed} "
69
+ "(no modeling tools exist in v1 — do not emit modeling tasks)",
70
+ f"- row_budget: {constraints.row_budget}",
71
+ ]
72
+ if constraints.token_budget is not None:
73
+ lines.append(f"- token_budget: {constraints.token_budget}")
74
+ if constraints.time_budget_seconds is not None:
75
+ lines.append(f"- time_budget_seconds: {constraints.time_budget_seconds}")
76
+ return "\n".join(lines)
77
+
78
+
79
+ def build_planner_prompt(
80
+ context: BusinessContext,
81
+ catalog: CatalogSummary,
82
+ tools: ToolRegistry,
83
+ query: str,
84
+ constraints: Constraints,
85
+ previous_error: str | None = None,
86
+ ) -> str:
87
+ """Return the human-message content for the planner LLM.
88
+
89
+ The system prompt (`config/prompts/planner.md`) is loaded separately by
90
+ `PlannerService`.
91
+ """
92
+ sections = [
93
+ f"# Business context\n\n{render_business_context(context)}",
94
+ f"# Catalog\n\n{catalog.render()}",
95
+ f"# Available tools\n\n{render_registry(tools)}",
96
+ f"# Constraints\n\n{render_constraints(constraints)}",
97
+ f"# Examples\n\n{render_examples()}",
98
+ f"# Question\n\n{query}",
99
+ ]
100
+ if previous_error:
101
+ sections.append(
102
+ "# Previous attempt failed validation\n\n"
103
+ f"{previous_error}\n\n"
104
+ "Emit a corrected TaskList. Do not repeat the same mistake."
105
+ )
106
+ return "\n\n".join(sections)
src/config/prompts/planner.md ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ You are the Planner for Data Eyond, an AI data scientist that works within the
2
+ CRISP-DM lifecycle. Your single job is to turn a business question into a
3
+ **static analysis plan**: one `TaskList` that downstream deterministic code
4
+ executes exactly as written.
5
+
6
+ You plan. You do not execute, and you do not write prose for the user. You emit
7
+ only a `TaskList` object that conforms to the provided schema.
8
+
9
+ # Hard rules (non-negotiable)
10
+
11
+ 1. **Emit intent, never code.** Never write SQL, pandas, or any code. The only
12
+ query you express is an inline `QueryIR` (a JSON intent object) inside a
13
+ `query_structured` tool call's `args.ir`.
14
+ 2. **The plan is static.** There is no replanning and no execution feedback. Plan
15
+ the whole analysis up front; assume each task runs once, in dependency order.
16
+ 3. **Use only tools from the "Available tools" list.** Never invent a tool name.
17
+ Every `tool_calls[].tool` must be one of the listed tool names.
18
+ 4. **Reference only data that exists.** Every `source_id`, `table_id`, and
19
+ `column_id` you put in an inline `QueryIR` must come from the "Catalog"
20
+ section. Copy the stable ids verbatim — downstream validation does a literal
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
+
27
+ - **Smallest plan that answers the question.** Do not exceed `Constraints.max_tasks`.
28
+ - **A task is an ordered chain of tool calls** with fully-specified arguments. A
29
+ simple question is one task with one tool call; a step that needs a follow-up
30
+ computation is a short chain or a dependent task.
31
+ - **Wire data between tasks with placeholders.** When a task needs an upstream
32
+ task's output as an argument, use the string `"${t<id>}"` (e.g. `"${t2}"`) as
33
+ the argument value. Set `depends_on` accordingly.
34
+ - **Built-in aggregation vs compute_* tools.** Use `query_structured` for
35
+ count/sum/avg/min/max/count_distinct, filtering, and grouping. For statistics
36
+ the IR cannot express (median, percentile, mode, standard deviation), run
37
+ `query_structured` to fetch the series, then a `compute_*` tool on its output.
38
+ - **Mixing structured + unstructured.** If qualitative context helps, add a
39
+ `retrieve_documents` task against an unstructured source listed in the catalog.
40
+ - **Parallelism.** List sibling tasks that have no data dependency on each other
41
+ in `parallelizable_with` (must be mutually consistent with `depends_on`).
42
+ - **CRISP-DM stages.** Tag each task with the stage it serves:
43
+ `data_understanding`, `data_preparation`, or `evaluation`. (Never `modeling`.)
44
+ - **success_criteria is a reporting signal**, not a control trigger. State, in
45
+ checkable terms (counts, rates, "produced", "above"/"below"), what a good
46
+ result looks like. It never causes a retry.
47
+ - **Surface uncertainty, don't guess.** If the question is ambiguous or the
48
+ catalog can't fully answer it, record it in `open_questions` and plan the best
49
+ defensible analysis anyway. Record interpretation choices in `assumptions`.
50
+
51
+ # Output
52
+
53
+ Return exactly one `TaskList`. The "Examples" section in the human message shows
54
+ the required shape. Match it.