Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a55aa5f717 | |||
| 1385cbafa9 | |||
| 61ba70c2ad | |||
| 7f7bdcc568 | |||
| 6e25266da3 | |||
| ea06ca266f | |||
| ac8aa4c868 |
8
.notes/note-1770964636626-3yh3ujrlr.json
Normal file
8
.notes/note-1770964636626-3yh3ujrlr.json
Normal file
File diff suppressed because one or more lines are too long
91
README.md
91
README.md
@@ -13,19 +13,30 @@ AI-powered code intelligence service for GitCaddy. Provides code review, documen
|
||||
|
||||
## Architecture
|
||||
|
||||
The AI service runs as a sidecar alongside the GitCaddy server. It exposes two gRPC services on the same port (HTTP/2 + HTTP/1.1):
|
||||
|
||||
- **GitCaddyAIService** - AI operations (review, triage, docs, chat) called by the server's AI client
|
||||
- **PluginService** - Plugin lifecycle protocol (initialize, health, events) managed by the server's external plugin manager
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ GitCaddy Server (Go) │
|
||||
│ └── AI Client (gRPC) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ GitCaddy AI Service (.NET 9) │ │
|
||||
│ │ ├── gRPC API │ │
|
||||
│ │ ├── MarketAlly.AIPlugin (Multi-provider AI) │ │
|
||||
│ │ └── License Validation │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────────────┐
|
||||
│ GitCaddy Server (Go) │
|
||||
│ ├── AI Client (gRPC) ──────────────── GitCaddyAIService │
|
||||
│ │ ReviewPullRequest, TriageIssue, │ │
|
||||
│ │ ExplainCode, GenerateDocumentation │ │
|
||||
│ │ │ │
|
||||
│ └── External Plugin Manager (gRPC) ─── PluginService │
|
||||
│ Initialize, HealthCheck (30s), │ │
|
||||
│ OnEvent, Shutdown ▼ │
|
||||
│ ┌──────────────────────────┐ │
|
||||
│ │ GitCaddy AI (.NET 9) │ │
|
||||
│ │ Port 5000 (h2c + HTTP) │ │
|
||||
│ │ ├── AI Providers │ │
|
||||
│ │ │ (Claude/OpenAI/ │ │
|
||||
│ │ │ Gemini) │ │
|
||||
│ │ └── License Validation │ │
|
||||
│ └──────────────────────────┘ │
|
||||
└──────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
@@ -94,7 +105,9 @@ Environment variable format: `AIService__DefaultProvider=Claude`
|
||||
|
||||
## API Reference
|
||||
|
||||
The service exposes a gRPC API defined in `protos/gitcaddy_ai.proto`.
|
||||
The service exposes two gRPC services on port 5000 (cleartext HTTP/2):
|
||||
|
||||
### AI Operations (`protos/gitcaddy_ai.proto`)
|
||||
|
||||
### Code Review
|
||||
|
||||
@@ -134,6 +147,21 @@ rpc ExecuteTask(ExecuteTaskRequest) returns (ExecuteTaskResponse);
|
||||
rpc Chat(stream ChatRequest) returns (stream ChatResponse);
|
||||
```
|
||||
|
||||
### Plugin Protocol (`protos/plugin.proto`)
|
||||
|
||||
The plugin protocol allows the GitCaddy server to manage this service as an external plugin:
|
||||
|
||||
```protobuf
|
||||
rpc Initialize(InitializeRequest) returns (InitializeResponse);
|
||||
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse);
|
||||
rpc HealthCheck(PluginHealthCheckRequest) returns (PluginHealthCheckResponse);
|
||||
rpc GetManifest(GetManifestRequest) returns (PluginManifest);
|
||||
rpc OnEvent(PluginEvent) returns (EventResponse);
|
||||
rpc HandleHTTP(HTTPRequest) returns (HTTPResponse);
|
||||
```
|
||||
|
||||
The server calls `Initialize` on startup, `HealthCheck` every 30 seconds, `OnEvent` for subscribed events (e.g., `license:updated`), and `Shutdown` on server stop. The service declares its capabilities (routes, permissions, license tier) via the `PluginManifest` returned during initialization.
|
||||
|
||||
## Client Libraries
|
||||
|
||||
### .NET Client
|
||||
@@ -141,7 +169,7 @@ rpc Chat(stream ChatRequest) returns (stream ChatResponse);
|
||||
```csharp
|
||||
using GitCaddy.AI.Client;
|
||||
|
||||
var client = new GitCaddyAIClient("http://localhost:5051");
|
||||
var client = new GitCaddyAIClient("http://localhost:5000");
|
||||
|
||||
var response = await client.ReviewPullRequestAsync(new ReviewPullRequestRequest
|
||||
{
|
||||
@@ -159,7 +187,7 @@ Console.WriteLine(response.Summary);
|
||||
```go
|
||||
import ai "git.marketally.com/gitcaddy/gitcaddy-ai/go/gitcaddy-ai-client"
|
||||
|
||||
client, err := ai.NewClient("localhost:5051")
|
||||
client, err := ai.NewClient("localhost:5000")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -213,14 +241,43 @@ go generate
|
||||
|
||||
## Integration with GitCaddy Server
|
||||
|
||||
Add the AI client to your GitCaddy server configuration:
|
||||
Add both sections to the server's `app.ini`:
|
||||
|
||||
**1. AI client configuration** (how the server calls AI operations):
|
||||
|
||||
```ini
|
||||
[ai]
|
||||
ENABLED = true
|
||||
SERVICE_URL = http://localhost:5051
|
||||
SERVICE_URL = localhost:5000
|
||||
DEFAULT_PROVIDER = claude
|
||||
DEFAULT_MODEL = claude-sonnet-4-20250514
|
||||
CLAUDE_API_KEY = sk-ant-...
|
||||
```
|
||||
|
||||
See the [server README](https://git.marketally.com/gitcaddy/gitcaddy-server#ai-features-configuration) for the full `[ai]` reference.
|
||||
|
||||
**2. Plugin registration** (how the server manages the sidecar's lifecycle):
|
||||
|
||||
```ini
|
||||
[plugins]
|
||||
ENABLED = true
|
||||
HEALTH_CHECK_INTERVAL = 30s
|
||||
|
||||
[plugins.gitcaddy-ai]
|
||||
ENABLED = true
|
||||
ADDRESS = localhost:5000
|
||||
HEALTH_TIMEOUT = 5s
|
||||
SUBSCRIBED_EVENTS = license:updated
|
||||
```
|
||||
|
||||
With both sections configured, the server will:
|
||||
- Call AI operations (review, triage, etc.) via `[ai] SERVICE_URL`
|
||||
- Manage the sidecar's lifecycle via the plugin protocol on `[plugins.gitcaddy-ai] ADDRESS`
|
||||
- Health-check the sidecar every 30 seconds and log status changes
|
||||
- Dispatch subscribed events (e.g., license updates) to the sidecar in real-time
|
||||
|
||||
**Transport**: All communication uses cleartext HTTP/2 (h2c). The sidecar's Kestrel server is configured for `Http1AndHttp2` on port 5000, supporting both gRPC (HTTP/2) and REST (HTTP/1.1) on the same port.
|
||||
|
||||
## Support
|
||||
|
||||
- Documentation: https://docs.gitcaddy.com/ai
|
||||
|
||||
107
protos/plugin.proto
Normal file
107
protos/plugin.proto
Normal file
@@ -0,0 +1,107 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package plugin.v1;
|
||||
|
||||
option go_package = "code.gitcaddy.com/server/v3/modules/plugins/pluginv1;pluginv1";
|
||||
option csharp_namespace = "GitCaddy.AI.Plugin.Proto";
|
||||
|
||||
import "google/protobuf/struct.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
// PluginService is the RPC interface that external plugins must implement.
|
||||
// The server calls these methods to manage the plugin's lifecycle and dispatch events.
|
||||
service PluginService {
|
||||
// Initialize is called when the server starts or the plugin is loaded
|
||||
rpc Initialize(InitializeRequest) returns (InitializeResponse);
|
||||
// Shutdown is called when the server is shutting down
|
||||
rpc Shutdown(ShutdownRequest) returns (ShutdownResponse);
|
||||
// HealthCheck checks if the plugin is healthy
|
||||
rpc HealthCheck(PluginHealthCheckRequest) returns (PluginHealthCheckResponse);
|
||||
// GetManifest returns the plugin's manifest describing its capabilities
|
||||
rpc GetManifest(GetManifestRequest) returns (PluginManifest);
|
||||
// OnEvent is called when an event the plugin is subscribed to occurs
|
||||
rpc OnEvent(PluginEvent) returns (EventResponse);
|
||||
// HandleHTTP proxies an HTTP request to the plugin
|
||||
rpc HandleHTTP(HTTPRequest) returns (HTTPResponse);
|
||||
}
|
||||
|
||||
message InitializeRequest {
|
||||
string server_version = 1;
|
||||
map<string, string> config = 2;
|
||||
// protocol_version is the plugin protocol version the server supports.
|
||||
// The current version is 1. Plugins should check this to know what RPCs
|
||||
// the server may call. A value of 0 means the server predates versioning.
|
||||
int32 protocol_version = 3;
|
||||
}
|
||||
|
||||
message InitializeResponse {
|
||||
bool success = 1;
|
||||
string error = 2;
|
||||
PluginManifest manifest = 3;
|
||||
// protocol_version is the plugin protocol version the plugin supports.
|
||||
// The current version is 1. The server uses this to avoid calling RPCs
|
||||
// that the plugin doesn't implement. A value of 0 means the plugin
|
||||
// predates versioning and is treated as protocol version 1.
|
||||
int32 protocol_version = 4;
|
||||
}
|
||||
|
||||
message ShutdownRequest {
|
||||
string reason = 1;
|
||||
}
|
||||
|
||||
message ShutdownResponse {
|
||||
bool success = 1;
|
||||
}
|
||||
|
||||
message PluginHealthCheckRequest {}
|
||||
|
||||
message PluginHealthCheckResponse {
|
||||
bool healthy = 1;
|
||||
string status = 2;
|
||||
map<string, string> details = 3;
|
||||
}
|
||||
|
||||
message GetManifestRequest {}
|
||||
|
||||
message PluginManifest {
|
||||
string name = 1;
|
||||
string version = 2;
|
||||
string description = 3;
|
||||
repeated string subscribed_events = 4;
|
||||
repeated PluginRoute routes = 5;
|
||||
repeated string required_permissions = 6;
|
||||
string license_tier = 7;
|
||||
}
|
||||
|
||||
message PluginRoute {
|
||||
string method = 1;
|
||||
string path = 2;
|
||||
string description = 3;
|
||||
}
|
||||
|
||||
message PluginEvent {
|
||||
string event_type = 1;
|
||||
google.protobuf.Struct payload = 2;
|
||||
google.protobuf.Timestamp timestamp = 3;
|
||||
int64 repo_id = 4;
|
||||
int64 org_id = 5;
|
||||
}
|
||||
|
||||
message EventResponse {
|
||||
bool handled = 1;
|
||||
string error = 2;
|
||||
}
|
||||
|
||||
message HTTPRequest {
|
||||
string method = 1;
|
||||
string path = 2;
|
||||
map<string, string> headers = 3;
|
||||
bytes body = 4;
|
||||
map<string, string> query_params = 5;
|
||||
}
|
||||
|
||||
message HTTPResponse {
|
||||
int32 status_code = 1;
|
||||
map<string, string> headers = 2;
|
||||
bytes body = 3;
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
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;
|
||||
|
||||
@@ -21,6 +22,7 @@ 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;
|
||||
@@ -30,6 +32,7 @@ public class AIController : ControllerBase
|
||||
ICodeIntelligenceService codeIntelligenceService,
|
||||
IIssueService issueService,
|
||||
IDocumentationService documentationService,
|
||||
IWorkflowService workflowService,
|
||||
IAIProviderFactory providerFactory,
|
||||
ILicenseValidator licenseValidator,
|
||||
ILogger<AIController> logger)
|
||||
@@ -38,6 +41,7 @@ public class AIController : ControllerBase
|
||||
_codeIntelligenceService = codeIntelligenceService;
|
||||
_issueService = issueService;
|
||||
_documentationService = documentationService;
|
||||
_workflowService = workflowService;
|
||||
_providerFactory = providerFactory;
|
||||
_licenseValidator = licenseValidator;
|
||||
_logger = logger;
|
||||
@@ -426,15 +430,8 @@ public class AIController : ControllerBase
|
||||
- Performance issues (unnecessary steps, missing caching)
|
||||
- Compatibility issues with the available runner labels
|
||||
- Best practice violations
|
||||
|
||||
Respond with a JSON object containing:
|
||||
{
|
||||
"valid": true/false,
|
||||
"issues": [{"line": 0, "severity": "error|warning|info", "message": "...", "fix": "..."}],
|
||||
"suggestions": ["..."]
|
||||
}
|
||||
Only respond with the JSON object, no other text.
|
||||
""")
|
||||
.RegisterPlugin<InspectWorkflowPlugin>()
|
||||
.Build();
|
||||
|
||||
var prompt = $"Workflow file: {request.FilePath}\n\n```yaml\n{request.Content}\n```";
|
||||
@@ -443,45 +440,26 @@ public class AIController : ControllerBase
|
||||
prompt += $"\n\nAvailable runner labels: {string.Join(", ", request.RunnerLabels)}";
|
||||
}
|
||||
|
||||
var aiResponse = await conversation.SendAsync(prompt, cancellationToken);
|
||||
var responseText = aiResponse.FinalMessage ?? "{}";
|
||||
var aiResponse = await conversation.SendForStructuredOutputAsync<InspectWorkflowPluginResult>(
|
||||
prompt, "InspectWorkflow", cancellationToken);
|
||||
|
||||
// Parse the AI response as JSON
|
||||
try
|
||||
{
|
||||
var result = System.Text.Json.JsonSerializer.Deserialize<InspectWorkflowResult>(
|
||||
responseText,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
var result = PluginHelpers.DeserializeOutput<InspectWorkflowPluginResult>(aiResponse.StructuredOutput);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
valid = result?.Valid ?? true,
|
||||
issues = result?.Issues?.Select(i => new
|
||||
{
|
||||
line = i.Line,
|
||||
severity = i.Severity,
|
||||
message = i.Message,
|
||||
fix = i.Fix
|
||||
}).ToList() ?? [],
|
||||
suggestions = result?.Suggestions ?? [],
|
||||
confidence = 0.8,
|
||||
input_tokens = 0,
|
||||
output_tokens = 0
|
||||
});
|
||||
}
|
||||
catch (System.Text.Json.JsonException)
|
||||
return Ok(new
|
||||
{
|
||||
// If AI didn't return valid JSON, wrap the text response
|
||||
return Ok(new
|
||||
valid = result.Valid,
|
||||
issues = result.Issues?.Select(i => new
|
||||
{
|
||||
valid = true,
|
||||
issues = Array.Empty<object>(),
|
||||
suggestions = new[] { responseText },
|
||||
confidence = 0.5,
|
||||
input_tokens = 0,
|
||||
output_tokens = 0
|
||||
});
|
||||
}
|
||||
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)
|
||||
{
|
||||
@@ -490,6 +468,41 @@ public class AIController : ControllerBase
|
||||
}
|
||||
}
|
||||
|
||||
/// <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
|
||||
@@ -659,6 +672,18 @@ public class IssueCommentDto
|
||||
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")]
|
||||
@@ -672,18 +697,3 @@ public class InspectWorkflowDto
|
||||
public List<string>? RunnerLabels { get; set; }
|
||||
}
|
||||
|
||||
// Internal model for parsing AI response
|
||||
internal class InspectWorkflowResult
|
||||
{
|
||||
public bool Valid { get; set; }
|
||||
public List<InspectWorkflowIssue>? Issues { get; set; }
|
||||
public List<string>? Suggestions { get; set; }
|
||||
}
|
||||
|
||||
internal class InspectWorkflowIssue
|
||||
{
|
||||
public int Line { get; set; }
|
||||
public string? Severity { get; set; }
|
||||
public string? Message { get; set; }
|
||||
public string? Fix { get; set; }
|
||||
}
|
||||
|
||||
79
src/GitCaddy.AI.Service/Plugins/ABTestAnalyzePlugin.cs
Normal file
79
src/GitCaddy.AI.Service/Plugins/ABTestAnalyzePlugin.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
74
src/GitCaddy.AI.Service/Plugins/ABTestGeneratePlugin.cs
Normal file
74
src/GitCaddy.AI.Service/Plugins/ABTestGeneratePlugin.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
162
src/GitCaddy.AI.Service/Plugins/CodeIntelligencePlugin.cs
Normal file
162
src/GitCaddy.AI.Service/Plugins/CodeIntelligencePlugin.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
192
src/GitCaddy.AI.Service/Plugins/CodeReviewPlugin.cs
Normal file
192
src/GitCaddy.AI.Service/Plugins/CodeReviewPlugin.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
100
src/GitCaddy.AI.Service/Plugins/DocumentationPlugin.cs
Normal file
100
src/GitCaddy.AI.Service/Plugins/DocumentationPlugin.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
173
src/GitCaddy.AI.Service/Plugins/IssuePlugin.cs
Normal file
173
src/GitCaddy.AI.Service/Plugins/IssuePlugin.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
115
src/GitCaddy.AI.Service/Plugins/LandingPageContentPlugin.cs
Normal file
115
src/GitCaddy.AI.Service/Plugins/LandingPageContentPlugin.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
186
src/GitCaddy.AI.Service/Plugins/LandingPageModels.cs
Normal file
186
src/GitCaddy.AI.Service/Plugins/LandingPageModels.cs
Normal 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; }
|
||||
}
|
||||
102
src/GitCaddy.AI.Service/Plugins/LandingPageTranslationPlugin.cs
Normal file
102
src/GitCaddy.AI.Service/Plugins/LandingPageTranslationPlugin.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
34
src/GitCaddy.AI.Service/Plugins/PluginHelpers.cs
Normal file
34
src/GitCaddy.AI.Service/Plugins/PluginHelpers.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
76
src/GitCaddy.AI.Service/Plugins/WorkflowInspectPlugin.cs
Normal file
76
src/GitCaddy.AI.Service/Plugins/WorkflowInspectPlugin.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
134
src/GitCaddy.AI.Service/Services/PluginServiceImpl.cs
Normal file
134
src/GitCaddy.AI.Service/Services/PluginServiceImpl.cs
Normal file
@@ -0,0 +1,134 @@
|
||||
// Copyright 2026 MarketAlly. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
using Grpc.Core;
|
||||
using GitCaddy.AI.Plugin.Proto;
|
||||
using GitCaddy.AI.Service.Licensing;
|
||||
|
||||
namespace GitCaddy.AI.Service.Services;
|
||||
|
||||
/// <summary>
|
||||
/// gRPC service implementation for the GitCaddy plugin protocol.
|
||||
/// Allows the GitCaddy server to manage this sidecar as a plugin.
|
||||
/// </summary>
|
||||
public class PluginServiceImpl : PluginService.PluginServiceBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The plugin protocol version this implementation supports.
|
||||
/// Increment when new RPCs are added to PluginService.
|
||||
/// </summary>
|
||||
private const int ProtocolVersion = 1;
|
||||
|
||||
private readonly ILogger<PluginServiceImpl> _logger;
|
||||
private readonly ILicenseValidator _licenseValidator;
|
||||
|
||||
public PluginServiceImpl(
|
||||
ILicenseValidator licenseValidator,
|
||||
ILogger<PluginServiceImpl> logger)
|
||||
{
|
||||
_licenseValidator = licenseValidator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public override Task<InitializeResponse> Initialize(
|
||||
InitializeRequest request, ServerCallContext context)
|
||||
{
|
||||
_logger.LogInformation(
|
||||
"Plugin initialized by server version {ServerVersion} (protocol v{ProtocolVersion})",
|
||||
request.ServerVersion, request.ProtocolVersion);
|
||||
|
||||
return Task.FromResult(new InitializeResponse
|
||||
{
|
||||
Success = true,
|
||||
Manifest = BuildManifest(),
|
||||
ProtocolVersion = ProtocolVersion
|
||||
});
|
||||
}
|
||||
|
||||
public override Task<ShutdownResponse> Shutdown(
|
||||
ShutdownRequest request, ServerCallContext context)
|
||||
{
|
||||
_logger.LogInformation("Plugin shutdown requested: {Reason}", request.Reason);
|
||||
|
||||
return Task.FromResult(new ShutdownResponse
|
||||
{
|
||||
Success = true
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task<PluginHealthCheckResponse> HealthCheck(
|
||||
PluginHealthCheckRequest request, ServerCallContext context)
|
||||
{
|
||||
var licenseResult = await _licenseValidator.ValidateAsync();
|
||||
var version = typeof(PluginServiceImpl).Assembly.GetName().Version?.ToString() ?? "1.0.0";
|
||||
|
||||
var response = new PluginHealthCheckResponse
|
||||
{
|
||||
Healthy = licenseResult.IsValid,
|
||||
Status = licenseResult.IsValid ? "healthy" : "degraded"
|
||||
};
|
||||
|
||||
response.Details["version"] = version;
|
||||
response.Details["license_tier"] = licenseResult.License?.Tier ?? "none";
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public override Task<PluginManifest> GetManifest(
|
||||
GetManifestRequest request, ServerCallContext context)
|
||||
{
|
||||
return Task.FromResult(BuildManifest());
|
||||
}
|
||||
|
||||
public override Task<EventResponse> OnEvent(
|
||||
PluginEvent request, ServerCallContext context)
|
||||
{
|
||||
_logger.LogInformation("Received event {EventType} for repo {RepoId}", request.EventType, request.RepoId);
|
||||
|
||||
return Task.FromResult(new EventResponse
|
||||
{
|
||||
Handled = true
|
||||
});
|
||||
}
|
||||
|
||||
public override Task<HTTPResponse> HandleHTTP(
|
||||
HTTPRequest request, ServerCallContext context)
|
||||
{
|
||||
// Not used — the sidecar is called directly via REST by the server.
|
||||
return Task.FromResult(new HTTPResponse
|
||||
{
|
||||
StatusCode = 501
|
||||
});
|
||||
}
|
||||
|
||||
private static PluginManifest BuildManifest()
|
||||
{
|
||||
var version = typeof(PluginServiceImpl).Assembly.GetName().Version?.ToString() ?? "1.0.0";
|
||||
|
||||
var manifest = new PluginManifest
|
||||
{
|
||||
Name = "GitCaddy AI Service",
|
||||
Version = version,
|
||||
Description = "AI-powered code intelligence service for GitCaddy",
|
||||
LicenseTier = "professional"
|
||||
};
|
||||
|
||||
manifest.SubscribedEvents.Add("license:updated");
|
||||
manifest.RequiredPermissions.Add("ai:*");
|
||||
|
||||
manifest.Routes.AddRange(new[]
|
||||
{
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/review/pull-request", Description = "Review a pull request" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/issues/triage", Description = "Triage an issue" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/issues/suggest-labels", Description = "Suggest labels for an issue" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/code/explain", Description = "Explain code" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/docs/generate", Description = "Generate documentation" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/docs/commit-message", Description = "Generate commit message" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/issues/respond", Description = "Generate a response to an issue" },
|
||||
new PluginRoute { Method = "POST", Path = "/api/v1/workflows/inspect", Description = "Inspect a workflow YAML file" },
|
||||
new PluginRoute { Method = "GET", Path = "/api/v1/health", Description = "Health check" }
|
||||
});
|
||||
|
||||
return manifest;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user