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:
60
.gitignore
vendored
Normal file
60
.gitignore
vendored
Normal 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
48
Dockerfile
Normal 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
35
GitCaddy.AI.sln
Normal 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
34
docker-compose.dev.yml
Normal 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
54
docker-compose.yml
Normal 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
413
protos/gitcaddy_ai.proto
Normal 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;
|
||||
}
|
||||
26
src/GitCaddy.AI.Client/GitCaddy.AI.Client.csproj
Normal file
26
src/GitCaddy.AI.Client/GitCaddy.AI.Client.csproj
Normal 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>
|
||||
102
src/GitCaddy.AI.Service/Configuration/AIProviderFactory.cs
Normal file
102
src/GitCaddy.AI.Service/Configuration/AIProviderFactory.cs
Normal 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}")
|
||||
};
|
||||
}
|
||||
}
|
||||
123
src/GitCaddy.AI.Service/Configuration/AIServiceOptions.cs
Normal file
123
src/GitCaddy.AI.Service/Configuration/AIServiceOptions.cs
Normal 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;
|
||||
}
|
||||
27
src/GitCaddy.AI.Service/Configuration/IAIProviderFactory.cs
Normal file
27
src/GitCaddy.AI.Service/Configuration/IAIProviderFactory.cs
Normal 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);
|
||||
}
|
||||
34
src/GitCaddy.AI.Service/GitCaddy.AI.Service.csproj
Normal file
34
src/GitCaddy.AI.Service/GitCaddy.AI.Service.csproj
Normal 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>
|
||||
48
src/GitCaddy.AI.Service/Licensing/ILicenseValidator.cs
Normal file
48
src/GitCaddy.AI.Service/Licensing/ILicenseValidator.cs
Normal 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; }
|
||||
}
|
||||
258
src/GitCaddy.AI.Service/Licensing/LicenseValidator.cs
Normal file
258
src/GitCaddy.AI.Service/Licensing/LicenseValidator.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
88
src/GitCaddy.AI.Service/Program.cs
Normal file
88
src/GitCaddy.AI.Service/Program.cs
Normal 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();
|
||||
155
src/GitCaddy.AI.Service/Services/ChatService.cs
Normal file
155
src/GitCaddy.AI.Service/Services/ChatService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
173
src/GitCaddy.AI.Service/Services/CodeIntelligenceService.cs
Normal file
173
src/GitCaddy.AI.Service/Services/CodeIntelligenceService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
189
src/GitCaddy.AI.Service/Services/CodeReviewService.cs
Normal file
189
src/GitCaddy.AI.Service/Services/CodeReviewService.cs
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
152
src/GitCaddy.AI.Service/Services/DocumentationService.cs
Normal file
152
src/GitCaddy.AI.Service/Services/DocumentationService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
266
src/GitCaddy.AI.Service/Services/GitCaddyAIServiceImpl.cs
Normal file
266
src/GitCaddy.AI.Service/Services/GitCaddyAIServiceImpl.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
15
src/GitCaddy.AI.Service/Services/IChatService.cs
Normal file
15
src/GitCaddy.AI.Service/Services/IChatService.cs
Normal 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);
|
||||
}
|
||||
16
src/GitCaddy.AI.Service/Services/ICodeIntelligenceService.cs
Normal file
16
src/GitCaddy.AI.Service/Services/ICodeIntelligenceService.cs
Normal 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);
|
||||
}
|
||||
15
src/GitCaddy.AI.Service/Services/ICodeReviewService.cs
Normal file
15
src/GitCaddy.AI.Service/Services/ICodeReviewService.cs
Normal 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);
|
||||
}
|
||||
15
src/GitCaddy.AI.Service/Services/IDocumentationService.cs
Normal file
15
src/GitCaddy.AI.Service/Services/IDocumentationService.cs
Normal 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);
|
||||
}
|
||||
16
src/GitCaddy.AI.Service/Services/IIssueService.cs
Normal file
16
src/GitCaddy.AI.Service/Services/IIssueService.cs
Normal 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);
|
||||
}
|
||||
16
src/GitCaddy.AI.Service/Services/IWorkflowService.cs
Normal file
16
src/GitCaddy.AI.Service/Services/IWorkflowService.cs
Normal 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);
|
||||
}
|
||||
195
src/GitCaddy.AI.Service/Services/IssueService.cs
Normal file
195
src/GitCaddy.AI.Service/Services/IssueService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
200
src/GitCaddy.AI.Service/Services/WorkflowService.cs
Normal file
200
src/GitCaddy.AI.Service/Services/WorkflowService.cs
Normal 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.
|
||||
"""
|
||||
};
|
||||
}
|
||||
}
|
||||
25
src/GitCaddy.AI.Service/appsettings.Development.json
Normal file
25
src/GitCaddy.AI.Service/appsettings.Development.json
Normal 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
|
||||
}
|
||||
}
|
||||
71
src/GitCaddy.AI.Service/appsettings.json
Normal file
71
src/GitCaddy.AI.Service/appsettings.json
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user