Skip to content

Commit 30dbf5e

Browse files
committed
Enables AI extension integration
Adds support for AI-powered features via a new extension mechanism. This includes dynamically loading the AI extension's CSS and JS, and retrying the plugin mounting process with exponential backoff to ensure proper initialization. Also, provides a basic GRPC querying functionality which could call AI methods, and converts the AI response to standard data format. The AI plugin can be built from source or downloaded from a binary URL.
1 parent b565347 commit 30dbf5e

File tree

4 files changed

+122
-14
lines changed

4 files changed

+122
-14
lines changed

cmd/testdata/stores.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ stores:
1010
kind:
1111
name: atest-ext-ai
1212
enabled: true
13-
url: ""
13+
url: "unix:///tmp/atest-ext-ai.sock"
1414
readonly: false
1515
disabled: false
1616
properties:

console/atest-ui/src/views/Extension.vue

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,33 +9,55 @@ const props = defineProps<Props>()
99
const loading = ref(true)
1010
const loadPlugin = async (): Promise<void> => {
1111
try {
12+
// First load CSS
1213
API.GetPageOfCSS(props.name, (d) => {
1314
const style = document.createElement('style');
1415
style.textContent = d.message;
1516
document.head.appendChild(style);
1617
});
1718
19+
// Then load JS and mount plugin
1820
API.GetPageOfJS(props.name, (d) => {
1921
const script = document.createElement('script');
2022
script.type = 'text/javascript';
2123
script.textContent = d.message;
2224
document.head.appendChild(script);
2325
24-
const plugin = window.ATestPlugin;
25-
26-
if (plugin && plugin.mount) {
27-
console.log('extension load success');
28-
const container = document.getElementById("plugin-container");
29-
if (container) {
30-
container.innerHTML = ''; // Clear previous content
31-
plugin.mount(container);
26+
// Implement retry mechanism with exponential backoff
27+
const checkPluginLoad = (retries = 0, maxRetries = 10) => {
28+
const plugin = (window as any).ATestPlugin;
29+
30+
console.log(`Plugin load attempt ${retries + 1}/${maxRetries + 1}`);
31+
32+
if (plugin && plugin.mount) {
33+
console.log('extension load success');
34+
const container = document.getElementById("plugin-container");
35+
if (container) {
36+
container.innerHTML = ''; // Clear previous content
37+
plugin.mount(container);
38+
loading.value = false;
39+
} else {
40+
console.error('Plugin container not found');
41+
loading.value = false;
42+
}
43+
} else if (retries < maxRetries) {
44+
// Incremental retry mechanism: 50ms, 100ms, 150ms...
45+
const delay = 50 + retries * 50;
46+
console.log(`ATestPlugin not ready, retrying in ${delay}ms (attempt ${retries + 1}/${maxRetries + 1})`);
47+
setTimeout(() => checkPluginLoad(retries + 1, maxRetries), delay);
48+
} else {
49+
console.error('ATestPlugin not found or missing mount method after max retries');
50+
console.error('Window.ATestPlugin value:', (window as any).ATestPlugin);
51+
loading.value = false;
3252
}
33-
}
53+
};
54+
55+
// Start the retry mechanism
56+
checkPluginLoad();
3457
});
3558
} catch (error) {
36-
console.log(`extension load error: ${(error as Error).message}`)
37-
} finally {
38-
console.log('extension load finally');
59+
console.log(`extension load error: ${(error as Error).message}`);
60+
loading.value = false; // Set loading to false on error
3961
}
4062
};
4163
try {

pkg/testing/remote/grpc_store.go

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ package remote
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"errors"
2223
"fmt"
2324
"strconv"
25+
"strings"
2426
"time"
2527

2628
"github.com/linuxsuren/api-testing/pkg/logging"
@@ -316,6 +318,12 @@ func (g *gRPCLoader) PProf(name string) []byte {
316318
}
317319

318320
func (g *gRPCLoader) Query(query map[string]string) (result testing.DataResult, err error) {
321+
// Detect AI method calls
322+
if method := query["method"]; strings.HasPrefix(method, "ai.") {
323+
return g.handleAIQuery(query)
324+
}
325+
326+
// Original standard query logic
319327
var dataResult *server.DataQueryResult
320328
offset, _ := strconv.ParseInt(query["offset"], 10, 64)
321329
limit, _ := strconv.ParseInt(query["limit"], 10, 64)
@@ -444,3 +452,65 @@ func (g *gRPCLoader) Close() {
444452
g.conn.Close()
445453
}
446454
}
455+
456+
// handleAIQuery handles AI-specific queries
457+
func (g *gRPCLoader) handleAIQuery(query map[string]string) (testing.DataResult, error) {
458+
method := query["method"]
459+
460+
var dataQuery *server.DataQuery
461+
switch method {
462+
case "ai.generate":
463+
dataQuery = &server.DataQuery{
464+
Type: "ai",
465+
Key: "generate",
466+
Sql: g.encodeAIGenerateParams(query),
467+
}
468+
case "ai.capabilities":
469+
dataQuery = &server.DataQuery{
470+
Type: "ai",
471+
Key: "capabilities",
472+
Sql: "", // No additional parameters needed
473+
}
474+
default:
475+
return testing.DataResult{}, fmt.Errorf("unsupported AI method: %s", method)
476+
}
477+
478+
// Call existing gRPC Query
479+
dataResult, err := g.client.Query(g.ctx, dataQuery)
480+
if err != nil {
481+
return testing.DataResult{}, err
482+
}
483+
484+
// Convert response to testing.DataResult format
485+
return g.convertAIResponse(dataResult), nil
486+
}
487+
488+
// encodeAIGenerateParams encodes AI generation parameters into SQL field
489+
func (g *gRPCLoader) encodeAIGenerateParams(query map[string]string) string {
490+
params := map[string]string{
491+
"model": query["model"],
492+
"prompt": query["prompt"],
493+
"config": query["config"],
494+
}
495+
data, _ := json.Marshal(params)
496+
return string(data)
497+
}
498+
499+
// convertAIResponse converts AI response to standard format
500+
func (g *gRPCLoader) convertAIResponse(dataResult *server.DataQueryResult) testing.DataResult {
501+
result := testing.DataResult{
502+
Pairs: pairToMap(dataResult.Data),
503+
}
504+
505+
// Map AI-specific response fields
506+
if content := result.Pairs["generated_sql"]; content != "" {
507+
result.Pairs["content"] = content // Follow AI interface standard
508+
}
509+
if result.Pairs["error"] != "" {
510+
result.Pairs["success"] = "false"
511+
} else {
512+
result.Pairs["success"] = "true"
513+
}
514+
515+
return result
516+
}

tools/make/run.mk

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,28 @@
55
include tools/make/env.mk
66

77
ATEST_UI = console/atest-ui
8+
AI_PLUGIN_DIR := $(or $(AI_PLUGIN_SOURCE),../atest-ext-ai)
89

910
##@ Local runs & init env
1011

12+
.PHONY: build-ai-plugin
13+
build-ai-plugin:
14+
@if [ -n "$(AI_PLUGIN_BINARY_URL)" ]; then \
15+
echo "📥 Downloading AI plugin binary from $(AI_PLUGIN_BINARY_URL)..."; \
16+
mkdir -p bin; \
17+
curl -L "$(AI_PLUGIN_BINARY_URL)" | tar xz -C bin/ --strip-components=1; \
18+
echo "✅ AI plugin binary downloaded"; \
19+
elif [ -d "$(AI_PLUGIN_DIR)" ]; then \
20+
echo "🔨 Building AI plugin from source..."; \
21+
cd $(AI_PLUGIN_DIR) && make build; \
22+
echo "✅ AI plugin built from source"; \
23+
else \
24+
echo "⚠️ AI plugin directory not found, skipping"; \
25+
fi
26+
1127
.PHONY: run-server
1228
run-server: ## Run the API Testing server
13-
run-server: build-ui run-backend
29+
run-server: build-ui build-ai-plugin run-backend
1430
run-backend:
1531
go run . server --local-storage 'bin/*.yaml' --console-path ${ATEST_UI}/dist \
1632
--extension-registry ghcr.io --download-timeout 10m

0 commit comments

Comments
 (0)