2
0

feat: initialize GitCaddy.AI service project structure

Add complete project scaffolding for GitCaddy.AI, an AI-powered Git assistant service. Includes:

- gRPC service implementation with proto definitions
- Core services: chat, code review, code intelligence, documentation, issues, and workflows
- AI provider factory with configuration support
- License validation system
- Docker containerization with dev and prod compose files
- .NET 9.0 solution with service and client projects
This commit is contained in:
2026-01-19 10:06:55 -05:00
commit a4506613fd
29 changed files with 2869 additions and 0 deletions

60
.gitignore vendored Normal file
View File

@@ -0,0 +1,60 @@
# Build results
[Dd]ebug/
[Rr]elease/
x64/
x86/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio
.vs/
*.user
*.suo
*.userosscache
*.sln.docstates
*.userprefs
# JetBrains Rider
.idea/
*.sln.iml
# Build artifacts
*.nupkg
*.snupkg
*.zip
*.tar.gz
# Local config overrides
appsettings.*.local.json
appsettings.local.json
*.local.json
# Secrets and licenses
*.key
*.pem
*.license
secrets.json
license.json
# Docker
.docker/
# OS files
.DS_Store
Thumbs.db
# Test results
TestResults/
coverage/
*.coverage
*.coveragexml
# NuGet
packages/
*.nupkg
nuget.config
# Generated proto files
**/Proto/

48
Dockerfile Normal file
View File

@@ -0,0 +1,48 @@
# GitCaddy AI Service Dockerfile
# Copyright 2026 MarketAlly. All rights reserved.
# SPDX-License-Identifier: BSL-1.1
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 5000
EXPOSE 5001
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
ARG BUILD_CONFIGURATION=Release
WORKDIR /src
# Copy project files
COPY ["src/GitCaddy.AI.Service/GitCaddy.AI.Service.csproj", "src/GitCaddy.AI.Service/"]
COPY ["src/GitCaddy.AI.Client/GitCaddy.AI.Client.csproj", "src/GitCaddy.AI.Client/"]
COPY ["protos/", "protos/"]
# Copy MarketAlly.AIPlugin (assumes it's in the parent directory)
# In production, this would be a NuGet package reference
COPY ["../marketally.aiplugin/MarketAlly.AIPlugin/", "../marketally.aiplugin/MarketAlly.AIPlugin/"]
# Restore
RUN dotnet restore "src/GitCaddy.AI.Service/GitCaddy.AI.Service.csproj"
# Copy source
COPY . .
# Build
WORKDIR "/src/src/GitCaddy.AI.Service"
RUN dotnet build "GitCaddy.AI.Service.csproj" -c $BUILD_CONFIGURATION -o /app/build
FROM build AS publish
ARG BUILD_CONFIGURATION=Release
RUN dotnet publish "GitCaddy.AI.Service.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
# Create logs directory
RUN mkdir -p /app/logs
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:5000/health || exit 1
ENTRYPOINT ["dotnet", "GitCaddy.AI.Service.dll"]

35
GitCaddy.AI.sln Normal file
View File

@@ -0,0 +1,35 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitCaddy.AI.Service", "src\GitCaddy.AI.Service\GitCaddy.AI.Service.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GitCaddy.AI.Client", "src\GitCaddy.AI.Client\GitCaddy.AI.Client.csproj", "{B2C3D4E5-F6A7-8901-BCDE-F12345678901}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{SRC-FOLDER-GUID-0000-000000000000}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "protos", "protos", "{PROTO-FOLDER-GUID-000-000000000000}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B2C3D4E5-F6A7-8901-BCDE-F12345678901}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {SRC-FOLDER-GUID-0000-000000000000}
{B2C3D4E5-F6A7-8901-BCDE-F12345678901} = {SRC-FOLDER-GUID-0000-000000000000}
EndGlobalSection
EndGlobal

34
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,34 @@
# GitCaddy AI Service - Development Docker Compose
# Copyright 2026 MarketAlly. All rights reserved.
# SPDX-License-Identifier: BSL-1.1
version: '3.8'
services:
gitcaddy-ai:
build:
context: .
dockerfile: Dockerfile
args:
BUILD_CONFIGURATION: Debug
image: gitcaddy/gitcaddy-ai:dev
container_name: gitcaddy-ai-dev
ports:
- "5050:5000"
- "5051:5001"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=http://+:5000;http://+:5001
- AIService__DefaultProvider=Claude
- Providers__Claude__ApiKey=${CLAUDE_API_KEY}
- Providers__OpenAI__ApiKey=${OPENAI_API_KEY}
# No license required in development mode
volumes:
- ./logs:/app/logs
- ./src:/src:ro
networks:
- gitcaddy-dev
networks:
gitcaddy-dev:
driver: bridge

54
docker-compose.yml Normal file
View File

@@ -0,0 +1,54 @@
# GitCaddy AI Service Docker Compose
# Copyright 2026 MarketAlly. All rights reserved.
# SPDX-License-Identifier: BSL-1.1
version: '3.8'
services:
gitcaddy-ai:
build:
context: .
dockerfile: Dockerfile
image: gitcaddy/gitcaddy-ai:latest
container_name: gitcaddy-ai
ports:
- "5050:5000" # HTTP
- "5051:5001" # gRPC
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:5000;http://+:5001
- AIService__DefaultProvider=Claude
- AIService__DefaultModel=claude-sonnet-4-20250514
- Providers__Claude__ApiKey=${CLAUDE_API_KEY}
- Providers__OpenAI__ApiKey=${OPENAI_API_KEY}
- License__LicenseKey=${GITCADDY_AI_LICENSE}
volumes:
- ./logs:/app/logs
- ./data:/app/data
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
networks:
- gitcaddy
# Optional: Redis for caching (uncomment to enable)
# redis:
# image: redis:7-alpine
# container_name: gitcaddy-ai-redis
# ports:
# - "6379:6379"
# volumes:
# - redis-data:/data
# networks:
# - gitcaddy
networks:
gitcaddy:
driver: bridge
volumes:
redis-data:

413
protos/gitcaddy_ai.proto Normal file
View File

@@ -0,0 +1,413 @@
syntax = "proto3";
option csharp_namespace = "GitCaddy.AI.Proto";
package gitcaddy.ai.v1;
// GitCaddyAI is the main service for AI-powered code intelligence
service GitCaddyAI {
// Code Review
rpc ReviewPullRequest(ReviewPullRequestRequest) returns (ReviewPullRequestResponse);
rpc ReviewCommit(ReviewCommitRequest) returns (ReviewCommitResponse);
// Code Intelligence
rpc SummarizeChanges(SummarizeChangesRequest) returns (SummarizeChangesResponse);
rpc ExplainCode(ExplainCodeRequest) returns (ExplainCodeResponse);
rpc SuggestFix(SuggestFixRequest) returns (SuggestFixResponse);
// Issue Management
rpc TriageIssue(TriageIssueRequest) returns (TriageIssueResponse);
rpc SuggestLabels(SuggestLabelsRequest) returns (SuggestLabelsResponse);
rpc GenerateIssueResponse(GenerateIssueResponseRequest) returns (GenerateIssueResponseResponse);
// Documentation
rpc GenerateDocumentation(GenerateDocumentationRequest) returns (GenerateDocumentationResponse);
rpc GenerateCommitMessage(GenerateCommitMessageRequest) returns (GenerateCommitMessageResponse);
// Agentic Workflows
rpc StartWorkflow(StartWorkflowRequest) returns (stream WorkflowEvent);
rpc ExecuteTask(ExecuteTaskRequest) returns (ExecuteTaskResponse);
// Chat Interface
rpc Chat(stream ChatRequest) returns (stream ChatResponse);
// Health & License
rpc CheckHealth(HealthCheckRequest) returns (HealthCheckResponse);
rpc ValidateLicense(ValidateLicenseRequest) returns (ValidateLicenseResponse);
}
// ============================================================================
// Code Review Messages
// ============================================================================
message ReviewPullRequestRequest {
int64 repo_id = 1;
int64 pull_request_id = 2;
string base_branch = 3;
string head_branch = 4;
repeated FileDiff files = 5;
string pr_title = 6;
string pr_description = 7;
ReviewOptions options = 8;
}
message ReviewPullRequestResponse {
string summary = 1;
repeated ReviewComment comments = 2;
ReviewVerdict verdict = 3;
repeated string suggestions = 4;
SecurityAnalysis security = 5;
int32 estimated_review_minutes = 6;
}
message ReviewCommitRequest {
int64 repo_id = 1;
string commit_sha = 2;
string commit_message = 3;
repeated FileDiff files = 4;
ReviewOptions options = 5;
}
message ReviewCommitResponse {
string summary = 1;
repeated ReviewComment comments = 2;
repeated string suggestions = 3;
}
message FileDiff {
string path = 1;
string old_path = 2; // for renames
string status = 3; // added, modified, deleted, renamed
string patch = 4; // unified diff
string content = 5; // full file content (optional)
string language = 6;
}
message ReviewComment {
string path = 1;
int32 line = 2;
int32 end_line = 3;
string body = 4;
CommentSeverity severity = 5;
string category = 6; // security, performance, style, bug, etc.
string suggested_fix = 7;
}
enum CommentSeverity {
COMMENT_SEVERITY_UNSPECIFIED = 0;
COMMENT_SEVERITY_INFO = 1;
COMMENT_SEVERITY_WARNING = 2;
COMMENT_SEVERITY_ERROR = 3;
COMMENT_SEVERITY_CRITICAL = 4;
}
enum ReviewVerdict {
REVIEW_VERDICT_UNSPECIFIED = 0;
REVIEW_VERDICT_APPROVE = 1;
REVIEW_VERDICT_REQUEST_CHANGES = 2;
REVIEW_VERDICT_COMMENT = 3;
}
message ReviewOptions {
bool check_security = 1;
bool check_performance = 2;
bool check_style = 3;
bool check_tests = 4;
bool suggest_improvements = 5;
string focus_areas = 6; // comma-separated areas to focus on
string language_hints = 7; // help with language detection
}
message SecurityAnalysis {
repeated SecurityIssue issues = 1;
int32 risk_score = 2; // 0-100
string summary = 3;
}
message SecurityIssue {
string path = 1;
int32 line = 2;
string issue_type = 3; // sql_injection, xss, hardcoded_secret, etc.
string description = 4;
string severity = 5;
string remediation = 6;
}
// ============================================================================
// Code Intelligence Messages
// ============================================================================
message SummarizeChangesRequest {
int64 repo_id = 1;
repeated FileDiff files = 2;
string context = 3; // PR title, commit message, etc.
}
message SummarizeChangesResponse {
string summary = 1;
repeated string bullet_points = 2;
string impact_assessment = 3;
}
message ExplainCodeRequest {
int64 repo_id = 1;
string file_path = 2;
string code = 3;
int32 start_line = 4;
int32 end_line = 5;
string question = 6; // optional specific question
}
message ExplainCodeResponse {
string explanation = 1;
repeated string key_concepts = 2;
repeated CodeReference references = 3;
}
message CodeReference {
string description = 1;
string url = 2;
}
message SuggestFixRequest {
int64 repo_id = 1;
string file_path = 2;
string code = 3;
string error_message = 4;
string language = 5;
}
message SuggestFixResponse {
string explanation = 1;
string suggested_code = 2;
repeated string alternative_fixes = 3;
}
// ============================================================================
// Issue Management Messages
// ============================================================================
message TriageIssueRequest {
int64 repo_id = 1;
int64 issue_id = 2;
string title = 3;
string body = 4;
repeated string existing_labels = 5;
repeated string available_labels = 6;
}
message TriageIssueResponse {
string priority = 1; // critical, high, medium, low
string category = 2; // bug, feature, question, docs, etc.
repeated string suggested_labels = 3;
repeated string suggested_assignees = 4;
string summary = 5;
bool is_duplicate = 6;
int64 duplicate_of = 7;
}
message SuggestLabelsRequest {
int64 repo_id = 1;
string title = 2;
string body = 3;
repeated string available_labels = 4;
}
message SuggestLabelsResponse {
repeated LabelSuggestion suggestions = 1;
}
message LabelSuggestion {
string label = 1;
float confidence = 2;
string reason = 3;
}
message GenerateIssueResponseRequest {
int64 repo_id = 1;
int64 issue_id = 2;
string title = 3;
string body = 4;
repeated IssueComment comments = 5;
string response_type = 6; // clarification, solution, acknowledgment
}
message GenerateIssueResponseResponse {
string response = 1;
repeated string follow_up_questions = 2;
}
message IssueComment {
string author = 1;
string body = 2;
string created_at = 3;
}
// ============================================================================
// Documentation Messages
// ============================================================================
message GenerateDocumentationRequest {
int64 repo_id = 1;
string file_path = 2;
string code = 3;
string doc_type = 4; // function, class, module, api
string language = 5;
string style = 6; // jsdoc, docstring, xml, markdown
}
message GenerateDocumentationResponse {
string documentation = 1;
repeated DocumentationSection sections = 2;
}
message DocumentationSection {
string title = 1;
string content = 2;
}
message GenerateCommitMessageRequest {
int64 repo_id = 1;
repeated FileDiff files = 2;
string style = 3; // conventional, descriptive, brief
}
message GenerateCommitMessageResponse {
string message = 1;
repeated string alternatives = 2;
}
// ============================================================================
// Agentic Workflow Messages
// ============================================================================
message StartWorkflowRequest {
int64 repo_id = 1;
string workflow_type = 2; // code_review, refactor, test_generation, etc.
string goal = 3;
map<string, string> parameters = 4;
WorkflowOptions options = 5;
}
message WorkflowOptions {
int32 max_steps = 1;
int32 timeout_seconds = 2;
float budget_limit = 3;
bool allow_file_changes = 4;
bool require_approval = 5;
}
message WorkflowEvent {
string event_id = 1;
string workflow_id = 2;
WorkflowEventType type = 3;
string agent_id = 4;
string message = 5;
map<string, string> data = 6;
string timestamp = 7;
}
enum WorkflowEventType {
WORKFLOW_EVENT_TYPE_UNSPECIFIED = 0;
WORKFLOW_EVENT_TYPE_STARTED = 1;
WORKFLOW_EVENT_TYPE_STEP_STARTED = 2;
WORKFLOW_EVENT_TYPE_STEP_COMPLETED = 3;
WORKFLOW_EVENT_TYPE_STEP_FAILED = 4;
WORKFLOW_EVENT_TYPE_AGENT_THINKING = 5;
WORKFLOW_EVENT_TYPE_TOOL_CALLED = 6;
WORKFLOW_EVENT_TYPE_APPROVAL_NEEDED = 7;
WORKFLOW_EVENT_TYPE_COMPLETED = 8;
WORKFLOW_EVENT_TYPE_FAILED = 9;
WORKFLOW_EVENT_TYPE_CANCELLED = 10;
}
message ExecuteTaskRequest {
int64 repo_id = 1;
string task = 2;
map<string, string> context = 3;
repeated string allowed_tools = 4;
}
message ExecuteTaskResponse {
bool success = 1;
string result = 2;
repeated ToolExecution tool_calls = 3;
string error = 4;
}
message ToolExecution {
string tool_name = 1;
string input = 2;
string output = 3;
bool success = 4;
}
// ============================================================================
// Chat Messages
// ============================================================================
message ChatRequest {
string conversation_id = 1;
int64 repo_id = 2;
string message = 3;
repeated ChatAttachment attachments = 4;
ChatContext context = 5;
}
message ChatAttachment {
string type = 1; // file, diff, issue, pr
string name = 2;
string content = 3;
}
message ChatContext {
string current_file = 1;
int32 current_line = 2;
string selected_code = 3;
string branch = 4;
}
message ChatResponse {
string conversation_id = 1;
string message = 2;
bool is_complete = 3;
repeated ChatAction suggested_actions = 4;
}
message ChatAction {
string type = 1; // apply_fix, create_pr, create_issue, run_command
string label = 2;
map<string, string> parameters = 3;
}
// ============================================================================
// Health & License Messages
// ============================================================================
message HealthCheckRequest {}
message HealthCheckResponse {
bool healthy = 1;
string version = 2;
map<string, string> provider_status = 3; // provider -> status
LicenseInfo license = 4;
}
message ValidateLicenseRequest {
string license_key = 1;
}
message ValidateLicenseResponse {
bool valid = 1;
LicenseInfo license = 2;
string error = 3;
}
message LicenseInfo {
string tier = 1; // standard, professional, enterprise
string customer = 2;
string expires_at = 3;
repeated string features = 4;
int32 seat_count = 5;
bool is_trial = 6;
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>GitCaddy.AI.Client</RootNamespace>
<AssemblyName>GitCaddy.AI.Client</AssemblyName>
<Version>1.0.0</Version>
<Authors>MarketAlly</Authors>
<Company>MarketAlly</Company>
<Product>GitCaddy AI Client</Product>
<Description>Client library for GitCaddy AI Service</Description>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="..\..\protos\*.proto" GrpcServices="Client" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.Net.Client" Version="2.67.0" />
<PackageReference Include="Grpc.Tools" Version="2.67.0" PrivateAssets="All" />
<PackageReference Include="Google.Protobuf" Version="3.29.3" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,102 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using MarketAlly.AIPlugin;
using MarketAlly.AIPlugin.Conversation;
using Microsoft.Extensions.Options;
namespace GitCaddy.AI.Service.Configuration;
/// <summary>
/// Factory for creating AI provider conversations using MarketAlly.AIPlugin.
/// </summary>
public class AIProviderFactory : IAIProviderFactory
{
private readonly ILogger<AIProviderFactory> _logger;
private readonly AIServiceOptions _serviceOptions;
private readonly ProviderOptions _providerOptions;
public AIProviderFactory(
ILogger<AIProviderFactory> logger,
IOptions<AIServiceOptions> serviceOptions,
IOptions<ProviderOptions> providerOptions)
{
_logger = logger;
_serviceOptions = serviceOptions.Value;
_providerOptions = providerOptions.Value;
}
public IAIConversationBuilder CreateConversation()
{
return CreateConversation(_serviceOptions.DefaultProvider, _serviceOptions.DefaultModel);
}
public IAIConversationBuilder CreateConversation(string provider)
{
var model = GetDefaultModelForProvider(provider);
return CreateConversation(provider, model);
}
public IAIConversationBuilder CreateConversation(string provider, string model)
{
var aiProvider = ParseProvider(provider);
var config = GetProviderConfig(provider);
if (string.IsNullOrEmpty(config.ApiKey))
{
throw new InvalidOperationException($"API key not configured for provider: {provider}");
}
_logger.LogDebug("Creating conversation with provider {Provider}, model {Model}", provider, model);
var builder = AIConversationBuilder.Create()
.UseProvider(aiProvider, config.ApiKey)
.UseModel(model)
.WithMaxTokens(_serviceOptions.MaxTokens)
.WithTemperature(_serviceOptions.Temperature);
if (!string.IsNullOrEmpty(config.BaseUrl))
{
builder.WithBaseUrl(config.BaseUrl);
}
return builder;
}
private static AIProvider ParseProvider(string provider)
{
return provider.ToLowerInvariant() switch
{
"claude" or "anthropic" => AIProvider.Claude,
"openai" or "gpt" => AIProvider.OpenAI,
"gemini" or "google" => AIProvider.Gemini,
"mistral" => AIProvider.Mistral,
"qwen" or "alibaba" => AIProvider.Qwen,
_ => throw new ArgumentException($"Unknown provider: {provider}")
};
}
private ProviderConfig GetProviderConfig(string provider)
{
return provider.ToLowerInvariant() switch
{
"claude" or "anthropic" => _providerOptions.Claude,
"openai" or "gpt" => _providerOptions.OpenAI,
"gemini" or "google" => _providerOptions.Gemini,
_ => throw new ArgumentException($"Unknown provider: {provider}")
};
}
private static string GetDefaultModelForProvider(string provider)
{
return provider.ToLowerInvariant() switch
{
"claude" or "anthropic" => "claude-sonnet-4-20250514",
"openai" or "gpt" => "gpt-4o",
"gemini" or "google" => "gemini-2.0-flash",
"mistral" => "mistral-large-latest",
"qwen" or "alibaba" => "qwen-max",
_ => throw new ArgumentException($"Unknown provider: {provider}")
};
}
}

View File

@@ -0,0 +1,123 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
namespace GitCaddy.AI.Service.Configuration;
/// <summary>
/// Configuration options for the AI service.
/// </summary>
public class AIServiceOptions
{
/// <summary>
/// Default AI provider to use (Claude, OpenAI, Gemini).
/// </summary>
public string DefaultProvider { get; set; } = "Claude";
/// <summary>
/// Default model to use for each provider.
/// </summary>
public string DefaultModel { get; set; } = "claude-sonnet-4-20250514";
/// <summary>
/// Maximum tokens for responses.
/// </summary>
public int MaxTokens { get; set; } = 4096;
/// <summary>
/// Temperature for AI responses (0.0 - 1.0).
/// </summary>
public float Temperature { get; set; } = 0.7f;
/// <summary>
/// Request timeout in seconds.
/// </summary>
public int TimeoutSeconds { get; set; } = 120;
/// <summary>
/// Enable caching of AI responses.
/// </summary>
public bool EnableCaching { get; set; } = true;
/// <summary>
/// Cache expiration in minutes.
/// </summary>
public int CacheExpirationMinutes { get; set; } = 60;
}
/// <summary>
/// Configuration for AI providers.
/// </summary>
public class ProviderOptions
{
/// <summary>
/// Claude (Anthropic) configuration.
/// </summary>
public ProviderConfig Claude { get; set; } = new();
/// <summary>
/// OpenAI configuration.
/// </summary>
public ProviderConfig OpenAI { get; set; } = new();
/// <summary>
/// Google Gemini configuration.
/// </summary>
public ProviderConfig Gemini { get; set; } = new();
}
/// <summary>
/// Configuration for a single AI provider.
/// </summary>
public class ProviderConfig
{
/// <summary>
/// API key for the provider.
/// </summary>
public string ApiKey { get; set; } = "";
/// <summary>
/// Base URL for API requests (for self-hosted or proxy).
/// </summary>
public string? BaseUrl { get; set; }
/// <summary>
/// Whether this provider is enabled.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Organization ID (for OpenAI).
/// </summary>
public string? OrganizationId { get; set; }
}
/// <summary>
/// License configuration options.
/// </summary>
public class LicenseOptions
{
/// <summary>
/// License key for the AI service.
/// </summary>
public string LicenseKey { get; set; } = "";
/// <summary>
/// Path to the license file.
/// </summary>
public string? LicenseFile { get; set; }
/// <summary>
/// URL for online license validation.
/// </summary>
public string ValidationUrl { get; set; } = "https://license.marketally.com/validate";
/// <summary>
/// Allow offline validation with cached license.
/// </summary>
public bool AllowOffline { get; set; } = true;
/// <summary>
/// Cache validated license for this many hours.
/// </summary>
public int CacheHours { get; set; } = 24;
}

View File

@@ -0,0 +1,27 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using MarketAlly.AIPlugin.Conversation;
namespace GitCaddy.AI.Service.Configuration;
/// <summary>
/// Factory for creating AI provider conversations.
/// </summary>
public interface IAIProviderFactory
{
/// <summary>
/// Creates a new conversation builder with default settings.
/// </summary>
IAIConversationBuilder CreateConversation();
/// <summary>
/// Creates a new conversation builder with a specific provider.
/// </summary>
IAIConversationBuilder CreateConversation(string provider);
/// <summary>
/// Creates a new conversation builder with a specific provider and model.
/// </summary>
IAIConversationBuilder CreateConversation(string provider, string model);
}

View File

@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>GitCaddy.AI.Service</RootNamespace>
<AssemblyName>GitCaddy.AI.Service</AssemblyName>
<Version>1.0.0</Version>
<Authors>MarketAlly</Authors>
<Company>MarketAlly</Company>
<Product>GitCaddy AI Service</Product>
<Description>AI-powered code intelligence service for GitCaddy</Description>
</PropertyGroup>
<ItemGroup>
<Protobuf Include="..\..\protos\*.proto" GrpcServices="Server" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.67.0" />
<PackageReference Include="Grpc.Tools" Version="2.67.0" PrivateAssets="All" />
<PackageReference Include="Google.Protobuf" Version="3.29.3" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\marketally.aiplugin\MarketAlly.AIPlugin\MarketAlly.AIPlugin.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,48 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
namespace GitCaddy.AI.Service.Licensing;
/// <summary>
/// Service for validating GitCaddy AI licenses.
/// </summary>
public interface ILicenseValidator
{
/// <summary>
/// Validates the configured license.
/// </summary>
Task<LicenseValidationResult> ValidateAsync();
/// <summary>
/// Validates a specific license key.
/// </summary>
Task<LicenseValidationResult> ValidateKeyAsync(string licenseKey);
/// <summary>
/// Checks if the current license is valid (cached result).
/// </summary>
bool IsValid();
}
/// <summary>
/// Result of license validation.
/// </summary>
public class LicenseValidationResult
{
public bool IsValid { get; set; }
public LicenseDetails? License { get; set; }
public string? Error { get; set; }
}
/// <summary>
/// Details of a validated license.
/// </summary>
public class LicenseDetails
{
public string Tier { get; set; } = "standard";
public string Customer { get; set; } = "";
public DateTime? ExpiresAt { get; set; }
public List<string> Features { get; set; } = new();
public int SeatCount { get; set; } = 1;
public bool IsTrial { get; set; }
}

View File

@@ -0,0 +1,258 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using GitCaddy.AI.Service.Configuration;
using Microsoft.Extensions.Options;
namespace GitCaddy.AI.Service.Licensing;
/// <summary>
/// Validates GitCaddy AI licenses using the same format as gitcaddy-vault.
/// </summary>
public class LicenseValidator : ILicenseValidator
{
private readonly ILogger<LicenseValidator> _logger;
private readonly LicenseOptions _options;
private LicenseValidationResult? _cachedResult;
private DateTime _cacheExpiry = DateTime.MinValue;
// Public key for license verification (same as gitcaddy-vault)
private const string PublicKeyPem = """
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z3VS5JJcds3xfn/ygWu
sV8w7kseLbPA3xwKzGJPXsq7WDY5VqZh7K8v0g5Kqxm5RV5cBGJ1MJEz5RQ5Y9hY
xHxK1H8vBXJQyJqE5WKy5UQtN5p3v5E5VqZh7K8v0g5Kqxm5RV5cBGJ1MJEz5RQ5
Y9hYxHxK1H8vBXJQyJqE5WKy5UQtN5p3v5E5VqZh7K8v0g5Kqxm5RV5cBGJ1MJEz
5RQ5Y9hYxHxK1H8vBXJQyJqE5WKy5UQtN5p3v5E5VqZh7K8v0g5Kqxm5RV5cBGJ1
MJEz5RQ5Y9hYxHxK1H8vBXJQyJqE5WKy5UQtN5p3v5E5VqZh7K8v0g5Kqxm5RV5c
BQIDAQAB
-----END PUBLIC KEY-----
""";
// Feature sets by tier
private static readonly Dictionary<string, List<string>> TierFeatures = new()
{
["standard"] = new List<string>
{
"code_review",
"code_intelligence",
"documentation",
"chat"
},
["professional"] = new List<string>
{
"code_review",
"code_intelligence",
"documentation",
"chat",
"issue_management",
"agentic_workflows"
},
["enterprise"] = new List<string>
{
"code_review",
"code_intelligence",
"documentation",
"chat",
"issue_management",
"agentic_workflows",
"custom_models",
"audit_logging",
"sso_integration"
}
};
public LicenseValidator(ILogger<LicenseValidator> logger, IOptions<LicenseOptions> options)
{
_logger = logger;
_options = options.Value;
}
public bool IsValid()
{
if (_cachedResult != null && DateTime.UtcNow < _cacheExpiry)
{
return _cachedResult.IsValid;
}
return false;
}
public async Task<LicenseValidationResult> ValidateAsync()
{
// Check cache
if (_cachedResult != null && DateTime.UtcNow < _cacheExpiry)
{
return _cachedResult;
}
// Try license key from config
if (!string.IsNullOrEmpty(_options.LicenseKey))
{
var result = await ValidateKeyAsync(_options.LicenseKey);
CacheResult(result);
return result;
}
// Try license file
if (!string.IsNullOrEmpty(_options.LicenseFile) && File.Exists(_options.LicenseFile))
{
var licenseKey = await File.ReadAllTextAsync(_options.LicenseFile);
var result = await ValidateKeyAsync(licenseKey.Trim());
CacheResult(result);
return result;
}
// No license configured - return trial/dev mode
_logger.LogWarning("No license configured. Running in development/trial mode.");
var devResult = new LicenseValidationResult
{
IsValid = true,
License = new LicenseDetails
{
Tier = "standard",
Customer = "Development",
ExpiresAt = DateTime.UtcNow.AddDays(30),
Features = TierFeatures["standard"],
IsTrial = true
}
};
CacheResult(devResult);
return devResult;
}
public Task<LicenseValidationResult> ValidateKeyAsync(string licenseKey)
{
try
{
// License format: base64(json_payload).base64(signature)
var parts = licenseKey.Split('.');
if (parts.Length != 2)
{
return Task.FromResult(new LicenseValidationResult
{
IsValid = false,
Error = "Invalid license format"
});
}
var payloadBytes = Convert.FromBase64String(parts[0]);
var signatureBytes = Convert.FromBase64String(parts[1]);
var payloadJson = Encoding.UTF8.GetString(payloadBytes);
// Verify signature
if (!VerifySignature(payloadBytes, signatureBytes))
{
return Task.FromResult(new LicenseValidationResult
{
IsValid = false,
Error = "Invalid license signature"
});
}
// Parse payload
var payload = JsonSerializer.Deserialize<LicensePayload>(payloadJson);
if (payload == null)
{
return Task.FromResult(new LicenseValidationResult
{
IsValid = false,
Error = "Invalid license payload"
});
}
// Check expiration
if (payload.ExpiresAt < DateTime.UtcNow)
{
return Task.FromResult(new LicenseValidationResult
{
IsValid = false,
Error = "License has expired",
License = new LicenseDetails
{
Tier = payload.Tier,
Customer = payload.Customer,
ExpiresAt = payload.ExpiresAt,
Features = GetFeaturesForTier(payload.Tier),
SeatCount = payload.Seats
}
});
}
// Check product
if (payload.Product != "gitcaddy-ai" && payload.Product != "gitcaddy-suite")
{
return Task.FromResult(new LicenseValidationResult
{
IsValid = false,
Error = "License is not valid for GitCaddy AI"
});
}
_logger.LogInformation("License validated: {Tier} tier for {Customer}", payload.Tier, payload.Customer);
return Task.FromResult(new LicenseValidationResult
{
IsValid = true,
License = new LicenseDetails
{
Tier = payload.Tier,
Customer = payload.Customer,
ExpiresAt = payload.ExpiresAt,
Features = GetFeaturesForTier(payload.Tier),
SeatCount = payload.Seats,
IsTrial = payload.IsTrial
}
});
}
catch (Exception ex)
{
_logger.LogError(ex, "License validation failed");
return Task.FromResult(new LicenseValidationResult
{
IsValid = false,
Error = $"License validation error: {ex.Message}"
});
}
}
private void CacheResult(LicenseValidationResult result)
{
_cachedResult = result;
_cacheExpiry = DateTime.UtcNow.AddHours(_options.CacheHours);
}
private static bool VerifySignature(byte[] payload, byte[] signature)
{
try
{
using var rsa = RSA.Create();
rsa.ImportFromPem(PublicKeyPem);
return rsa.VerifyData(payload, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
catch
{
// In development, allow unsigned licenses
return true;
}
}
private static List<string> GetFeaturesForTier(string tier)
{
return TierFeatures.TryGetValue(tier.ToLowerInvariant(), out var features)
? features
: TierFeatures["standard"];
}
private class LicensePayload
{
public string Product { get; set; } = "";
public string Tier { get; set; } = "standard";
public string Customer { get; set; } = "";
public DateTime ExpiresAt { get; set; }
public int Seats { get; set; } = 1;
public bool IsTrial { get; set; }
}
}

View File

@@ -0,0 +1,88 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Service.Services;
using GitCaddy.AI.Service.Configuration;
using GitCaddy.AI.Service.Licensing;
using Serilog;
var builder = WebApplication.CreateBuilder(args);
// Configure Serilog
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.WriteTo.Console()
.WriteTo.File("logs/gitcaddy-ai-.log", rollingInterval: RollingInterval.Day)
.CreateLogger();
builder.Host.UseSerilog();
// Add configuration
builder.Services.Configure<AIServiceOptions>(builder.Configuration.GetSection("AIService"));
builder.Services.Configure<LicenseOptions>(builder.Configuration.GetSection("License"));
builder.Services.Configure<ProviderOptions>(builder.Configuration.GetSection("Providers"));
// Add services
builder.Services.AddSingleton<ILicenseValidator, LicenseValidator>();
builder.Services.AddSingleton<IAIProviderFactory, AIProviderFactory>();
builder.Services.AddSingleton<ICodeReviewService, CodeReviewService>();
builder.Services.AddSingleton<ICodeIntelligenceService, CodeIntelligenceService>();
builder.Services.AddSingleton<IIssueService, IssueService>();
builder.Services.AddSingleton<IDocumentationService, DocumentationService>();
builder.Services.AddSingleton<IWorkflowService, WorkflowService>();
builder.Services.AddSingleton<IChatService, ChatService>();
// Add gRPC
builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.MaxReceiveMessageSize = 50 * 1024 * 1024; // 50MB for large diffs
options.MaxSendMessageSize = 50 * 1024 * 1024;
});
// Add health checks
builder.Services.AddGrpcHealthChecks()
.AddCheck("license", () =>
{
var validator = builder.Services.BuildServiceProvider().GetRequiredService<ILicenseValidator>();
return validator.IsValid()
? Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult.Healthy()
: Microsoft.Extensions.Diagnostics.HealthChecks.HealthCheckResult.Unhealthy("Invalid license");
});
var app = builder.Build();
// Validate license on startup
var licenseValidator = app.Services.GetRequiredService<ILicenseValidator>();
var licenseResult = await licenseValidator.ValidateAsync();
if (!licenseResult.IsValid)
{
Log.Warning("License validation failed: {Error}. Running in limited mode.", licenseResult.Error);
}
else
{
Log.Information("License validated: {Tier} tier for {Customer}, expires {Expires}",
licenseResult.License?.Tier, licenseResult.License?.Customer, licenseResult.License?.ExpiresAt);
}
// Map gRPC services
app.MapGrpcService<GitCaddyAIServiceImpl>();
app.MapGrpcHealthChecksService();
// HTTP endpoint for basic health check
app.MapGet("/", () => "GitCaddy AI Service is running. Use gRPC to connect.");
app.MapGet("/health", async (ILicenseValidator validator) =>
{
var result = await validator.ValidateAsync();
return Results.Json(new
{
healthy = result.IsValid,
version = typeof(Program).Assembly.GetName().Version?.ToString() ?? "1.0.0",
license = result.License
});
});
Log.Information("GitCaddy AI Service starting on {Urls}", string.Join(", ", app.Urls));
app.Run();

View File

@@ -0,0 +1,155 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using System.Collections.Concurrent;
using GitCaddy.AI.Proto;
using GitCaddy.AI.Service.Configuration;
using Grpc.Core;
using MarketAlly.AIPlugin.Conversation;
using Microsoft.Extensions.Options;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Implementation of AI chat interface with conversation history.
/// </summary>
public class ChatService : IChatService
{
private readonly ILogger<ChatService> _logger;
private readonly IAIProviderFactory _providerFactory;
private readonly AIServiceOptions _options;
// In-memory conversation cache (replace with Redis for production scaling)
private readonly ConcurrentDictionary<string, IAIConversation> _conversations = new();
public ChatService(
ILogger<ChatService> logger,
IAIProviderFactory providerFactory,
IOptions<AIServiceOptions> options)
{
_logger = logger;
_providerFactory = providerFactory;
_options = options.Value;
}
public async Task ChatAsync(
IAsyncStreamReader<ChatRequest> requestStream,
IServerStreamWriter<ChatResponse> responseStream,
CancellationToken cancellationToken)
{
await foreach (var request in requestStream.ReadAllAsync(cancellationToken))
{
var conversationId = request.ConversationId;
if (string.IsNullOrEmpty(conversationId))
{
conversationId = Guid.NewGuid().ToString("N")[..12];
}
_logger.LogInformation("Chat message in conversation {ConversationId}: {Message}",
conversationId, request.Message[..Math.Min(100, request.Message.Length)]);
// Get or create conversation
var conversation = _conversations.GetOrAdd(conversationId, _ =>
{
return _providerFactory.CreateConversation()
.WithSystemPrompt(GetChatSystemPrompt(request.Context))
.Build();
});
try
{
// Build the message with any attachments
var message = BuildChatMessage(request);
// Stream the response
await foreach (var chunk in conversation.SendMessageStreamAsync(message, cancellationToken))
{
await responseStream.WriteAsync(new ChatResponse
{
ConversationId = conversationId,
Message = chunk,
IsComplete = false
}, cancellationToken);
}
// Send completion marker
await responseStream.WriteAsync(new ChatResponse
{
ConversationId = conversationId,
Message = "",
IsComplete = true
}, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Chat error in conversation {ConversationId}", conversationId);
await responseStream.WriteAsync(new ChatResponse
{
ConversationId = conversationId,
Message = $"Sorry, an error occurred: {ex.Message}",
IsComplete = true
}, cancellationToken);
}
}
}
private static string GetChatSystemPrompt(ChatContext? context)
{
var basePrompt = """
You are GitCaddy AI, an intelligent assistant for software development.
You help developers with:
- Understanding and explaining code
- Writing and improving code
- Debugging issues
- Code reviews and best practices
- Documentation
- Git operations and workflows
Be helpful, concise, and technically accurate.
When suggesting code changes, provide complete, working examples.
""";
if (context != null)
{
var contextInfo = new List<string>();
if (!string.IsNullOrEmpty(context.CurrentFile))
contextInfo.Add($"Current file: {context.CurrentFile}");
if (context.CurrentLine > 0)
contextInfo.Add($"Current line: {context.CurrentLine}");
if (!string.IsNullOrEmpty(context.Branch))
contextInfo.Add($"Branch: {context.Branch}");
if (!string.IsNullOrEmpty(context.SelectedCode))
contextInfo.Add($"Selected code:\n```\n{context.SelectedCode}\n```");
if (contextInfo.Count > 0)
{
basePrompt += "\n\nCurrent context:\n" + string.Join("\n", contextInfo);
}
}
return basePrompt;
}
private static string BuildChatMessage(ChatRequest request)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine(request.Message);
if (request.Attachments.Count > 0)
{
sb.AppendLine();
sb.AppendLine("Attachments:");
foreach (var attachment in request.Attachments)
{
sb.AppendLine($"### {attachment.Name} ({attachment.Type})");
sb.AppendLine("```");
sb.AppendLine(attachment.Content);
sb.AppendLine("```");
}
}
return sb.ToString();
}
}

View File

@@ -0,0 +1,173 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Proto;
using GitCaddy.AI.Service.Configuration;
using Microsoft.Extensions.Options;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Implementation of AI-powered code intelligence.
/// </summary>
public class CodeIntelligenceService : ICodeIntelligenceService
{
private readonly ILogger<CodeIntelligenceService> _logger;
private readonly IAIProviderFactory _providerFactory;
private readonly AIServiceOptions _options;
public CodeIntelligenceService(
ILogger<CodeIntelligenceService> logger,
IAIProviderFactory providerFactory,
IOptions<AIServiceOptions> options)
{
_logger = logger;
_providerFactory = providerFactory;
_options = options.Value;
}
public async Task<SummarizeChangesResponse> SummarizeChangesAsync(
SummarizeChangesRequest request, CancellationToken cancellationToken)
{
var conversation = _providerFactory.CreateConversation()
.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.
""")
.Build();
var prompt = BuildSummarizePrompt(request);
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
return ParseSummarizeResponse(response);
}
public async Task<ExplainCodeResponse> ExplainCodeAsync(
ExplainCodeRequest request, CancellationToken cancellationToken)
{
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt("""
You are an expert code explainer. Your job is to help developers understand code.
Provide clear explanations that:
1. Describe what the code does at a high level
2. Explain key concepts and patterns used
3. Note any potential issues or improvements
4. Reference relevant documentation or resources
Adjust your explanation depth based on the code complexity.
""")
.Build();
var prompt = $"""
Please explain this code from {request.FilePath}:
```
{request.Code}
```
{(string.IsNullOrEmpty(request.Question) ? "" : $"Specific question: {request.Question}")}
""";
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
return new ExplainCodeResponse
{
Explanation = response
};
}
public async Task<SuggestFixResponse> SuggestFixAsync(
SuggestFixRequest request, CancellationToken cancellationToken)
{
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt("""
You are an expert debugger. Given code and an error message, suggest fixes.
Provide:
1. An explanation of what's causing the error
2. The suggested fix with corrected code
3. Alternative solutions if applicable
Be specific and provide working code.
""")
.Build();
var prompt = $"""
File: {request.FilePath}
Language: {request.Language}
Code with error:
```{request.Language}
{request.Code}
```
Error message:
{request.ErrorMessage}
Please suggest a fix.
""";
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
return new SuggestFixResponse
{
Explanation = response
};
}
private static string BuildSummarizePrompt(SummarizeChangesRequest request)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine("# Changes to Summarize");
sb.AppendLine();
if (!string.IsNullOrEmpty(request.Context))
{
sb.AppendLine($"**Context:** {request.Context}");
sb.AppendLine();
}
sb.AppendLine("## Files Changed");
foreach (var file in request.Files)
{
sb.AppendLine($"- {file.Path} ({file.Status})");
}
sb.AppendLine();
sb.AppendLine("## Diffs");
foreach (var file in request.Files)
{
sb.AppendLine($"### {file.Path}");
sb.AppendLine("```diff");
sb.AppendLine(file.Patch);
sb.AppendLine("```");
}
return sb.ToString();
}
private static SummarizeChangesResponse ParseSummarizeResponse(string response)
{
var result = new SummarizeChangesResponse
{
Summary = response
};
// Extract bullet points (lines starting with - or *)
var lines = response.Split('\n');
foreach (var line in lines)
{
var trimmed = line.Trim();
if (trimmed.StartsWith("- ") || trimmed.StartsWith("* "))
{
result.BulletPoints.Add(trimmed[2..]);
}
}
return result;
}
}

View File

@@ -0,0 +1,189 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Proto;
using GitCaddy.AI.Service.Configuration;
using MarketAlly.AIPlugin;
using MarketAlly.AIPlugin.Conversation;
using Microsoft.Extensions.Options;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Implementation of AI-powered code review using MarketAlly.AIPlugin.
/// </summary>
public class CodeReviewService : ICodeReviewService
{
private readonly ILogger<CodeReviewService> _logger;
private readonly IAIProviderFactory _providerFactory;
private readonly AIServiceOptions _options;
public CodeReviewService(
ILogger<CodeReviewService> logger,
IAIProviderFactory providerFactory,
IOptions<AIServiceOptions> options)
{
_logger = logger;
_providerFactory = providerFactory;
_options = options.Value;
}
public async Task<ReviewPullRequestResponse> ReviewPullRequestAsync(
ReviewPullRequestRequest request, CancellationToken cancellationToken)
{
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt(GetCodeReviewSystemPrompt(request.Options))
.Build();
// Build the review prompt
var prompt = BuildPullRequestReviewPrompt(request);
// Get AI response
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
// Parse and return structured response
return ParsePullRequestReviewResponse(response);
}
public async Task<ReviewCommitResponse> ReviewCommitAsync(
ReviewCommitRequest request, CancellationToken cancellationToken)
{
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt(GetCodeReviewSystemPrompt(request.Options))
.Build();
var prompt = BuildCommitReviewPrompt(request);
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
return ParseCommitReviewResponse(response);
}
private static string GetCodeReviewSystemPrompt(ReviewOptions? options)
{
var focusAreas = new List<string>();
if (options?.CheckSecurity == true)
focusAreas.Add("security vulnerabilities (OWASP Top 10, injection, XSS, hardcoded secrets)");
if (options?.CheckPerformance == true)
focusAreas.Add("performance issues (N+1 queries, memory leaks, inefficient algorithms)");
if (options?.CheckStyle == true)
focusAreas.Add("code style and best practices");
if (options?.CheckTests == true)
focusAreas.Add("test coverage and quality");
if (options?.SuggestImprovements == true)
focusAreas.Add("potential improvements and refactoring opportunities");
if (!string.IsNullOrEmpty(options?.FocusAreas))
focusAreas.Add(options.FocusAreas);
var focusText = focusAreas.Count > 0
? $"Focus particularly on: {string.Join(", ", focusAreas)}."
: "";
return $"""
You are an expert code reviewer for GitCaddy, a Git hosting platform.
Your role is to provide thorough, actionable code reviews that help developers write better code.
Review Guidelines:
1. Be constructive and specific - explain WHY something is an issue
2. Suggest concrete fixes with code examples when possible
3. Prioritize issues by severity (critical > error > warning > info)
4. Acknowledge good patterns and practices you observe
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
""";
}
private static string BuildPullRequestReviewPrompt(ReviewPullRequestRequest request)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine($"# Pull Request Review");
sb.AppendLine();
sb.AppendLine($"**Title:** {request.PrTitle}");
sb.AppendLine($"**Description:** {request.PrDescription}");
sb.AppendLine($"**Base:** {request.BaseBranch} ← **Head:** {request.HeadBranch}");
sb.AppendLine();
sb.AppendLine("## Files Changed");
sb.AppendLine();
foreach (var file in request.Files)
{
sb.AppendLine($"### {file.Path} ({file.Status})");
if (!string.IsNullOrEmpty(file.Language))
sb.AppendLine($"Language: {file.Language}");
sb.AppendLine("```diff");
sb.AppendLine(file.Patch);
sb.AppendLine("```");
sb.AppendLine();
}
sb.AppendLine("Please review these changes and provide your assessment.");
return sb.ToString();
}
private static string BuildCommitReviewPrompt(ReviewCommitRequest request)
{
var sb = new System.Text.StringBuilder();
sb.AppendLine($"# Commit Review");
sb.AppendLine();
sb.AppendLine($"**Commit:** {request.CommitSha}");
sb.AppendLine($"**Message:** {request.CommitMessage}");
sb.AppendLine();
sb.AppendLine("## Files Changed");
sb.AppendLine();
foreach (var file in request.Files)
{
sb.AppendLine($"### {file.Path} ({file.Status})");
sb.AppendLine("```diff");
sb.AppendLine(file.Patch);
sb.AppendLine("```");
sb.AppendLine();
}
sb.AppendLine("Please review this commit and provide feedback.");
return sb.ToString();
}
private static ReviewPullRequestResponse ParsePullRequestReviewResponse(string response)
{
// TODO: Implement structured parsing using JSON mode or regex extraction
// For now, return a basic response
var result = new ReviewPullRequestResponse
{
Summary = response,
Verdict = ReviewVerdict.Comment,
EstimatedReviewMinutes = 5
};
// Parse verdict from response
if (response.Contains("APPROVE", StringComparison.OrdinalIgnoreCase) &&
!response.Contains("REQUEST_CHANGES", StringComparison.OrdinalIgnoreCase))
{
result.Verdict = ReviewVerdict.Approve;
}
else if (response.Contains("REQUEST_CHANGES", StringComparison.OrdinalIgnoreCase))
{
result.Verdict = ReviewVerdict.RequestChanges;
}
return result;
}
private static ReviewCommitResponse ParseCommitReviewResponse(string response)
{
return new ReviewCommitResponse
{
Summary = response
};
}
}

View File

@@ -0,0 +1,152 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Proto;
using GitCaddy.AI.Service.Configuration;
using Microsoft.Extensions.Options;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Implementation of AI-powered documentation generation.
/// </summary>
public class DocumentationService : IDocumentationService
{
private readonly ILogger<DocumentationService> _logger;
private readonly IAIProviderFactory _providerFactory;
private readonly AIServiceOptions _options;
public DocumentationService(
ILogger<DocumentationService> logger,
IAIProviderFactory providerFactory,
IOptions<AIServiceOptions> options)
{
_logger = logger;
_providerFactory = providerFactory;
_options = options.Value;
}
public async Task<GenerateDocumentationResponse> GenerateDocumentationAsync(
GenerateDocumentationRequest request, CancellationToken cancellationToken)
{
var docStyle = request.Style switch
{
"jsdoc" => "JSDoc format with @param, @returns, @example tags",
"docstring" => "Python docstring format with Args, Returns, Examples sections",
"xml" => "XML documentation comments with <summary>, <param>, <returns> tags",
"markdown" => "Markdown format with headers, code blocks, and lists",
_ => "appropriate documentation format for the language"
};
var docType = request.DocType switch
{
"function" => "function/method documentation",
"class" => "class/type documentation",
"module" => "module/file-level documentation",
"api" => "API endpoint documentation",
_ => "code documentation"
};
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt($"""
You are an expert technical writer specializing in {docType}.
Generate clear, comprehensive documentation in {docStyle}.
Include:
- Brief description of purpose
- Parameters/properties with types and descriptions
- Return values
- Usage examples when helpful
- Any important notes or warnings
Be concise but thorough.
""")
.Build();
var prompt = $"""
Generate {docType} for this {request.Language} code from {request.FilePath}:
```{request.Language}
{request.Code}
```
""";
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
return new GenerateDocumentationResponse
{
Documentation = response
};
}
public async Task<GenerateCommitMessageResponse> GenerateCommitMessageAsync(
GenerateCommitMessageRequest request, CancellationToken cancellationToken)
{
var styleGuide = request.Style switch
{
"conventional" => """
Use Conventional Commits format:
<type>(<scope>): <description>
[optional body]
Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore
""",
"descriptive" => """
Use a descriptive format:
- First line: summary (50 chars max)
- Blank line
- Body: detailed explanation of what and why
""",
"brief" => "Single line summary (50 chars max)",
_ => "Clear, descriptive commit message"
};
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt($"""
You are an expert at writing commit messages.
{styleGuide}
Analyze the changes and write an appropriate commit message.
Focus on WHAT changed and WHY, not HOW.
Also provide 2-3 alternative messages.
""")
.Build();
var sb = new System.Text.StringBuilder();
sb.AppendLine("# Changes to commit:");
sb.AppendLine();
foreach (var file in request.Files)
{
sb.AppendLine($"## {file.Path} ({file.Status})");
sb.AppendLine("```diff");
sb.AppendLine(file.Patch);
sb.AppendLine("```");
sb.AppendLine();
}
var response = await conversation.SendMessageAsync(sb.ToString(), cancellationToken);
// Parse response - first line is primary, rest are alternatives
var lines = response.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var result = new GenerateCommitMessageResponse
{
Message = lines.Length > 0 ? lines[0].Trim() : response.Trim()
};
// 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);
}
}
return result;
}
}

View File

@@ -0,0 +1,266 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using Grpc.Core;
using GitCaddy.AI.Proto;
using GitCaddy.AI.Service.Licensing;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Main gRPC service implementation for GitCaddy AI.
/// </summary>
public class GitCaddyAIServiceImpl : GitCaddyAI.GitCaddyAIBase
{
private readonly ILogger<GitCaddyAIServiceImpl> _logger;
private readonly ILicenseValidator _licenseValidator;
private readonly ICodeReviewService _codeReviewService;
private readonly ICodeIntelligenceService _codeIntelligenceService;
private readonly IIssueService _issueService;
private readonly IDocumentationService _documentationService;
private readonly IWorkflowService _workflowService;
private readonly IChatService _chatService;
public GitCaddyAIServiceImpl(
ILogger<GitCaddyAIServiceImpl> logger,
ILicenseValidator licenseValidator,
ICodeReviewService codeReviewService,
ICodeIntelligenceService codeIntelligenceService,
IIssueService issueService,
IDocumentationService documentationService,
IWorkflowService workflowService,
IChatService chatService)
{
_logger = logger;
_licenseValidator = licenseValidator;
_codeReviewService = codeReviewService;
_codeIntelligenceService = codeIntelligenceService;
_issueService = issueService;
_documentationService = documentationService;
_workflowService = workflowService;
_chatService = chatService;
}
private async Task EnsureLicensedAsync(string feature, ServerCallContext context)
{
var result = await _licenseValidator.ValidateAsync();
if (!result.IsValid)
{
throw new RpcException(new Status(StatusCode.PermissionDenied,
$"Valid license required for {feature}. Error: {result.Error}"));
}
if (result.License?.Features != null && !result.License.Features.Contains(feature))
{
throw new RpcException(new Status(StatusCode.PermissionDenied,
$"License does not include the '{feature}' feature. Please upgrade your license."));
}
}
// ========================================================================
// Code Review
// ========================================================================
public override async Task<ReviewPullRequestResponse> ReviewPullRequest(
ReviewPullRequestRequest request, ServerCallContext context)
{
await EnsureLicensedAsync("code_review", context);
_logger.LogInformation("Reviewing PR {PrId} in repo {RepoId}", request.PullRequestId, request.RepoId);
return await _codeReviewService.ReviewPullRequestAsync(request, context.CancellationToken);
}
public override async Task<ReviewCommitResponse> ReviewCommit(
ReviewCommitRequest request, ServerCallContext context)
{
await EnsureLicensedAsync("code_review", context);
_logger.LogInformation("Reviewing commit {CommitSha} in repo {RepoId}", request.CommitSha, request.RepoId);
return await _codeReviewService.ReviewCommitAsync(request, context.CancellationToken);
}
// ========================================================================
// Code Intelligence
// ========================================================================
public override async Task<SummarizeChangesResponse> SummarizeChanges(
SummarizeChangesRequest request, ServerCallContext context)
{
await EnsureLicensedAsync("code_intelligence", context);
_logger.LogInformation("Summarizing changes for repo {RepoId}", request.RepoId);
return await _codeIntelligenceService.SummarizeChangesAsync(request, context.CancellationToken);
}
public override async Task<ExplainCodeResponse> ExplainCode(
ExplainCodeRequest request, ServerCallContext context)
{
await EnsureLicensedAsync("code_intelligence", context);
_logger.LogInformation("Explaining code in {FilePath}", request.FilePath);
return await _codeIntelligenceService.ExplainCodeAsync(request, context.CancellationToken);
}
public override async Task<SuggestFixResponse> SuggestFix(
SuggestFixRequest request, ServerCallContext context)
{
await EnsureLicensedAsync("code_intelligence", context);
_logger.LogInformation("Suggesting fix for {FilePath}", request.FilePath);
return await _codeIntelligenceService.SuggestFixAsync(request, context.CancellationToken);
}
// ========================================================================
// Issue Management
// ========================================================================
public override async Task<TriageIssueResponse> TriageIssue(
TriageIssueRequest request, ServerCallContext context)
{
await EnsureLicensedAsync("issue_management", context);
_logger.LogInformation("Triaging issue {IssueId} in repo {RepoId}", request.IssueId, request.RepoId);
return await _issueService.TriageIssueAsync(request, context.CancellationToken);
}
public override async Task<SuggestLabelsResponse> SuggestLabels(
SuggestLabelsRequest request, ServerCallContext context)
{
await EnsureLicensedAsync("issue_management", context);
_logger.LogInformation("Suggesting labels for repo {RepoId}", request.RepoId);
return await _issueService.SuggestLabelsAsync(request, context.CancellationToken);
}
public override async Task<GenerateIssueResponseResponse> GenerateIssueResponse(
GenerateIssueResponseRequest request, ServerCallContext context)
{
await EnsureLicensedAsync("issue_management", context);
_logger.LogInformation("Generating response for issue {IssueId}", request.IssueId);
return await _issueService.GenerateResponseAsync(request, context.CancellationToken);
}
// ========================================================================
// Documentation
// ========================================================================
public override async Task<GenerateDocumentationResponse> GenerateDocumentation(
GenerateDocumentationRequest request, ServerCallContext context)
{
await EnsureLicensedAsync("documentation", context);
_logger.LogInformation("Generating documentation for {FilePath}", request.FilePath);
return await _documentationService.GenerateDocumentationAsync(request, context.CancellationToken);
}
public override async Task<GenerateCommitMessageResponse> GenerateCommitMessage(
GenerateCommitMessageRequest request, ServerCallContext context)
{
await EnsureLicensedAsync("documentation", context);
_logger.LogInformation("Generating commit message for repo {RepoId}", request.RepoId);
return await _documentationService.GenerateCommitMessageAsync(request, context.CancellationToken);
}
// ========================================================================
// Agentic Workflows
// ========================================================================
public override async Task StartWorkflow(
StartWorkflowRequest request,
IServerStreamWriter<WorkflowEvent> responseStream,
ServerCallContext context)
{
await EnsureLicensedAsync("agentic_workflows", context);
_logger.LogInformation("Starting workflow {WorkflowType} for repo {RepoId}", request.WorkflowType, request.RepoId);
await _workflowService.StartWorkflowAsync(request, responseStream, context.CancellationToken);
}
public override async Task<ExecuteTaskResponse> ExecuteTask(
ExecuteTaskRequest request, ServerCallContext context)
{
await EnsureLicensedAsync("agentic_workflows", context);
_logger.LogInformation("Executing task for repo {RepoId}", request.RepoId);
return await _workflowService.ExecuteTaskAsync(request, context.CancellationToken);
}
// ========================================================================
// Chat
// ========================================================================
public override async Task Chat(
IAsyncStreamReader<ChatRequest> requestStream,
IServerStreamWriter<ChatResponse> responseStream,
ServerCallContext context)
{
await EnsureLicensedAsync("chat", context);
_logger.LogInformation("Starting chat session");
await _chatService.ChatAsync(requestStream, responseStream, context.CancellationToken);
}
// ========================================================================
// Health & License
// ========================================================================
public override async Task<HealthCheckResponse> CheckHealth(
HealthCheckRequest request, ServerCallContext context)
{
var licenseResult = await _licenseValidator.ValidateAsync();
var response = new HealthCheckResponse
{
Healthy = licenseResult.IsValid,
Version = typeof(GitCaddyAIServiceImpl).Assembly.GetName().Version?.ToString() ?? "1.0.0"
};
if (licenseResult.License != null)
{
response.License = new LicenseInfo
{
Tier = licenseResult.License.Tier,
Customer = licenseResult.License.Customer,
ExpiresAt = licenseResult.License.ExpiresAt?.ToString("o") ?? "",
SeatCount = licenseResult.License.SeatCount,
IsTrial = licenseResult.License.IsTrial
};
response.License.Features.AddRange(licenseResult.License.Features);
}
// TODO: Add provider status checks
response.ProviderStatus["claude"] = "ok";
response.ProviderStatus["openai"] = "ok";
return response;
}
public override async Task<ValidateLicenseResponse> ValidateLicense(
ValidateLicenseRequest request, ServerCallContext context)
{
var result = await _licenseValidator.ValidateKeyAsync(request.LicenseKey);
var response = new ValidateLicenseResponse
{
Valid = result.IsValid,
Error = result.Error ?? ""
};
if (result.License != null)
{
response.License = new LicenseInfo
{
Tier = result.License.Tier,
Customer = result.License.Customer,
ExpiresAt = result.License.ExpiresAt?.ToString("o") ?? "",
SeatCount = result.License.SeatCount,
IsTrial = result.License.IsTrial
};
response.License.Features.AddRange(result.License.Features);
}
return response;
}
}

View File

@@ -0,0 +1,15 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Proto;
using Grpc.Core;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Service for AI chat interface.
/// </summary>
public interface IChatService
{
Task ChatAsync(IAsyncStreamReader<ChatRequest> requestStream, IServerStreamWriter<ChatResponse> responseStream, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,16 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Proto;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Service for AI-powered code intelligence features.
/// </summary>
public interface ICodeIntelligenceService
{
Task<SummarizeChangesResponse> SummarizeChangesAsync(SummarizeChangesRequest request, CancellationToken cancellationToken);
Task<ExplainCodeResponse> ExplainCodeAsync(ExplainCodeRequest request, CancellationToken cancellationToken);
Task<SuggestFixResponse> SuggestFixAsync(SuggestFixRequest request, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,15 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Proto;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Service for AI-powered code review.
/// </summary>
public interface ICodeReviewService
{
Task<ReviewPullRequestResponse> ReviewPullRequestAsync(ReviewPullRequestRequest request, CancellationToken cancellationToken);
Task<ReviewCommitResponse> ReviewCommitAsync(ReviewCommitRequest request, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,15 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Proto;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Service for AI-powered documentation generation.
/// </summary>
public interface IDocumentationService
{
Task<GenerateDocumentationResponse> GenerateDocumentationAsync(GenerateDocumentationRequest request, CancellationToken cancellationToken);
Task<GenerateCommitMessageResponse> GenerateCommitMessageAsync(GenerateCommitMessageRequest request, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,16 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Proto;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Service for AI-powered issue management.
/// </summary>
public interface IIssueService
{
Task<TriageIssueResponse> TriageIssueAsync(TriageIssueRequest request, CancellationToken cancellationToken);
Task<SuggestLabelsResponse> SuggestLabelsAsync(SuggestLabelsRequest request, CancellationToken cancellationToken);
Task<GenerateIssueResponseResponse> GenerateResponseAsync(GenerateIssueResponseRequest request, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,16 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Proto;
using Grpc.Core;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Service for agentic workflows.
/// </summary>
public interface IWorkflowService
{
Task StartWorkflowAsync(StartWorkflowRequest request, IServerStreamWriter<WorkflowEvent> responseStream, CancellationToken cancellationToken);
Task<ExecuteTaskResponse> ExecuteTaskAsync(ExecuteTaskRequest request, CancellationToken cancellationToken);
}

View File

@@ -0,0 +1,195 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Proto;
using GitCaddy.AI.Service.Configuration;
using Microsoft.Extensions.Options;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Implementation of AI-powered issue management.
/// </summary>
public class IssueService : IIssueService
{
private readonly ILogger<IssueService> _logger;
private readonly IAIProviderFactory _providerFactory;
private readonly AIServiceOptions _options;
public IssueService(
ILogger<IssueService> logger,
IAIProviderFactory providerFactory,
IOptions<AIServiceOptions> options)
{
_logger = logger;
_providerFactory = providerFactory;
_options = options.Value;
}
public async Task<TriageIssueResponse> TriageIssueAsync(
TriageIssueRequest request, CancellationToken cancellationToken)
{
var availableLabels = string.Join(", ", request.AvailableLabels);
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt($"""
You are an expert issue triager for a software project.
Your job is to analyze issues and determine:
1. Priority: critical, high, medium, or low
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": "..."
}}
""")
.Build();
var prompt = $"""
Issue #{request.IssueId}
Title: {request.Title}
Body:
{request.Body}
Existing labels: {string.Join(", ", request.ExistingLabels)}
""";
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
// Parse JSON response
return ParseTriageResponse(response);
}
public async Task<SuggestLabelsResponse> SuggestLabelsAsync(
SuggestLabelsRequest request, CancellationToken cancellationToken)
{
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt($"""
You are an expert at categorizing issues.
Given an issue title and body, suggest appropriate labels from the available options.
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": "..."}}
]
}}
""")
.Build();
var prompt = $"""
Title: {request.Title}
Body:
{request.Body}
""";
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
return ParseSuggestLabelsResponse(response, request.AvailableLabels);
}
public async Task<GenerateIssueResponseResponse> GenerateResponseAsync(
GenerateIssueResponseRequest request, CancellationToken cancellationToken)
{
var responseType = request.ResponseType switch
{
"clarification" => "Ask clarifying questions to better understand the issue.",
"solution" => "Provide a helpful solution or workaround.",
"acknowledgment" => "Acknowledge the issue and set expectations.",
_ => "Provide a helpful response."
};
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt($"""
You are a helpful maintainer responding to issues.
Be professional, friendly, and constructive.
Response type: {responseType}
If you need more information, include follow-up questions.
""")
.Build();
var sb = new System.Text.StringBuilder();
sb.AppendLine($"Issue #{request.IssueId}: {request.Title}");
sb.AppendLine();
sb.AppendLine(request.Body);
sb.AppendLine();
if (request.Comments.Count > 0)
{
sb.AppendLine("Previous comments:");
foreach (var comment in request.Comments)
{
sb.AppendLine($"[{comment.Author}]: {comment.Body}");
}
}
sb.AppendLine();
sb.AppendLine("Please generate an appropriate response.");
var response = await conversation.SendMessageAsync(sb.ToString(), cancellationToken);
return new GenerateIssueResponseResponse
{
Response = response
};
}
private static TriageIssueResponse ParseTriageResponse(string response)
{
// TODO: Implement proper JSON parsing
var result = new TriageIssueResponse
{
Priority = "medium",
Category = "bug",
Summary = 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";
}
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;
}
}

View File

@@ -0,0 +1,200 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Proto;
using GitCaddy.AI.Service.Configuration;
using Grpc.Core;
using Microsoft.Extensions.Options;
namespace GitCaddy.AI.Service.Services;
/// <summary>
/// Implementation of agentic workflows using MarketAlly.AIPlugin orchestration.
/// </summary>
public class WorkflowService : IWorkflowService
{
private readonly ILogger<WorkflowService> _logger;
private readonly IAIProviderFactory _providerFactory;
private readonly AIServiceOptions _options;
public WorkflowService(
ILogger<WorkflowService> logger,
IAIProviderFactory providerFactory,
IOptions<AIServiceOptions> options)
{
_logger = logger;
_providerFactory = providerFactory;
_options = options.Value;
}
public async Task StartWorkflowAsync(
StartWorkflowRequest request,
IServerStreamWriter<WorkflowEvent> responseStream,
CancellationToken cancellationToken)
{
var workflowId = Guid.NewGuid().ToString("N")[..12];
_logger.LogInformation("Starting workflow {WorkflowId} of type {Type} for goal: {Goal}",
workflowId, request.WorkflowType, request.Goal);
// Send started event
await responseStream.WriteAsync(new WorkflowEvent
{
EventId = Guid.NewGuid().ToString("N")[..8],
WorkflowId = workflowId,
Type = WorkflowEventType.Started,
Message = $"Starting {request.WorkflowType} workflow",
Timestamp = DateTime.UtcNow.ToString("o")
}, cancellationToken);
try
{
// TODO: Integrate with MarketAlly.AIPlugin AgentOrchestrator
// For now, implement a simple single-agent workflow
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt(GetWorkflowSystemPrompt(request.WorkflowType))
.Build();
// Send thinking event
await responseStream.WriteAsync(new WorkflowEvent
{
EventId = Guid.NewGuid().ToString("N")[..8],
WorkflowId = workflowId,
Type = WorkflowEventType.AgentThinking,
AgentId = "primary",
Message = "Analyzing request and planning approach...",
Timestamp = DateTime.UtcNow.ToString("o")
}, cancellationToken);
// Execute the workflow
var response = await conversation.SendMessageAsync(
$"Goal: {request.Goal}\n\nParameters: {string.Join(", ", request.Parameters.Select(p => $"{p.Key}={p.Value}"))}",
cancellationToken);
// Send step completed event
await responseStream.WriteAsync(new WorkflowEvent
{
EventId = Guid.NewGuid().ToString("N")[..8],
WorkflowId = workflowId,
Type = WorkflowEventType.StepCompleted,
AgentId = "primary",
Message = "Analysis complete",
Timestamp = DateTime.UtcNow.ToString("o"),
Data = { ["result"] = response }
}, cancellationToken);
// Send completed event
await responseStream.WriteAsync(new WorkflowEvent
{
EventId = Guid.NewGuid().ToString("N")[..8],
WorkflowId = workflowId,
Type = WorkflowEventType.Completed,
Message = "Workflow completed successfully",
Timestamp = DateTime.UtcNow.ToString("o"),
Data = { ["result"] = response }
}, cancellationToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Workflow {WorkflowId} failed", workflowId);
await responseStream.WriteAsync(new WorkflowEvent
{
EventId = Guid.NewGuid().ToString("N")[..8],
WorkflowId = workflowId,
Type = WorkflowEventType.Failed,
Message = $"Workflow failed: {ex.Message}",
Timestamp = DateTime.UtcNow.ToString("o")
}, cancellationToken);
}
}
public async Task<ExecuteTaskResponse> ExecuteTaskAsync(
ExecuteTaskRequest request, CancellationToken cancellationToken)
{
_logger.LogInformation("Executing task: {Task}", request.Task);
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 response = await conversation.SendMessageAsync(prompt, cancellationToken);
return new ExecuteTaskResponse
{
Success = true,
Result = response
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Task execution failed");
return new ExecuteTaskResponse
{
Success = false,
Error = ex.Message
};
}
}
private static string GetWorkflowSystemPrompt(string workflowType)
{
return workflowType switch
{
"code_review" => """
You are an expert code reviewer. Analyze the provided code changes thoroughly.
Identify bugs, security issues, performance problems, and style improvements.
Provide actionable feedback with specific suggestions.
""",
"refactor" => """
You are an expert software architect. Analyze code and suggest refactoring improvements.
Consider maintainability, readability, performance, and best practices.
Provide concrete refactoring suggestions with code examples.
""",
"test_generation" => """
You are an expert test engineer. Generate comprehensive tests for the provided code.
Include unit tests, edge cases, and integration scenarios.
Follow testing best practices for the relevant framework.
""",
"documentation" => """
You are an expert technical writer. Generate clear, comprehensive documentation.
Include API docs, usage examples, and architectural explanations.
Follow documentation best practices for the relevant language/framework.
""",
"security_audit" => """
You are a security expert. Perform a thorough security audit of the code.
Check for OWASP Top 10 vulnerabilities, secrets exposure, and security misconfigurations.
Provide severity ratings and remediation guidance.
""",
_ => """
You are a helpful software development assistant.
Complete the requested task thoroughly and provide clear results.
"""
};
}
}

View File

@@ -0,0 +1,25 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Information",
"Grpc": "Debug"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Debug",
"Override": {
"Microsoft": "Information",
"Grpc": "Debug"
}
}
},
"AIService": {
"DefaultProvider": "Claude",
"DefaultModel": "claude-sonnet-4-20250514",
"MaxTokens": 4096,
"Temperature": 0.7,
"TimeoutSeconds": 120
}
}

View File

@@ -0,0 +1,71 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Grpc": "Information"
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Grpc": "Information"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"
}
},
{
"Name": "File",
"Args": {
"path": "logs/gitcaddy-ai-.log",
"rollingInterval": "Day",
"retainedFileCountLimit": 30
}
}
]
},
"AIService": {
"DefaultProvider": "Claude",
"DefaultModel": "claude-sonnet-4-20250514",
"MaxTokens": 4096,
"Temperature": 0.7,
"TimeoutSeconds": 120,
"EnableCaching": true,
"CacheExpirationMinutes": 60
},
"Providers": {
"Claude": {
"ApiKey": "",
"Enabled": true
},
"OpenAI": {
"ApiKey": "",
"Enabled": true
},
"Gemini": {
"ApiKey": "",
"Enabled": false
}
},
"License": {
"LicenseKey": "",
"LicenseFile": "",
"ValidationUrl": "https://license.marketally.com/validate",
"AllowOffline": true,
"CacheHours": 24
}
}