@pgsage/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +126 -0
  2. package/dist/corrector/index.d.ts +72 -0
  3. package/dist/corrector/index.d.ts.map +1 -0
  4. package/dist/corrector/index.js +113 -0
  5. package/dist/corrector/index.js.map +1 -0
  6. package/dist/db/index.d.ts +3 -0
  7. package/dist/db/index.d.ts.map +1 -0
  8. package/dist/db/index.js +2 -0
  9. package/dist/db/index.js.map +1 -0
  10. package/dist/db/pool.d.ts +46 -0
  11. package/dist/db/pool.d.ts.map +1 -0
  12. package/dist/db/pool.js +46 -0
  13. package/dist/db/pool.js.map +1 -0
  14. package/dist/embeddings/index.d.ts +4 -0
  15. package/dist/embeddings/index.d.ts.map +1 -0
  16. package/dist/embeddings/index.js +2 -0
  17. package/dist/embeddings/index.js.map +1 -0
  18. package/dist/embeddings/types.d.ts +39 -0
  19. package/dist/embeddings/types.d.ts.map +1 -0
  20. package/dist/embeddings/types.js +16 -0
  21. package/dist/embeddings/types.js.map +1 -0
  22. package/dist/embeddings/voyage.d.ts +70 -0
  23. package/dist/embeddings/voyage.d.ts.map +1 -0
  24. package/dist/embeddings/voyage.js +163 -0
  25. package/dist/embeddings/voyage.js.map +1 -0
  26. package/dist/estimator/index.d.ts +53 -0
  27. package/dist/estimator/index.d.ts.map +1 -0
  28. package/dist/estimator/index.js +57 -0
  29. package/dist/estimator/index.js.map +1 -0
  30. package/dist/executor/index.d.ts +56 -0
  31. package/dist/executor/index.d.ts.map +1 -0
  32. package/dist/executor/index.js +86 -0
  33. package/dist/executor/index.js.map +1 -0
  34. package/dist/explainer/index.d.ts +39 -0
  35. package/dist/explainer/index.d.ts.map +1 -0
  36. package/dist/explainer/index.js +79 -0
  37. package/dist/explainer/index.js.map +1 -0
  38. package/dist/explainer/prompt.d.ts +37 -0
  39. package/dist/explainer/prompt.d.ts.map +1 -0
  40. package/dist/explainer/prompt.js +102 -0
  41. package/dist/explainer/prompt.js.map +1 -0
  42. package/dist/index.d.ts +19 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +24 -0
  45. package/dist/index.js.map +1 -0
  46. package/dist/introspector/index.d.ts +127 -0
  47. package/dist/introspector/index.d.ts.map +1 -0
  48. package/dist/introspector/index.js +460 -0
  49. package/dist/introspector/index.js.map +1 -0
  50. package/dist/orchestrator/index.d.ts +113 -0
  51. package/dist/orchestrator/index.d.ts.map +1 -0
  52. package/dist/orchestrator/index.js +126 -0
  53. package/dist/orchestrator/index.js.map +1 -0
  54. package/dist/planner/index.d.ts +83 -0
  55. package/dist/planner/index.d.ts.map +1 -0
  56. package/dist/planner/index.js +67 -0
  57. package/dist/planner/index.js.map +1 -0
  58. package/dist/planner/prompt.d.ts +37 -0
  59. package/dist/planner/prompt.d.ts.map +1 -0
  60. package/dist/planner/prompt.js +436 -0
  61. package/dist/planner/prompt.js.map +1 -0
  62. package/dist/retriever/index.d.ts +90 -0
  63. package/dist/retriever/index.d.ts.map +1 -0
  64. package/dist/retriever/index.js +164 -0
  65. package/dist/retriever/index.js.map +1 -0
  66. package/dist/validator/index.d.ts +26 -0
  67. package/dist/validator/index.d.ts.map +1 -0
  68. package/dist/validator/index.js +205 -0
  69. package/dist/validator/index.js.map +1 -0
  70. package/package.json +63 -0
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Orchestrator
3
+ *
4
+ * Ties retriever → planner → validator → estimator → executor → corrector
5
+ * into a single agent loop and exposes the SDK's primary entrypoint:
6
+ * `agent.query(question)`.
7
+ *
8
+ * Each stage is composable — callers can import individual stage factories
9
+ * directly if they need finer control. The orchestrator just wires them up
10
+ * with sensible defaults.
11
+ */
12
+ import { retrieve } from "../retriever/index.js";
13
+ import { createPlanner } from "../planner/index.js";
14
+ import { validate } from "../validator/index.js";
15
+ import { createEstimator } from "../estimator/index.js";
16
+ import { createExecutor } from "../executor/index.js";
17
+ import { createCorrector } from "../corrector/index.js";
18
+ import { createExplainer } from "../explainer/index.js";
19
+ // ---------------------------------------------------------------------------
20
+ // Factory
21
+ // ---------------------------------------------------------------------------
22
+ /**
23
+ * Create the pgsage agent. All stage instances are created once and reused
24
+ * across query() calls.
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const agent = createAgent({
29
+ * pool,
30
+ * readonlyPool,
31
+ * embedder: new VoyageEmbeddingProvider({ apiKey: process.env.VOYAGE_API_KEY }),
32
+ * });
33
+ * const response = await agent.query("What is the median household income in RI?");
34
+ * ```
35
+ */
36
+ export function createAgent(config) {
37
+ const topK = config.topK ?? 10;
38
+ const planner = createPlanner({ model: config.model });
39
+ const estimator = createEstimator({
40
+ pool: config.pool,
41
+ costThreshold: config.costThreshold,
42
+ rowThreshold: config.rowThreshold,
43
+ });
44
+ const executor = createExecutor({
45
+ pool: config.readonlyPool,
46
+ rowLimit: config.rowLimit,
47
+ statementTimeout: config.statementTimeout,
48
+ });
49
+ const corrector = createCorrector({
50
+ planner,
51
+ maxRetries: config.maxRetries,
52
+ });
53
+ const explainer = createExplainer({
54
+ model: config.explainerModel ?? config.model,
55
+ });
56
+ return {
57
+ async query(question, options = {}) {
58
+ const progress = options.onProgress ?? (() => undefined);
59
+ // Step 0: Retrieve relevant schema chunks via pgvector similarity search
60
+ progress(0, "Searching schema\u2026");
61
+ const context = await retrieve(config.pool, question, config.embedder, { topK });
62
+ // Step 1: Generate the initial SQL plan
63
+ progress(1, "Generating SQL\u2026");
64
+ const initialPlan = await planner.plan(question, context);
65
+ // Step 2: Emit before the first execute attempt
66
+ progress(2, "Validating & executing\u2026");
67
+ // Build the execute pipeline: validate → estimate → execute
68
+ // (Used by both the corrector and the final execution)
69
+ const executePipeline = async (sql) => {
70
+ // 2a. AST safety validation
71
+ const validation = validate(sql);
72
+ if (!validation.valid) {
73
+ throw new Error(`Validation failed: ${validation.reason ?? "unknown reason"}`);
74
+ }
75
+ // 2b. Cost estimation (reject if too expensive)
76
+ const estimate = await estimator.estimate(sql);
77
+ if (estimate.exceedsThreshold) {
78
+ throw new Error(`Cost check failed: ${estimate.thresholdReason ?? "exceeds threshold"}`);
79
+ }
80
+ // 2c. Execute on the read-only pool
81
+ return executor.execute(sql);
82
+ };
83
+ // Step 3 (conditional): Run through the corrector
84
+ // The corrector emits step 3 ("Correcting query…") only when it retries.
85
+ let correctorAttempt = 0;
86
+ const correction = await corrector.correct(question, context, initialPlan, async (sql) => {
87
+ // Emit "Correcting" on second+ attempts (first attempt is covered by step 2 above)
88
+ if (correctorAttempt > 0) {
89
+ progress(3, "Correcting query\u2026");
90
+ }
91
+ correctorAttempt++;
92
+ return executePipeline(sql);
93
+ });
94
+ if (correction.exhausted) {
95
+ throw new Error(`pgsage could not produce a valid query after ${config.maxRetries ?? 3} retries. ` +
96
+ `Last error: ${correction.attempts.at(-1)?.error ?? "unknown"}`);
97
+ }
98
+ // Final execution: re-run the winning plan to get the result rows
99
+ const finalResult = await executePipeline(correction.plan.sql);
100
+ // Step 4: Generate a natural-language answer via the explainer LLM
101
+ progress(4, "Summarizing results\u2026");
102
+ const answer = await explainer.explain({
103
+ question,
104
+ sql: correction.plan.sql,
105
+ assumptions: correction.plan.assumptions,
106
+ displayHint: correction.plan.displayHint,
107
+ fields: finalResult.fields,
108
+ rows: finalResult.rows,
109
+ rowCount: finalResult.rowCount,
110
+ truncated: finalResult.truncated,
111
+ });
112
+ return {
113
+ answer,
114
+ sql: correction.plan.sql,
115
+ assumptions: correction.plan.assumptions,
116
+ displayHint: correction.plan.displayHint,
117
+ rows: finalResult.rows,
118
+ rowCount: finalResult.rowCount,
119
+ fields: finalResult.fields,
120
+ truncated: finalResult.truncated,
121
+ attempts: correction.attempts,
122
+ };
123
+ },
124
+ };
125
+ }
126
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/orchestrator/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAIH,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AA4FxD,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,WAAW,CAAC,MAA0B;IACpD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;IAE/B,MAAM,OAAO,GAAG,aAAa,CAAC,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IAEvD,MAAM,SAAS,GAAG,eAAe,CAAC;QAChC,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,YAAY,EAAE,MAAM,CAAC,YAAY;KAClC,CAAC,CAAC;IAEH,MAAM,QAAQ,GAAG,cAAc,CAAC;QAC9B,IAAI,EAAE,MAAM,CAAC,YAAY;QACzB,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;KAC1C,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,eAAe,CAAC;QAChC,OAAO;QACP,UAAU,EAAE,MAAM,CAAC,UAAU;KAC9B,CAAC,CAAC;IAEH,MAAM,SAAS,GAAG,eAAe,CAAC;QAChC,KAAK,EAAE,MAAM,CAAC,cAAc,IAAI,MAAM,CAAC,KAAK;KAC7C,CAAC,CAAC;IAEH,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,QAAgB,EAAE,UAAwB,EAAE;YACtD,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YAEzD,yEAAyE;YACzE,QAAQ,CAAC,CAAC,EAAE,wBAAwB,CAAC,CAAC;YACtC,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;YAEjF,wCAAwC;YACxC,QAAQ,CAAC,CAAC,EAAE,sBAAsB,CAAC,CAAC;YACpC,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;YAE1D,gDAAgD;YAChD,QAAQ,CAAC,CAAC,EAAE,8BAA8B,CAAC,CAAC;YAE5C,4DAA4D;YAC5D,uDAAuD;YACvD,MAAM,eAAe,GAAG,KAAK,EAAE,GAAW,EAA4B,EAAE;gBACtE,4BAA4B;gBAC5B,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;gBACjC,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;oBACtB,MAAM,IAAI,KAAK,CAAC,sBAAsB,UAAU,CAAC,MAAM,IAAI,gBAAgB,EAAE,CAAC,CAAC;gBACjF,CAAC;gBAED,gDAAgD;gBAChD,MAAM,QAAQ,GAAG,MAAM,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;gBAC/C,IAAI,QAAQ,CAAC,gBAAgB,EAAE,CAAC;oBAC9B,MAAM,IAAI,KAAK,CAAC,sBAAsB,QAAQ,CAAC,eAAe,IAAI,mBAAmB,EAAE,CAAC,CAAC;gBAC3F,CAAC;gBAED,oCAAoC;gBACpC,OAAO,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAC/B,CAAC,CAAC;YAEF,kDAAkD;YAClD,yEAAyE;YACzE,IAAI,gBAAgB,GAAG,CAAC,CAAC;YACzB,MAAM,UAAU,GAAG,MAAM,SAAS,CAAC,OAAO,CACxC,QAAQ,EACR,OAAO,EACP,WAAW,EACX,KAAK,EAAE,GAAW,EAAE,EAAE;gBACpB,mFAAmF;gBACnF,IAAI,gBAAgB,GAAG,CAAC,EAAE,CAAC;oBACzB,QAAQ,CAAC,CAAC,EAAE,wBAAwB,CAAC,CAAC;gBACxC,CAAC;gBACD,gBAAgB,EAAE,CAAC;gBACnB,OAAO,eAAe,CAAC,GAAG,CAAC,CAAC;YAC9B,CAAC,CACF,CAAC;YAEF,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;gBACzB,MAAM,IAAI,KAAK,CACb,gDAAgD,MAAM,CAAC,UAAU,IAAI,CAAC,YAAY;oBAChF,eAAe,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,SAAS,EAAE,CAClE,CAAC;YACJ,CAAC;YAED,kEAAkE;YAClE,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAE/D,mEAAmE;YACnE,QAAQ,CAAC,CAAC,EAAE,2BAA2B,CAAC,CAAC;YACzC,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC;gBACrC,QAAQ;gBACR,GAAG,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG;gBACxB,WAAW,EAAE,UAAU,CAAC,IAAI,CAAC,WAAW;gBACxC,WAAW,EAAE,UAAU,CAAC,IAAI,CAAC,WAAW;gBACxC,MAAM,EAAE,WAAW,CAAC,MAAM;gBAC1B,IAAI,EAAE,WAAW,CAAC,IAAI;gBACtB,QAAQ,EAAE,WAAW,CAAC,QAAQ;gBAC9B,SAAS,EAAE,WAAW,CAAC,SAAS;aACjC,CAAC,CAAC;YAEH,OAAO;gBACL,MAAM;gBACN,GAAG,EAAE,UAAU,CAAC,IAAI,CAAC,GAAG;gBACxB,WAAW,EAAE,UAAU,CAAC,IAAI,CAAC,WAAW;gBACxC,WAAW,EAAE,UAAU,CAAC,IAAI,CAAC,WAAW;gBACxC,IAAI,EAAE,WAAW,CAAC,IAAI;gBACtB,QAAQ,EAAE,WAAW,CAAC,QAAQ;gBAC9B,MAAM,EAAE,WAAW,CAAC,MAAM;gBAC1B,SAAS,EAAE,WAAW,CAAC,SAAS;gBAChC,QAAQ,EAAE,UAAU,CAAC,QAAQ;aAC9B,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Planner
3
+ *
4
+ * Calls Claude (via Mastra Agent) with the retrieved schema context and
5
+ * few-shot examples to generate a SQL query plus stated assumptions about
6
+ * ambiguous terms.
7
+ *
8
+ * Usage:
9
+ * const planner = createPlanner({ model: "anthropic/claude-sonnet-4-6" });
10
+ * const result = await planner.plan(question, retrievedChunks);
11
+ * // result.sql — the generated SELECT query
12
+ * // result.assumptions — list of interpretive assumptions
13
+ */
14
+ import type { RetrievedSchemaChunk } from "../retriever/index.js";
15
+ import { type FewShotExample } from "./prompt.js";
16
+ /** Display hint values — matches the four visualization types in the web UI. */
17
+ export type DisplayHint = "big" | "statrow" | "bar" | "grouped" | "table";
18
+ export interface PlanResult {
19
+ /** The generated PostgreSQL SELECT query. Empty string if no query could be produced. */
20
+ sql: string;
21
+ /** Plain-English assumptions made about ambiguous terms or requirements. */
22
+ assumptions: string[];
23
+ /**
24
+ * Suggested UI visualization layout for the result.
25
+ * - `"big"` — single scalar answer (one row, one numeric value)
26
+ * - `"statrow"` — 2–4 related metrics about the same subject
27
+ * - `"bar"` — ranked list of one metric across many geographies
28
+ * - `"grouped"` — two-series comparison across geographies
29
+ * - `"table"` — default fallback for general tabular data
30
+ */
31
+ displayHint: DisplayHint;
32
+ }
33
+ export interface PlannerConfig {
34
+ /**
35
+ * Mastra model router string.
36
+ * @default "anthropic/claude-sonnet-4-6"
37
+ */
38
+ model?: string;
39
+ /**
40
+ * Override the default few-shot examples sent with every plan call.
41
+ * Pass an empty array to disable examples.
42
+ */
43
+ examples?: FewShotExample[];
44
+ }
45
+ /** One prior attempt that failed, used by the corrector to re-plan. */
46
+ export interface PriorAttempt {
47
+ /** The SQL that was generated and failed (or returned suspicious results). */
48
+ sql: string;
49
+ /** The error message or zero-row explanation from the executor/corrector. */
50
+ error: string;
51
+ }
52
+ export interface PlanOptions {
53
+ /**
54
+ * Per-call few-shot example override. Falls back to the config-level
55
+ * examples, then the built-in Census examples.
56
+ */
57
+ examples?: FewShotExample[];
58
+ /**
59
+ * Prior failed attempts to include in the prompt so the model can learn
60
+ * from its mistakes. Populated by the corrector on retry calls.
61
+ */
62
+ errorContext?: PriorAttempt[];
63
+ }
64
+ export interface Planner {
65
+ /**
66
+ * Generate a SQL query and assumptions for a natural-language question.
67
+ *
68
+ * @param question The user's natural-language question.
69
+ * @param context Schema chunks retrieved from pgvector similarity search.
70
+ * @param options Optional per-call overrides.
71
+ */
72
+ plan(question: string, context: RetrievedSchemaChunk[], options?: PlanOptions): Promise<PlanResult>;
73
+ }
74
+ /**
75
+ * Creates a reusable Planner that wraps a Mastra Agent.
76
+ * The Agent is instantiated once and reused across all plan() calls.
77
+ *
78
+ * Requires the ANTHROPIC_API_KEY environment variable to be set.
79
+ */
80
+ export declare function createPlanner(config?: PlannerConfig): Planner;
81
+ export { SYSTEM_PROMPT, DEFAULT_FEW_SHOT_EXAMPLES, formatSchemaContext, buildUserMessage } from "./prompt.js";
82
+ export type { FewShotExample } from "./prompt.js";
83
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/planner/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAIH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAClE,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AAMrB,gFAAgF;AAChF,MAAM,MAAM,WAAW,GAAG,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,CAAC;AAE1E,MAAM,WAAW,UAAU;IACzB,yFAAyF;IACzF,GAAG,EAAE,MAAM,CAAC;IACZ,4EAA4E;IAC5E,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB;;;;;;;OAOG;IACH,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;CAC7B;AAED,uEAAuE;AACvE,MAAM,WAAW,YAAY;IAC3B,8EAA8E;IAC9E,GAAG,EAAE,MAAM,CAAC;IACZ,6EAA6E;IAC7E,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,WAAW;IAC1B;;;OAGG;IACH,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAC;IAC5B;;;OAGG;IACH,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,OAAO;IACtB;;;;;;OAMG;IACH,IAAI,CACF,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,oBAAoB,EAAE,EAC/B,OAAO,CAAC,EAAE,WAAW,GACpB,OAAO,CAAC,UAAU,CAAC,CAAC;CACxB;AA2BD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,GAAE,aAAkB,GAAG,OAAO,CA2BjE;AAMD,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAC9G,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Planner
3
+ *
4
+ * Calls Claude (via Mastra Agent) with the retrieved schema context and
5
+ * few-shot examples to generate a SQL query plus stated assumptions about
6
+ * ambiguous terms.
7
+ *
8
+ * Usage:
9
+ * const planner = createPlanner({ model: "anthropic/claude-sonnet-4-6" });
10
+ * const result = await planner.plan(question, retrievedChunks);
11
+ * // result.sql — the generated SELECT query
12
+ * // result.assumptions — list of interpretive assumptions
13
+ */
14
+ import { Agent } from "@mastra/core/agent";
15
+ import { z } from "zod";
16
+ import { SYSTEM_PROMPT, DEFAULT_FEW_SHOT_EXAMPLES, buildUserMessage, } from "./prompt.js";
17
+ // ---------------------------------------------------------------------------
18
+ // Zod schema for structured output
19
+ // ---------------------------------------------------------------------------
20
+ const planResultSchema = z.object({
21
+ sql: z.string().describe("A valid PostgreSQL SELECT query, or empty string if none can be produced"),
22
+ assumptions: z
23
+ .array(z.string())
24
+ .describe("Plain-English assumptions about ambiguous terms or interpretations"),
25
+ displayHint: z
26
+ .enum(["big", "statrow", "bar", "grouped", "table"])
27
+ .describe("Suggested UI visualization layout. " +
28
+ "Use 'big' for a single scalar answer (one row, one primary numeric value, e.g. a county's median income). " +
29
+ "Use 'statrow' for 2-4 related metrics about the same subject (e.g. a housing snapshot with home value, rent, and homeownership rate). " +
30
+ "Use 'bar' for a ranked list of one metric across many geographies (e.g. top 10 counties by income). " +
31
+ "Use 'grouped' for two-series comparisons across geographies (e.g. rent in 2020 vs 2024 by state). " +
32
+ "Use 'table' as the default fallback for general tabular data that does not fit the above."),
33
+ });
34
+ // ---------------------------------------------------------------------------
35
+ // Factory
36
+ // ---------------------------------------------------------------------------
37
+ /**
38
+ * Creates a reusable Planner that wraps a Mastra Agent.
39
+ * The Agent is instantiated once and reused across all plan() calls.
40
+ *
41
+ * Requires the ANTHROPIC_API_KEY environment variable to be set.
42
+ */
43
+ export function createPlanner(config = {}) {
44
+ const modelId = config.model ?? "anthropic/claude-sonnet-4-6";
45
+ const configExamples = config.examples ?? DEFAULT_FEW_SHOT_EXAMPLES;
46
+ const agent = new Agent({
47
+ id: "pgsage-planner",
48
+ name: "pgsage-planner",
49
+ instructions: SYSTEM_PROMPT,
50
+ model: modelId,
51
+ });
52
+ return {
53
+ async plan(question, context, options = {}) {
54
+ const examples = options.examples ?? configExamples;
55
+ const userMessage = buildUserMessage(question, context, examples, options.errorContext);
56
+ const result = await agent.generate(userMessage, {
57
+ structuredOutput: { schema: planResultSchema },
58
+ });
59
+ return result.object;
60
+ },
61
+ };
62
+ }
63
+ // ---------------------------------------------------------------------------
64
+ // Convenience re-exports
65
+ // ---------------------------------------------------------------------------
66
+ export { SYSTEM_PROMPT, DEFAULT_FEW_SHOT_EXAMPLES, formatSchemaContext, buildUserMessage } from "./prompt.js";
67
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/planner/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EACL,aAAa,EACb,yBAAyB,EACzB,gBAAgB,GAEjB,MAAM,aAAa,CAAC;AA0ErB,8EAA8E;AAC9E,mCAAmC;AACnC,8EAA8E;AAE9E,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IAChC,GAAG,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,0EAA0E,CAAC;IACpG,WAAW,EAAE,CAAC;SACX,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACjB,QAAQ,CAAC,oEAAoE,CAAC;IACjF,WAAW,EAAE,CAAC;SACX,IAAI,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;SACnD,QAAQ,CACP,qCAAqC;QACnC,4GAA4G;QAC5G,wIAAwI;QACxI,sGAAsG;QACtG,oGAAoG;QACpG,2FAA2F,CAC9F;CACJ,CAAC,CAAC;AAEH,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;;;;GAKG;AACH,MAAM,UAAU,aAAa,CAAC,SAAwB,EAAE;IACtD,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,IAAI,6BAA6B,CAAC;IAC9D,MAAM,cAAc,GAAG,MAAM,CAAC,QAAQ,IAAI,yBAAyB,CAAC;IAEpE,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC;QACtB,EAAE,EAAE,gBAAgB;QACpB,IAAI,EAAE,gBAAgB;QACtB,YAAY,EAAE,aAAa;QAC3B,KAAK,EAAE,OAAO;KACf,CAAC,CAAC;IAEH,OAAO;QACL,KAAK,CAAC,IAAI,CACR,QAAgB,EAChB,OAA+B,EAC/B,UAAuB,EAAE;YAEzB,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,cAAc,CAAC;YACpD,MAAM,WAAW,GAAG,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;YAExF,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,QAAQ,CAAC,WAAW,EAAE;gBAC/C,gBAAgB,EAAE,EAAE,MAAM,EAAE,gBAAgB,EAAE;aAC/C,CAAC,CAAC;YAEH,OAAO,MAAM,CAAC,MAAM,CAAC;QACvB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,yBAAyB;AACzB,8EAA8E;AAE9E,OAAO,EAAE,aAAa,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Planner prompt utilities
3
+ *
4
+ * Contains:
5
+ * - SYSTEM_PROMPT: the base system prompt for the SQL planner
6
+ * - FEW_SHOT_EXAMPLES: representative Q→SQL pairs for the Census schema
7
+ * - formatSchemaContext(): converts RetrievedSchemaChunk[] into a compact,
8
+ * LLM-readable schema block
9
+ */
10
+ import type { RetrievedSchemaChunk } from "../retriever/index.js";
11
+ export declare const SYSTEM_PROMPT = "You are an expert PostgreSQL query writer for a schema-aware SQL agent.\n\n## Your task\nGiven a natural-language question and the relevant schema context, produce:\n1. A single, valid PostgreSQL SELECT query that answers the question.\n2. A list of assumptions you made about any ambiguous terms or requirements.\n\n## Rules\n- Output ONLY a SELECT statement. Never produce INSERT, UPDATE, DELETE, DROP, ALTER, TRUNCATE, CREATE, GRANT, REVOKE, COPY, or any other non-SELECT statement.\n- Never produce multiple statements separated by semicolons.\n- Never use EXECUTE, PERFORM, or dynamic SQL.\n- Always use schema-qualified table names (e.g. public.geography, not just geography).\n- Prefer explicit column names over SELECT *.\n- Always write the estimate column as `e.estimate` \u2014 never alias it away from that name (e.g. do NOT write `e.estimate AS median_income`). Other columns may use aliases for readability.\n- Add a LIMIT clause if the question asks for top-N results; default to LIMIT 100 if a row cap is not otherwise specified and the query could return unbounded rows.\n- When sorting on a nullable numeric column, always add NULLS LAST (e.g. ORDER BY e.estimate DESC NULLS LAST) so that NULL values do not appear at the top of the result.\n- Use standard PostgreSQL functions only. Do not use vendor extensions.\n- If you cannot produce a valid SELECT query from the given context, return an empty string for sql and explain the issue in assumptions.\n\n## Variable categories\nThe variables table uses exactly these 8 category values. Use the correct one \u2014 do not guess:\n\n income \u2014 household income, per-capita income, Gini index, income brackets\n poverty \u2014 population below the poverty line, poverty ratios\n housing \u2014 housing units, home values, gross rent, occupancy, rent burden\n employment \u2014 labor force participation, unemployment counts, industry, occupation\n demographics \u2014 total population, age, sex, race, Hispanic/Latino origin\n education \u2014 educational attainment (high school, bachelor's, graduate degrees)\n transportation \u2014 commute mode (drove alone, transit, worked from home), travel time\n health_insurance \u2014 health insurance coverage by age and sex\n\nCommon mistakes to avoid:\n - Bachelor's degree \u2192 category = 'education' (NOT 'demographics')\n - Poverty / below poverty line \u2192 category = 'poverty' (NOT 'income')\n - Commute mode, work from home, travel time \u2192 category = 'transportation' (NOT 'employment')\n - Health insurance coverage \u2192 category = 'health_insurance'\n\n## Estimates are raw counts, not percentages\nMost estimate values are raw COUNTS (number of people, number of housing units), NOT percentages or rates.\nTo express a rate or percentage, you must compute it by dividing a count by its relevant total:\n\n homeownership rate = 100.0 * owner_occupied.estimate / NULLIF(total_occupied.estimate, 0)\n poverty rate = 100.0 * below_poverty.estimate / NULLIF(total_for_poverty.estimate, 0)\n drove-alone rate = 100.0 * drove_alone.estimate / NULLIF(total_workers.estimate, 0)\n\nNever compare a raw count estimate directly to a percentage threshold. For example:\n WRONG: owner_e.estimate < 60 \u2014 this compares a unit count to 60, not 60%\n RIGHT: 100.0 * owner_e.estimate / NULLIF(total_e.estimate, 0) < 60\n\nWhen a question asks for a rate or percentage, always join in the denominator total variable\nand compute the ratio explicitly.\n\n## ILIKE precision\nILIKE label patterns can match multiple variables. When a single specific metric is needed:\n- Use a tighter pattern (e.g. `v.label ILIKE '%median household income in the past 12 months%'` rather than `'%median household income%'` which also matches age-group breakdowns).\n- Or add `AND v.concept ILIKE '...'` to narrow further.\n- Or filter to a known variable_code directly (e.g. `e.variable_code = 'B19013_001E'`) when the code is clear from the question or schema context.\nAmbiguous ILIKE matches combined with ORDER BY will surface whichever matched variable has the highest/lowest value \u2014 which may not be the intended one.\n\n## Assumption surfacing\nFor every ambiguous term, geographic scope decision, join assumption, or data interpretation, add a plain-English sentence to the assumptions list. Examples:\n- \"Interpreting 'large counties' as counties with an estimated population > 100,000.\"\n- \"Using the most recent ACS 5-year estimate (2024 vintage).\"\n- \"Joining estimates to geography on geo_id to resolve county names.\"\n\n## Display hint\nChoose the best UI visualization layout for the result:\n- \"big\" \u2014 a single scalar answer: one row, one primary numeric value (e.g. a county's median income or total population).\n- \"statrow\" \u2014 2\u20134 related metrics about the same subject in one row (e.g. a housing snapshot with home value, rent, and homeownership rate).\n- \"bar\" \u2014 a ranked list of one metric across many geographies (e.g. top 10 counties by income).\n- \"grouped\" \u2014 a two-series comparison across geographies (e.g. rent in 2020 vs 2024 by state).\n- \"table\" \u2014 default fallback for anything that does not fit the above.\n\n## Output format\nRespond with a JSON object matching this exact shape:\n{\n \"sql\": \"<your SELECT query or empty string>\",\n \"assumptions\": [\"<assumption 1>\", \"<assumption 2>\"],\n \"displayHint\": \"<big | statrow | bar | grouped | table>\"\n}\nDo not include any text outside the JSON object.";
12
+ export interface FewShotExample {
13
+ question: string;
14
+ schemaContext: string;
15
+ result: {
16
+ sql: string;
17
+ assumptions: string[];
18
+ displayHint: "big" | "statrow" | "bar" | "grouped" | "table";
19
+ };
20
+ }
21
+ export declare const DEFAULT_FEW_SHOT_EXAMPLES: FewShotExample[];
22
+ /**
23
+ * Converts an array of RetrievedSchemaChunk into a compact, LLM-readable
24
+ * schema context block. Table-level chunks are listed first, followed by
25
+ * column chunks grouped under their parent table.
26
+ */
27
+ export declare function formatSchemaContext(chunks: RetrievedSchemaChunk[]): string;
28
+ /**
29
+ * Builds the full user message to send to the planning agent.
30
+ * Includes the schema context, optional few-shot examples, optional prior
31
+ * failed attempts (errorContext), and the question.
32
+ */
33
+ export declare function buildUserMessage(question: string, context: RetrievedSchemaChunk[], examples: FewShotExample[], errorContext?: {
34
+ sql: string;
35
+ error: string;
36
+ }[]): string;
37
+ //# sourceMappingURL=prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../../src/planner/prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAMlE,eAAO,MAAM,aAAa,68KAiFuB,CAAC;AAMlD,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,EAAE,CAAC;QAAC,WAAW,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,CAAA;KAAE,CAAC;CAC9G;AAED,eAAO,MAAM,yBAAyB,EAAE,cAAc,EAwOrD,CAAC;AAMF;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,oBAAoB,EAAE,GAAG,MAAM,CA6D1E;AAMD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,oBAAoB,EAAE,EAC/B,QAAQ,EAAE,cAAc,EAAE,EAC1B,YAAY,CAAC,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,EAAE,GAC9C,MAAM,CAkDR"}