Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
.DS_Store
node_modules/
backend/.npm-cache/
.env
.env.local
Project_BigSet_brief.md
notion-docs.md
image.png
image-1.png
.omx/
/car
7 changes: 7 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,10 @@ BETTER_AUTH_URL=http://localhost:3501
CLIENT_ORIGIN=http://localhost:3500
DATABASE_URL=postgres://bigset:bigset@localhost:5432/bigset
PORT=3501

# Optional. Dataset-builder planner uses deterministic fallback when missing.
OPENROUTER_API_KEY=""
OPENROUTER_MODEL=openai/gpt-4.1-mini

# Optional. Local TinyFish CLI harness uses this when executing search/fetch/agent runs.
TINYFISH_API_KEY=""
6 changes: 5 additions & 1 deletion backend/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import "dotenv/config";
import { config } from "dotenv";
import { defineConfig } from "drizzle-kit";

config({ path: ".env.local" });
config({ path: "../.env.local" });
config();

const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) {
throw new Error("DATABASE_URL is required for Drizzle Kit");
Expand Down
1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"private": true,
"scripts": {
"dev": "tsx watch src/index.ts",
"builder:plan": "tsx src/dataset-builder/run-prototype.ts",
"build": "tsc",
"start": "node dist/index.js",
"db:push": "drizzle-kit push"
Expand Down
136 changes: 136 additions & 0 deletions backend/src/dataset-builder/agent-harness.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import type {
AgentHarnessStage,
DatasetBuildPlan,
DatasetColumnDefinition,
DatasetSourceStrategy,
} from "./types.js";

export function createHarnessStages(
sourceStrategy: DatasetSourceStrategy,
hasMissingInputs: boolean
): AgentHarnessStage[] {
const stages: AgentHarnessStage[] = [];

if (hasMissingInputs) {
stages.push({
id: "clarify-missing-inputs",
title: "Ask for missing inputs",
purpose:
"Pause before scraping when the target sites need user-specific values.",
tool: "user_input",
canRunWithoutUser: false,
});
}

stages.push(
{
id: "discover-source-candidates",
title: "Discover source candidates",
purpose: "Use TinyFish Search to find source pages likely to contain rows.",
tool: "tinyfish_search",
canRunWithoutUser: true,
},
{
id: "fetch-source-pages",
title: "Fetch clean source content",
purpose: "Use TinyFish Fetch before browser automation for speed and cost.",
tool: "tinyfish_fetch",
canRunWithoutUser: true,
}
);

if (sourceStrategy !== "search_fetch") {
stages.push({
id: "browser-deep-extraction",
title: "Deep browser extraction",
purpose:
"Use TinyFish Agent or browser automation only when search/fetch cannot reach the value.",
tool: "tinyfish_agent",
canRunWithoutUser: !hasMissingInputs,
});
}

stages.push(
{
id: "validate-cells",
title: "Validate extracted cells",
purpose:
"Reject rows that do not match schema, keep source URLs, and mark missing cells.",
tool: "validator",
canRunWithoutUser: true,
},
{
id: "upsert-rows",
title: "Replace changed row values",
purpose:
"Use the identity column to replace current values instead of appending duplicates.",
tool: "database",
canRunWithoutUser: true,
}
);

return stages;
}

export function createTinyFishAgentGoal(plan: DatasetBuildPlan): string {
const columnDescriptions = plan.schema.columns
.map((column) => `- ${column.name}: ${column.description}`)
.join("\n");

return [
`Build rows for dataset: ${plan.datasetName}.`,
`User request: ${plan.userRequest}`,
`Identity column: ${plan.schema.identityColumnName}`,
"Return only rows backed by source URLs.",
"Mark missing fields as null instead of guessing.",
"Columns:",
columnDescriptions,
].join("\n");
}

export function createTinyFishAgentOutputSchema(plan: DatasetBuildPlan) {
const rowProperties = Object.fromEntries(
plan.schema.columns.map((column) => [column.name, jsonSchemaForColumn(column)])
);

return {
type: "object",
additionalProperties: false,
required: ["rows", "sourceUrls", "validationIssues"],
properties: {
rows: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: [plan.schema.identityColumnName],
properties: rowProperties,
},
Comment on lines +96 to +108

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Output schema ignores isRequired columns.

Rows currently require only the identity field, so required columns (for example source_url) can be omitted and still pass schema validation.

Proposed fix
 export function createTinyFishAgentOutputSchema(plan: DatasetBuildPlan) {
   const rowProperties = Object.fromEntries(
     plan.schema.columns.map((column) => [column.name, jsonSchemaForColumn(column)])
   );
+  const requiredRowColumns = Array.from(
+    new Set([
+      plan.schema.identityColumnName,
+      ...plan.schema.columns
+        .filter((column) => column.isRequired)
+        .map((column) => column.name),
+    ])
+  );
 
   return {
@@
       rows: {
         type: "array",
         items: {
           type: "object",
           additionalProperties: false,
-          required: [plan.schema.identityColumnName],
+          required: requiredRowColumns,
           properties: rowProperties,
         },
       },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return {
type: "object",
additionalProperties: false,
required: ["rows", "sourceUrls", "validationIssues"],
properties: {
rows: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: [plan.schema.identityColumnName],
properties: rowProperties,
},
export function createTinyFishAgentOutputSchema(plan: DatasetBuildPlan) {
const rowProperties = Object.fromEntries(
plan.schema.columns.map((column) => [column.name, jsonSchemaForColumn(column)])
);
const requiredRowColumns = Array.from(
new Set([
plan.schema.identityColumnName,
...plan.schema.columns
.filter((column) => column.isRequired)
.map((column) => column.name),
])
);
return {
type: "object",
additionalProperties: false,
required: ["rows", "sourceUrls", "validationIssues"],
properties: {
rows: {
type: "array",
items: {
type: "object",
additionalProperties: false,
required: requiredRowColumns,
properties: rowProperties,
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/dataset-builder/agent-harness.ts` around lines 96 - 108, The JSON
schema for rows only requires plan.schema.identityColumnName and ignores other
columns marked as isRequired; update the construction of the rows schema in
agent-harness.ts so the required array includes plan.schema.identityColumnName
plus every column name from plan.schema.columns (or equivalent) where isRequired
=== true (deduplicate if necessary) before assigning it to the rows.required
field; ensure you keep additionalProperties: false and the existing
rowProperties mapping intact so validation enforces those required columns
(e.g., update the code that sets required: [plan.schema.identityColumnName] to
compute and use a merged requiredFields array).

},
sourceUrls: {
type: "array",
items: { type: "string" },
},
validationIssues: {
type: "array",
items: { type: "string" },
},
},
};
}

function jsonSchemaForColumn(column: DatasetColumnDefinition) {
if (column.kind === "number") {
return { type: ["number", "null"], description: column.description };
}
if (column.kind === "boolean") {
return { type: ["boolean", "null"], description: column.description };
}
if (column.kind === "json") {
return {
type: ["object", "array", "null"],
description: column.description,
};
}
return { type: ["string", "null"], description: column.description };
}
Loading