375 lines
9.0 KiB
TypeScript
375 lines
9.0 KiB
TypeScript
import { evalite } from "evalite";
|
|
import { Levenshtein } from "autoevals";
|
|
import { AIQueryService } from "~/v3/services/aiQueryService.server";
|
|
import { runsSchema } from "~/v3/querySchemas";
|
|
import dotenv from "dotenv";
|
|
import { wrapAISDKModel } from "evalite/ai-sdk";
|
|
import { openai } from "@ai-sdk/openai";
|
|
|
|
dotenv.config({ path: "../../.env" });
|
|
|
|
// Helper to normalize queries for comparison
|
|
function normalizeQuery(query: string): string {
|
|
return query
|
|
.replace(/\s+/g, " ")
|
|
.replace(/\(\s+/g, "(")
|
|
.replace(/\s+\)/g, ")")
|
|
.trim()
|
|
.toLowerCase();
|
|
}
|
|
|
|
// Type for parsed query results
|
|
interface ParsedQueryResult {
|
|
success: boolean;
|
|
query?: string;
|
|
error?: string;
|
|
}
|
|
|
|
// Custom scorer that checks if the generated query is semantically similar
|
|
// and also syntactically valid
|
|
const QuerySimilarity = {
|
|
name: "QuerySimilarity",
|
|
scorer: async ({
|
|
input,
|
|
output,
|
|
expected,
|
|
}: {
|
|
input: string;
|
|
output: string;
|
|
expected?: string;
|
|
}) => {
|
|
if (!expected) {
|
|
return 0;
|
|
}
|
|
|
|
// Parse the output to extract the query
|
|
const outputParsed = JSON.parse(output) as ParsedQueryResult;
|
|
const expectedParsed = JSON.parse(expected) as ParsedQueryResult;
|
|
|
|
// Check success status first
|
|
if (outputParsed.success !== expectedParsed.success) {
|
|
return 0;
|
|
}
|
|
|
|
// If both failed, check if error messages are similar
|
|
if (!outputParsed.success && !expectedParsed.success) {
|
|
// Give partial credit for correctly identifying an error case
|
|
return 0.5;
|
|
}
|
|
|
|
// If both succeeded, compare the queries
|
|
if (outputParsed.success && expectedParsed.success) {
|
|
const normalizedOutput = normalizeQuery(outputParsed.query ?? "");
|
|
const normalizedExpected = normalizeQuery(expectedParsed.query ?? "");
|
|
|
|
// Key patterns to check
|
|
const patterns = [
|
|
// Table name
|
|
/from\s+runs/i,
|
|
// Status filter patterns
|
|
/status\s*=\s*'[^']+'/i,
|
|
/status\s+in\s*\([^)]+\)/i,
|
|
// Time patterns
|
|
/interval\s+\d+\s+(day|hour|minute|week|month)/i,
|
|
/triggered_at\s*>/i,
|
|
// Aggregation patterns
|
|
/count\(\)/i,
|
|
/sum\(/i,
|
|
/avg\(/i,
|
|
/group\s+by/i,
|
|
// Ordering
|
|
/order\s+by/i,
|
|
// Limit
|
|
/limit\s+\d+/i,
|
|
];
|
|
|
|
let matchScore = 0;
|
|
let totalPatterns = 0;
|
|
|
|
for (const pattern of patterns) {
|
|
const outputMatch = pattern.test(normalizedOutput);
|
|
const expectedMatch = pattern.test(normalizedExpected);
|
|
|
|
if (expectedMatch) {
|
|
totalPatterns++;
|
|
if (outputMatch) {
|
|
matchScore++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Base score from pattern matching
|
|
const patternScore = totalPatterns > 0 ? matchScore / totalPatterns : 0.5;
|
|
|
|
// Use Levenshtein for overall similarity
|
|
const levenshteinResult = await Levenshtein({
|
|
output: normalizedOutput,
|
|
expected: normalizedExpected,
|
|
});
|
|
const levenshteinScore = levenshteinResult?.score ?? 0;
|
|
|
|
// Weighted combination
|
|
return 0.6 * patternScore + 0.4 * levenshteinScore;
|
|
}
|
|
|
|
return 0;
|
|
},
|
|
};
|
|
|
|
evalite("AI Query Generator", {
|
|
data: async () => {
|
|
return [
|
|
// Basic SELECT queries
|
|
{
|
|
input: "Show me all runs",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT *
|
|
FROM runs
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
{
|
|
input: "Get the 10 most recent runs",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT *
|
|
FROM runs
|
|
ORDER BY triggered_at DESC
|
|
LIMIT 10`,
|
|
}),
|
|
},
|
|
|
|
// Status filtering
|
|
{
|
|
input: "Show failed runs",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT *
|
|
FROM runs
|
|
WHERE status = 'Failed'
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
{
|
|
input: "Get all completed runs",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT *
|
|
FROM runs
|
|
WHERE status = 'Completed'
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
{
|
|
input: "Find runs that crashed or timed out",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT *
|
|
FROM runs
|
|
WHERE status IN ('Crashed', 'Timed out')
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
|
|
// Time-based filtering
|
|
{
|
|
input: "Runs from the last 7 days",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT *
|
|
FROM runs
|
|
WHERE triggered_at > now() - INTERVAL 7 DAY
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
{
|
|
input: "Show runs from the past hour",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT *
|
|
FROM runs
|
|
WHERE triggered_at > now() - INTERVAL 1 HOUR
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
{
|
|
input: "Failed runs in the last 24 hours",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT *
|
|
FROM runs
|
|
WHERE status = 'Failed'
|
|
AND triggered_at > now() - INTERVAL 1 DAY
|
|
ORDER BY triggered_at DESC
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
|
|
// Aggregations
|
|
{
|
|
input: "Count of runs by status",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT status, count() AS count
|
|
FROM runs
|
|
GROUP BY status
|
|
ORDER BY count DESC`,
|
|
}),
|
|
},
|
|
{
|
|
input: "How many runs per task?",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT task_identifier, count() AS run_count
|
|
FROM runs
|
|
GROUP BY task_identifier
|
|
ORDER BY run_count DESC
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
{
|
|
input: "Average execution duration by task",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT task_identifier, avg(execution_duration) AS avg_duration
|
|
FROM runs
|
|
GROUP BY task_identifier
|
|
ORDER BY avg_duration DESC
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
{
|
|
input: "Total cost by task in the last 30 days",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT task_identifier, sum(total_cost) AS total_cost
|
|
FROM runs
|
|
WHERE triggered_at > now() - INTERVAL 30 DAY
|
|
GROUP BY task_identifier
|
|
ORDER BY total_cost DESC
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
|
|
// Complex queries
|
|
{
|
|
input: "Top 10 most expensive failed runs from last week",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT run_id, task_identifier, status, total_cost, triggered_at
|
|
FROM runs
|
|
WHERE status = 'Failed'
|
|
AND triggered_at > now() - INTERVAL 7 DAY
|
|
ORDER BY total_cost DESC
|
|
LIMIT 10`,
|
|
}),
|
|
},
|
|
{
|
|
input: "Runs using large machines that took more than 5 minutes",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT *
|
|
FROM runs
|
|
WHERE machine IN ('large-1x', 'large-2x')
|
|
AND usage_duration > 300000
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
{
|
|
input: "Show p95 execution duration by task for completed runs",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT task_identifier, quantile(0.95)(execution_duration) AS p95_duration
|
|
FROM runs
|
|
WHERE status = 'Completed'
|
|
AND execution_duration IS NOT NULL
|
|
GROUP BY task_identifier
|
|
ORDER BY p95_duration DESC
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
|
|
// Specific columns
|
|
{
|
|
input: "Just show run IDs and their statuses",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT run_id, status
|
|
FROM runs
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
{
|
|
input: "Get run_id, task, status and cost for recent runs",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT run_id, task_identifier, status, total_cost
|
|
FROM runs
|
|
ORDER BY triggered_at DESC
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
|
|
// Root runs
|
|
{
|
|
input: "Show only root runs (not child runs)",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT *
|
|
FROM runs
|
|
WHERE is_root_run = 1
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
|
|
// Queue filtering
|
|
{
|
|
input: "Runs in the shared queue",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT *
|
|
FROM runs
|
|
WHERE queue LIKE '%shared%'
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
|
|
// Tags
|
|
{
|
|
input: "Find runs with tag 'important'",
|
|
expected: JSON.stringify({
|
|
success: true,
|
|
query: `SELECT *
|
|
FROM runs
|
|
WHERE has(tags, 'important')
|
|
LIMIT 100`,
|
|
}),
|
|
},
|
|
|
|
// Error cases
|
|
{
|
|
input: "Do something",
|
|
expected: JSON.stringify({
|
|
success: false,
|
|
error: "Please be more specific about what data you want to query",
|
|
}),
|
|
},
|
|
{
|
|
input: "Show me the weather",
|
|
expected: JSON.stringify({
|
|
success: false,
|
|
error: "I can only generate queries for task run data",
|
|
}),
|
|
},
|
|
];
|
|
},
|
|
task: async (input) => {
|
|
const service = new AIQueryService([runsSchema], wrapAISDKModel(openai("gpt-4o-mini")));
|
|
|
|
const result = await service.call(input);
|
|
return JSON.stringify(result);
|
|
},
|
|
scorers: [QuerySimilarity, Levenshtein],
|
|
});
|