Files
natural-language-postgres/app/actions.ts
T
2024-11-06 11:37:50 +00:00

184 lines
6.2 KiB
TypeScript

"use server";
import { Config, configSchema, explanationsSchema, Result } from "@/lib/types";
import { openai } from "@ai-sdk/openai";
import { sql } from "@vercel/postgres";
import { generateObject } from "ai";
import { z } from "zod";
export const generateQuery = async (input: string) => {
"use server";
try {
const result = await generateObject({
model: openai("gpt-4o"),
system: `You are a SQL (postgres) and data visualization expert. Your job is to help the user write a SQL query to retrieve the data they need. The table schema is as follows:
unicorns (
id SERIAL PRIMARY KEY,
company VARCHAR(255) NOT NULL UNIQUE,
valuation DECIMAL(10, 2) NOT NULL,
date_joined DATE,
country VARCHAR(255) NOT NULL,
city VARCHAR(255) NOT NULL,
industry VARCHAR(255) NOT NULL,
select_investors TEXT NOT NULL
);
Only retrieval queries are allowed.
For things like industry, company names and other string fields, use the ILIKE operator and convert both the search term and the field to lowercase using LOWER() function. For example: LOWER(industry) ILIKE LOWER('%search_term%').
Note: select_investors is a comma-separated list of investors. Trim whitespace to ensure you're grouping properly. Note, some fields may be null or have only one value.
When answering questions about a specific field, ensure you are selecting the identifying column (ie. what is Vercel's valuation would select company and valuation').
The industries available are:
- healthcare & life sciences
- consumer & retail
- financial services
- enterprise tech
- insurance
- media & entertainment
- industrials
- health
If the user asks for a category that is not in the list, infer based on the list above.
Note: valuation is in billions of dollars so 10b would be 10.0.
Note: if the user asks for a rate, return it as a decimal. For example, 0.1 would be 10%.
If the user asks for 'over time' data, return by year.
When searching for UK or USA, write out United Kingdom or United States respectively.
EVERY QUERY SHOULD RETURN QUANTITATIVE DATA THAT CAN BE PLOTTED ON A CHART! There should always be at least two columns. If the user asks for a single column, return the column and the count of the column. If the user asks for a rate, return the rate as a decimal. For example, 0.1 would be 10%.
`,
prompt: `Generate the query necessary to retrieve the data the user wants: ${input}`,
schema: z.object({
query: z.string(),
}),
});
return result.object.query;
} catch (e) {
console.error(e);
throw new Error("Failed to generate query");
}
};
export const runGenerateSQLQuery = async (query: string) => {
"use server";
// Check if the query is a SELECT statement
if (
!query.trim().toLowerCase().startsWith("select") ||
query.trim().toLowerCase().includes("drop") ||
query.trim().toLowerCase().includes("delete") ||
query.trim().toLowerCase().includes("insert")
) {
throw new Error("Only SELECT queries are allowed");
}
let data: any;
try {
data = await sql.query(query);
} catch (e: any) {
if (e.message.includes('relation "unicorns" does not exist')) {
console.log(
"Table does not exist, creating and seeding it with dummy data now...",
);
// throw error
throw Error("Table does not exist");
} else {
throw e;
}
}
return data.rows as Result[];
};
export const explainQuery = async (input: string, sqlQuery: string) => {
"use server";
try {
const result = await generateObject({
model: openai("gpt-4o"),
schema: z.object({
explanations: explanationsSchema,
}),
system: `You are a SQL (postgres) expert. Your job is to explain to the user write a SQL query you wrote to retrieve the data they asked for. The table schema is as follows:
unicorns (
id SERIAL PRIMARY KEY,
company VARCHAR(255) NOT NULL UNIQUE,
valuation DECIMAL(10, 2) NOT NULL,
date_joined DATE,
country VARCHAR(255) NOT NULL,
city VARCHAR(255) NOT NULL,
industry VARCHAR(255) NOT NULL,
select_investors TEXT NOT NULL
);
When you explain you must take a section of the query, and then explain it. Each "section" should be unique. So in a query like: "SELECT * FROM unicorns limit 20", the sections could be "SELECT *", "FROM UNICORNS", "LIMIT 20".
If a section doesnt have any explanation, include it, but leave the explanation empty.
`,
prompt: `Explain the SQL query you generated to retrieve the data the user wanted. Assume the user is not an expert in SQL. Break down the query into steps. Be concise.
User Query:
${input}
Generated SQL Query:
${sqlQuery}`,
});
return result.object;
} catch (e) {
console.error(e);
throw new Error("Failed to generate query");
}
};
export const generateChartConfig = async (
results: Result[],
userQuery: string,
) => {
"use server";
const system = `You are a data visualization expert. `;
try {
const { object: config } = await generateObject({
model: openai("gpt-4o"),
system,
prompt: `Given the following data from a SQL query result, generate the chart config that best visualises the data and answers the users query.
For multiple groups use multi-lines.
Here is an example complete config:
export const chartConfig = {
type: "pie",
xKey: "month",
yKeys: ["sales", "profit", "expenses"],
colors: {
sales: "#4CAF50", // Green for sales
profit: "#2196F3", // Blue for profit
expenses: "#F44336" // Red for expenses
},
legend: true
}
User Query:
${userQuery}
Data:
${JSON.stringify(results, null, 2)}`,
schema: configSchema,
});
const colors: Record<string, string> = {};
config.yKeys.forEach((key, index) => {
colors[key] = `hsl(var(--chart-${index + 1}))`;
});
const updatedConfig: Config = { ...config, colors };
return { config: updatedConfig };
} catch (e) {
// @ts-expect-errore
console.error(e.message);
throw new Error("Failed to generate chart suggestion");
}
};