Skip to content

Commit 0e68f5e

Browse files
authored
Add suggest() method for LLM-powered query recommendations with site UI integration (#950)
1 parent 8e0b771 commit 0e68f5e

10 files changed

Lines changed: 440 additions & 12 deletions

File tree

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
AVA is a fundamental shift from rule-based analytics to AI-native capabilities:
2929

3030
- **Natural Language Queries**: Ask questions about your data in plain English
31+
- **Query Suggestions**: Get AI-recommended analysis queries based on your data characteristics
3132
- **LLM-Powered Analysis**: Leverages large language models for intelligent data analysis
3233
- **Smart Data Handling**: Automatically chooses between in-memory processing and SQLite based on data size
3334
- **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);
7879
// or extract from text
7980
await ava.loadText('杭州 100,上海 200,北京 300');
8081

82+
// Get suggested analysis queries
83+
const queries = await ava.suggest(5); // Get top 5 suggested queries (default: 3)
84+
console.log(queries);
85+
// [
86+
// {
87+
// query: 'What is the average revenue by region?',
88+
// score: 0.95,
89+
// reason: 'Understanding revenue distribution across regions helps identify high-performing areas'
90+
// },
91+
// ...
92+
// ]
93+
8194
// Ask questions in natural language
8295
const result = await ava.analysis('What is the average revenue by region?');
8396
console.log(result);
8497

98+
// Or use a suggested query
99+
const suggestedResult = await ava.analysis(queries[0].query);
100+
console.log(suggestedResult);
101+
85102
// Clean up
86103
ava.dispose();
87104
```

__tests__/suggest.test.ts

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/**
2+
* Tests for suggest functionality
3+
*/
4+
5+
import * as path from 'path';
6+
7+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
8+
9+
import { AVA } from '../src/ava';
10+
11+
describe('Suggest Tests', () => {
12+
let ava: AVA;
13+
const testDataPath = path.join(__dirname, '../data/companies.csv');
14+
15+
const apiKey = process.env.LING_1T_API_KEY;
16+
const skipLLMTests = !apiKey;
17+
18+
const getLLMConfig = () => ({
19+
model: 'ling-1t',
20+
apiKey: apiKey || '',
21+
baseURL: 'https://api.tbox.cn/api/llm/v1',
22+
});
23+
24+
beforeEach(() => {
25+
if (skipLLMTests) {
26+
// eslint-disable-next-line no-console
27+
console.log('Skipping LLM integration test: LING_1T_API_KEY not set');
28+
return;
29+
}
30+
31+
ava = new AVA({
32+
llm: getLLMConfig(),
33+
});
34+
});
35+
36+
afterEach(() => {
37+
if (ava) {
38+
ava.dispose();
39+
}
40+
});
41+
42+
describe('suggest method', () => {
43+
it('should throw error when suggesting without loading data', async () => {
44+
if (skipLLMTests) return;
45+
46+
await expect(ava.suggest()).rejects.toThrow('No data loaded');
47+
});
48+
49+
it('should return 3 suggestions by default', async () => {
50+
if (skipLLMTests) return;
51+
52+
try {
53+
await ava.loadCSV(testDataPath);
54+
const suggestions = await ava.suggest();
55+
56+
expect(suggestions).toBeDefined();
57+
expect(Array.isArray(suggestions)).toBe(true);
58+
expect(suggestions.length).toBe(3);
59+
60+
// Verify each suggestion has required fields
61+
for (const suggestion of suggestions) {
62+
expect(suggestion).toHaveProperty('query');
63+
expect(suggestion).toHaveProperty('score');
64+
expect(suggestion).toHaveProperty('reason');
65+
expect(typeof suggestion.query).toBe('string');
66+
expect(typeof suggestion.score).toBe('number');
67+
expect(typeof suggestion.reason).toBe('string');
68+
expect(suggestion.query.length).toBeGreaterThan(0);
69+
expect(suggestion.reason.length).toBeGreaterThan(0);
70+
// Score should be between 0 and 1
71+
expect(suggestion.score).toBeGreaterThanOrEqual(0);
72+
expect(suggestion.score).toBeLessThanOrEqual(1);
73+
}
74+
} catch (error) {
75+
// eslint-disable-next-line no-console
76+
console.log('Skipping test due to API error:', error instanceof Error ? error.message : String(error));
77+
}
78+
}, 60000);
79+
80+
it('should return specified number of suggestions', async () => {
81+
if (skipLLMTests) return;
82+
83+
try {
84+
await ava.loadCSV(testDataPath);
85+
const suggestions = await ava.suggest(5);
86+
87+
expect(suggestions).toBeDefined();
88+
expect(Array.isArray(suggestions)).toBe(true);
89+
expect(suggestions.length).toBe(5);
90+
} catch (error) {
91+
// eslint-disable-next-line no-console
92+
console.log('Skipping test due to API error:', error instanceof Error ? error.message : String(error));
93+
}
94+
}, 60000);
95+
96+
it('should return suggestions sorted by score in descending order', async () => {
97+
if (skipLLMTests) return;
98+
99+
try {
100+
await ava.loadCSV(testDataPath);
101+
const suggestions = await ava.suggest();
102+
103+
// Verify suggestions are sorted by score descending
104+
for (let i = 0; i < suggestions.length - 1; i++) {
105+
expect(suggestions[i].score).toBeGreaterThanOrEqual(suggestions[i + 1].score);
106+
}
107+
} catch (error) {
108+
// eslint-disable-next-line no-console
109+
console.log('Skipping test due to API error:', error instanceof Error ? error.message : String(error));
110+
}
111+
}, 60000);
112+
113+
it('should work with loadObject', async () => {
114+
if (skipLLMTests) return;
115+
116+
try {
117+
await ava.loadObject([
118+
{ city: '杭州', gdp: 18753 },
119+
{ city: '上海', gdp: 43214 },
120+
{ city: '北京', gdp: 35371 },
121+
]);
122+
123+
const suggestions = await ava.suggest();
124+
125+
expect(suggestions).toBeDefined();
126+
expect(Array.isArray(suggestions)).toBe(true);
127+
expect(suggestions.length).toBe(3);
128+
} catch (error) {
129+
// eslint-disable-next-line no-console
130+
console.log('Skipping test due to API error:', error instanceof Error ? error.message : String(error));
131+
}
132+
}, 60000);
133+
134+
it('should generate queries that can be used with analysis', async () => {
135+
if (skipLLMTests) return;
136+
137+
try {
138+
await ava.loadCSV(testDataPath);
139+
const suggestions = await ava.suggest(1);
140+
141+
expect(suggestions.length).toBeGreaterThan(0);
142+
143+
// Try to analyze using the first suggested query
144+
const result = await ava.analysis(suggestions[0].query);
145+
146+
expect(result).toBeDefined();
147+
expect(result.text).toBeDefined();
148+
expect(typeof result.text).toBe('string');
149+
expect(result.text.length).toBeGreaterThan(0);
150+
} catch (error) {
151+
// eslint-disable-next-line no-console
152+
console.log('Skipping test due to API error:', error instanceof Error ? error.message : String(error));
153+
}
154+
}, 120000); // Longer timeout for this test
155+
});
156+
});

examples/suggest-example.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Example: Using the suggest() method
3+
*
4+
* This example demonstrates how to use the suggest() method
5+
* to get AI-recommended analysis queries based on your data.
6+
*/
7+
8+
import { AVA } from '@antv/ava';
9+
10+
async function main() {
11+
// Initialize AVA with LLM config
12+
const ava = new AVA({
13+
llm: {
14+
model: 'ling-1t',
15+
apiKey: process.env.LING_1T_API_KEY || 'YOUR_API_KEY',
16+
baseURL: 'https://api.tbox.cn/api/llm/v1',
17+
},
18+
});
19+
20+
// Load sample data
21+
await ava.loadObject([
22+
{ city: '杭州', gdp: 18753, population: 1220 },
23+
{ city: '上海', gdp: 43214, population: 2489 },
24+
{ city: '北京', gdp: 35371, population: 2188 },
25+
{ city: '深圳', gdp: 30664, population: 1768 },
26+
{ city: '广州', gdp: 28839, population: 1868 },
27+
]);
28+
29+
console.log('Getting query suggestions...\n');
30+
31+
// Get 5 suggested queries
32+
const queries = await ava.suggest(5);
33+
34+
console.log('Suggested Analysis Queries:\n');
35+
queries.forEach((suggestion, index) => {
36+
console.log(`${index + 1}. Query: ${suggestion.query}`);
37+
console.log(` Score: ${(suggestion.score * 100).toFixed(1)}%`);
38+
console.log(` Reason: ${suggestion.reason}\n`);
39+
});
40+
41+
// Use the top suggested query for analysis
42+
if (queries.length > 0) {
43+
console.log('\nAnalyzing with the top suggestion...\n');
44+
const result = await ava.analysis(queries[0].query);
45+
console.log('Analysis Result:');
46+
console.log(result.text);
47+
}
48+
49+
// Clean up
50+
ava.dispose();
51+
}
52+
53+
main().catch(console.error);

llms.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ AVA, a complete rewrite focused on **AI-native Visual Analytics**. The first **A
99
## Key Features
1010

1111
- **Natural Language Queries**: Ask questions about your data in plain English
12+
- **Query Suggestions**: Get AI-recommended analysis queries based on your data characteristics
1213
- **LLM-Powered Analysis**: Leverages large language models for intelligent data analysis
1314
- **Smart Data Handling**: Automatically chooses between in-memory processing and SQLite based on data size
1415
- **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
3536
await ava.loadURL('https://api.example.com/data', (response) => response.data);
3637
await ava.loadText('杭州 100,上海 200,北京 300');
3738

39+
// Get AI-suggested queries
40+
const suggestions = await ava.suggest(5); // Get top 5 suggestions (default: 3)
41+
console.log(suggestions[0]);
42+
// { query: "What is the average GDP by city?", score: 0.95, reason: "Reveals economic patterns across cities" }
43+
3844
// Ask questions in natural language
3945
const result = await ava.analysis('What is the average revenue by region?');
4046
console.log(result);
4147

48+
// Or use a suggested query
49+
const suggestedResult = await ava.analysis(suggestions[0].query);
50+
console.log(suggestedResult);
51+
4252
// Clean up
4353
ava.dispose();
4454
```
@@ -65,6 +75,12 @@ AVA uses a modular pipeline architecture that processes user queries through dis
6575
- `loadURL(url: string, transform?: Function)` - Load data from URL
6676
- `loadText(text: string)` - Extract data from unstructured text using LLM
6777

78+
### Query Suggestions
79+
- `suggest(count?: number)` - Get AI-recommended analysis queries based on dataset characteristics (default: 3)
80+
- Returns: Array of `{ query: string, score: number, reason: string }`
81+
- Queries are sorted by score in descending order
82+
- Score ranges from 0 to 1, indicating meaningfulness
83+
6884
### Analysis
6985
- `analysis(query: string)` - Analyze data with natural language query
7086
- Returns: `{ text, code, sql, visualizationHTML }`

site/components/Documentation.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,21 @@ ava.dispose();`}</pre>
235235
</div>
236236
</div>
237237
</div>
238+
239+
{/* Suggest */}
240+
<div>
241+
<h3 className="text-lg font-semibold text-gray-800 mb-3">Query Suggestions</h3>
242+
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
243+
<code className="text-sm text-[#78d3f8] block mb-2">suggest(count?: number)</code>
244+
<p className="text-sm text-gray-600 mb-3">Get AI-recommended analysis queries based on dataset characteristics (default: 3)</p>
245+
<div className="text-xs text-gray-500 space-y-1">
246+
<div className="font-semibold mb-2">Returns array of objects with:</div>
247+
<div><code className="bg-white px-1 rounded">query</code> — Suggested analysis question</div>
248+
<div><code className="bg-white px-1 rounded">score</code> — Meaningfulness score (0-1)</div>
249+
<div><code className="bg-white px-1 rounded">reason</code> — Explanation for the suggestion</div>
250+
</div>
251+
</div>
252+
</div>
238253
</div>
239254
</section>
240255

@@ -245,6 +260,25 @@ ava.dispose();`}</pre>
245260
Usage Examples
246261
</h2>
247262
<div className="space-y-6">
263+
<div className="border-l-4 border-[#78d3f8] pl-4">
264+
<h4 className="font-semibold text-gray-800 mb-2">Query Suggestions</h4>
265+
<div className="bg-gray-900 text-gray-100 rounded-lg p-4 font-mono text-xs overflow-x-auto">
266+
<pre className="whitespace-pre">{`await ava.loadObject([
267+
{ city: 'Hangzhou', gdp: 18753 },
268+
{ city: 'Shanghai', gdp: 43214 }
269+
]);
270+
271+
// Get 5 suggested queries
272+
const suggestions = await ava.suggest(5);
273+
console.log(suggestions[0]);
274+
// { query: "What is the average GDP?",
275+
// score: 0.95,
276+
// reason: "Reveals economic patterns" }
277+
278+
// Use suggested query for analysis
279+
const result = await ava.analysis(suggestions[0].query);`}</pre>
280+
</div>
281+
</div>
248282
<div className="border-l-4 border-[#78d3f8] pl-4">
249283
<h4 className="font-semibold text-gray-800 mb-2">Browser File Upload</h4>
250284
<div className="bg-gray-900 text-gray-100 rounded-lg p-4 font-mono text-xs overflow-x-auto">

0 commit comments

Comments
 (0)