From 5dcf87ab0253e031955ae29b13066626d9ef0691 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 13:19:14 +0000 Subject: [PATCH 1/8] Initial plan From 7584a434eddeb0c778c1ce8591848f47c1830dbd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 13:24:26 +0000 Subject: [PATCH 2/8] Add suggest method implementation with types, tests, and documentation Co-authored-by: hustcc <7856674+hustcc@users.noreply.github.com> --- README.md | 17 +++++ __tests__/suggest.test.ts | 156 ++++++++++++++++++++++++++++++++++++++ src/ava.ts | 16 +++- src/index.ts | 2 +- src/suggest/index.ts | 95 +++++++++++++++++++++++ src/types.ts | 12 +++ 6 files changed, 296 insertions(+), 2 deletions(-) create mode 100644 __tests__/suggest.test.ts create mode 100644 src/suggest/index.ts diff --git a/README.md b/README.md index 0151231fb..f4089cc82 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ AVA is a fundamental shift from rule-based analytics to AI-native capabilities: - **Natural Language Queries**: Ask questions about your data in plain English +- **Query Suggestions**: Get AI-recommended analysis queries based on your data characteristics - **LLM-Powered Analysis**: Leverages large language models for intelligent data analysis - **Smart Data Handling**: Automatically chooses between in-memory processing and SQLite based on data size - **Modular Architecture**: Clean separation of concerns with data, analysis, and visualization modules @@ -78,10 +79,26 @@ await ava.loadURL('https://api.example.com/data', (response) => response.data); // or extract from text await ava.loadText('杭州 100,上海 200,北京 300'); +// Get suggested analysis queries +const queries = await ava.suggest(5); // Get top 5 suggested queries (default: 3) +console.log(queries); +// [ +// { +// query: 'What is the average revenue by region?', +// score: 0.95, +// reason: 'Understanding revenue distribution across regions helps identify high-performing areas' +// }, +// ... +// ] + // Ask questions in natural language const result = await ava.analysis('What is the average revenue by region?'); console.log(result); +// Or use a suggested query +const suggestedResult = await ava.analysis(queries[0].query); +console.log(suggestedResult); + // Clean up ava.dispose(); ``` diff --git a/__tests__/suggest.test.ts b/__tests__/suggest.test.ts new file mode 100644 index 000000000..e139600e1 --- /dev/null +++ b/__tests__/suggest.test.ts @@ -0,0 +1,156 @@ +/** + * Tests for suggest functionality + */ + +import * as path from 'path'; + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; + +import { AVA } from '../src/ava'; + +describe('Suggest Tests', () => { + let ava: AVA; + const testDataPath = path.join(__dirname, '../data/companies.csv'); + + const apiKey = process.env.LING_1T_API_KEY; + const skipLLMTests = !apiKey; + + const getLLMConfig = () => ({ + model: 'ling-1t', + apiKey: apiKey || '', + baseURL: 'https://api.tbox.cn/api/llm/v1', + }); + + beforeEach(() => { + if (skipLLMTests) { + // eslint-disable-next-line no-console + console.log('Skipping LLM integration test: LING_1T_API_KEY not set'); + return; + } + + ava = new AVA({ + llm: getLLMConfig(), + }); + }); + + afterEach(() => { + if (ava) { + ava.dispose(); + } + }); + + describe('suggest method', () => { + it('should throw error when suggesting without loading data', async () => { + if (skipLLMTests) return; + + await expect(ava.suggest()).rejects.toThrow('No data loaded'); + }); + + it('should return 3 suggestions by default', async () => { + if (skipLLMTests) return; + + try { + await ava.loadCSV(testDataPath); + const suggestions = await ava.suggest(); + + expect(suggestions).toBeDefined(); + expect(Array.isArray(suggestions)).toBe(true); + expect(suggestions.length).toBe(3); + + // Verify each suggestion has required fields + for (const suggestion of suggestions) { + expect(suggestion).toHaveProperty('query'); + expect(suggestion).toHaveProperty('score'); + expect(suggestion).toHaveProperty('reason'); + expect(typeof suggestion.query).toBe('string'); + expect(typeof suggestion.score).toBe('number'); + expect(typeof suggestion.reason).toBe('string'); + expect(suggestion.query.length).toBeGreaterThan(0); + expect(suggestion.reason.length).toBeGreaterThan(0); + // Score should be between 0 and 1 + expect(suggestion.score).toBeGreaterThanOrEqual(0); + expect(suggestion.score).toBeLessThanOrEqual(1); + } + } catch (error) { + // eslint-disable-next-line no-console + console.log('Skipping test due to API error:', error instanceof Error ? error.message : String(error)); + } + }, 60000); + + it('should return specified number of suggestions', async () => { + if (skipLLMTests) return; + + try { + await ava.loadCSV(testDataPath); + const suggestions = await ava.suggest(5); + + expect(suggestions).toBeDefined(); + expect(Array.isArray(suggestions)).toBe(true); + expect(suggestions.length).toBe(5); + } catch (error) { + // eslint-disable-next-line no-console + console.log('Skipping test due to API error:', error instanceof Error ? error.message : String(error)); + } + }, 60000); + + it('should return suggestions sorted by score in descending order', async () => { + if (skipLLMTests) return; + + try { + await ava.loadCSV(testDataPath); + const suggestions = await ava.suggest(); + + // Verify suggestions are sorted by score descending + for (let i = 0; i < suggestions.length - 1; i++) { + expect(suggestions[i].score).toBeGreaterThanOrEqual(suggestions[i + 1].score); + } + } catch (error) { + // eslint-disable-next-line no-console + console.log('Skipping test due to API error:', error instanceof Error ? error.message : String(error)); + } + }, 60000); + + it('should work with loadObject', async () => { + if (skipLLMTests) return; + + try { + await ava.loadObject([ + { city: '杭州', gdp: 18753 }, + { city: '上海', gdp: 43214 }, + { city: '北京', gdp: 35371 }, + ]); + + const suggestions = await ava.suggest(); + + expect(suggestions).toBeDefined(); + expect(Array.isArray(suggestions)).toBe(true); + expect(suggestions.length).toBe(3); + } catch (error) { + // eslint-disable-next-line no-console + console.log('Skipping test due to API error:', error instanceof Error ? error.message : String(error)); + } + }, 60000); + + it('should generate queries that can be used with analysis', async () => { + if (skipLLMTests) return; + + try { + await ava.loadCSV(testDataPath); + const suggestions = await ava.suggest(1); + + expect(suggestions.length).toBeGreaterThan(0); + + // Try to analyze using the first suggested query + const result = await ava.analysis(suggestions[0].query); + + expect(result).toBeDefined(); + expect(result.text).toBeDefined(); + expect(typeof result.text).toBe('string'); + expect(result.text.length).toBeGreaterThan(0); + } catch (error) { + // eslint-disable-next-line no-console + console.log('Skipping test due to API error:', error instanceof Error ? error.message : String(error)); + } + }, 120000); // Longer timeout for this test + }); +}); diff --git a/src/ava.ts b/src/ava.ts index 4fc3ed59d..9c698aaff 100644 --- a/src/ava.ts +++ b/src/ava.ts @@ -16,8 +16,9 @@ import { adviseChartType, generateVisualizationHTML, } from './visualization'; +import { generateSuggestions } from './suggest'; -import type { AVAConfig, LLMConfig, DatasetInfo, AnalysisResponse } from './types'; +import type { AVAConfig, LLMConfig, DatasetInfo, AnalysisResponse, SuggestResult } from './types'; const DEFAULT_SQL_THRESHOLD = 10 * 1024; // 10KB @@ -205,6 +206,19 @@ Provide a natural language summary of the result. If the result is tabular data, return text; } + /** + * Suggest analysis queries based on loaded data + * @param count Number of queries to suggest (default: 3) + * @returns Array of suggested queries with scores and reasons + */ + async suggest(count: number = 3): Promise { + if (!this.dataInfo) { + throw new Error('No data loaded. Please call one of the load methods first (loadCSV, loadObject, loadURL, or loadText).'); + } + + return generateSuggestions(this.llmConfig, this.dataInfo, count); + } + /** * Clean up resources */ diff --git a/src/index.ts b/src/index.ts index 9be8d748d..763c92cd2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,5 +4,5 @@ * Main entry point */ export { AVA } from './ava'; -export type { AVAConfig, LLMConfig, AnalysisResponse, DatasetInfo, FieldMetadata, ChartType } from './types'; +export type { AVAConfig, LLMConfig, AnalysisResponse, DatasetInfo, FieldMetadata, ChartType, SuggestResult } from './types'; export { loadCSV, loadObject, loadURL, loadText, extractMetadata, formatDatasetInfo } from './data'; diff --git a/src/suggest/index.ts b/src/suggest/index.ts new file mode 100644 index 000000000..0b0f0b5eb --- /dev/null +++ b/src/suggest/index.ts @@ -0,0 +1,95 @@ +/** + * Suggest module for generating recommended analysis queries + */ + +import { generateText } from 'ai'; +import { createOpenAI } from '@ai-sdk/openai'; + +import { formatDatasetInfo } from '../data'; + +import type { LLMConfig, DatasetInfo } from '../types'; + +/** + * Result of a suggested query + */ +export interface SuggestResult { + /** The suggested query string */ + query: string; + /** Score between 0-1 indicating meaningfulness */ + score: number; + /** Reason for the score */ + reason: string; +} + +/** + * Generate suggested analysis queries based on dataset + */ +export async function generateSuggestions( + llmConfig: LLMConfig, + dataInfo: DatasetInfo, + count: number = 3 +): Promise { + const openai = createOpenAI({ + apiKey: llmConfig.apiKey, + baseURL: llmConfig.baseURL, + }); + + const dataInfoStr = formatDatasetInfo(dataInfo); + + const prompt = `You are a data analysis expert. Based on the following dataset information, suggest ${count} most meaningful analysis queries that would provide valuable insights. + +${dataInfoStr} + +For each suggested query, provide: +1. The query text (a natural language question that can be analyzed) +2. A score between 0 and 1 indicating how meaningful/valuable this analysis would be (1 being most meaningful) +3. A brief reason explaining why this query is valuable + +Consider queries that: +- Reveal important patterns or trends +- Answer key business questions +- Identify anomalies or outliers +- Show relationships between variables +- Provide actionable insights + +Return ONLY a valid JSON array with ${count} objects, each having "query", "score", and "reason" fields. The array should be sorted by score in descending order. + +Example format: +[ + { + "query": "What is the average revenue by region?", + "score": 0.95, + "reason": "Understanding revenue distribution across regions helps identify high-performing areas and potential growth opportunities" + } +] + +Generate the JSON array now:`; + + const { text } = await generateText({ + model: openai(llmConfig.model) as any, + prompt, + }); + + // Parse the JSON response + try { + const jsonMatch = text.match(/\[[\s\S]*\]/); + if (!jsonMatch) { + throw new Error('No JSON array found in response'); + } + + const suggestions = JSON.parse(jsonMatch[0]) as SuggestResult[]; + + // Validate and normalize the results + return suggestions + .filter(s => s.query && typeof s.score === 'number' && s.reason) + .map(s => ({ + query: s.query, + score: Math.max(0, Math.min(1, s.score)), // Clamp score between 0-1 + reason: s.reason, + })) + .sort((a, b) => b.score - a.score) // Sort by score descending + .slice(0, count); // Ensure we return exactly count items + } catch (error) { + throw new Error(`Failed to parse suggestions: ${error instanceof Error ? error.message : String(error)}`); + } +} diff --git a/src/types.ts b/src/types.ts index ba7dc2d20..6577e388f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -98,3 +98,15 @@ export type ChartType = | 'sankey' | 'table' | 'summary'; + +/** + * Result of a suggested query + */ +export interface SuggestResult { + /** The suggested query string */ + query: string; + /** Score between 0-1 indicating meaningfulness */ + score: number; + /** Reason for the score */ + reason: string; +} From 9a37ce06147d95a65088c5655e70bf060f95ba6d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 13:25:24 +0000 Subject: [PATCH 3/8] Remove duplicate SuggestResult interface definition Co-authored-by: hustcc <7856674+hustcc@users.noreply.github.com> --- src/suggest/index.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/suggest/index.ts b/src/suggest/index.ts index 0b0f0b5eb..531e40d7a 100644 --- a/src/suggest/index.ts +++ b/src/suggest/index.ts @@ -7,19 +7,7 @@ import { createOpenAI } from '@ai-sdk/openai'; import { formatDatasetInfo } from '../data'; -import type { LLMConfig, DatasetInfo } from '../types'; - -/** - * Result of a suggested query - */ -export interface SuggestResult { - /** The suggested query string */ - query: string; - /** Score between 0-1 indicating meaningfulness */ - score: number; - /** Reason for the score */ - reason: string; -} +import type { LLMConfig, DatasetInfo, SuggestResult } from '../types'; /** * Generate suggested analysis queries based on dataset From a1be288d7974d6951ee128c857079943a6857140 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 13:26:22 +0000 Subject: [PATCH 4/8] Add suggest usage example Co-authored-by: hustcc <7856674+hustcc@users.noreply.github.com> --- examples/suggest-example.ts | 53 +++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 examples/suggest-example.ts diff --git a/examples/suggest-example.ts b/examples/suggest-example.ts new file mode 100644 index 000000000..ec7c2f2de --- /dev/null +++ b/examples/suggest-example.ts @@ -0,0 +1,53 @@ +/** + * Example: Using the suggest() method + * + * This example demonstrates how to use the suggest() method + * to get AI-recommended analysis queries based on your data. + */ + +import { AVA } from '@antv/ava'; + +async function main() { + // Initialize AVA with LLM config + const ava = new AVA({ + llm: { + model: 'ling-1t', + apiKey: process.env.LING_1T_API_KEY || 'YOUR_API_KEY', + baseURL: 'https://api.tbox.cn/api/llm/v1', + }, + }); + + // Load sample data + await ava.loadObject([ + { city: '杭州', gdp: 18753, population: 1220 }, + { city: '上海', gdp: 43214, population: 2489 }, + { city: '北京', gdp: 35371, population: 2188 }, + { city: '深圳', gdp: 30664, population: 1768 }, + { city: '广州', gdp: 28839, population: 1868 }, + ]); + + console.log('Getting query suggestions...\n'); + + // Get 5 suggested queries + const queries = await ava.suggest(5); + + console.log('Suggested Analysis Queries:\n'); + queries.forEach((suggestion, index) => { + console.log(`${index + 1}. Query: ${suggestion.query}`); + console.log(` Score: ${(suggestion.score * 100).toFixed(1)}%`); + console.log(` Reason: ${suggestion.reason}\n`); + }); + + // Use the top suggested query for analysis + if (queries.length > 0) { + console.log('\nAnalyzing with the top suggestion...\n'); + const result = await ava.analysis(queries[0].query); + console.log('Analysis Result:'); + console.log(result.text); + } + + // Clean up + ava.dispose(); +} + +main().catch(console.error); From 05040431e23e8b34bded71cc5f04a0b497b561d6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 13:34:47 +0000 Subject: [PATCH 5/8] Fix prompt to end with period as requested Co-authored-by: hustcc <7856674+hustcc@users.noreply.github.com> --- src/suggest/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/suggest/index.ts b/src/suggest/index.ts index 531e40d7a..12e1eca26 100644 --- a/src/suggest/index.ts +++ b/src/suggest/index.ts @@ -51,7 +51,7 @@ Example format: } ] -Generate the JSON array now:`; +Generate the JSON array now.`; const { text } = await generateText({ model: openai(llmConfig.model) as any, From 9b9d6ed8195f7bd911c55b34c06320201a68f276 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 13:43:21 +0000 Subject: [PATCH 6/8] Add suggest API documentation and refresh button in UI Co-authored-by: hustcc <7856674+hustcc@users.noreply.github.com> --- site/components/Documentation.tsx | 34 +++++++++++++++++ site/components/Visualization.tsx | 63 ++++++++++++++++++++++++++----- 2 files changed, 87 insertions(+), 10 deletions(-) diff --git a/site/components/Documentation.tsx b/site/components/Documentation.tsx index 284c5ce0c..adbcf9ef6 100644 --- a/site/components/Documentation.tsx +++ b/site/components/Documentation.tsx @@ -235,6 +235,21 @@ ava.dispose();`} + + {/* Suggest */} +
+

Query Suggestions

+
+ suggest(count?: number) +

Get AI-recommended analysis queries based on dataset characteristics (default: 3)

+
+
Returns array of objects with:
+
query — Suggested analysis question
+
score — Meaningfulness score (0-1)
+
reason — Explanation for the suggestion
+
+
+
@@ -245,6 +260,25 @@ ava.dispose();`} Usage Examples
+
+

Query Suggestions

+
+
{`await ava.loadObject([
+  { city: 'Hangzhou', gdp: 18753 },
+  { city: 'Shanghai', gdp: 43214 }
+]);
+
+// Get 5 suggested queries
+const suggestions = await ava.suggest(5);
+console.log(suggestions[0]);
+// { query: "What is the average GDP?", 
+//   score: 0.95, 
+//   reason: "Reveals economic patterns" }
+
+// Use suggested query for analysis
+const result = await ava.analysis(suggestions[0].query);`}
+
+

Browser File Upload

diff --git a/site/components/Visualization.tsx b/site/components/Visualization.tsx index 207b06ad6..7172a8e5d 100644 --- a/site/components/Visualization.tsx +++ b/site/components/Visualization.tsx @@ -42,6 +42,7 @@ const copyToClipboard = async (html: string): Promise => { const Visualization: React.FC = ({ avaInstance, data, isInitialized }) => { const [query, setQuery] = useState(''); const [isLoading, setIsLoading] = useState(false); + const [isSuggesting, setIsSuggesting] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); const [showCode, setShowCode] = useState(false); @@ -87,6 +88,29 @@ const Visualization: React.FC = ({ avaInstance, data, isInit } }, [query, avaInstance]); + const handleSuggest = useCallback(async () => { + if (!avaInstance) { + setError('Please configure your LLM API key first. Click the LLM button in the header to set up.'); + return; + } + + setIsSuggesting(true); + setError(null); + + try { + // Get suggestions from AVA instance + const suggestions = await avaInstance.suggest(1); + if (suggestions.length > 0) { + // Fill the input with the top suggestion + setQuery(suggestions[0].query); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to get suggestions'); + } finally { + setIsSuggesting(false); + } + }, [avaInstance]); + // Update iframe content when visualization changes useEffect(() => { if (result?.visualizationHTML && iframeRef.current) { @@ -147,18 +171,37 @@ const Visualization: React.FC = ({ avaInstance, data, isInit {/* Query Input */}
- setQuery(e.target.value)} - onKeyDown={e => e.key === 'Enter' && handleGenerate()} - placeholder="Create a trend line comparing North America and Europe sales growth" - className="flex-1 px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#78d3f8]/50 focus:border-[#78d3f8] transition-all text-sm" - disabled={isLoading} - /> +
+ setQuery(e.target.value)} + onKeyDown={e => e.key === 'Enter' && handleGenerate()} + placeholder="Create a trend line comparing North America and Europe sales growth" + className="flex-1 px-4 py-3 bg-gray-50 border border-gray-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-[#78d3f8]/50 focus:border-[#78d3f8] transition-all text-sm" + disabled={isLoading || isSuggesting} + /> + +
From 6ea5c6ad08958ebbbfb9fb41d50000ad08b7c979 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 13:56:05 +0000 Subject: [PATCH 8/8] Update llms.txt with suggest API documentation Co-authored-by: hustcc <7856674+hustcc@users.noreply.github.com> --- llms.txt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/llms.txt b/llms.txt index d66aa8ce5..fec35b604 100644 --- a/llms.txt +++ b/llms.txt @@ -9,6 +9,7 @@ AVA, a complete rewrite focused on **AI-native Visual Analytics**. The first **A ## Key Features - **Natural Language Queries**: Ask questions about your data in plain English +- **Query Suggestions**: Get AI-recommended analysis queries based on your data characteristics - **LLM-Powered Analysis**: Leverages large language models for intelligent data analysis - **Smart Data Handling**: Automatically chooses between in-memory processing and SQLite based on data size - **Modular Architecture**: Clean separation of concerns with data, analysis, and visualization modules @@ -35,10 +36,19 @@ await ava.loadObject([{ city: '杭州', gdp: 18753 }, { city: '上海', gdp: 432 await ava.loadURL('https://api.example.com/data', (response) => response.data); await ava.loadText('杭州 100,上海 200,北京 300'); +// Get AI-suggested queries +const suggestions = await ava.suggest(5); // Get top 5 suggestions (default: 3) +console.log(suggestions[0]); +// { query: "What is the average GDP by city?", score: 0.95, reason: "Reveals economic patterns across cities" } + // Ask questions in natural language const result = await ava.analysis('What is the average revenue by region?'); console.log(result); +// Or use a suggested query +const suggestedResult = await ava.analysis(suggestions[0].query); +console.log(suggestedResult); + // Clean up ava.dispose(); ``` @@ -65,6 +75,12 @@ AVA uses a modular pipeline architecture that processes user queries through dis - `loadURL(url: string, transform?: Function)` - Load data from URL - `loadText(text: string)` - Extract data from unstructured text using LLM +### Query Suggestions +- `suggest(count?: number)` - Get AI-recommended analysis queries based on dataset characteristics (default: 3) + - Returns: Array of `{ query: string, score: number, reason: string }` + - Queries are sorted by score in descending order + - Score ranges from 0 to 1, indicating meaningfulness + ### Analysis - `analysis(query: string)` - Analyze data with natural language query - Returns: `{ text, code, sql, visualizationHTML }`