2
0
Files
gitcaddy-server/modules/ai/client.go
logikonline 61e835358c feat(actions-manager): add AI service integration for code review and issue triage
Integrate GitCaddy AI service with support for code review, issue triage, documentation generation, code explanation, and chat interface. Add AI client module with HTTP communication, configuration settings, API routes (web and REST), service layer, and UI templates for issue sidebar. Include comprehensive configuration options in app.example.ini for enabling/disabling features and service connection settings.
2026-01-19 11:06:39 -05:00

200 lines
5.8 KiB
Go

// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: MIT
package ai
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"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 any, 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, fmt.Errorf("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, fmt.Errorf("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, fmt.Errorf("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, fmt.Errorf("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, fmt.Errorf("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, fmt.Errorf("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, fmt.Errorf("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
}
// 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
}