Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f7bdcc568 | |||
| 6e25266da3 | |||
| ea06ca266f | |||
| ac8aa4c868 | |||
| a1bc4faef4 |
8
.notes/note-1770961030548-nk5207bv5.json
Normal file
8
.notes/note-1770961030548-nk5207bv5.json
Normal file
File diff suppressed because one or more lines are too long
8
.notes/note-1770964636626-3yh3ujrlr.json
Normal file
8
.notes/note-1770964636626-3yh3ujrlr.json
Normal file
File diff suppressed because one or more lines are too long
91
README.md
91
README.md
@@ -13,19 +13,30 @@ AI-powered code intelligence service for GitCaddy. Provides code review, documen
|
||||
|
||||
## Architecture
|
||||
|
||||
The AI service runs as a sidecar alongside the GitCaddy server. It exposes two gRPC services on the same port (HTTP/2 + HTTP/1.1):
|
||||
|
||||
- **GitCaddyAIService** - AI operations (review, triage, docs, chat) called by the server's AI client
|
||||
- **PluginService** - Plugin lifecycle protocol (initialize, health, events) managed by the server's external plugin manager
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ GitCaddy Server (Go) │
|
||||
│ └── AI Client (gRPC) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ GitCaddy AI Service (.NET 9) │ │
|
||||
│ │ ├── gRPC API │ │
|
||||
│ │ ├── MarketAlly.AIPlugin (Multi-provider AI) │ │
|
||||
│ │ └── License Validation │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ GitCaddy Server (Go) │
|
||||
│ ├── AI Client (gRPC) ──────────────── GitCaddyAIService │
|
||||
│ │ ReviewPullRequest, TriageIssue, │ │
|
||||
│ │ ExplainCode, GenerateDocumentation │ │
|
||||
│ │ │ │
|
||||
│ └── External Plugin Manager (gRPC) ─── PluginService │
|
||||
│ Initialize, HealthCheck (30s), │ │
|
||||
│ OnEvent, Shutdown ▼ │
|
||||
│ ┌──────────────────────────┐ │
|
||||
│ │ GitCaddy AI (.NET 9) │ │
|
||||
│ │ Port 5000 (h2c + HTTP) │ │
|
||||
│ │ ├── AI Providers │ │
|
||||
│ │ │ (Claude/OpenAI/ │ │
|
||||
│ │ │ Gemini) │ │
|
||||
│ │ └── License Validation │ │
|
||||
│ └──────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
@@ -94,7 +105,9 @@ Environment variable format: `AIService__DefaultProvider=Claude`
|
||||
|
||||
## API Reference
|
||||
|
||||
The service exposes a gRPC API defined in `protos/gitcaddy_ai.proto`.
|
||||
The service exposes two gRPC services on port 5000 (cleartext HTTP/2):
|
||||
|
||||
### AI Operations (`protos/gitcaddy_ai.proto`)
|
||||
|
||||
### Code Review
|
||||
|
||||
@@ -134,6 +147,21 @@ rpc ExecuteTask(ExecuteTaskRequest) returns (ExecuteTaskResponse);
|
||||
rpc Chat(stream ChatRequest) returns (stream ChatResponse);
|
||||
```
|
||||
|
||||
### Plugin Protocol (`protos/plugin.proto`)
|
||||
|
||||
The plugin protocol allows the GitCaddy server to manage this service as an external plugin:
|
||||
|
||||
```protobuf
|
||||
rpc Initialize(InitializeRequest) returns (InitializeResponse);
|
||||
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse);
|
||||
rpc HealthCheck(PluginHealthCheckRequest) returns (PluginHealthCheckResponse);
|
||||
rpc GetManifest(GetManifestRequest) returns (PluginManifest);
|
||||
rpc OnEvent(PluginEvent) returns (EventResponse);
|
||||
rpc HandleHTTP(HTTPRequest) returns (HTTPResponse);
|
||||
```
|
||||
|
||||
The server calls `Initialize` on startup, `HealthCheck` every 30 seconds, `OnEvent` for subscribed events (e.g., `license:updated`), and `Shutdown` on server stop. The service declares its capabilities (routes, permissions, license tier) via the `PluginManifest` returned during initialization.
|
||||
|
||||
## Client Libraries
|
||||
|
||||
### .NET Client
|
||||
@@ -141,7 +169,7 @@ rpc Chat(stream ChatRequest) returns (stream ChatResponse);
|
||||
```csharp
|
||||
using GitCaddy.AI.Client;
|
||||
|
||||
var client = new GitCaddyAIClient("http://localhost:5051");
|
||||
var client = new GitCaddyAIClient("http://localhost:5000");
|
||||
|
||||
var response = await client.ReviewPullRequestAsync(new ReviewPullRequestRequest
|
||||
{
|
||||
@@ -159,7 +187,7 @@ Console.WriteLine(response.Summary);
|
||||
```go
|
||||
import ai "git.marketally.com/gitcaddy/gitcaddy-ai/go/gitcaddy-ai-client"
|
||||
|
||||
client, err := ai.NewClient("localhost:5051")
|
||||
client, err := ai.NewClient("localhost:5000")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -213,14 +241,43 @@ go generate
|
||||
|
||||
## Integration with GitCaddy Server
|
||||
|
||||
Add the AI client to your GitCaddy server configuration:
|
||||
Add both sections to the server's `app.ini`:
|
||||
|
||||
**1. AI client configuration** (how the server calls AI operations):
|
||||
|
||||
```ini
|
||||
[ai]
|
||||
ENABLED = true
|
||||
SERVICE_URL = http://localhost:5051
|
||||
SERVICE_URL = localhost:5000
|
||||
DEFAULT_PROVIDER = claude
|
||||
DEFAULT_MODEL = claude-sonnet-4-20250514
|
||||
CLAUDE_API_KEY = sk-ant-...
|
||||
```
|
||||
|
||||
See the [server README](https://git.marketally.com/gitcaddy/gitcaddy-server#ai-features-configuration) for the full `[ai]` reference.
|
||||
|
||||
**2. Plugin registration** (how the server manages the sidecar's lifecycle):
|
||||
|
||||
```ini
|
||||
[plugins]
|
||||
ENABLED = true
|
||||
HEALTH_CHECK_INTERVAL = 30s
|
||||
|
||||
[plugins.gitcaddy-ai]
|
||||
ENABLED = true
|
||||
ADDRESS = localhost:5000
|
||||
HEALTH_TIMEOUT = 5s
|
||||
SUBSCRIBED_EVENTS = license:updated
|
||||
```
|
||||
|
||||
With both sections configured, the server will:
|
||||
- Call AI operations (review, triage, etc.) via `[ai] SERVICE_URL`
|
||||
- Manage the sidecar's lifecycle via the plugin protocol on `[plugins.gitcaddy-ai] ADDRESS`
|
||||
- Health-check the sidecar every 30 seconds and log status changes
|
||||
- Dispatch subscribed events (e.g., license updates) to the sidecar in real-time
|
||||
|
||||
**Transport**: All communication uses cleartext HTTP/2 (h2c). The sidecar's Kestrel server is configured for `Http1AndHttp2` on port 5000, supporting both gRPC (HTTP/2) and REST (HTTP/1.1) on the same port.
|
||||
|
||||
## Support
|
||||
|
||||
- Documentation: https://docs.gitcaddy.com/ai
|
||||
|
||||
107
protos/plugin.proto
Normal file
107
protos/plugin.proto
Normal file
@@ -0,0 +1,107 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package plugin.v1;
|
||||
|
||||
option go_package = "code.gitcaddy.com/server/v3/modules/plugins/pluginv1;pluginv1";
|
||||
option csharp_namespace = "GitCaddy.AI.Plugin.Proto";
|
||||
|
||||
import "google/protobuf/struct.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
// PluginService is the RPC interface that external plugins must implement.
|
||||
// The server calls these methods to manage the plugin's lifecycle and dispatch events.
|
||||
service PluginService {
|
||||
// Initialize is called when the server starts or the plugin is loaded
|
||||
rpc Initialize(InitializeRequest) returns (InitializeResponse);
|
||||
// Shutdown is called when the server is shutting down
|
||||
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse);
|
||||
// HealthCheck checks if the plugin is healthy
|
||||
rpc HealthCheck(PluginHealthCheckRequest) returns (PluginHealthCheckResponse);
|
||||
// GetManifest returns the plugin's manifest describing its capabilities
|
||||
rpc GetManifest(GetManifestRequest) returns (PluginManifest);
|
||||
// OnEvent is called when an event the plugin is subscribed to occurs
|
||||
rpc OnEvent(PluginEvent) returns (EventResponse);
|
||||
// HandleHTTP proxies an HTTP request to the plugin
|
||||
rpc HandleHTTP(HTTPRequest) returns (HTTPResponse);
|
||||
}
|
||||
|
||||
message InitializeRequest {
|
||||
string server_version = 1;
|
||||
map<string, string> config = 2;
|
||||
// protocol_version is the plugin protocol version the server supports.
|
||||
// The current version is 1. Plugins should check this to know what RPCs
|
||||
// the server may call. A value of 0 means the server predates versioning.
|
||||
int32 protocol_version = 3;
|
||||
}
|
||||
|
||||
message InitializeResponse {
|
||||
bool success = 1;
|
||||
string error = 2;
|
||||
PluginManifest manifest = 3;
|
||||
// protocol_version is the plugin protocol version the plugin supports.
|
||||
// The current version is 1. The server uses this to avoid calling RPCs
|
||||
// that the plugin doesn't implement. A value of 0 means the plugin
|
||||
// predates versioning and is treated as protocol version 1.
|
||||
int32 protocol_version = 4;
|
||||
}
|
||||
|
||||
message ShutdownRequest {
|
||||
string reason = 1;
|
||||
}
|
||||
|
||||
message ShutdownResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message PluginHealthCheckRequest {}
|
||||
|
||||
message PluginHealthCheckResponse {
|
||||
bool healthy = 1;
|
||||
string status = 2;
|
||||
map<string, string> details = 3;
|
||||
}
|
||||
|
||||
message GetManifestRequest {}
|
||||
|
||||
message PluginManifest {
|
||||
string name = 1;
|
||||
string version = 2;
|
||||
string description = 3;
|
||||
repeated string subscribed_events = 4;
|
||||
repeated PluginRoute routes = 5;
|
||||
repeated string required_permissions = 6;
|
||||
string license_tier = 7;
|
||||
}
|
||||
|
||||
message PluginRoute {
|
||||
string method = 1;
|
||||
string path = 2;
|
||||
string description = 3;
|
||||
}
|
||||
|
||||
message PluginEvent {
|
||||
string event_type = 1;
|
||||
google.protobuf.Struct payload = 2;
|
||||
google.protobuf.Timestamp timestamp = 3;
|
||||
int64 repo_id = 4;
|
||||
int64 org_id = 5;
|
||||
}
|
||||
|
||||
message EventResponse {
|
||||
bool handled = 1;
|
||||
string error = 2;
|
||||
}
|
||||
|
||||
message HTTPRequest {
|
||||
string method = 1;
|
||||
string path = 2;
|
||||
map<string, string> headers = 3;
|
||||
bytes body = 4;
|
||||
map<string, string> query_params = 5;
|
||||
}
|
||||
|
||||
message HTTPResponse {
|
||||
int32 status_code = 1;
|
||||
map<string, string> headers = 2;
|
||||
bytes body = 3;
|
||||
}
|
||||
@@ -58,6 +58,27 @@ public class AIProviderFactory : IAIProviderFactory
|
||||
return builder;
|
||||
}
|
||||
|
||||
public AIConversationBuilder CreateConversation(string provider, string model, string apiKey)
|
||||
{
|
||||
var aiProvider = ParseProvider(provider);
|
||||
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
// Fall back to configured key if the per-request key is empty
|
||||
return CreateConversation(provider, model);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Creating conversation with per-request provider override: {Provider}, model {Model}", provider, model);
|
||||
|
||||
var builder = AIConversationBuilder.Create()
|
||||
.UseProvider(aiProvider, apiKey)
|
||||
.UseModel(model)
|
||||
.WithMaxTokens(_serviceOptions.MaxTokens)
|
||||
.WithTemperature(_serviceOptions.Temperature);
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static AIProvider ParseProvider(string provider)
|
||||
{
|
||||
return provider.ToLowerInvariant() switch
|
||||
|
||||
@@ -24,4 +24,10 @@ public interface IAIProviderFactory
|
||||
/// Creates a new conversation builder with a specific provider and model.
|
||||
/// </summary>
|
||||
AIConversationBuilder CreateConversation(string provider, string model);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new conversation builder with a specific provider, model, and API key.
|
||||
/// Used for per-request provider overrides from the server's cascade config.
|
||||
/// </summary>
|
||||
AIConversationBuilder CreateConversation(string provider, string model, string apiKey);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// SPDX-License-Identifier: BSL-1.1
|
||||
|
||||
using GitCaddy.AI.Service.Services;
|
||||
using GitCaddy.AI.Service.Configuration;
|
||||
using GitCaddy.AI.Service.Licensing;
|
||||
using MarketAlly.AIPlugin.Conversation;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace GitCaddy.AI.Service.Controllers;
|
||||
@@ -19,6 +21,8 @@ public class AIController : ControllerBase
|
||||
private readonly ICodeIntelligenceService _codeIntelligenceService;
|
||||
private readonly IIssueService _issueService;
|
||||
private readonly IDocumentationService _documentationService;
|
||||
private readonly IWorkflowService _workflowService;
|
||||
private readonly IAIProviderFactory _providerFactory;
|
||||
private readonly ILicenseValidator _licenseValidator;
|
||||
private readonly ILogger<AIController> _logger;
|
||||
|
||||
@@ -27,6 +31,8 @@ public class AIController : ControllerBase
|
||||
ICodeIntelligenceService codeIntelligenceService,
|
||||
IIssueService issueService,
|
||||
IDocumentationService documentationService,
|
||||
IWorkflowService workflowService,
|
||||
IAIProviderFactory providerFactory,
|
||||
ILicenseValidator licenseValidator,
|
||||
ILogger<AIController> logger)
|
||||
{
|
||||
@@ -34,10 +40,27 @@ public class AIController : ControllerBase
|
||||
_codeIntelligenceService = codeIntelligenceService;
|
||||
_issueService = issueService;
|
||||
_documentationService = documentationService;
|
||||
_workflowService = workflowService;
|
||||
_providerFactory = providerFactory;
|
||||
_licenseValidator = licenseValidator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a conversation builder using per-request provider config if available,
|
||||
/// otherwise falls back to the sidecar's default configuration.
|
||||
/// </summary>
|
||||
private AIConversationBuilder CreateConversation(ProviderConfigDto? providerConfig)
|
||||
{
|
||||
if (providerConfig is not null &&
|
||||
!string.IsNullOrEmpty(providerConfig.Provider))
|
||||
{
|
||||
var model = !string.IsNullOrEmpty(providerConfig.Model) ? providerConfig.Model : providerConfig.Provider;
|
||||
return _providerFactory.CreateConversation(providerConfig.Provider, model, providerConfig.ApiKey ?? "");
|
||||
}
|
||||
return _providerFactory.CreateConversation();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Health check endpoint
|
||||
/// </summary>
|
||||
@@ -388,6 +411,123 @@ public class AIController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inspect a workflow YAML file for issues
|
||||
/// </summary>
|
||||
[HttpPost("workflows/inspect")]
|
||||
public async Task<IActionResult> InspectWorkflow([FromBody] InspectWorkflowDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var conversation = CreateConversation(request.ProviderConfig)
|
||||
.WithSystemPrompt("""
|
||||
You are a CI/CD workflow expert. Analyze the provided GitHub Actions / Gitea Actions
|
||||
workflow YAML file for issues including:
|
||||
- Syntax errors and invalid YAML
|
||||
- Missing required fields (runs-on, steps, etc.)
|
||||
- Security issues (hardcoded secrets, overly broad permissions, script injection)
|
||||
- Performance issues (unnecessary steps, missing caching)
|
||||
- Compatibility issues with the available runner labels
|
||||
- Best practice violations
|
||||
|
||||
Respond with a JSON object containing:
|
||||
{
|
||||
"valid": true/false,
|
||||
"issues": [{"line": 0, "severity": "error|warning|info", "message": "...", "fix": "..."}],
|
||||
"suggestions": ["..."]
|
||||
}
|
||||
Only respond with the JSON object, no other text.
|
||||
""")
|
||||
.Build();
|
||||
|
||||
var prompt = $"Workflow file: {request.FilePath}\n\n```yaml\n{request.Content}\n```";
|
||||
if (request.RunnerLabels is { Count: > 0 })
|
||||
{
|
||||
prompt += $"\n\nAvailable runner labels: {string.Join(", ", request.RunnerLabels)}";
|
||||
}
|
||||
|
||||
var aiResponse = await conversation.SendAsync(prompt, cancellationToken);
|
||||
var responseText = aiResponse.FinalMessage ?? "{}";
|
||||
|
||||
// Parse the AI response as JSON
|
||||
try
|
||||
{
|
||||
var result = System.Text.Json.JsonSerializer.Deserialize<InspectWorkflowResult>(
|
||||
responseText,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
valid = result?.Valid ?? true,
|
||||
issues = result?.Issues?.Select(i => new
|
||||
{
|
||||
line = i.Line,
|
||||
severity = i.Severity,
|
||||
message = i.Message,
|
||||
fix = i.Fix
|
||||
}).ToList() ?? [],
|
||||
suggestions = result?.Suggestions ?? [],
|
||||
confidence = 0.8,
|
||||
input_tokens = 0,
|
||||
output_tokens = 0
|
||||
});
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
{
|
||||
// If AI didn't return valid JSON, wrap the text response
|
||||
return Ok(new
|
||||
{
|
||||
valid = true,
|
||||
issues = Array.Empty<object>(),
|
||||
suggestions = new[] { responseText },
|
||||
confidence = 0.5,
|
||||
input_tokens = 0,
|
||||
output_tokens = 0
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to inspect workflow");
|
||||
return StatusCode(500, new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute a generic AI task
|
||||
/// </summary>
|
||||
[HttpPost("execute-task")]
|
||||
public async Task<IActionResult> ExecuteTask([FromBody] ExecuteTaskDto request, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var protoRequest = new Proto.ExecuteTaskRequest
|
||||
{
|
||||
RepoId = request.RepoId,
|
||||
Task = request.Task ?? ""
|
||||
};
|
||||
foreach (var kvp in request.Context ?? new Dictionary<string, string>())
|
||||
{
|
||||
protoRequest.Context[kvp.Key] = kvp.Value;
|
||||
}
|
||||
protoRequest.AllowedTools.AddRange(request.AllowedTools ?? []);
|
||||
|
||||
var response = await _workflowService.ExecuteTaskAsync(protoRequest, cancellationToken);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
success = response.Success,
|
||||
result = response.Result,
|
||||
error = response.Error
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to execute task");
|
||||
return StatusCode(500, new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
private static object MapReviewResponse(Proto.ReviewPullRequestResponse response)
|
||||
{
|
||||
return new
|
||||
@@ -425,8 +565,23 @@ public class AIController : ControllerBase
|
||||
}
|
||||
|
||||
// DTO classes for REST API
|
||||
|
||||
/// <summary>
|
||||
/// Per-request provider configuration override.
|
||||
/// When provided, overrides the sidecar's default provider/model/key for this request.
|
||||
/// </summary>
|
||||
public class ProviderConfigDto
|
||||
{
|
||||
public string? Provider { get; set; }
|
||||
public string? Model { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("api_key")]
|
||||
public string? ApiKey { get; set; }
|
||||
}
|
||||
|
||||
public class ReviewPullRequestDto
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
|
||||
public ProviderConfigDto? ProviderConfig { get; set; }
|
||||
public long RepoId { get; set; }
|
||||
public long PullRequestId { get; set; }
|
||||
public string? BaseBranch { get; set; }
|
||||
@@ -460,6 +615,8 @@ public class ReviewOptionsDto
|
||||
|
||||
public class TriageIssueDto
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
|
||||
public ProviderConfigDto? ProviderConfig { get; set; }
|
||||
public long RepoId { get; set; }
|
||||
public long IssueId { get; set; }
|
||||
public string? Title { get; set; }
|
||||
@@ -470,6 +627,8 @@ public class TriageIssueDto
|
||||
|
||||
public class SuggestLabelsDto
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
|
||||
public ProviderConfigDto? ProviderConfig { get; set; }
|
||||
public long RepoId { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Body { get; set; }
|
||||
@@ -478,6 +637,8 @@ public class SuggestLabelsDto
|
||||
|
||||
public class ExplainCodeDto
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
|
||||
public ProviderConfigDto? ProviderConfig { get; set; }
|
||||
public long RepoId { get; set; }
|
||||
public string? FilePath { get; set; }
|
||||
public string? Code { get; set; }
|
||||
@@ -488,6 +649,8 @@ public class ExplainCodeDto
|
||||
|
||||
public class SummarizeChangesDto
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
|
||||
public ProviderConfigDto? ProviderConfig { get; set; }
|
||||
public long RepoId { get; set; }
|
||||
public List<FileDiffDto>? Files { get; set; }
|
||||
public string? Context { get; set; }
|
||||
@@ -495,6 +658,8 @@ public class SummarizeChangesDto
|
||||
|
||||
public class GenerateDocumentationDto
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
|
||||
public ProviderConfigDto? ProviderConfig { get; set; }
|
||||
public long RepoId { get; set; }
|
||||
public string? FilePath { get; set; }
|
||||
public string? Code { get; set; }
|
||||
@@ -505,6 +670,8 @@ public class GenerateDocumentationDto
|
||||
|
||||
public class GenerateCommitMessageDto
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
|
||||
public ProviderConfigDto? ProviderConfig { get; set; }
|
||||
public long RepoId { get; set; }
|
||||
public List<FileDiffDto>? Files { get; set; }
|
||||
public string? Style { get; set; }
|
||||
@@ -512,6 +679,8 @@ public class GenerateCommitMessageDto
|
||||
|
||||
public class GenerateIssueResponseDto
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
|
||||
public ProviderConfigDto? ProviderConfig { get; set; }
|
||||
public long RepoId { get; set; }
|
||||
public long IssueId { get; set; }
|
||||
public string? Title { get; set; }
|
||||
@@ -527,3 +696,44 @@ public class IssueCommentDto
|
||||
public string? Body { get; set; }
|
||||
public string? CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
public class ExecuteTaskDto
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
|
||||
public ProviderConfigDto? ProviderConfig { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("repo_id")]
|
||||
public long RepoId { get; set; }
|
||||
public string? Task { get; set; }
|
||||
public Dictionary<string, string>? Context { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("allowed_tools")]
|
||||
public List<string>? AllowedTools { get; set; }
|
||||
}
|
||||
|
||||
public class InspectWorkflowDto
|
||||
{
|
||||
[System.Text.Json.Serialization.JsonPropertyName("provider_config")]
|
||||
public ProviderConfigDto? ProviderConfig { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("repo_id")]
|
||||
public long RepoId { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("file_path")]
|
||||
public string? FilePath { get; set; }
|
||||
public string? Content { get; set; }
|
||||
[System.Text.Json.Serialization.JsonPropertyName("runner_labels")]
|
||||
public List<string>? RunnerLabels { get; set; }
|
||||
}
|
||||
|
||||
// Internal model for parsing AI response
|
||||
internal class InspectWorkflowResult
|
||||
{
|
||||
public bool Valid { get; set; }
|
||||
public List<InspectWorkflowIssue>? Issues { get; set; }
|
||||
public List<string>? Suggestions { get; set; }
|
||||
}
|
||||
|
||||
internal class InspectWorkflowIssue
|
||||
{
|
||||
public int Line { get; set; }
|
||||
public string? Severity { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public string? Fix { get; set; }
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ builder.Services.AddSingleton<IIssueService, IssueService>();
|
||||
builder.Services.AddSingleton<IDocumentationService, DocumentationService>();
|
||||
builder.Services.AddSingleton<IWorkflowService, WorkflowService>();
|
||||
builder.Services.AddSingleton<IChatService, ChatService>();
|
||||
builder.Services.AddSingleton<PluginServiceImpl>();
|
||||
|
||||
// Add gRPC
|
||||
builder.Services.AddGrpc(options =>
|
||||
@@ -48,6 +49,14 @@ builder.Services.AddEndpointsApiExplorer();
|
||||
// Add health checks
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
{
|
||||
options.ListenAnyIP(5000, listenOptions =>
|
||||
{
|
||||
listenOptions.Protocols = Microsoft.AspNetCore.Server.Kestrel.Core.HttpProtocols.Http1AndHttp2;
|
||||
});
|
||||
});
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Validate license on startup
|
||||
@@ -65,6 +74,7 @@ else
|
||||
|
||||
// Map gRPC services
|
||||
app.MapGrpcService<GitCaddyAIServiceImpl>();
|
||||
app.MapGrpcService<PluginServiceImpl>();
|
||||
app.MapHealthChecks("/healthz");
|
||||
|
||||
// Map REST API controllers
|
||||
|
||||
134
src/GitCaddy.AI.Service/Services/PluginServiceImpl.cs
Normal file
134
src/GitCaddy.AI.Service/Services/PluginServiceImpl.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
using Grpc.Core;
|
||||
using GitCaddy.AI.Plugin.Proto;
|
||||
using GitCaddy.AI.Service.Licensing;
|
||||
|
||||
namespace GitCaddy.AI.Service.Services;
|
||||
|
||||
/// <summary>
|
||||
/// gRPC service implementation for the GitCaddy plugin protocol.
|
||||
/// Allows the GitCaddy server to manage this sidecar as a plugin.
|
||||
/// </summary>
|
||||
public class PluginServiceImpl : PluginService.PluginServiceBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The plugin protocol version this implementation supports.
|
||||
/// Increment when new RPCs are added to PluginService.
|
||||
/// </summary>
|
||||
private const int ProtocolVersion = 1;
|
||||
|
||||
private readonly ILogger<PluginServiceImpl> _logger;
|
||||
private readonly ILicenseValidator _licenseValidator;
|
||||
|
||||
public PluginServiceImpl(
|
||||
ILicenseValidator licenseValidator,
|
||||
ILogger<PluginServiceImpl> logger)
|
||||
{
|
||||
_licenseValidator = licenseValidator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override Task<InitializeResponse> Initialize(
|
||||
InitializeRequest request, ServerCallContext context)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Plugin initialized by server version {ServerVersion} (protocol v{ProtocolVersion})",
|
||||
request.ServerVersion, request.ProtocolVersion);
|
||||
|
||||
return Task.FromResult(new InitializeResponse
|
||||
{
|
||||
Success = true,
|
||||
Manifest = BuildManifest(),
|
||||
ProtocolVersion = ProtocolVersion
|
||||
});
|
||||
}
|
||||
|
||||
public override Task<ShutdownResponse> Shutdown(
|
||||
ShutdownRequest request, ServerCallContext context)
|
||||
{
|
||||
_logger.LogInformation("Plugin shutdown requested: {Reason}", request.Reason);
|
||||
|
||||
return Task.FromResult(new ShutdownResponse
|
||||
{
|
||||
Success = true
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task<PluginHealthCheckResponse> HealthCheck(
|
||||
PluginHealthCheckRequest request, ServerCallContext context)
|
||||
{
|
||||
var licenseResult = await _licenseValidator.ValidateAsync();
|
||||
var version = typeof(PluginServiceImpl).Assembly.GetName().Version?.ToString() ?? "1.0.0";
|
||||
|
||||
var response = new PluginHealthCheckResponse
|
||||
{
|
||||
Healthy = licenseResult.IsValid,
|
||||
Status = licenseResult.IsValid ? "healthy" : "degraded"
|
||||
};
|
||||
|
||||
response.Details["version"] = version;
|
||||
response.Details["license_tier"] = licenseResult.License?.Tier ?? "none";
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public override Task<PluginManifest> GetManifest(
|
||||
GetManifestRequest request, ServerCallContext context)
|
||||
{
|
||||
return Task.FromResult(BuildManifest());
|
||||
}
|
||||
|
||||
public override Task<EventResponse> OnEvent(
|
||||
PluginEvent request, ServerCallContext context)
|
||||
{
|
||||
_logger.LogInformation("Received event {EventType} for repo {RepoId}", request.EventType, request.RepoId);
|
||||
|
||||
return Task.FromResult(new EventResponse
|
||||
{
|
||||
Handled = true
|
||||
});
|
||||
}
|
||||
|
||||
public override Task<HTTPResponse> HandleHTTP(
|
||||
HTTPRequest request, ServerCallContext context)
|
||||
{
|
||||
// Not used — the sidecar is called directly via REST by the server.
|
||||
return Task.FromResult(new HTTPResponse
|
||||
{
|
||||
StatusCode = 501
|
||||
});
|
||||
}
|
||||
|
||||
private static PluginManifest BuildManifest()
|
||||
{
|
||||
var version = typeof(PluginServiceImpl).Assembly.GetName().Version?.ToString() ?? "1.0.0";
|
||||
|
||||
var manifest = new PluginManifest
|
||||
{
|
||||
Name = "GitCaddy AI Service",
|
||||
Version = version,
|
||||
Description = "AI-powered code intelligence service for GitCaddy",
|
||||
LicenseTier = "professional"
|
||||
};
|
||||
|
||||
manifest.SubscribedEvents.Add("license:updated");
|
||||
manifest.RequiredPermissions.Add("ai:*");
|
||||
|
||||
manifest.Routes.AddRange(new[]
|
||||
{
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/review/pull-request", Description = "Review a pull request" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/issues/triage", Description = "Triage an issue" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/issues/suggest-labels", Description = "Suggest labels for an issue" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/code/explain", Description = "Explain code" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/docs/generate", Description = "Generate documentation" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/docs/commit-message", Description = "Generate commit message" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/issues/respond", Description = "Generate a response to an issue" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/workflows/inspect", Description = "Inspect a workflow YAML file" },
|
||||
new PluginRoute { Method = "GET", Path = "/api/v1/health", Description = "Health check" }
|
||||
});
|
||||
|
||||
return manifest;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user