2
0
Files
gitcaddy-server/modules/ai/client.go
logikonline f42c6c39f9
All checks were successful
Build and Release / Create Release (push) Has been skipped
Build and Release / Unit Tests (push) Successful in 6m49s
Build and Release / Integration Tests (PostgreSQL) (push) Successful in 7m6s
Build and Release / Lint (push) Successful in 7m15s
Build and Release / Build Binaries (amd64, windows, windows-latest) (push) Has been skipped
Build and Release / Build Binaries (amd64, darwin, macos) (push) Has been skipped
Build and Release / Build Binaries (amd64, linux, linux-latest) (push) Has been skipped
Build and Release / Build Binaries (arm64, darwin, macos) (push) Has been skipped
Build and Release / Build Binary (linux/arm64) (push) Has been skipped
feat(ai-service): complete ai production readiness tasks
Implement critical production readiness features for AI integration: per-request provider config, admin dashboard, workflow inspection, and plugin framework foundation.

Per-Request Provider Config:
- Add ProviderConfig struct to all AI request types
- Update queue to resolve provider/model/API key from cascade (repo > org > system)
- Pass resolved config to AI sidecar on every request
- Fixes multi-tenant issue where all orgs shared sidecar's hardcoded config

Admin AI Dashboard:
- Add /admin/ai page with sidecar health status
- Display global operation stats (total, 24h, success/fail/escalated counts)
- Show operations by tier, top 5 repos, token usage
- Recent operations table with repo, operation, status, duration
- Add GetGlobalOperationStats model method

Workflow Inspection:
- Add InspectWorkflow client method and types
- Implement workflow-inspect queue handler
- Add notifier trigger on workflow file push
- Analyzes YAML for syntax errors, security issues, best practices
- Returns structured issues with line numbers and suggested fixes

Plugin Framework (Phase 5 Foundation):
- Add external plugin config loading from app.ini
- Define ExternalPlugin interface and manager
- Add plugin.proto contract (Initialize, Shutdown, HealthCheck, OnEvent, HandleHTTP)
- Implement health monitoring with auto-restart for managed plugins
- Add event routing to subscribed plugins
- HTTP proxy support for plugin-served routes

This completes Tasks 1-4 from the production readiness plan and establishes the foundation for managed plugin lifecycle.
2026-02-13 01:16:58 -05:00

224 lines
6.7 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
}
// 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
}