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.
200 lines
5.8 KiB
Go
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
|
|
}
|