refactor(ai): migrate AI services to structured output plugins
Convert all AI services to use plugin-based structured output: - Create dedicated plugins for code intelligence, review, docs, issues, and workflows - Replace JSON parsing with SendForStructuredOutputAsync - Add PluginHelpers for consistent deserialization - Remove inline prompt instructions in favor of plugin definitions - Eliminate brittle JSON parsing and error handling - Improve type safety and maintainability across all AI features Affected services: CodeIntelligence, CodeReview, Documentation, Issue, Workflow inspection
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -429,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```";
|
||||
@@ -446,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)
|
||||
{
|
||||
@@ -722,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; }
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
}
|
||||
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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user