2
0

8 Commits

Author SHA1 Message Date
a55aa5f717 refactor(ai): migrate AI services to structured output plugins
All checks were successful
Build and Test / build (push) Successful in 18s
Release / build (push) Successful in 35s
Convert all AI services to use plugin-based structured output:
- Create dedicated plugins for code intelligence, review, docs, issues, and workflows
- Replace JSON parsing with SendForStructuredOutputAsync
- Add PluginHelpers for consistent deserialization
- Remove inline prompt instructions in favor of plugin definitions
- Eliminate brittle JSON parsing and error handling
- Improve type safety and maintainability across all AI features

Affected services: CodeIntelligence, CodeReview, Documentation, Issue, Workflow inspection
2026-03-07 16:10:56 -05:00
1385cbafa9 feat(ai): add AI plugins for A/B test generation and analysis
Implement AI-powered A/B testing capabilities for landing pages:
- ABTestGeneratePlugin: creates experiment variants with config overrides
- ABTestAnalyzePlugin: evaluates results and determines statistical significance
- Generate 1-3 test variants focusing on high-impact changes (headlines, CTAs, value props)
- Analyze conversion rates with 95% confidence threshold
- Require minimum 100 impressions per variant before declaring winner
- Return structured recommendations for next actions
2026-03-07 15:57:28 -05:00
61ba70c2ad feat(ai): add AI plugins for landing page content generation
Implement AI plugins for automated landing page creation:
- LandingPageContentPlugin: generates hero, features, stats, CTAs from repo metadata
- LandingPageTranslationPlugin: translates landing page content to target languages
- Shared model classes matching Go server's expected JSON structure
- Uses structured output via tool_choice for reliable parsing
- Supports 20+ icon types for features and value props
- Integrates with WorkflowService for task execution
2026-03-07 15:52:11 -05:00
7f7bdcc568 Update AIController.cs
All checks were successful
Build and Test / build (push) Successful in 28s
Release / build (push) Successful in 40s
2026-03-07 12:39:22 -05:00
6e25266da3 feat(plugins): add protocol versioning to plugin interface
All checks were successful
Build and Test / build (push) Successful in 20s
Release / build (push) Successful in 37s
Add protocol_version field to Initialize RPC for forward compatibility as the plugin protocol evolves.

Changes:
- Add protocol_version to InitializeRequest (server → plugin)
- Add protocol_version to InitializeResponse (plugin → server)
- Set current protocol version to 1
- Version 0 indicates pre-versioning implementations (treated as v1)

This allows the server and plugins to negotiate capabilities:
- Server can avoid calling RPCs that older plugins don't implement
- Plugins can detect newer servers and enable advanced features
- Graceful degradation when versions mismatch

The AI service now reports protocol version 1 during initialization.
2026-02-13 02:17:56 -05:00
ea06ca266f docs(ai-service): update readme with plugin protocol integration
Update AI service README to reflect the dual-service architecture (AI operations + plugin protocol) and complete integration guide.

Architecture Updates:
- Document both GitCaddyAIService and PluginService running on port 5000
- Add architecture diagram showing server's AI client and plugin manager connections
- Clarify h2c (cleartext HTTP/2) transport for gRPC + REST on same port

API Reference:
- Add plugin protocol RPC methods (Initialize, HealthCheck, OnEvent, Shutdown)
- Explain plugin lifecycle and health monitoring (30s intervals)
- Document manifest-based capability declaration

Integration Guide:
- Split configuration into [ai] (operations) and [plugins.gitcaddy-ai] (lifecycle)
- Explain how both sections work together for complete integration
- Update client examples to use port 5000 (was 5051)
- Add transport details and event subscription explanation

This provides a complete picture of how the AI service integrates with the server as both an AI operations provider and a managed plugin.
2026-02-13 01:54:41 -05:00
ac8aa4c868 feat(ai-service): implement plugin protocol for managed lifecycle
Add gRPC-based plugin protocol implementation to the AI sidecar, enabling the GitCaddy server to manage it as an external plugin with lifecycle control and health monitoring.

Plugin Protocol Implementation:
- Add plugin.proto with PluginService definition (Initialize, Shutdown, HealthCheck, GetManifest, OnEvent, HandleHTTP)
- Implement PluginServiceImpl gRPC service in C#
- Return manifest declaring AI service capabilities, routes, and required permissions
- Integrate license validation into health checks
- Register plugin service alongside existing AI service

Server Integration:
- Configure Kestrel for HTTP/1.1 + HTTP/2 on port 5000 (enables gRPC + REST)
- Map PluginService gRPC endpoint at /plugin.v1.PluginService
- Enable server to call Initialize on startup, HealthCheck periodically, and Shutdown on graceful stop

This completes Phase 5 of the AI integration, allowing the server's external plugin manager to monitor and control the sidecar's lifecycle instead of relying on manual process management.
2026-02-13 01:44:55 -05:00
a1bc4faef4 feat(ai-service): add per-request provider config and workflow inspection
All checks were successful
Build and Test / build (push) Successful in 21s
Release / build (push) Successful in 40s
Add support for per-request AI provider configuration and workflow YAML inspection endpoint, enabling multi-tenant AI operations.

Per-Request Provider Config:
- Add CreateConversation overload accepting provider, model, and API key
- Add CreateConversation helper method in AIController to resolve config cascade
- When ProviderConfig is provided in request, override sidecar defaults
- Falls back to sidecar configuration when not provided (backwards compatible)

This fixes the critical multi-tenant gap where all organizations shared the sidecar's hardcoded provider configuration.

Workflow Inspection:
- Add POST /api/v1/workflows/inspect endpoint
- Analyzes GitHub Actions/Gitea Actions YAML for issues
- Detects syntax errors, security issues, performance problems, and best practice violations
- Returns structured JSON with issues (line, severity, message, fix) and suggestions
- Supports runner label compatibility checking

Both features support the ProviderConfigDto pattern for tenant-specific AI configuration.
2026-02-13 01:15:47 -05:00
25 changed files with 2327 additions and 200 deletions

View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -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
View 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;
}

View File

@@ -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

View File

@@ -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);
}

View File

@@ -2,7 +2,10 @@
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Service.Services;
using GitCaddy.AI.Service.Configuration;
using GitCaddy.AI.Service.Licensing;
using GitCaddy.AI.Service.Plugins;
using MarketAlly.AIPlugin.Conversation;
using Microsoft.AspNetCore.Mvc;
namespace GitCaddy.AI.Service.Controllers;
@@ -19,6 +22,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 +32,8 @@ public class AIController : ControllerBase
ICodeIntelligenceService codeIntelligenceService,
IIssueService issueService,
IDocumentationService documentationService,
IWorkflowService workflowService,
IAIProviderFactory providerFactory,
ILicenseValidator licenseValidator,
ILogger<AIController> logger)
{
@@ -34,10 +41,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 +412,97 @@ 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
""")
.RegisterPlugin<InspectWorkflowPlugin>()
.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.SendForStructuredOutputAsync<InspectWorkflowPluginResult>(
prompt, "InspectWorkflow", cancellationToken);
var result = PluginHelpers.DeserializeOutput<InspectWorkflowPluginResult>(aiResponse.StructuredOutput);
return Ok(new
{
valid = result.Valid,
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 (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 +540,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 +590,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 +602,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 +612,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 +624,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 +633,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 +645,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 +654,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 +671,29 @@ 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; }
}

View File

@@ -0,0 +1,79 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using System.Text.Json.Serialization;
using MarketAlly.AIPlugin;
namespace GitCaddy.AI.Service.Plugins;
/// <summary>
/// Result DTO for A/B test analysis.
/// </summary>
public class ABTestAnalyzeResult
{
[JsonPropertyName("status")]
public string Status { get; set; }
[JsonPropertyName("winner_variant_id")]
public long WinnerVariantId { get; set; }
[JsonPropertyName("confidence")]
public double Confidence { get; set; }
[JsonPropertyName("summary")]
public string Summary { get; set; }
[JsonPropertyName("recommendation")]
public string Recommendation { get; set; }
}
/// <summary>
/// AI plugin that analyzes A/B test experiment results.
/// Evaluates conversion rates, impression counts, and event distributions
/// to determine if there is a statistically significant winner.
/// </summary>
[AIPlugin("AnalyzeABTestResults",
"Analyze A/B test experiment results and determine the outcome. " +
"Evaluate conversion rates, impression counts, and statistical significance. " +
"Require at least 100 impressions per variant before declaring a winner. " +
"Use a minimum 95% confidence threshold.")]
public class ABTestAnalyzePlugin : IAIPlugin
{
[AIParameter("Analysis status: must be exactly one of 'winner', 'needs_more_data', or 'no_difference'", required: true)]
public string Status { get; set; }
[AIParameter("ID of the winning variant (use 0 if no winner)", required: true)]
public long WinnerVariantId { get; set; }
[AIParameter("Statistical confidence level from 0.0 to 1.0", required: true)]
public double Confidence { get; set; }
[AIParameter("Brief human-readable summary of the results", required: true)]
public string Summary { get; set; }
[AIParameter("Actionable recommendation for what to do next", required: true)]
public string Recommendation { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["status"] = typeof(string),
["winnervariantid"] = typeof(long),
["confidence"] = typeof(double),
["summary"] = typeof(string),
["recommendation"] = typeof(string)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new ABTestAnalyzeResult
{
Status = Status,
WinnerVariantId = WinnerVariantId,
Confidence = Confidence,
Summary = Summary,
Recommendation = Recommendation
};
return Task.FromResult(new AIPluginResult(result, "A/B test analysis complete"));
}
}

View File

@@ -0,0 +1,74 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using System.Text.Json.Serialization;
using MarketAlly.AIPlugin;
namespace GitCaddy.AI.Service.Plugins;
/// <summary>
/// A/B test variant generated by AI, with a partial config override.
/// </summary>
public class ABTestVariant
{
[JsonPropertyName("name")]
public string Name { get; set; }
/// <summary>
/// Partial LandingConfig fields to override for this variant.
/// Stored as object to preserve arbitrary nested JSON structure.
/// </summary>
[JsonPropertyName("config_override")]
public object ConfigOverride { get; set; }
[JsonPropertyName("weight")]
public int Weight { get; set; }
}
/// <summary>
/// Result DTO for A/B test experiment generation.
/// </summary>
public class ABTestGenerateResult
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("variants")]
public List<ABTestVariant> Variants { get; set; }
}
/// <summary>
/// AI plugin that generates A/B test experiment variants for a landing page.
/// The AI analyzes the current landing page config and produces meaningful
/// variant overrides to test (headlines, CTAs, value props, etc.).
/// </summary>
[AIPlugin("GenerateABTestExperiment",
"Generate an A/B test experiment for a landing page. " +
"Analyze the current landing page config and create meaningful test variants " +
"that focus on high-impact changes like headlines, CTAs, and value propositions. " +
"Do NOT include a control variant — it is added automatically.")]
public class ABTestGeneratePlugin : IAIPlugin
{
[AIParameter("Short descriptive name for the experiment (e.g. 'Headline Impact Test')", required: true)]
public string ExperimentName { get; set; }
[AIParameter("Test variants to create. Each must have 'name' (string), 'config_override' (object with partial landing config fields to override), and 'weight' (integer traffic percentage, should sum to ~50 since control gets 50%). Generate 1-3 variants with meaningfully different but plausible changes.", required: true)]
public List<ABTestVariant> Variants { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["experimentname"] = typeof(string),
["variants"] = typeof(List<ABTestVariant>)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new ABTestGenerateResult
{
Name = ExperimentName,
Variants = Variants ?? new List<ABTestVariant>()
};
return Task.FromResult(new AIPluginResult(result, "A/B test experiment generated"));
}
}

View File

@@ -0,0 +1,162 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using System.Text.Json.Serialization;
using MarketAlly.AIPlugin;
namespace GitCaddy.AI.Service.Plugins;
// ============================================================================
// Result DTOs
// ============================================================================
public class SummarizeChangesResult
{
[JsonPropertyName("summary")]
public string Summary { get; set; }
[JsonPropertyName("bullet_points")]
public List<string> BulletPoints { get; set; }
[JsonPropertyName("impact_assessment")]
public string ImpactAssessment { get; set; }
}
public class CodeReferenceItem
{
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("url")]
public string Url { get; set; }
}
public class ExplainCodeResult
{
[JsonPropertyName("explanation")]
public string Explanation { get; set; }
[JsonPropertyName("key_concepts")]
public List<string> KeyConcepts { get; set; }
[JsonPropertyName("references")]
public List<CodeReferenceItem> References { get; set; }
}
public class SuggestFixResult
{
[JsonPropertyName("explanation")]
public string Explanation { get; set; }
[JsonPropertyName("suggested_code")]
public string SuggestedCode { get; set; }
[JsonPropertyName("alternative_fixes")]
public List<string> AlternativeFixes { get; set; }
}
// ============================================================================
// Plugins
// ============================================================================
[AIPlugin("SummarizeChanges",
"Summarize code changes with a brief paragraph, key bullet points, " +
"and an impact assessment.")]
public class SummarizeChangesPlugin : IAIPlugin
{
[AIParameter("Brief summary paragraph describing the overall changes", required: true)]
public string Summary { get; set; }
[AIParameter("Key changes as a list of concise bullet point strings", required: true)]
public List<string> BulletPoints { get; set; }
[AIParameter("Assessment of the potential impact of these changes", required: true)]
public string ImpactAssessment { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["summary"] = typeof(string),
["bulletpoints"] = typeof(List<string>),
["impactassessment"] = typeof(string)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new SummarizeChangesResult
{
Summary = Summary,
BulletPoints = BulletPoints ?? [],
ImpactAssessment = ImpactAssessment ?? ""
};
return Task.FromResult(new AIPluginResult(result, "Changes summarized"));
}
}
[AIPlugin("ExplainCode",
"Explain what code does, including key concepts, patterns used, " +
"and relevant documentation references.")]
public class ExplainCodePlugin : IAIPlugin
{
[AIParameter("Clear explanation of what the code does, how it works, and any notable patterns", required: true)]
public string Explanation { get; set; }
[AIParameter("Key programming concepts and patterns found in the code, as a list of strings", required: true)]
public List<string> KeyConcepts { get; set; }
[AIParameter("Relevant documentation references. Each item must have 'description' (string) and 'url' (string). Use empty array if no relevant references.", required: true)]
public List<CodeReferenceItem> References { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["explanation"] = typeof(string),
["keyconcepts"] = typeof(List<string>),
["references"] = typeof(List<CodeReferenceItem>)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new ExplainCodeResult
{
Explanation = Explanation,
KeyConcepts = KeyConcepts ?? [],
References = References ?? []
};
return Task.FromResult(new AIPluginResult(result, "Code explained"));
}
}
[AIPlugin("SuggestFix",
"Analyze code with an error and suggest a fix, including an explanation " +
"of the cause, corrected code, and alternative solutions.")]
public class SuggestFixPlugin : IAIPlugin
{
[AIParameter("Explanation of what is causing the error", required: true)]
public string Explanation { get; set; }
[AIParameter("The corrected code that fixes the error", required: true)]
public string SuggestedCode { get; set; }
[AIParameter("Alternative fix approaches as a list of description strings", required: true)]
public List<string> AlternativeFixes { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["explanation"] = typeof(string),
["suggestedcode"] = typeof(string),
["alternativefixes"] = typeof(List<string>)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new SuggestFixResult
{
Explanation = Explanation,
SuggestedCode = SuggestedCode ?? "",
AlternativeFixes = AlternativeFixes ?? []
};
return Task.FromResult(new AIPluginResult(result, "Fix suggested"));
}
}

View File

@@ -0,0 +1,192 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using System.Text.Json.Serialization;
using MarketAlly.AIPlugin;
namespace GitCaddy.AI.Service.Plugins;
// ============================================================================
// Result DTOs
// ============================================================================
public class ReviewCommentItem
{
[JsonPropertyName("path")]
public string Path { get; set; }
[JsonPropertyName("line")]
public int Line { get; set; }
[JsonPropertyName("end_line")]
public int EndLine { get; set; }
[JsonPropertyName("body")]
public string Body { get; set; }
[JsonPropertyName("severity")]
public string Severity { get; set; }
[JsonPropertyName("category")]
public string Category { get; set; }
[JsonPropertyName("suggested_fix")]
public string SuggestedFix { get; set; }
}
public class SecurityIssueItem
{
[JsonPropertyName("path")]
public string Path { get; set; }
[JsonPropertyName("line")]
public int Line { get; set; }
[JsonPropertyName("issue_type")]
public string IssueType { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("severity")]
public string Severity { get; set; }
[JsonPropertyName("remediation")]
public string Remediation { get; set; }
}
public class ReviewPullRequestResult
{
[JsonPropertyName("summary")]
public string Summary { get; set; }
[JsonPropertyName("verdict")]
public string Verdict { get; set; }
[JsonPropertyName("comments")]
public List<ReviewCommentItem> Comments { get; set; }
[JsonPropertyName("suggestions")]
public List<string> Suggestions { get; set; }
[JsonPropertyName("security_issues")]
public List<SecurityIssueItem> SecurityIssues { get; set; }
[JsonPropertyName("security_risk_score")]
public int SecurityRiskScore { get; set; }
[JsonPropertyName("security_summary")]
public string SecuritySummary { get; set; }
[JsonPropertyName("estimated_review_minutes")]
public int EstimatedReviewMinutes { get; set; }
}
public class ReviewCommitResult
{
[JsonPropertyName("summary")]
public string Summary { get; set; }
[JsonPropertyName("comments")]
public List<ReviewCommentItem> Comments { get; set; }
[JsonPropertyName("suggestions")]
public List<string> Suggestions { get; set; }
}
// ============================================================================
// Plugins
// ============================================================================
[AIPlugin("ReviewPullRequest",
"Review a pull request and provide structured feedback including a verdict, " +
"line-by-line comments with severity levels, security analysis, and improvement suggestions.")]
public class ReviewPullRequestPlugin : IAIPlugin
{
[AIParameter("Brief overview of the changes and overall assessment", required: true)]
public string Summary { get; set; }
[AIParameter("Review verdict: must be exactly 'approve', 'request_changes', or 'comment'", required: true)]
public string Verdict { get; set; }
[AIParameter("Line-by-line review comments. Each item must have 'path' (string, file path), 'line' (int), 'end_line' (int, 0 if single line), 'body' (string, the comment), 'severity' (string: 'info', 'warning', 'error', or 'critical'), 'category' (string: 'security', 'performance', 'style', 'bug', etc.), 'suggested_fix' (string, code fix or empty)", required: true)]
public List<ReviewCommentItem> Comments { get; set; }
[AIParameter("General improvement suggestions as a list of strings", required: true)]
public List<string> Suggestions { get; set; }
[AIParameter("Security issues found. Each item must have 'path' (string), 'line' (int), 'issue_type' (string: 'sql_injection', 'xss', 'hardcoded_secret', etc.), 'description' (string), 'severity' (string), 'remediation' (string). Use empty array if no issues.", required: true)]
public List<SecurityIssueItem> SecurityIssues { get; set; }
[AIParameter("Security risk score from 0 (no risk) to 100 (critical risk)", required: true)]
public int SecurityRiskScore { get; set; }
[AIParameter("Brief summary of security analysis findings", required: true)]
public string SecuritySummary { get; set; }
[AIParameter("Estimated time in minutes to review these changes", required: true)]
public int EstimatedReviewMinutes { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["summary"] = typeof(string),
["verdict"] = typeof(string),
["comments"] = typeof(List<ReviewCommentItem>),
["suggestions"] = typeof(List<string>),
["securityissues"] = typeof(List<SecurityIssueItem>),
["securityriskscore"] = typeof(int),
["securitysummary"] = typeof(string),
["estimatedreviewminutes"] = typeof(int)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new ReviewPullRequestResult
{
Summary = Summary,
Verdict = Verdict,
Comments = Comments ?? [],
Suggestions = Suggestions ?? [],
SecurityIssues = SecurityIssues ?? [],
SecurityRiskScore = SecurityRiskScore,
SecuritySummary = SecuritySummary ?? "",
EstimatedReviewMinutes = EstimatedReviewMinutes
};
return Task.FromResult(new AIPluginResult(result, "Pull request review complete"));
}
}
[AIPlugin("ReviewCommit",
"Review a single commit and provide structured feedback including comments " +
"with severity levels and improvement suggestions.")]
public class ReviewCommitPlugin : IAIPlugin
{
[AIParameter("Brief overview of the commit and overall assessment", required: true)]
public string Summary { get; set; }
[AIParameter("Line-by-line review comments. Each item must have 'path' (string), 'line' (int), 'end_line' (int, 0 if single line), 'body' (string), 'severity' (string: 'info', 'warning', 'error', or 'critical'), 'category' (string), 'suggested_fix' (string or empty)", required: true)]
public List<ReviewCommentItem> Comments { get; set; }
[AIParameter("General improvement suggestions as a list of strings", required: true)]
public List<string> Suggestions { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["summary"] = typeof(string),
["comments"] = typeof(List<ReviewCommentItem>),
["suggestions"] = typeof(List<string>)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new ReviewCommitResult
{
Summary = Summary,
Comments = Comments ?? [],
Suggestions = Suggestions ?? []
};
return Task.FromResult(new AIPluginResult(result, "Commit review complete"));
}
}

View File

@@ -0,0 +1,100 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using System.Text.Json.Serialization;
using MarketAlly.AIPlugin;
namespace GitCaddy.AI.Service.Plugins;
// ============================================================================
// Result DTOs
// ============================================================================
public class DocumentationSectionItem
{
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("content")]
public string Content { get; set; }
}
public class GenerateDocumentationResult
{
[JsonPropertyName("documentation")]
public string Documentation { get; set; }
[JsonPropertyName("sections")]
public List<DocumentationSectionItem> Sections { get; set; }
}
public class GenerateCommitMessageResult
{
[JsonPropertyName("message")]
public string Message { get; set; }
[JsonPropertyName("alternatives")]
public List<string> Alternatives { get; set; }
}
// ============================================================================
// Plugins
// ============================================================================
[AIPlugin("GenerateDocumentation",
"Generate comprehensive code documentation including a full documentation " +
"string and organized sections for different aspects of the code.")]
public class GenerateDocumentationPlugin : IAIPlugin
{
[AIParameter("The complete documentation text in the requested format (jsdoc, docstring, xml, or markdown)", required: true)]
public string Documentation { get; set; }
[AIParameter("Documentation broken into sections. Each item must have 'title' (string, section heading like 'Description', 'Parameters', 'Returns', 'Examples') and 'content' (string, section body)", required: true)]
public List<DocumentationSectionItem> Sections { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["documentation"] = typeof(string),
["sections"] = typeof(List<DocumentationSectionItem>)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new GenerateDocumentationResult
{
Documentation = Documentation,
Sections = Sections ?? []
};
return Task.FromResult(new AIPluginResult(result, "Documentation generated"));
}
}
[AIPlugin("GenerateCommitMessage",
"Generate a commit message for code changes, including a primary message " +
"and 2-3 alternative options.")]
public class GenerateCommitMessagePlugin : IAIPlugin
{
[AIParameter("The primary commit message following the requested style", required: true)]
public string Message { get; set; }
[AIParameter("2-3 alternative commit message options", required: true)]
public List<string> Alternatives { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["message"] = typeof(string),
["alternatives"] = typeof(List<string>)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new GenerateCommitMessageResult
{
Message = Message,
Alternatives = Alternatives ?? []
};
return Task.FromResult(new AIPluginResult(result, "Commit message generated"));
}
}

View File

@@ -0,0 +1,173 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using System.Text.Json.Serialization;
using MarketAlly.AIPlugin;
namespace GitCaddy.AI.Service.Plugins;
// ============================================================================
// Result DTOs
// ============================================================================
public class TriageIssueResult
{
[JsonPropertyName("priority")]
public string Priority { get; set; }
[JsonPropertyName("category")]
public string Category { get; set; }
[JsonPropertyName("suggested_labels")]
public List<string> SuggestedLabels { get; set; }
[JsonPropertyName("suggested_assignees")]
public List<string> SuggestedAssignees { get; set; }
[JsonPropertyName("summary")]
public string Summary { get; set; }
[JsonPropertyName("is_duplicate")]
public bool IsDuplicate { get; set; }
[JsonPropertyName("duplicate_of")]
public long DuplicateOf { get; set; }
}
public class LabelSuggestionItem
{
[JsonPropertyName("label")]
public string Label { get; set; }
[JsonPropertyName("confidence")]
public float Confidence { get; set; }
[JsonPropertyName("reason")]
public string Reason { get; set; }
}
public class SuggestLabelsResult
{
[JsonPropertyName("suggestions")]
public List<LabelSuggestionItem> Suggestions { get; set; }
}
public class GenerateIssueResponseResult
{
[JsonPropertyName("response")]
public string Response { get; set; }
[JsonPropertyName("follow_up_questions")]
public List<string> FollowUpQuestions { get; set; }
}
// ============================================================================
// Plugins
// ============================================================================
[AIPlugin("TriageIssue",
"Triage a software issue by determining its priority, category, " +
"suggested labels, and whether it is a duplicate.")]
public class TriageIssuePlugin : IAIPlugin
{
[AIParameter("Issue priority: must be exactly 'critical', 'high', 'medium', or 'low'", required: true)]
public string Priority { get; set; }
[AIParameter("Issue category: e.g. 'bug', 'feature', 'question', 'docs', 'enhancement'", required: true)]
public string Category { get; set; }
[AIParameter("Suggested labels from the available labels list", required: true)]
public List<string> SuggestedLabels { get; set; }
[AIParameter("Suggested assignees (usernames) who might be best to handle this. Use empty array if unknown.", required: true)]
public List<string> SuggestedAssignees { get; set; }
[AIParameter("Brief summary of the issue and recommended actions", required: true)]
public string Summary { get; set; }
[AIParameter("Whether this issue appears to be a duplicate of an existing issue", required: true)]
public bool IsDuplicate { get; set; }
[AIParameter("Issue ID of the original if this is a duplicate, or 0 if not a duplicate", required: true)]
public long DuplicateOf { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["priority"] = typeof(string),
["category"] = typeof(string),
["suggestedlabels"] = typeof(List<string>),
["suggestedassignees"] = typeof(List<string>),
["summary"] = typeof(string),
["isduplicate"] = typeof(bool),
["duplicateof"] = typeof(long)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new TriageIssueResult
{
Priority = Priority,
Category = Category,
SuggestedLabels = SuggestedLabels ?? [],
SuggestedAssignees = SuggestedAssignees ?? [],
Summary = Summary,
IsDuplicate = IsDuplicate,
DuplicateOf = DuplicateOf
};
return Task.FromResult(new AIPluginResult(result, "Issue triaged"));
}
}
[AIPlugin("SuggestLabels",
"Suggest appropriate labels for an issue from the available label options, " +
"with confidence scores and reasoning.")]
public class SuggestLabelsPlugin : IAIPlugin
{
[AIParameter("Label suggestions. Each item must have 'label' (string, from available labels), 'confidence' (float, 0.0-1.0), 'reason' (string, why this label applies)", required: true)]
public List<LabelSuggestionItem> Suggestions { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["suggestions"] = typeof(List<LabelSuggestionItem>)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new SuggestLabelsResult
{
Suggestions = Suggestions ?? []
};
return Task.FromResult(new AIPluginResult(result, "Labels suggested"));
}
}
[AIPlugin("GenerateIssueResponse",
"Generate a professional response to a software issue, " +
"including optional follow-up questions for clarification.")]
public class GenerateIssueResponsePlugin : IAIPlugin
{
[AIParameter("The response text to post as a comment on the issue", required: true)]
public string Response { get; set; }
[AIParameter("Follow-up questions to ask the reporter for clarification. Use empty array if none needed.", required: true)]
public List<string> FollowUpQuestions { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["response"] = typeof(string),
["followupquestions"] = typeof(List<string>)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new GenerateIssueResponseResult
{
Response = Response,
FollowUpQuestions = FollowUpQuestions ?? []
};
return Task.FromResult(new AIPluginResult(result, "Issue response generated"));
}
}

View File

@@ -0,0 +1,115 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using MarketAlly.AIPlugin;
namespace GitCaddy.AI.Service.Plugins;
/// <summary>
/// AI plugin that captures structured landing page content generated by the AI model.
/// Used with SendForStructuredOutputAsync to force the AI to return data via tool_choice
/// rather than free-form text that requires parsing.
/// </summary>
[AIPlugin("GenerateLandingPageContent",
"Generate compelling landing page content for a software repository. " +
"Analyze the repository metadata provided and fill in all fields with professional, " +
"benefit-focused marketing copy.")]
public class LandingPageContentPlugin : IAIPlugin
{
[AIParameter("Project/brand name", required: true)]
public string BrandName { get; set; }
[AIParameter("Short brand tagline (one line)", required: true)]
public string BrandTagline { get; set; }
[AIParameter("Compelling hero headline (max 10 words)", required: true)]
public string HeroHeadline { get; set; }
[AIParameter("Supporting hero subheadline explaining the value (1-2 sentences)", required: true)]
public string HeroSubheadline { get; set; }
[AIParameter("Primary call-to-action button label (e.g. 'Get Started')", required: true)]
public string PrimaryCtaLabel { get; set; }
[AIParameter("Secondary call-to-action button label (e.g. 'View Source')", required: true)]
public string SecondaryCtaLabel { get; set; }
[AIParameter("Statistics to display. Each must have 'value' and 'label' string properties. Generate 3-4 stats based on repo data or compelling metrics.", required: true)]
public List<LandingPageStat> Stats { get; set; }
[AIParameter("Key value propositions. Each must have 'title', 'description', and 'icon' properties. Icon must be one of: zap, shield, rocket, check, star, heart, lock, globe, clock, gear, code, terminal, package, database, cloud, cpu, graph, people, tools, light-bulb. Generate exactly 3.", required: true)]
public List<LandingPageValueProp> ValueProps { get; set; }
[AIParameter("Features to highlight. Each must have 'title', 'description', and 'icon' properties (same icon options as value props). Generate 3-6 based on what the README describes.", required: true)]
public List<LandingPageFeature> Features { get; set; }
[AIParameter("Bottom CTA section headline (e.g. 'Ready to get started?')", required: true)]
public string CtaHeadline { get; set; }
[AIParameter("Bottom CTA section subheadline", required: true)]
public string CtaSubheadline { get; set; }
[AIParameter("Bottom CTA section button label (e.g. 'Get Started Free')", required: true)]
public string CtaButtonLabel { get; set; }
[AIParameter("SEO page title (50-60 characters)", required: true)]
public string SeoTitle { get; set; }
[AIParameter("SEO meta description (150-160 characters)", required: true)]
public string SeoDescription { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["brandname"] = typeof(string),
["brandtagline"] = typeof(string),
["heroheadline"] = typeof(string),
["herosubheadline"] = typeof(string),
["primaryctalabel"] = typeof(string),
["secondaryctalabel"] = typeof(string),
["stats"] = typeof(List<LandingPageStat>),
["valueprops"] = typeof(List<LandingPageValueProp>),
["features"] = typeof(List<LandingPageFeature>),
["ctaheadline"] = typeof(string),
["ctasubheadline"] = typeof(string),
["ctabuttonlabel"] = typeof(string),
["seotitle"] = typeof(string),
["seodescription"] = typeof(string)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
// Properties are auto-populated by the framework before this runs.
// Assemble the result DTO matching the Go server's expected JSON structure.
var result = new LandingPageContentResult
{
Brand = new LandingPageBrand
{
Name = BrandName,
Tagline = BrandTagline
},
Hero = new LandingPageHero
{
Headline = HeroHeadline,
Subheadline = HeroSubheadline,
PrimaryCta = new LandingPageCtaLabel { Label = PrimaryCtaLabel },
SecondaryCta = new LandingPageCtaLabel { Label = SecondaryCtaLabel }
},
Stats = Stats ?? new List<LandingPageStat>(),
ValueProps = ValueProps ?? new List<LandingPageValueProp>(),
Features = Features ?? new List<LandingPageFeature>(),
CtaSection = new LandingPageCtaSection
{
Headline = CtaHeadline,
Subheadline = CtaSubheadline,
ButtonLabel = CtaButtonLabel
},
Seo = new LandingPageSeo
{
Title = SeoTitle,
Description = SeoDescription
}
};
return Task.FromResult(new AIPluginResult(result, "Landing page content generated"));
}
}

View File

@@ -0,0 +1,186 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using System.Text.Json.Serialization;
namespace GitCaddy.AI.Service.Plugins;
// --- Shared model classes for AI-generated landing page content ---
/// <summary>
/// A statistic/metric to display on the landing page (e.g., "100+ Stars").
/// </summary>
public class LandingPageStat
{
[JsonPropertyName("value")]
public string Value { get; set; }
[JsonPropertyName("label")]
public string Label { get; set; }
}
/// <summary>
/// A value proposition with icon for the landing page.
/// </summary>
public class LandingPageValueProp
{
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("icon")]
public string Icon { get; set; }
}
/// <summary>
/// A feature with icon for the landing page.
/// </summary>
public class LandingPageFeature
{
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
[JsonPropertyName("icon")]
public string Icon { get; set; }
}
// --- Result DTOs for content generation (matches Go aiGeneratedConfig struct) ---
public class LandingPageContentResult
{
[JsonPropertyName("brand")]
public LandingPageBrand Brand { get; set; }
[JsonPropertyName("hero")]
public LandingPageHero Hero { get; set; }
[JsonPropertyName("stats")]
public List<LandingPageStat> Stats { get; set; }
[JsonPropertyName("value_props")]
public List<LandingPageValueProp> ValueProps { get; set; }
[JsonPropertyName("features")]
public List<LandingPageFeature> Features { get; set; }
[JsonPropertyName("cta_section")]
public LandingPageCtaSection CtaSection { get; set; }
[JsonPropertyName("seo")]
public LandingPageSeo Seo { get; set; }
}
public class LandingPageBrand
{
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("tagline")]
public string Tagline { get; set; }
}
public class LandingPageHero
{
[JsonPropertyName("headline")]
public string Headline { get; set; }
[JsonPropertyName("subheadline")]
public string Subheadline { get; set; }
[JsonPropertyName("primary_cta")]
public LandingPageCtaLabel PrimaryCta { get; set; }
[JsonPropertyName("secondary_cta")]
public LandingPageCtaLabel SecondaryCta { get; set; }
}
public class LandingPageCtaLabel
{
[JsonPropertyName("label")]
public string Label { get; set; }
}
public class LandingPageCtaSection
{
[JsonPropertyName("headline")]
public string Headline { get; set; }
[JsonPropertyName("subheadline")]
public string Subheadline { get; set; }
[JsonPropertyName("button_label")]
public string ButtonLabel { get; set; }
}
public class LandingPageSeo
{
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
}
// --- Result DTOs for translation overlay (deep-merged onto base config) ---
public class LandingPageTranslationResult
{
[JsonPropertyName("hero")]
public LandingPageTranslationHero Hero { get; set; }
[JsonPropertyName("stats")]
public List<LandingPageStat> Stats { get; set; }
[JsonPropertyName("value_props")]
public List<LandingPageTranslationSection> ValueProps { get; set; }
[JsonPropertyName("features")]
public List<LandingPageTranslationSection> Features { get; set; }
[JsonPropertyName("cta_section")]
public LandingPageTranslationCta CtaSection { get; set; }
[JsonPropertyName("seo")]
public LandingPageSeo Seo { get; set; }
}
public class LandingPageTranslationHero
{
[JsonPropertyName("headline")]
public string Headline { get; set; }
[JsonPropertyName("subheadline")]
public string Subheadline { get; set; }
[JsonPropertyName("primary_cta")]
public LandingPageCtaLabel PrimaryCta { get; set; }
[JsonPropertyName("secondary_cta")]
public LandingPageCtaLabel SecondaryCta { get; set; }
}
public class LandingPageTranslationSection
{
[JsonPropertyName("title")]
public string Title { get; set; }
[JsonPropertyName("description")]
public string Description { get; set; }
}
public class LandingPageTranslationCta
{
[JsonPropertyName("headline")]
public string Headline { get; set; }
[JsonPropertyName("subheadline")]
public string Subheadline { get; set; }
[JsonPropertyName("button")]
public LandingPageCtaLabel Button { get; set; }
}

View File

@@ -0,0 +1,102 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using MarketAlly.AIPlugin;
namespace GitCaddy.AI.Service.Plugins;
/// <summary>
/// AI plugin that captures translated landing page content.
/// Used with SendForStructuredOutputAsync to force the AI to return a translation
/// overlay that gets deep-merged onto the base landing page config.
/// </summary>
[AIPlugin("TranslateLandingPageContent",
"Translate landing page content to the target language. " +
"Provide natural, marketing-quality translations. Keep technical terms, " +
"brand names, and URLs untranslated. Maintain the same number of items in arrays.")]
public class LandingPageTranslationPlugin : IAIPlugin
{
[AIParameter("Translated hero headline", required: true)]
public string HeroHeadline { get; set; }
[AIParameter("Translated hero subheadline", required: true)]
public string HeroSubheadline { get; set; }
[AIParameter("Translated primary CTA button label", required: true)]
public string PrimaryCtaLabel { get; set; }
[AIParameter("Translated secondary CTA button label", required: true)]
public string SecondaryCtaLabel { get; set; }
[AIParameter("Translated stats. Each must have 'value' (keep original numbers/values) and 'label' (translated) string properties. Must have the same number of items as the source.", required: true)]
public List<LandingPageStat> Stats { get; set; }
[AIParameter("Translated value propositions. Each must have 'title' and 'description' string properties (translated). Must have the same number of items as the source.", required: true)]
public List<LandingPageTranslationSection> ValueProps { get; set; }
[AIParameter("Translated features. Each must have 'title' and 'description' string properties (translated). Must have the same number of items as the source.", required: true)]
public List<LandingPageTranslationSection> Features { get; set; }
[AIParameter("Translated bottom CTA section headline", required: true)]
public string CtaHeadline { get; set; }
[AIParameter("Translated bottom CTA section subheadline", required: true)]
public string CtaSubheadline { get; set; }
[AIParameter("Translated bottom CTA button label", required: true)]
public string CtaButtonLabel { get; set; }
[AIParameter("Translated SEO page title", required: true)]
public string SeoTitle { get; set; }
[AIParameter("Translated SEO meta description", required: true)]
public string SeoDescription { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["heroheadline"] = typeof(string),
["herosubheadline"] = typeof(string),
["primaryctalabel"] = typeof(string),
["secondaryctalabel"] = typeof(string),
["stats"] = typeof(List<LandingPageStat>),
["valueprops"] = typeof(List<LandingPageTranslationSection>),
["features"] = typeof(List<LandingPageTranslationSection>),
["ctaheadline"] = typeof(string),
["ctasubheadline"] = typeof(string),
["ctabuttonlabel"] = typeof(string),
["seotitle"] = typeof(string),
["seodescription"] = typeof(string)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
// Properties are auto-populated by the framework.
// Assemble the translation overlay DTO matching the expected deep-merge structure.
var result = new LandingPageTranslationResult
{
Hero = new LandingPageTranslationHero
{
Headline = HeroHeadline,
Subheadline = HeroSubheadline,
PrimaryCta = new LandingPageCtaLabel { Label = PrimaryCtaLabel },
SecondaryCta = new LandingPageCtaLabel { Label = SecondaryCtaLabel }
},
Stats = Stats ?? new List<LandingPageStat>(),
ValueProps = ValueProps ?? new List<LandingPageTranslationSection>(),
Features = Features ?? new List<LandingPageTranslationSection>(),
CtaSection = new LandingPageTranslationCta
{
Headline = CtaHeadline,
Subheadline = CtaSubheadline,
Button = new LandingPageCtaLabel { Label = CtaButtonLabel }
},
Seo = new LandingPageSeo
{
Title = SeoTitle,
Description = SeoDescription
}
};
return Task.FromResult(new AIPluginResult(result, "Landing page content translated"));
}
}

View File

@@ -0,0 +1,34 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using System.Text.Json;
namespace GitCaddy.AI.Service.Plugins;
/// <summary>
/// Shared helpers for working with AIPlugin structured output.
/// </summary>
public static class PluginHelpers
{
/// <summary>
/// Safely deserializes StructuredOutput to the target type, handling the various
/// runtime types that the framework may return (typed object, JsonElement, or string).
/// </summary>
public static T DeserializeOutput<T>(object? structuredOutput) where T : class, new()
{
if (structuredOutput is T typed)
return typed;
string json;
if (structuredOutput is string str)
json = str;
else if (structuredOutput is JsonElement jsonElement)
json = jsonElement.GetRawText();
else if (structuredOutput != null)
json = JsonSerializer.Serialize(structuredOutput, structuredOutput.GetType());
else
return new T();
return JsonSerializer.Deserialize<T>(json) ?? new T();
}
}

View File

@@ -0,0 +1,76 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using System.Text.Json.Serialization;
using MarketAlly.AIPlugin;
namespace GitCaddy.AI.Service.Plugins;
// ============================================================================
// Result DTOs
// ============================================================================
public class WorkflowIssueItem
{
[JsonPropertyName("line")]
public int Line { get; set; }
[JsonPropertyName("severity")]
public string Severity { get; set; }
[JsonPropertyName("message")]
public string Message { get; set; }
[JsonPropertyName("fix")]
public string Fix { get; set; }
}
public class InspectWorkflowPluginResult
{
[JsonPropertyName("valid")]
public bool Valid { get; set; }
[JsonPropertyName("issues")]
public List<WorkflowIssueItem> Issues { get; set; }
[JsonPropertyName("suggestions")]
public List<string> Suggestions { get; set; }
}
// ============================================================================
// Plugin
// ============================================================================
[AIPlugin("InspectWorkflow",
"Inspect a CI/CD workflow YAML file for issues including syntax errors, " +
"missing fields, security problems, performance issues, and best practice violations.")]
public class InspectWorkflowPlugin : IAIPlugin
{
[AIParameter("Whether the workflow file is valid (no errors found)", required: true)]
public bool Valid { get; set; }
[AIParameter("Issues found in the workflow. Each item must have 'line' (int, line number or 0), 'severity' (string: 'error', 'warning', or 'info'), 'message' (string, description of the issue), 'fix' (string, suggested fix)", required: true)]
public List<WorkflowIssueItem> Issues { get; set; }
[AIParameter("General improvement suggestions as a list of strings", required: true)]
public List<string> Suggestions { get; set; }
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["valid"] = typeof(bool),
["issues"] = typeof(List<WorkflowIssueItem>),
["suggestions"] = typeof(List<string>)
};
public Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
{
var result = new InspectWorkflowPluginResult
{
Valid = Valid,
Issues = Issues ?? [],
Suggestions = Suggestions ?? []
};
return Task.FromResult(new AIPluginResult(result, "Workflow inspection complete"));
}
}

View File

@@ -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

View File

@@ -3,12 +3,13 @@
using GitCaddy.AI.Proto;
using GitCaddy.AI.Service.Configuration;
using GitCaddy.AI.Service.Plugins;
using Microsoft.Extensions.Options;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Implementation of AI-powered code intelligence.
/// Implementation of AI-powered code intelligence using MarketAlly.AIPlugin structured output.
/// </summary>
public class CodeIntelligenceService : ICodeIntelligenceService
{
@@ -33,16 +34,28 @@ public class CodeIntelligenceService : ICodeIntelligenceService
.WithSystemPrompt("""
You are an expert at summarizing code changes.
Provide clear, concise summaries that help developers understand what changed and why.
Output a brief summary paragraph followed by bullet points of key changes.
Also assess the potential impact of these changes.
""")
.RegisterPlugin<SummarizeChangesPlugin>()
.Build();
var prompt = BuildSummarizePrompt(request);
var response = await conversation.SendAsync(prompt, cancellationToken);
return ParseSummarizeResponse(response.FinalMessage ?? "");
var response = await conversation.SendForStructuredOutputAsync<SummarizeChangesResult>(
prompt, "SummarizeChanges", cancellationToken);
var result = PluginHelpers.DeserializeOutput<SummarizeChangesResult>(response.StructuredOutput);
var protoResponse = new SummarizeChangesResponse
{
Summary = result.Summary ?? "",
ImpactAssessment = result.ImpactAssessment ?? ""
};
if (result.BulletPoints != null)
protoResponse.BulletPoints.AddRange(result.BulletPoints);
return protoResponse;
}
public async Task<ExplainCodeResponse> ExplainCodeAsync(
@@ -60,6 +73,7 @@ public class CodeIntelligenceService : ICodeIntelligenceService
Adjust your explanation depth based on the code complexity.
""")
.RegisterPlugin<ExplainCodePlugin>()
.Build();
var prompt = $"""
@@ -72,12 +86,32 @@ public class CodeIntelligenceService : ICodeIntelligenceService
{(string.IsNullOrEmpty(request.Question) ? "" : $"Specific question: {request.Question}")}
""";
var response = await conversation.SendAsync(prompt, cancellationToken);
var response = await conversation.SendForStructuredOutputAsync<ExplainCodeResult>(
prompt, "ExplainCode", cancellationToken);
return new ExplainCodeResponse
var result = PluginHelpers.DeserializeOutput<ExplainCodeResult>(response.StructuredOutput);
var protoResponse = new ExplainCodeResponse
{
Explanation = response.FinalMessage ?? ""
Explanation = result.Explanation ?? ""
};
if (result.KeyConcepts != null)
protoResponse.KeyConcepts.AddRange(result.KeyConcepts);
if (result.References != null)
{
foreach (var r in result.References)
{
protoResponse.References.Add(new CodeReference
{
Description = r.Description ?? "",
Url = r.Url ?? ""
});
}
}
return protoResponse;
}
public async Task<SuggestFixResponse> SuggestFixAsync(
@@ -94,6 +128,7 @@ public class CodeIntelligenceService : ICodeIntelligenceService
Be specific and provide working code.
""")
.RegisterPlugin<SuggestFixPlugin>()
.Build();
var prompt = $"""
@@ -111,12 +146,21 @@ public class CodeIntelligenceService : ICodeIntelligenceService
Please suggest a fix.
""";
var response = await conversation.SendAsync(prompt, cancellationToken);
var response = await conversation.SendForStructuredOutputAsync<SuggestFixResult>(
prompt, "SuggestFix", cancellationToken);
return new SuggestFixResponse
var result = PluginHelpers.DeserializeOutput<SuggestFixResult>(response.StructuredOutput);
var protoResponse = new SuggestFixResponse
{
Explanation = response.FinalMessage ?? ""
Explanation = result.Explanation ?? "",
SuggestedCode = result.SuggestedCode ?? ""
};
if (result.AlternativeFixes != null)
protoResponse.AlternativeFixes.AddRange(result.AlternativeFixes);
return protoResponse;
}
private static string BuildSummarizePrompt(SummarizeChangesRequest request)
@@ -149,25 +193,4 @@ public class CodeIntelligenceService : ICodeIntelligenceService
return sb.ToString();
}
private static SummarizeChangesResponse ParseSummarizeResponse(string response)
{
var result = new SummarizeChangesResponse
{
Summary = response
};
// Extract bullet points (lines starting with - or *)
var lines = response.Split('\n');
foreach (var line in lines)
{
var trimmed = line.Trim();
if (trimmed.StartsWith("- ") || trimmed.StartsWith("* "))
{
result.BulletPoints.Add(trimmed[2..]);
}
}
return result;
}
}

View File

@@ -3,14 +3,13 @@
using GitCaddy.AI.Proto;
using GitCaddy.AI.Service.Configuration;
using MarketAlly.AIPlugin;
using MarketAlly.AIPlugin.Conversation;
using GitCaddy.AI.Service.Plugins;
using Microsoft.Extensions.Options;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Implementation of AI-powered code review using MarketAlly.AIPlugin.
/// Implementation of AI-powered code review using MarketAlly.AIPlugin structured output.
/// </summary>
public class CodeReviewService : ICodeReviewService
{
@@ -33,16 +32,16 @@ public class CodeReviewService : ICodeReviewService
{
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt(GetCodeReviewSystemPrompt(request.Options))
.RegisterPlugin<ReviewPullRequestPlugin>()
.Build();
// Build the review prompt
var prompt = BuildPullRequestReviewPrompt(request);
// Get AI response
var response = await conversation.SendAsync(prompt, cancellationToken);
var response = await conversation.SendForStructuredOutputAsync<ReviewPullRequestResult>(
prompt, "ReviewPullRequest", cancellationToken);
// Parse and return structured response
return ParsePullRequestReviewResponse(response.FinalMessage ?? "");
var result = PluginHelpers.DeserializeOutput<ReviewPullRequestResult>(response.StructuredOutput);
return MapPullRequestResult(result);
}
public async Task<ReviewCommitResponse> ReviewCommitAsync(
@@ -50,12 +49,16 @@ public class CodeReviewService : ICodeReviewService
{
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt(GetCodeReviewSystemPrompt(request.Options))
.RegisterPlugin<ReviewCommitPlugin>()
.Build();
var prompt = BuildCommitReviewPrompt(request);
var response = await conversation.SendAsync(prompt, cancellationToken);
return ParseCommitReviewResponse(response.FinalMessage ?? "");
var response = await conversation.SendForStructuredOutputAsync<ReviewCommitResult>(
prompt, "ReviewCommit", cancellationToken);
var result = PluginHelpers.DeserializeOutput<ReviewCommitResult>(response.StructuredOutput);
return MapCommitResult(result);
}
private static string GetCodeReviewSystemPrompt(ReviewOptions? options)
@@ -92,14 +95,6 @@ public class CodeReviewService : ICodeReviewService
5. Consider the broader context and impact of changes
{focusText}
Output Format:
Provide your review in a structured format:
- SUMMARY: A brief overview of the changes and overall assessment
- VERDICT: APPROVE, REQUEST_CHANGES, or COMMENT
- COMMENTS: Specific line-by-line feedback with severity levels
- SECURITY: Any security concerns found
- SUGGESTIONS: General improvement suggestions
""";
}
@@ -154,36 +149,105 @@ public class CodeReviewService : ICodeReviewService
return sb.ToString();
}
private static ReviewPullRequestResponse ParsePullRequestReviewResponse(string response)
private static ReviewPullRequestResponse MapPullRequestResult(ReviewPullRequestResult result)
{
// TODO: Implement structured parsing using JSON mode or regex extraction
// For now, return a basic response
var result = new ReviewPullRequestResponse
var response = new ReviewPullRequestResponse
{
Summary = response,
Verdict = ReviewVerdict.Comment,
EstimatedReviewMinutes = 5
Summary = result.Summary ?? "",
Verdict = ParseVerdict(result.Verdict),
EstimatedReviewMinutes = result.EstimatedReviewMinutes
};
// Parse verdict from response
if (response.Contains("APPROVE", StringComparison.OrdinalIgnoreCase) &&
!response.Contains("REQUEST_CHANGES", StringComparison.OrdinalIgnoreCase))
if (result.Comments != null)
{
result.Verdict = ReviewVerdict.Approve;
}
else if (response.Contains("REQUEST_CHANGES", StringComparison.OrdinalIgnoreCase))
{
result.Verdict = ReviewVerdict.RequestChanges;
foreach (var c in result.Comments)
{
response.Comments.Add(new ReviewComment
{
Path = c.Path ?? "",
Line = c.Line,
EndLine = c.EndLine,
Body = c.Body ?? "",
Severity = ParseSeverity(c.Severity),
Category = c.Category ?? "",
SuggestedFix = c.SuggestedFix ?? ""
});
}
}
return result;
if (result.Suggestions != null)
response.Suggestions.AddRange(result.Suggestions);
if (result.SecurityIssues is { Count: > 0 })
{
response.Security = new SecurityAnalysis
{
RiskScore = result.SecurityRiskScore,
Summary = result.SecuritySummary ?? ""
};
foreach (var si in result.SecurityIssues)
{
response.Security.Issues.Add(new SecurityIssue
{
Path = si.Path ?? "",
Line = si.Line,
IssueType = si.IssueType ?? "",
Description = si.Description ?? "",
Severity = si.Severity ?? "",
Remediation = si.Remediation ?? ""
});
}
}
return response;
}
private static ReviewCommitResponse ParseCommitReviewResponse(string response)
private static ReviewCommitResponse MapCommitResult(ReviewCommitResult result)
{
return new ReviewCommitResponse
var response = new ReviewCommitResponse
{
Summary = response
Summary = result.Summary ?? ""
};
if (result.Comments != null)
{
foreach (var c in result.Comments)
{
response.Comments.Add(new ReviewComment
{
Path = c.Path ?? "",
Line = c.Line,
EndLine = c.EndLine,
Body = c.Body ?? "",
Severity = ParseSeverity(c.Severity),
Category = c.Category ?? "",
SuggestedFix = c.SuggestedFix ?? ""
});
}
}
if (result.Suggestions != null)
response.Suggestions.AddRange(result.Suggestions);
return response;
}
private static ReviewVerdict ParseVerdict(string? verdict) =>
verdict?.ToLowerInvariant() switch
{
"approve" => ReviewVerdict.Approve,
"request_changes" => ReviewVerdict.RequestChanges,
"comment" => ReviewVerdict.Comment,
_ => ReviewVerdict.Comment
};
private static CommentSeverity ParseSeverity(string? severity) =>
severity?.ToLowerInvariant() switch
{
"critical" => CommentSeverity.Critical,
"error" => CommentSeverity.Error,
"warning" => CommentSeverity.Warning,
"info" => CommentSeverity.Info,
_ => CommentSeverity.Info
};
}

View File

@@ -3,12 +3,13 @@
using GitCaddy.AI.Proto;
using GitCaddy.AI.Service.Configuration;
using GitCaddy.AI.Service.Plugins;
using Microsoft.Extensions.Options;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Implementation of AI-powered documentation generation.
/// Implementation of AI-powered documentation generation using MarketAlly.AIPlugin structured output.
/// </summary>
public class DocumentationService : IDocumentationService
{
@@ -61,6 +62,7 @@ public class DocumentationService : IDocumentationService
Be concise but thorough.
""")
.RegisterPlugin<GenerateDocumentationPlugin>()
.Build();
var prompt = $"""
@@ -71,12 +73,29 @@ public class DocumentationService : IDocumentationService
```
""";
var response = await conversation.SendAsync(prompt, cancellationToken);
var response = await conversation.SendForStructuredOutputAsync<GenerateDocumentationResult>(
prompt, "GenerateDocumentation", cancellationToken);
return new GenerateDocumentationResponse
var result = PluginHelpers.DeserializeOutput<GenerateDocumentationResult>(response.StructuredOutput);
var protoResponse = new GenerateDocumentationResponse
{
Documentation = response.FinalMessage ?? ""
Documentation = result.Documentation ?? ""
};
if (result.Sections != null)
{
foreach (var s in result.Sections)
{
protoResponse.Sections.Add(new DocumentationSection
{
Title = s.Title ?? "",
Content = s.Content ?? ""
});
}
}
return protoResponse;
}
public async Task<GenerateCommitMessageResponse> GenerateCommitMessageAsync(
@@ -110,9 +129,8 @@ public class DocumentationService : IDocumentationService
Analyze the changes and write an appropriate commit message.
Focus on WHAT changed and WHY, not HOW.
Also provide 2-3 alternative messages.
""")
.RegisterPlugin<GenerateCommitMessagePlugin>()
.Build();
var sb = new System.Text.StringBuilder();
@@ -128,26 +146,19 @@ public class DocumentationService : IDocumentationService
sb.AppendLine();
}
var response = await conversation.SendAsync(sb.ToString(), cancellationToken);
var responseContent = response.FinalMessage ?? "";
var response = await conversation.SendForStructuredOutputAsync<GenerateCommitMessageResult>(
sb.ToString(), "GenerateCommitMessage", cancellationToken);
// Parse response - first line is primary, rest are alternatives
var lines = responseContent.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var result = new GenerateCommitMessageResponse
var result = PluginHelpers.DeserializeOutput<GenerateCommitMessageResult>(response.StructuredOutput);
var protoResponse = new GenerateCommitMessageResponse
{
Message = lines.Length > 0 ? lines[0].Trim() : responseContent.Trim()
Message = result.Message ?? ""
};
// Extract alternatives if present
for (var i = 1; i < lines.Length && result.Alternatives.Count < 3; i++)
{
var line = lines[i].Trim();
if (!string.IsNullOrEmpty(line) && !line.StartsWith('#') && !line.StartsWith('-'))
{
result.Alternatives.Add(line);
}
}
if (result.Alternatives != null)
protoResponse.Alternatives.AddRange(result.Alternatives);
return result;
return protoResponse;
}
}

View File

@@ -3,12 +3,13 @@
using GitCaddy.AI.Proto;
using GitCaddy.AI.Service.Configuration;
using GitCaddy.AI.Service.Plugins;
using Microsoft.Extensions.Options;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Implementation of AI-powered issue management.
/// Implementation of AI-powered issue management using MarketAlly.AIPlugin structured output.
/// </summary>
public class IssueService : IIssueService
{
@@ -40,15 +41,9 @@ public class IssueService : IIssueService
2. Category: bug, feature, question, docs, enhancement, etc.
3. Suggested labels from the available labels: {{availableLabels}}
4. A brief summary of the issue
Respond in JSON format:
{
"priority": "...",
"category": "...",
"suggested_labels": ["..."],
"summary": "..."
}
5. Whether this is a duplicate of an existing issue
""")
.RegisterPlugin<TriageIssuePlugin>()
.Build();
var prompt = $"""
@@ -61,10 +56,27 @@ public class IssueService : IIssueService
Existing labels: {string.Join(", ", request.ExistingLabels)}
""";
var response = await conversation.SendAsync(prompt, cancellationToken);
var response = await conversation.SendForStructuredOutputAsync<TriageIssueResult>(
prompt, "TriageIssue", cancellationToken);
// Parse JSON response
return ParseTriageResponse(response.FinalMessage ?? "");
var result = PluginHelpers.DeserializeOutput<TriageIssueResult>(response.StructuredOutput);
var protoResponse = new TriageIssueResponse
{
Priority = result.Priority ?? "medium",
Category = result.Category ?? "bug",
Summary = result.Summary ?? "",
IsDuplicate = result.IsDuplicate,
DuplicateOf = result.DuplicateOf
};
if (result.SuggestedLabels != null)
protoResponse.SuggestedLabels.AddRange(result.SuggestedLabels);
if (result.SuggestedAssignees != null)
protoResponse.SuggestedAssignees.AddRange(result.SuggestedAssignees);
return protoResponse;
}
public async Task<SuggestLabelsResponse> SuggestLabelsAsync(
@@ -78,14 +90,8 @@ public class IssueService : IIssueService
Available labels: {{string.Join(", ", request.AvailableLabels)}}
For each suggested label, provide a confidence score (0.0-1.0) and reason.
Respond in JSON format:
{
"suggestions": [
{"label": "...", "confidence": 0.9, "reason": "..."}
]
}
""")
.RegisterPlugin<SuggestLabelsPlugin>()
.Build();
var prompt = $"""
@@ -95,9 +101,27 @@ public class IssueService : IIssueService
{request.Body}
""";
var response = await conversation.SendAsync(prompt, cancellationToken);
var response = await conversation.SendForStructuredOutputAsync<SuggestLabelsResult>(
prompt, "SuggestLabels", cancellationToken);
return ParseSuggestLabelsResponse(response.FinalMessage ?? "", request.AvailableLabels);
var result = PluginHelpers.DeserializeOutput<SuggestLabelsResult>(response.StructuredOutput);
var protoResponse = new SuggestLabelsResponse();
if (result.Suggestions != null)
{
foreach (var s in result.Suggestions)
{
protoResponse.Suggestions.Add(new LabelSuggestion
{
Label = s.Label ?? "",
Confidence = s.Confidence,
Reason = s.Reason ?? ""
});
}
}
return protoResponse;
}
public async Task<GenerateIssueResponseResponse> GenerateResponseAsync(
@@ -120,6 +144,7 @@ public class IssueService : IIssueService
If you need more information, include follow-up questions.
""")
.RegisterPlugin<GenerateIssueResponsePlugin>()
.Build();
var sb = new System.Text.StringBuilder();
@@ -140,56 +165,19 @@ public class IssueService : IIssueService
sb.AppendLine();
sb.AppendLine("Please generate an appropriate response.");
var response = await conversation.SendAsync(sb.ToString(), cancellationToken);
var response = await conversation.SendForStructuredOutputAsync<GenerateIssueResponseResult>(
sb.ToString(), "GenerateIssueResponse", cancellationToken);
return new GenerateIssueResponseResponse
{
Response = response.FinalMessage ?? ""
};
}
var result = PluginHelpers.DeserializeOutput<GenerateIssueResponseResult>(response.StructuredOutput);
private static TriageIssueResponse ParseTriageResponse(string response)
{
// TODO: Implement proper JSON parsing
var result = new TriageIssueResponse
var protoResponse = new GenerateIssueResponseResponse
{
Priority = "medium",
Category = "bug",
Summary = response
Response = result.Response ?? ""
};
// Simple extraction (improve with JSON parsing)
if (response.Contains("\"priority\":", StringComparison.OrdinalIgnoreCase))
{
if (response.Contains("critical", StringComparison.OrdinalIgnoreCase))
result.Priority = "critical";
else if (response.Contains("high", StringComparison.OrdinalIgnoreCase))
result.Priority = "high";
else if (response.Contains("low", StringComparison.OrdinalIgnoreCase))
result.Priority = "low";
}
if (result.FollowUpQuestions != null)
protoResponse.FollowUpQuestions.AddRange(result.FollowUpQuestions);
return result;
}
private static SuggestLabelsResponse ParseSuggestLabelsResponse(string response, IEnumerable<string> availableLabels)
{
var result = new SuggestLabelsResponse();
// Simple heuristic: check which available labels appear in response
foreach (var label in availableLabels)
{
if (response.Contains(label, StringComparison.OrdinalIgnoreCase))
{
result.Suggestions.Add(new LabelSuggestion
{
Label = label,
Confidence = 0.8f,
Reason = "Label mentioned in AI analysis"
});
}
}
return result;
return protoResponse;
}
}

View 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;
}
}

View File

@@ -1,8 +1,10 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using System.Text.Json;
using GitCaddy.AI.Proto;
using GitCaddy.AI.Service.Configuration;
using GitCaddy.AI.Service.Plugins;
using Grpc.Core;
using Microsoft.Extensions.Options;
@@ -118,37 +120,18 @@ public class WorkflowService : IWorkflowService
try
{
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt("""
You are a helpful assistant executing tasks for a software development workflow.
Complete the requested task and provide clear results.
If you cannot complete the task, explain why.
""")
.Build();
var context = string.Join("\n", request.Context.Select(c => $"{c.Key}: {c.Value}"));
var prompt = $"""
Task: {request.Task}
Context:
{context}
Allowed tools: {string.Join(", ", request.AllowedTools)}
Please complete this task.
""";
var aiResponse = await conversation.SendAsync(prompt, cancellationToken);
return new ExecuteTaskResponse
return request.Task switch
{
Success = true,
Result = aiResponse.FinalMessage ?? ""
"landing_page_generate" => await ExecuteLandingPageGenerateAsync(request, cancellationToken),
"landing_page_translate" => await ExecuteLandingPageTranslateAsync(request, cancellationToken),
"ab_test_generate" => await ExecuteABTestGenerateAsync(request, cancellationToken),
"ab_test_analyze" => await ExecuteABTestAnalyzeAsync(request, cancellationToken),
_ => await ExecuteGenericTaskAsync(request, cancellationToken)
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Task execution failed");
_logger.LogError(ex, "Task execution failed: {Task}", request.Task);
return new ExecuteTaskResponse
{
@@ -158,6 +141,250 @@ public class WorkflowService : IWorkflowService
}
}
/// <summary>
/// Generates landing page content using structured output via the LandingPageContentPlugin.
/// The AI is forced (via tool_choice) to call the plugin, ensuring clean structured data
/// instead of free-form text that requires parsing.
/// </summary>
private async Task<ExecuteTaskResponse> ExecuteLandingPageGenerateAsync(
ExecuteTaskRequest request, CancellationToken cancellationToken)
{
_logger.LogInformation("Generating landing page content via structured output plugin");
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt("""
You are a landing page copywriter for open-source software projects.
Analyze the repository metadata and generate compelling, professional landing page content.
Use action-oriented, benefit-focused copy. Keep it professional but engaging.
""")
.RegisterPlugin<LandingPageContentPlugin>()
.Build();
var ctx = request.Context;
var prompt = $"""
Generate landing page content for this repository:
Name: {GetContextValue(ctx, "repo_name")}
Description: {GetContextValue(ctx, "repo_description")}
Topics: {GetContextValue(ctx, "topics")}
Primary Language: {GetContextValue(ctx, "primary_language")}
Stars: {GetContextValue(ctx, "stars", "0")}
Forks: {GetContextValue(ctx, "forks", "0")}
README:
{GetContextValue(ctx, "readme")}
""";
var response = await conversation.SendForStructuredOutputAsync<LandingPageContentResult>(
prompt, "GenerateLandingPageContent", cancellationToken);
var json = SerializeStructuredOutput(response.StructuredOutput);
return new ExecuteTaskResponse
{
Success = true,
Result = json
};
}
/// <summary>
/// Translates landing page content using structured output via the LandingPageTranslationPlugin.
/// The AI is forced (via tool_choice) to call the plugin, ensuring clean structured data.
/// </summary>
private async Task<ExecuteTaskResponse> ExecuteLandingPageTranslateAsync(
ExecuteTaskRequest request, CancellationToken cancellationToken)
{
var ctx = request.Context;
var targetLanguage = GetContextValue(ctx, "target_language");
var targetCode = GetContextValue(ctx, "target_code");
_logger.LogInformation("Translating landing page content to {Language} ({Code}) via structured output plugin",
targetLanguage, targetCode);
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt($"""
You are a professional translator specializing in marketing copy.
Translate landing page content to {targetLanguage} ({targetCode}) with natural,
culturally-adapted phrasing. Do NOT translate brand names, technical terms, or URLs.
Use marketing-quality translations, not literal/robotic ones.
""")
.RegisterPlugin<LandingPageTranslationPlugin>()
.Build();
var prompt = $"""
Translate the following landing page content to {targetLanguage} ({targetCode}).
Maintain the exact same number of items in arrays (stats, value_props, features).
Adapt idioms and expressions for the target culture.
Source content:
{GetContextValue(ctx, "source_content")}
""";
var response = await conversation.SendForStructuredOutputAsync<LandingPageTranslationResult>(
prompt, "TranslateLandingPageContent", cancellationToken);
var json = SerializeStructuredOutput(response.StructuredOutput);
return new ExecuteTaskResponse
{
Success = true,
Result = json
};
}
/// <summary>
/// Generates A/B test experiment variants using structured output via the ABTestGeneratePlugin.
/// </summary>
private async Task<ExecuteTaskResponse> ExecuteABTestGenerateAsync(
ExecuteTaskRequest request, CancellationToken cancellationToken)
{
_logger.LogInformation("Generating A/B test experiment via structured output plugin");
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt("""
You are a conversion rate optimization expert. Analyze landing page configurations
and create meaningful A/B test experiments that focus on high-impact changes.
Generate variants that test different headlines, CTAs, value propositions, or layouts.
Each variant should be meaningfully different but plausible.
The control variant (original config) is added automatically do NOT include it.
""")
.RegisterPlugin<ABTestGeneratePlugin>()
.Build();
var ctx = request.Context;
var prompt = $"""
Analyze this landing page config and create an A/B test experiment.
Focus on high-impact changes: headlines, CTAs, value propositions.
Generate 1-3 test variants. Each variant's config_override should be a partial
LandingConfig with only the fields that differ from the control.
Repository: {GetContextValue(ctx, "repo_name")}
Description: {GetContextValue(ctx, "repo_description")}
Current landing page config:
{GetContextValue(ctx, "landing_config")}
""";
var response = await conversation.SendForStructuredOutputAsync<ABTestGenerateResult>(
prompt, "GenerateABTestExperiment", cancellationToken);
var json = SerializeStructuredOutput(response.StructuredOutput);
return new ExecuteTaskResponse
{
Success = true,
Result = json
};
}
/// <summary>
/// Analyzes A/B test experiment results using structured output via the ABTestAnalyzePlugin.
/// </summary>
private async Task<ExecuteTaskResponse> ExecuteABTestAnalyzeAsync(
ExecuteTaskRequest request, CancellationToken cancellationToken)
{
_logger.LogInformation("Analyzing A/B test results via structured output plugin");
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt("""
You are a data analyst specializing in A/B test evaluation.
Analyze experiment results using statistical methods to determine significance.
Require at least 100 impressions per variant before declaring a winner.
Use a minimum 95% confidence threshold for statistical significance.
Be conservative only declare a winner when the data clearly supports it.
""")
.RegisterPlugin<ABTestAnalyzePlugin>()
.Build();
var ctx = request.Context;
var prompt = $"""
Analyze these A/B test results. Look at conversion rates, impression counts,
and event distributions across variants. Determine if there is a statistically
significant winner.
Experiment:
{GetContextValue(ctx, "experiment")}
Variants with metrics:
{GetContextValue(ctx, "variants")}
""";
var response = await conversation.SendForStructuredOutputAsync<ABTestAnalyzeResult>(
prompt, "AnalyzeABTestResults", cancellationToken);
var json = SerializeStructuredOutput(response.StructuredOutput);
return new ExecuteTaskResponse
{
Success = true,
Result = json
};
}
/// <summary>
/// Fallback for tasks that don't have a dedicated plugin.
/// Uses generic SendAsync with free-form text response.
/// </summary>
private async Task<ExecuteTaskResponse> ExecuteGenericTaskAsync(
ExecuteTaskRequest request, CancellationToken cancellationToken)
{
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt("""
You are a helpful assistant executing tasks for a software development workflow.
Complete the requested task and provide clear results.
If you cannot complete the task, explain why.
""")
.Build();
var context = string.Join("\n", request.Context.Select(c => $"{c.Key}: {c.Value}"));
var prompt = $"""
Task: {request.Task}
Context:
{context}
Allowed tools: {string.Join(", ", request.AllowedTools)}
Please complete this task.
""";
var aiResponse = await conversation.SendAsync(prompt, cancellationToken);
return new ExecuteTaskResponse
{
Success = true,
Result = aiResponse.FinalMessage ?? ""
};
}
/// <summary>
/// Safely serializes StructuredOutput to JSON regardless of its runtime type.
/// </summary>
private static string SerializeStructuredOutput(object structuredOutput)
{
if (structuredOutput == null)
return "{}";
if (structuredOutput is string str)
return str;
if (structuredOutput is JsonElement jsonElement)
return jsonElement.GetRawText();
return JsonSerializer.Serialize(structuredOutput, structuredOutput.GetType());
}
/// <summary>
/// Gets a value from the request context map with a default fallback.
/// </summary>
private static string GetContextValue(
Google.Protobuf.Collections.MapField<string, string> context,
string key, string defaultValue = "")
{
return context.TryGetValue(key, out var value) ? value : defaultValue;
}
private static string GetWorkflowSystemPrompt(string workflowType)
{
return workflowType switch