-
Notifications
You must be signed in to change notification settings - Fork 189
Add Phase 1 schema-inference CLI (prompt → DatasetSchema) #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7ff1b9c
e7dfe58
f544da7
f54e368
a0279ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,3 +19,5 @@ yarn-debug.log* | |
| *.bak | ||
| tmp/ | ||
| temp/ | ||
|
|
||
| backend/output/ | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,36 @@ | ||||||||||||||||||||||||||||||||||||
| You are a data engineering assistant that converts natural-language prompts into structured dataset schemas. Given a user prompt describing a dataset they want to build, you produce a precise schema definition. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Your job is to: | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| 1. Identify the universe of entities the user wants to collect. Each entity becomes one row in the dataset. | ||||||||||||||||||||||||||||||||||||
| 2. Pick a clear primary key — the column whose values uniquely identify each row. This is usually a name, ID, or canonical URL. Exactly one column must have `is_primary_key: true`, and its `name` must equal `primary_key`. The primary key column must have `nullable: false` and `is_enumerable: true`. | ||||||||||||||||||||||||||||||||||||
| 3. Choose useful columns. Each column captures one fact about the entity. Use snake_case names. Mark `is_enumerable: true` only on columns whose values can be used to list all rows (typically just the primary key, and occasionally one or two others when a source page lists them alongside the primary key). | ||||||||||||||||||||||||||||||||||||
| 4. Set `retrieval_strategy`: | ||||||||||||||||||||||||||||||||||||
| - `search_fetch` — the data lives on a static page or sitemap that can be fetched as HTML. | ||||||||||||||||||||||||||||||||||||
| - `browser` — the source is a JavaScript-heavy SPA, requires scroll/click to reveal items, or paginates client-side. | ||||||||||||||||||||||||||||||||||||
| - `hybrid` — unclear; the pipeline will try search_fetch first and fall back to browser. | ||||||||||||||||||||||||||||||||||||
| 5. Set `source_hint` to a specific URL whenever possible (e.g. `https://www.ycombinator.com/companies?industry=Fintech`). Avoid vague descriptions. | ||||||||||||||||||||||||||||||||||||
| 6. Write a `retrieval_hint` for each column describing where/how the value can be found later. Downstream agents will use this to fill the column for each row. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| Rules: | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| - `dataset_name` must be snake_case. | ||||||||||||||||||||||||||||||||||||
| - All column `name` values must be snake_case and unique. | ||||||||||||||||||||||||||||||||||||
| - Prefer concrete column choices over speculative ones — better to omit a column than guess wildly. | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| # @MMeteorL's comments/suggestions: | ||||||||||||||||||||||||||||||||||||
| # This may be too early in the agent workflow to suggest these without more | ||||||||||||||||||||||||||||||||||||
| # context. In the current agent system, the agent would first use Tinyfish | ||||||||||||||||||||||||||||||||||||
| # search to search for candidate urls and fetch those results for analysis | ||||||||||||||||||||||||||||||||||||
| # of what is the best way to retrieve. | ||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||
| # For the same reason, instead of source_hint, it may be helpful to generate | ||||||||||||||||||||||||||||||||||||
| # search_query_hint. | ||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||
| # Retrieval_hint is a great idea, however, because of the varied sources | ||||||||||||||||||||||||||||||||||||
| # that Tinyfish search would propose, this may not be the best fit. This | ||||||||||||||||||||||||||||||||||||
| # should be a decision left for downstream agent with more information. | ||||||||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||||||||
| # My recommendation is to produce: 'search_queries': generate [X number] of | ||||||||||||||||||||||||||||||||||||
| # search queries based on the user prompt and data spec. These search queries | ||||||||||||||||||||||||||||||||||||
| # should be differentiated from each other. | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+20
to
+36
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove inline review-discussion text from the production prompt. These lines are not ignored by the LLM; they become active prompt content and can conflict with the required schema contract, causing inconsistent outputs. Suggested fix-
-# `@MMeteorL`'s comments/suggestions:
-# This may be too early in the agent workflow to suggest these without more
-# context. In the current agent system, the agent would first use Tinyfish
-# search to search for candidate urls and fetch those results for analysis
-# of what is the best way to retrieve.
-#
-# For the same reason, instead of source_hint, it may be helpful to generate
-# search_query_hint.
-#
-# Retrieval_hint is a great idea, however, because of the varied sources
-# that Tinyfish search would propose, this may not be the best fit. This
-# should be a decision left for downstream agent with more information.
-#
-# My recommendation is to produce: 'search_queries': generate [X number] of
-# search queries based on the user prompt and data spec. These search queries
-# should be differentiated from each other.📝 Committable suggestion
Suggested change
🧰 Tools🪛 LanguageTool[style] ~32-~32: Consider using a more formal alternative. (MORE_INFO) 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { mkdirSync, writeFileSync } from "node:fs"; | ||
| import { randomBytes } from "node:crypto"; | ||
| import { join } from "node:path"; | ||
|
|
||
| import { inferSchema } from "./pipeline/schema-inference.js"; | ||
|
|
||
| function parsePrompt(argv: string[]): string { | ||
| const idx = argv.findIndex((a) => a === "--prompt"); | ||
| if (idx === -1 || idx === argv.length - 1) { | ||
| throw new Error('Usage: npm run infer-schema -- --prompt "<your prompt>"'); | ||
| } | ||
| const value = argv[idx + 1]; | ||
| if (!value.trim()) throw new Error("--prompt requires a non-empty value"); | ||
| return value; | ||
| } | ||
|
|
||
| function generateRunId(): string { | ||
| const ts = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14); | ||
| const rand = randomBytes(3).toString("hex"); | ||
| return `${ts}-${rand}`; | ||
| } | ||
|
|
||
| async function main() { | ||
| const prompt = parsePrompt(process.argv.slice(2)); | ||
| const runId = generateRunId(); | ||
| const outDir = join("output", runId); | ||
|
|
||
| console.log(`Inferring schema for: "${prompt}"`); | ||
| const schema = await inferSchema(prompt); | ||
|
|
||
| mkdirSync(outDir, { recursive: true }); | ||
| const schemaPath = join(outDir, "schema.json"); | ||
| writeFileSync(schemaPath, JSON.stringify(schema, null, 2) + "\n"); | ||
|
|
||
| console.log(`Run ID: ${runId}`); | ||
| console.log(`Schema: backend/${schemaPath}`); | ||
| } | ||
|
|
||
| main().catch((err) => { | ||
| console.error(err); | ||
|
Comment on lines
+28
to
+40
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid logging raw prompts and raw error objects. This CLI can handle sensitive prompt content; printing full prompt and unsanitized errors increases leakage risk in shared logs. Suggested hardening- console.log(`Inferring schema for: "${prompt}"`);
+ console.log(`Inferring schema (prompt length: ${prompt.length})`);
main().catch((err) => {
- console.error(err);
+ const message = err instanceof Error ? err.message : String(err);
+ console.error(`infer-schema failed: ${message}`);
process.exit(1);
});🤖 Prompt for AI Agents |
||
| process.exit(1); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import { readFileSync } from "node:fs"; | ||
| import { generateText, Output, NoObjectGeneratedError } from "ai"; | ||
| import { createOpenRouter } from "@openrouter/ai-sdk-provider"; | ||
|
|
||
| import { env } from "../env.js"; | ||
| import { datasetSchemaSchema, type DatasetSchema } from "./types.js"; | ||
|
|
||
| const SYSTEM_PROMPT = readFileSync( | ||
| new URL("../../prompts/schema-inference.txt", import.meta.url), | ||
| "utf8", | ||
| ); | ||
|
|
||
| function getModel() { | ||
| if (!env.OPENROUTER_API_KEY) { | ||
| throw new Error("Missing required environment variable: OPENROUTER_API_KEY"); | ||
| } | ||
| const openrouter = createOpenRouter({ apiKey: env.OPENROUTER_API_KEY }); | ||
| return openrouter("anthropic/claude-sonnet-4-6"); | ||
| } | ||
|
|
||
| export async function inferSchema(prompt: string): Promise<DatasetSchema> { | ||
| const model = getModel(); | ||
| try { | ||
| return await callOnce(model, prompt); | ||
| } catch (error) { | ||
| if (NoObjectGeneratedError.isInstance(error)) { | ||
| const detail = error.cause ? String(error.cause) : error.text; | ||
| const retry = `${prompt}\n\nYour previous output failed validation:\n${detail}\n\nReturn a corrected DatasetSchema.`; | ||
| return await callOnce(model, retry); | ||
| } | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| async function callOnce( | ||
| model: Parameters<typeof generateText>[0]["model"], | ||
| prompt: string, | ||
| ): Promise<DatasetSchema> { | ||
| const { output } = await generateText({ | ||
| model, | ||
| output: Output.object({ schema: datasetSchemaSchema }), | ||
| system: SYSTEM_PROMPT, | ||
| maxTokens: 4096, | ||
| prompt, | ||
| }); | ||
| if (!output) throw new Error("Model did not generate a valid schema object"); | ||
| return output; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This may be too early in the agent workflow to suggest these without more context. In the current agent system, the agent would first use Tinyfish search to search for candidate urls and fetch those results for analysis of what is the best way to retrieve.
For the same reason, instead of source_hint, it may be helpful to generate search_query_hint.
Retrieval_hint is a great idea, however, because of the varied sources that Tinyfish search would propose, this may not be the best fit. This should be a decision left for downstream agent with more information.
My recommendation is to produce 4: 'search_queries": generate [X number] of search queries based on the user prompt and data spec. These search queries should be differentiated from each other.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed — added your comments verbatim to the prompt file so we don't lose this context (commit f54e368). We'll revisit the schema fields (replacing retrieval_strategy/source_hint/retrieval_hint with search_queries) once the downstream agent work is further along and we have a clearer picture of how TinyFish Search fits into the flow.