Implement comprehensive A/B testing system for landing page optimization: - Database models for experiments, variants, and events - AI-powered variant generation and analysis - Visitor tracking with conversion metrics - Experiment lifecycle management (draft/active/paused/completed) - Email notifications for experiment results - Cron job for automated experiment monitoring - UI for viewing experiment results and statistics
238 lines
7.2 KiB
Go
238 lines
7.2 KiB
Go
// Copyright 2026 MarketAlly. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package ai
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"time"
|
|
|
|
"code.gitcaddy.com/server/v3/modules/json"
|
|
"code.gitcaddy.com/server/v3/modules/log"
|
|
"code.gitcaddy.com/server/v3/modules/setting"
|
|
)
|
|
|
|
// Client is the AI service client
|
|
type Client struct {
|
|
httpClient *http.Client
|
|
baseURL string
|
|
token string
|
|
}
|
|
|
|
var defaultClient *Client
|
|
|
|
// GetClient returns the default AI client
|
|
func GetClient() *Client {
|
|
if defaultClient == nil {
|
|
defaultClient = NewClient(setting.AI.ServiceURL, setting.AI.ServiceToken, setting.AI.Timeout)
|
|
}
|
|
return defaultClient
|
|
}
|
|
|
|
// NewClient creates a new AI service client
|
|
func NewClient(baseURL, token string, timeout time.Duration) *Client {
|
|
return &Client{
|
|
httpClient: &http.Client{
|
|
Timeout: timeout,
|
|
},
|
|
baseURL: baseURL,
|
|
token: token,
|
|
}
|
|
}
|
|
|
|
// IsEnabled returns true if AI service is enabled
|
|
func IsEnabled() bool {
|
|
return setting.AI.Enabled
|
|
}
|
|
|
|
// doRequest performs an HTTP request to the AI service
|
|
func (c *Client) doRequest(ctx context.Context, method, endpoint string, body, result any) error {
|
|
var reqBody io.Reader
|
|
if body != nil {
|
|
jsonBody, err := json.Marshal(body)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal request: %w", err)
|
|
}
|
|
reqBody = bytes.NewBuffer(jsonBody)
|
|
}
|
|
|
|
url := fmt.Sprintf("http://%s/api/v1%s", c.baseURL, endpoint)
|
|
req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
if c.token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+c.token)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("request failed: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode >= 400 {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("AI service error (status %d): %s", resp.StatusCode, string(body))
|
|
}
|
|
|
|
if result != nil {
|
|
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
|
|
return fmt.Errorf("failed to decode response: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReviewPullRequest requests an AI review of a pull request
|
|
func (c *Client) ReviewPullRequest(ctx context.Context, req *ReviewPullRequestRequest) (*ReviewPullRequestResponse, error) {
|
|
if !IsEnabled() || !setting.AI.EnableCodeReview {
|
|
return nil, errors.New("AI code review is not enabled")
|
|
}
|
|
|
|
var resp ReviewPullRequestResponse
|
|
if err := c.doRequest(ctx, http.MethodPost, "/review/pull-request", req, &resp); err != nil {
|
|
log.Error("AI ReviewPullRequest failed: %v", err)
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// TriageIssue requests AI triage for an issue
|
|
func (c *Client) TriageIssue(ctx context.Context, req *TriageIssueRequest) (*TriageIssueResponse, error) {
|
|
if !IsEnabled() || !setting.AI.EnableIssueTriage {
|
|
return nil, errors.New("AI issue triage is not enabled")
|
|
}
|
|
|
|
var resp TriageIssueResponse
|
|
if err := c.doRequest(ctx, http.MethodPost, "/issues/triage", req, &resp); err != nil {
|
|
log.Error("AI TriageIssue failed: %v", err)
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// SuggestLabels requests AI label suggestions
|
|
func (c *Client) SuggestLabels(ctx context.Context, req *SuggestLabelsRequest) (*SuggestLabelsResponse, error) {
|
|
if !IsEnabled() || !setting.AI.EnableIssueTriage {
|
|
return nil, errors.New("AI issue triage is not enabled")
|
|
}
|
|
|
|
var resp SuggestLabelsResponse
|
|
if err := c.doRequest(ctx, http.MethodPost, "/issues/suggest-labels", req, &resp); err != nil {
|
|
log.Error("AI SuggestLabels failed: %v", err)
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// ExplainCode requests an AI explanation of code
|
|
func (c *Client) ExplainCode(ctx context.Context, req *ExplainCodeRequest) (*ExplainCodeResponse, error) {
|
|
if !IsEnabled() || !setting.AI.EnableExplainCode {
|
|
return nil, errors.New("AI code explanation is not enabled")
|
|
}
|
|
|
|
var resp ExplainCodeResponse
|
|
if err := c.doRequest(ctx, http.MethodPost, "/code/explain", req, &resp); err != nil {
|
|
log.Error("AI ExplainCode failed: %v", err)
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GenerateDocumentation requests AI-generated documentation
|
|
func (c *Client) GenerateDocumentation(ctx context.Context, req *GenerateDocumentationRequest) (*GenerateDocumentationResponse, error) {
|
|
if !IsEnabled() || !setting.AI.EnableDocGen {
|
|
return nil, errors.New("AI documentation generation is not enabled")
|
|
}
|
|
|
|
var resp GenerateDocumentationResponse
|
|
if err := c.doRequest(ctx, http.MethodPost, "/docs/generate", req, &resp); err != nil {
|
|
log.Error("AI GenerateDocumentation failed: %v", err)
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GenerateCommitMessage requests an AI-generated commit message
|
|
func (c *Client) GenerateCommitMessage(ctx context.Context, req *GenerateCommitMessageRequest) (*GenerateCommitMessageResponse, error) {
|
|
if !IsEnabled() || !setting.AI.EnableDocGen {
|
|
return nil, errors.New("AI documentation generation is not enabled")
|
|
}
|
|
|
|
var resp GenerateCommitMessageResponse
|
|
if err := c.doRequest(ctx, http.MethodPost, "/docs/commit-message", req, &resp); err != nil {
|
|
log.Error("AI GenerateCommitMessage failed: %v", err)
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// SummarizeChanges requests an AI summary of code changes
|
|
func (c *Client) SummarizeChanges(ctx context.Context, req *SummarizeChangesRequest) (*SummarizeChangesResponse, error) {
|
|
if !IsEnabled() {
|
|
return nil, errors.New("AI service is not enabled")
|
|
}
|
|
|
|
var resp SummarizeChangesResponse
|
|
if err := c.doRequest(ctx, http.MethodPost, "/code/summarize", req, &resp); err != nil {
|
|
log.Error("AI SummarizeChanges failed: %v", err)
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// GenerateIssueResponse requests an AI-generated response to an issue
|
|
func (c *Client) GenerateIssueResponse(ctx context.Context, req *GenerateIssueResponseRequest) (*GenerateIssueResponseResponse, error) {
|
|
if !IsEnabled() || !setting.AI.AllowAutoRespond {
|
|
return nil, errors.New("AI auto-respond is not enabled")
|
|
}
|
|
|
|
var resp GenerateIssueResponseResponse
|
|
if err := c.doRequest(ctx, http.MethodPost, "/issues/respond", req, &resp); err != nil {
|
|
log.Error("AI GenerateIssueResponse failed: %v", err)
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// InspectWorkflow sends a workflow for AI inspection
|
|
func (c *Client) InspectWorkflow(ctx context.Context, req *InspectWorkflowRequest) (*InspectWorkflowResponse, error) {
|
|
resp := &InspectWorkflowResponse{}
|
|
if err := c.doRequest(ctx, "POST", "/api/v1/workflows/inspect", req, resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
// ExecuteTask executes a generic AI task via the sidecar
|
|
func (c *Client) ExecuteTask(ctx context.Context, req *ExecuteTaskRequest) (*ExecuteTaskResponse, error) {
|
|
if !IsEnabled() {
|
|
return nil, errors.New("AI service is not enabled")
|
|
}
|
|
|
|
var resp ExecuteTaskResponse
|
|
if err := c.doRequest(ctx, http.MethodPost, "/execute-task", req, &resp); err != nil {
|
|
log.Error("AI ExecuteTask failed: %v", err)
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
// CheckHealth checks the health of the AI service
|
|
func (c *Client) CheckHealth(ctx context.Context) (*HealthCheckResponse, error) {
|
|
var resp HealthCheckResponse
|
|
if err := c.doRequest(ctx, http.MethodGet, "/health", nil, &resp); err != nil {
|
|
return nil, err
|
|
}
|
|
return &resp, nil
|
|
}
|