Skip to content

Commit 1d95342

Browse files
authored
feat: implement AI plugin calling interface with dedicated endpoints (#826)
1 parent f10635b commit 1d95342

File tree

7 files changed

+2983
-3283
lines changed

7 files changed

+2983
-3283
lines changed

pkg/server/ai_interface.go

Lines changed: 77 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -16,43 +16,85 @@ limitations under the License.
1616

1717
package server
1818

19-
// AIRequest represents a standard request to an AI plugin
20-
// Following the simplicity principle: model name + prompt + optional config
21-
type AIRequest struct {
22-
Model string `json:"model"` // Model identifier (e.g., "gpt-4", "claude")
23-
Prompt string `json:"prompt"` // The prompt or instruction
24-
Config map[string]interface{} `json:"config"` // Optional configuration (temperature, max_tokens, etc.)
25-
}
26-
27-
// AIResponse represents a standard response from an AI plugin
28-
type AIResponse struct {
29-
Content string `json:"content"` // The generated response
30-
Meta map[string]interface{} `json:"meta"` // Optional metadata (model info, timing, etc.)
31-
}
32-
33-
// AICapabilities represents what an AI plugin can do
34-
type AICapabilities struct {
35-
Models []string `json:"models"` // Supported models
36-
Features []string `json:"features"` // Supported features (chat, completion, etc.)
37-
Limits map[string]int `json:"limits"` // Limits (max_tokens, rate_limit, etc.)
38-
Description string `json:"description"` // Plugin description
39-
Version string `json:"version"` // Plugin version
40-
}
19+
// AI Plugin Communication Interface Standards
20+
// AI plugins use the existing testing.Loader.Query(map[string]string) interface
4121

4222
// Standard AI plugin communication methods
4323
const (
44-
AIMethodGenerate = "ai.generate" // Generate content from prompt
45-
AIMethodCapabilities = "ai.capabilities" // Get plugin capabilities
24+
AIMethodGenerate = "ai.generate" // Generate content from prompt
25+
AIMethodCapabilities = "ai.capabilities" // Get plugin capabilities
4626
)
4727

48-
// Standard plugin communication message format
49-
type PluginRequest struct {
50-
Method string `json:"method"` // Method name
51-
Payload interface{} `json:"payload"` // Request payload
52-
}
53-
54-
type PluginResponse struct {
55-
Success bool `json:"success"` // Whether request succeeded
56-
Data interface{} `json:"data"` // Response data
57-
Error string `json:"error"` // Error message if failed
58-
}
28+
// AI Plugin Query Parameter Standards
29+
// AI plugins are called using loader.Query(query map[string]string) with these parameters:
30+
31+
// For ai.generate:
32+
// - "method": "ai.generate"
33+
// - "model": model identifier (e.g., "gpt-4", "claude")
34+
// - "prompt": the prompt or instruction
35+
// - "config": optional JSON configuration string (e.g., `{"temperature": 0.7, "max_tokens": 1000}`)
36+
37+
// For ai.capabilities:
38+
// - "method": "ai.capabilities"
39+
40+
// AI Plugin Response Standards
41+
// AI plugins return response through testing.DataResult.Pairs with these keys:
42+
43+
// For successful ai.generate:
44+
// - "content": the generated content
45+
// - "meta": optional JSON metadata string (model info, timing, etc.)
46+
// - "success": "true"
47+
48+
// For successful ai.capabilities:
49+
// - "capabilities": JSON string containing plugin capabilities
50+
// - "models": JSON array of supported models (fallback if capabilities not available)
51+
// - "features": JSON array of supported features (fallback)
52+
// - "description": plugin description (fallback)
53+
// - "version": plugin version (fallback)
54+
// - "success": "true"
55+
56+
// For errors:
57+
// - "error": error message
58+
// - "success": "false"
59+
60+
// Plugin Discovery
61+
// AI plugins are identified by having "ai" in their categories field:
62+
// categories: ["ai"]
63+
64+
// Usage Examples:
65+
//
66+
// Get AI plugins:
67+
// stores, err := server.GetStores(ctx, &SimpleQuery{Kind: "ai"})
68+
//
69+
// Call AI plugin:
70+
// loader, err := server.getLoaderByStoreName("my-ai-plugin")
71+
// result, err := loader.Query(map[string]string{
72+
// "method": "ai.generate",
73+
// "model": "gpt-4",
74+
// "prompt": "Hello world",
75+
// "config": `{"temperature": 0.7}`,
76+
// })
77+
// content := result.Pairs["content"]
78+
79+
// Documentation structures (for reference only, actual types are generated from proto)
80+
// See server.proto for the actual message definitions:
81+
//
82+
// AIRequest fields:
83+
// - plugin_name: AI plugin name
84+
// - model: Model identifier (e.g., "gpt-4", "claude")
85+
// - prompt: The prompt or instruction
86+
// - config: JSON configuration string (optional)
87+
//
88+
// AIResponse fields:
89+
// - content: Generated content
90+
// - meta: JSON metadata string (optional)
91+
// - success: Whether the call succeeded
92+
// - error: Error message if failed
93+
//
94+
// AICapabilitiesResponse fields:
95+
// - models: Supported models
96+
// - features: Supported features
97+
// - description: Plugin description
98+
// - version: Plugin version
99+
// - success: Whether the call succeeded
100+
// - error: Error message if failed

pkg/server/remote_server.go

Lines changed: 138 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,63 +1766,146 @@ func (s *UniqueSlice[T]) GetAll() []T {
17661766

17671767
var errNoTestSuiteFound = errors.New("no test suite found")
17681768

1769-
// AI Plugin Communication Methods
1770-
// These methods provide communication interface for AI plugins using the existing ExtManager
1769+
// CallAI calls an AI plugin to generate content
1770+
func (s *server) CallAI(ctx context.Context, req *AIRequest) (*AIResponse, error) {
1771+
if req.GetPluginName() == "" {
1772+
return &AIResponse{
1773+
Success: false,
1774+
Error: "plugin_name is required",
1775+
}, nil
1776+
}
17711777

1772-
// GetAIPlugins returns all plugins with "ai" category
1773-
func (s *server) GetAIPlugins(ctx context.Context, req *Empty) (*StoreKinds, error) {
1774-
stores, err := s.GetStoreKinds(ctx, req)
1778+
// Get the loader for the AI plugin
1779+
loader, err := s.getLoaderByStoreName(req.GetPluginName())
17751780
if err != nil {
1776-
return nil, err
1777-
}
1778-
1779-
var aiPlugins []*StoreKind
1780-
for _, store := range stores.Data {
1781-
for _, category := range store.Categories {
1782-
if category == "ai" {
1783-
aiPlugins = append(aiPlugins, store)
1784-
break
1785-
}
1781+
return &AIResponse{
1782+
Success: false,
1783+
Error: fmt.Sprintf("failed to get AI plugin '%s': %v", req.GetPluginName(), err),
1784+
}, nil
1785+
}
1786+
defer loader.Close()
1787+
1788+
// Prepare query parameters
1789+
query := map[string]string{
1790+
"method": AIMethodGenerate,
1791+
"model": req.GetModel(),
1792+
"prompt": req.GetPrompt(),
1793+
}
1794+
1795+
// Add config (always include, even if empty)
1796+
query["config"] = req.GetConfig()
1797+
1798+
// Call the plugin using the Query interface
1799+
result, err := loader.Query(query)
1800+
if err != nil {
1801+
return &AIResponse{
1802+
Success: false,
1803+
Error: fmt.Sprintf("AI plugin call failed: %v", err),
1804+
}, nil
1805+
}
1806+
1807+
// Extract response from result
1808+
response := &AIResponse{
1809+
Success: true,
1810+
}
1811+
1812+
// Get content from result
1813+
if content, ok := result.Pairs["content"]; ok {
1814+
response.Content = content
1815+
}
1816+
1817+
// Get metadata if available
1818+
if meta, ok := result.Pairs["meta"]; ok {
1819+
response.Meta = meta
1820+
}
1821+
1822+
// Check for errors from plugin
1823+
if errorMsg, ok := result.Pairs["error"]; ok && errorMsg != "" {
1824+
response.Success = false
1825+
response.Error = errorMsg
1826+
}
1827+
1828+
// Check success flag from plugin
1829+
if success, ok := result.Pairs["success"]; ok && success == "false" {
1830+
response.Success = false
1831+
if response.Error == "" {
1832+
response.Error = "AI plugin returned failure status"
17861833
}
17871834
}
1788-
1789-
return &StoreKinds{Data: aiPlugins}, nil
1790-
}
1791-
1792-
// SendAIRequest sends a request to an AI plugin using the standard plugin communication protocol
1793-
func (s *server) SendAIRequest(ctx context.Context, pluginName string, req *AIRequest) (*AIResponse, error) {
1794-
// This would communicate with the AI plugin via unix socket using PluginRequest/PluginResponse
1795-
// Implementation would be similar to other plugin communications
1796-
1797-
// TODO: Send pluginReq to plugin via storeExtMgr communication channel
1798-
// pluginReq := &PluginRequest{
1799-
// Method: AIMethodGenerate,
1800-
// Payload: req,
1801-
// }
1802-
1803-
remoteServerLogger.Info("Sending AI request", "plugin", pluginName, "model", req.Model)
1804-
1805-
return &AIResponse{
1806-
Content: "AI response placeholder - implementation needed",
1807-
Meta: map[string]interface{}{"plugin": pluginName, "model": req.Model},
1808-
}, nil
1809-
}
1810-
1811-
// GetAICapabilities gets capabilities from an AI plugin
1812-
func (s *server) GetAICapabilities(ctx context.Context, pluginName string) (*AICapabilities, error) {
1813-
// TODO: Send pluginReq to plugin via storeExtMgr communication channel
1814-
// pluginReq := &PluginRequest{
1815-
// Method: AIMethodCapabilities,
1816-
// Payload: &Empty{},
1817-
// }
1818-
1819-
remoteServerLogger.Info("Getting AI capabilities", "plugin", pluginName)
1820-
1821-
return &AICapabilities{
1822-
Models: []string{"placeholder-model"},
1823-
Features: []string{"generate", "capabilities"},
1824-
Limits: map[string]int{"max_tokens": 4096},
1825-
Description: "AI plugin capabilities placeholder",
1826-
Version: "1.0.0",
1827-
}, nil
1835+
1836+
remoteServerLogger.Info("AI plugin called",
1837+
"plugin", req.GetPluginName(),
1838+
"model", req.GetModel(),
1839+
"success", response.GetSuccess())
1840+
1841+
return response, nil
1842+
}
1843+
1844+
// GetAICapabilities gets the capabilities of an AI plugin
1845+
func (s *server) GetAICapabilities(ctx context.Context, req *AICapabilitiesRequest) (*AICapabilitiesResponse, error) {
1846+
if req.GetPluginName() == "" {
1847+
return &AICapabilitiesResponse{
1848+
Success: false,
1849+
Error: "plugin_name is required",
1850+
}, nil
1851+
}
1852+
1853+
// Get the loader for the AI plugin
1854+
loader, err := s.getLoaderByStoreName(req.GetPluginName())
1855+
if err != nil {
1856+
return &AICapabilitiesResponse{
1857+
Success: false,
1858+
Error: fmt.Sprintf("failed to get AI plugin '%s': %v", req.GetPluginName(), err),
1859+
}, nil
1860+
}
1861+
defer loader.Close()
1862+
1863+
// Query for capabilities
1864+
query := map[string]string{
1865+
"method": AIMethodCapabilities,
1866+
}
1867+
1868+
result, err := loader.Query(query)
1869+
if err != nil {
1870+
return &AICapabilitiesResponse{
1871+
Success: false,
1872+
Error: fmt.Sprintf("failed to get capabilities: %v", err),
1873+
}, nil
1874+
}
1875+
1876+
// Build response from result
1877+
response := &AICapabilitiesResponse{
1878+
Success: true,
1879+
}
1880+
1881+
// Parse capabilities from result
1882+
if models, ok := result.Pairs["models"]; ok {
1883+
// Try to parse as JSON array first
1884+
response.Models = strings.Split(models, ",")
1885+
}
1886+
1887+
if features, ok := result.Pairs["features"]; ok {
1888+
response.Features = strings.Split(features, ",")
1889+
}
1890+
1891+
if description, ok := result.Pairs["description"]; ok {
1892+
response.Description = description
1893+
}
1894+
1895+
if version, ok := result.Pairs["version"]; ok {
1896+
response.Version = version
1897+
}
1898+
1899+
// Check for errors
1900+
if errorMsg, ok := result.Pairs["error"]; ok && errorMsg != "" {
1901+
response.Success = false
1902+
response.Error = errorMsg
1903+
}
1904+
1905+
remoteServerLogger.Info("AI plugin capabilities retrieved",
1906+
"plugin", req.GetPluginName(),
1907+
"models", len(response.GetModels()),
1908+
"features", len(response.GetFeatures()))
1909+
1910+
return response, nil
18281911
}

0 commit comments

Comments
 (0)