2
0

feat: add client libraries, examples, and project documentation

Add Go and .NET client libraries for GitCaddy AI Service with usage examples. Include Business Source License 1.1, Makefile for build automation, and comprehensive README. Update service configuration and all service classes to support new client integration.
This commit is contained in:
2026-01-19 10:44:24 -05:00
parent a4506613fd
commit 17581918dd
22 changed files with 1204 additions and 62 deletions

101
LICENSE Normal file
View File

@@ -0,0 +1,101 @@
Business Source License 1.1
License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
"Business Source License" is a trademark of MariaDB Corporation Ab.
-----------------------------------------------------------------------------
Parameters
Licensor: MarketAlly LLC
Licensed Work: GitCaddy AI Service
The Licensed Work is (c) 2026 MarketAlly LLC
Additional Use Grant: You may make use of the Licensed Work, provided that
you do not use the Licensed Work for an AI Service.
An "AI Service" is a commercial offering that allows
third parties (other than your employees and contractors
acting on your behalf) to access the functionality of
the Licensed Work by means of AI-powered code
intelligence features.
Change Date: Four years from the date the Licensed Work is published.
Change License: MIT License
-----------------------------------------------------------------------------
Terms
The Licensor hereby grants you the right to copy, modify, create derivative
works, redistribute, and make non-production use of the Licensed Work. The
Licensor may make an Additional Use Grant, above, permitting limited
production use.
Effective on the Change Date, or the fourth anniversary of the first publicly
available distribution of a specific version of the Licensed Work under this
License, whichever comes first, the Licensor hereby grants you rights under
the terms of the Change License, and the rights granted in the paragraph
above terminate.
If your use of the Licensed Work does not comply with the requirements
currently in effect as described in this License, you must purchase a
commercial license from the Licensor, its affiliated entities, or authorized
resellers, or you must refrain from using the Licensed Work.
All copies of the original and modified Licensed Work, and derivative works
of the Licensed Work, are subject to this License. This License applies
separately for each version of the Licensed Work and the Change Date may vary
for each version of the Licensed Work released by Licensor.
You must conspicuously display this License on each original or modified copy
of the Licensed Work. If you receive the Licensed Work in original or
modified form from a third party, the terms and conditions set forth in this
License apply to your use of that work.
Any use of the Licensed Work in violation of this License will automatically
terminate your rights under this License for the current and all other
versions of the Licensed Work.
This License does not grant you any right in any trademark or logo of
Licensor or its affiliates (provided that you may use a trademark or logo of
Licensor as expressly required by this License).
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON
AN "AS IS" BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS,
EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND
TITLE.
MarketAlly LLC hereby grants you permission to use this License's text to
license your works, and to refer to it using the trademark "Business Source
License", as long as you comply with the Covenants of Licensor below.
-----------------------------------------------------------------------------
Covenants of Licensor
In consideration of the right to use this License's text and the "Business
Source License" name and trademark, Licensor covenants to MariaDB, and to all
other recipients of the licensed work to be provided by Licensor:
1. To specify as the Change License the MIT License, or a license
that is no less permissive than the MIT License, as the Change License.
2. To either: (a) specify an Additional Use Grant that does not impose any
additional restriction on use of the Licensed Work beyond those set forth
in this License, or (b) insert the text "None" to indicate no Additional
Use Grant.
3. To specify a Change Date.
4. Not to modify this License in any other way.
-----------------------------------------------------------------------------
Notice
The Business Source License (this document, or the "License") is not an Open
Source license. However, the Licensed Work will eventually be made available
under an Open Source License, as stated in this License.

91
Makefile Normal file
View File

@@ -0,0 +1,91 @@
# GitCaddy AI Service Makefile
# Copyright 2026 MarketAlly. All rights reserved.
.PHONY: all build run test clean docker proto
# Default target
all: build
# Build the service
build:
dotnet build GitCaddy.AI.sln -c Release
# Run the service in development mode
run:
cd src/GitCaddy.AI.Service && dotnet run
# Run tests
test:
dotnet test GitCaddy.AI.sln
# Clean build artifacts
clean:
dotnet clean GitCaddy.AI.sln
rm -rf src/*/bin src/*/obj
rm -rf go/gitcaddy-ai-client/proto/*.pb.go
# Build Docker image
docker:
docker build -t gitcaddy/gitcaddy-ai:latest .
# Run with Docker Compose
docker-up:
docker-compose up -d
# Stop Docker Compose
docker-down:
docker-compose down
# Generate Go protobuf files
proto:
cd go/gitcaddy-ai-client && go generate
# Install development dependencies
deps:
dotnet restore GitCaddy.AI.sln
cd go/gitcaddy-ai-client && go mod download
# Format code
fmt:
dotnet format GitCaddy.AI.sln
cd go/gitcaddy-ai-client && go fmt ./...
# Lint code
lint:
dotnet format GitCaddy.AI.sln --verify-no-changes
cd go/gitcaddy-ai-client && go vet ./...
# Run example (dotnet)
example-dotnet:
cd examples/dotnet && dotnet run
# Run example (go)
example-go:
cd examples/go && go run main.go
# Create a release
release:
@echo "Creating release..."
dotnet publish src/GitCaddy.AI.Service/GitCaddy.AI.Service.csproj -c Release -o dist/
@echo "Release created in dist/"
# Show help
help:
@echo "GitCaddy AI Service"
@echo ""
@echo "Usage: make [target]"
@echo ""
@echo "Targets:"
@echo " build Build the service"
@echo " run Run the service in development mode"
@echo " test Run tests"
@echo " clean Clean build artifacts"
@echo " docker Build Docker image"
@echo " docker-up Start with Docker Compose"
@echo " docker-down Stop Docker Compose"
@echo " proto Generate Go protobuf files"
@echo " deps Install dependencies"
@echo " fmt Format code"
@echo " lint Lint code"
@echo " release Create release artifacts"
@echo " help Show this help"

234
README.md Normal file
View File

@@ -0,0 +1,234 @@
# GitCaddy AI Service
AI-powered code intelligence service for GitCaddy. Provides code review, documentation generation, issue triage, and agentic workflows.
## Features
- **Code Review**: AI-powered pull request and commit reviews with security analysis
- **Code Intelligence**: Explain code, suggest fixes, summarize changes
- **Issue Management**: Auto-triage issues, suggest labels, generate responses
- **Documentation**: Generate docs for code, commit messages
- **Agentic Workflows**: Multi-step AI workflows for complex tasks
- **Chat Interface**: Interactive AI assistant for developers
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ GitCaddy Server (Go) │
│ └── AI Client (gRPC) │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ GitCaddy AI Service (.NET 9) │ │
│ │ ├── gRPC API │ │
│ │ ├── MarketAlly.AIPlugin (Multi-provider AI) │ │
│ │ └── License Validation │ │
│ └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
## Quick Start
### Prerequisites
- .NET 9.0 SDK
- Docker (optional)
- API key for Claude, OpenAI, or Gemini
### Development
```bash
# Clone the repository
git clone https://git.marketally.com/gitcaddy/gitcaddy-ai.git
cd gitcaddy-ai
# Set environment variables
export Providers__Claude__ApiKey="your-api-key"
# Run the service
cd src/GitCaddy.AI.Service
dotnet run
```
### Docker
```bash
# Set environment variables
export CLAUDE_API_KEY="your-api-key"
export GITCADDY_AI_LICENSE="your-license-key"
# Run with Docker Compose
docker-compose up -d
```
## Configuration
Configuration is done via `appsettings.json` or environment variables:
```json
{
"AIService": {
"DefaultProvider": "Claude",
"DefaultModel": "claude-sonnet-4-20250514",
"MaxTokens": 4096,
"Temperature": 0.7
},
"Providers": {
"Claude": {
"ApiKey": "sk-ant-...",
"Enabled": true
},
"OpenAI": {
"ApiKey": "sk-...",
"Enabled": true
}
},
"License": {
"LicenseKey": "your-license-key"
}
}
```
Environment variable format: `AIService__DefaultProvider=Claude`
## API Reference
The service exposes a gRPC API defined in `protos/gitcaddy_ai.proto`.
### Code Review
```protobuf
rpc ReviewPullRequest(ReviewPullRequestRequest) returns (ReviewPullRequestResponse);
rpc ReviewCommit(ReviewCommitRequest) returns (ReviewCommitResponse);
```
### Code Intelligence
```protobuf
rpc SummarizeChanges(SummarizeChangesRequest) returns (SummarizeChangesResponse);
rpc ExplainCode(ExplainCodeRequest) returns (ExplainCodeResponse);
rpc SuggestFix(SuggestFixRequest) returns (SuggestFixResponse);
```
### Issue Management
```protobuf
rpc TriageIssue(TriageIssueRequest) returns (TriageIssueResponse);
rpc SuggestLabels(SuggestLabelsRequest) returns (SuggestLabelsResponse);
rpc GenerateIssueResponse(GenerateIssueResponseRequest) returns (GenerateIssueResponseResponse);
```
### Documentation
```protobuf
rpc GenerateDocumentation(GenerateDocumentationRequest) returns (GenerateDocumentationResponse);
rpc GenerateCommitMessage(GenerateCommitMessageRequest) returns (GenerateCommitMessageResponse);
```
### Workflows & Chat
```protobuf
rpc StartWorkflow(StartWorkflowRequest) returns (stream WorkflowEvent);
rpc ExecuteTask(ExecuteTaskRequest) returns (ExecuteTaskResponse);
rpc Chat(stream ChatRequest) returns (stream ChatResponse);
```
## Client Libraries
### .NET Client
```csharp
using GitCaddy.AI.Client;
var client = new GitCaddyAIClient("http://localhost:5051");
var response = await client.ReviewPullRequestAsync(new ReviewPullRequestRequest
{
RepoId = 1,
PullRequestId = 42,
PrTitle = "Add feature X",
Files = { new FileDiff { Path = "src/foo.cs", Patch = "..." } }
});
Console.WriteLine(response.Summary);
```
### Go Client
```go
import ai "git.marketally.com/gitcaddy/gitcaddy-ai/go/gitcaddy-ai-client"
client, err := ai.NewClient("localhost:5051")
if err != nil {
log.Fatal(err)
}
defer client.Close()
resp, err := client.ReviewPullRequest(ctx, &pb.ReviewPullRequestRequest{
RepoId: 1,
PullRequestId: 42,
PrTitle: "Add feature X",
Files: []*pb.FileDiff{{Path: "src/foo.go", Patch: "..."}},
})
```
## Licensing
GitCaddy AI is licensed under the Business Source License 1.1 (BSL-1.1).
### License Tiers
| Tier | Features |
|------|----------|
| **Standard** | Code review, code intelligence, documentation, chat |
| **Professional** | + Issue management, agentic workflows |
| **Enterprise** | + Custom models, audit logging, SSO integration |
### Trial Mode
Without a license key, the service runs in trial mode with Standard tier features for 30 days.
## Development
### Building
```bash
dotnet build
```
### Testing
```bash
dotnet test
```
### Generating Proto Files
For Go client:
```bash
cd go/gitcaddy-ai-client
go generate
```
## Integration with GitCaddy Server
Add the AI client to your GitCaddy server configuration:
```ini
[ai]
ENABLED = true
SERVICE_URL = http://localhost:5051
```
## Support
- Documentation: https://docs.gitcaddy.com/ai
- Issues: https://git.marketally.com/gitcaddy/gitcaddy-ai/issues
- Email: support@marketally.com
## License
Copyright 2026 MarketAlly. All rights reserved.
Licensed under the Business Source License 1.1 (BSL-1.1).

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\GitCaddy.AI.Client\GitCaddy.AI.Client.csproj" />
</ItemGroup>
</Project>

163
examples/dotnet/Program.cs Normal file
View File

@@ -0,0 +1,163 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
// Example: Using GitCaddy AI Client in .NET
using GitCaddy.AI.Client;
using GitCaddy.AI.Proto;
Console.WriteLine("GitCaddy AI Client Example");
Console.WriteLine("==========================\n");
// Create client
using var client = new GitCaddyAIClient("http://localhost:5051");
// Check health
Console.WriteLine("Checking service health...");
var health = await client.CheckHealthAsync();
Console.WriteLine($"Service healthy: {health.Healthy}");
Console.WriteLine($"Version: {health.Version}");
if (health.License != null)
{
Console.WriteLine($"License: {health.License.Tier} tier for {health.License.Customer}");
}
Console.WriteLine();
// Example 1: Code Review
Console.WriteLine("Example 1: Reviewing a Pull Request");
Console.WriteLine("------------------------------------");
var reviewResponse = await client.ReviewPullRequestAsync(new ReviewPullRequestRequest
{
RepoId = 1,
PullRequestId = 42,
PrTitle = "Add user authentication",
PrDescription = "Implements JWT-based authentication for the API",
BaseBranch = "main",
HeadBranch = "feature/auth",
Files =
{
new FileDiff
{
Path = "src/auth/jwt.go",
Status = "added",
Language = "go",
Patch = @"
+package auth
+
+import ""github.com/golang-jwt/jwt/v5""
+
+func GenerateToken(userID string) (string, error) {
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ ""user_id"": userID,
+ })
+ return token.SignedString([]byte(""secret""))
+}
"
}
},
Options = new ReviewOptions
{
CheckSecurity = true,
CheckPerformance = true,
SuggestImprovements = true
}
});
Console.WriteLine($"Verdict: {reviewResponse.Verdict}");
Console.WriteLine($"Summary: {reviewResponse.Summary[..Math.Min(500, reviewResponse.Summary.Length)]}...\n");
// Example 2: Explain Code
Console.WriteLine("Example 2: Explaining Code");
Console.WriteLine("---------------------------");
var explainResponse = await client.ExplainCodeAsync(new ExplainCodeRequest
{
RepoId = 1,
FilePath = "src/utils/retry.go",
Code = @"
func Retry(attempts int, sleep time.Duration, f func() error) error {
var err error
for i := 0; i < attempts; i++ {
if err = f(); err == nil {
return nil
}
time.Sleep(sleep)
sleep *= 2
}
return fmt.Errorf(""after %d attempts: %w"", attempts, err)
}
",
Question = "What pattern is this implementing?"
});
Console.WriteLine($"Explanation: {explainResponse.Explanation[..Math.Min(500, explainResponse.Explanation.Length)]}...\n");
// Example 3: Generate Commit Message
Console.WriteLine("Example 3: Generating Commit Message");
Console.WriteLine("-------------------------------------");
var commitResponse = await client.GenerateCommitMessageAsync(new GenerateCommitMessageRequest
{
RepoId = 1,
Style = "conventional",
Files =
{
new FileDiff
{
Path = "src/api/handlers.go",
Status = "modified",
Patch = @"
@@ -45,6 +45,15 @@ func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
+func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
+ id := chi.URLParam(r, ""id"")
+ if err := h.userService.Delete(r.Context(), id); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.WriteHeader(http.StatusNoContent)
+}
"
}
}
});
Console.WriteLine($"Suggested message: {commitResponse.Message}");
if (commitResponse.Alternatives.Count > 0)
{
Console.WriteLine("Alternatives:");
foreach (var alt in commitResponse.Alternatives)
{
Console.WriteLine($" - {alt}");
}
}
Console.WriteLine();
// Example 4: Triage Issue
Console.WriteLine("Example 4: Triaging an Issue");
Console.WriteLine("-----------------------------");
var triageResponse = await client.TriageIssueAsync(new TriageIssueRequest
{
RepoId = 1,
IssueId = 123,
Title = "App crashes when uploading large files",
Body = @"
When I try to upload a file larger than 100MB, the application crashes with an out of memory error.
Steps to reproduce:
1. Go to upload page
2. Select a file > 100MB
3. Click upload
4. App crashes
Expected: Should show an error or handle gracefully
Actual: Application crashes
Environment: Windows 11, Chrome 120
",
AvailableLabels = { "bug", "enhancement", "question", "documentation", "performance", "security", "crash" }
});
Console.WriteLine($"Priority: {triageResponse.Priority}");
Console.WriteLine($"Category: {triageResponse.Category}");
Console.WriteLine($"Suggested labels: {string.Join(", ", triageResponse.SuggestedLabels)}");
Console.WriteLine($"Summary: {triageResponse.Summary[..Math.Min(300, triageResponse.Summary.Length)]}...\n");
Console.WriteLine("Done!");

7
examples/go/go.mod Normal file
View File

@@ -0,0 +1,7 @@
module git.marketally.com/gitcaddy/gitcaddy-ai/examples/go
go 1.23
require git.marketally.com/gitcaddy/gitcaddy-ai/go/gitcaddy-ai-client v0.0.0
replace git.marketally.com/gitcaddy/gitcaddy-ai/go/gitcaddy-ai-client => ../../go/gitcaddy-ai-client

147
examples/go/main.go Normal file
View File

@@ -0,0 +1,147 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
// Example: Using GitCaddy AI Client in Go
package main
import (
"context"
"fmt"
"log"
"time"
ai "git.marketally.com/gitcaddy/gitcaddy-ai/go/gitcaddy-ai-client"
pb "git.marketally.com/gitcaddy/gitcaddy-ai/go/gitcaddy-ai-client/proto"
)
func main() {
fmt.Println("GitCaddy AI Client Example (Go)")
fmt.Println("================================\n")
// Create client
client, err := ai.NewClient("localhost:5051")
if err != nil {
log.Fatalf("Failed to create client: %v", err)
}
defer client.Close()
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
// Check health
fmt.Println("Checking service health...")
health, err := client.CheckHealth(ctx)
if err != nil {
log.Fatalf("Health check failed: %v", err)
}
fmt.Printf("Service healthy: %v\n", health.Healthy)
fmt.Printf("Version: %s\n", health.Version)
if health.License != nil {
fmt.Printf("License: %s tier for %s\n", health.License.Tier, health.License.Customer)
}
fmt.Println()
// Example 1: Code Review
fmt.Println("Example 1: Reviewing a Pull Request")
fmt.Println("------------------------------------")
reviewResp, err := client.ReviewPullRequest(ctx, &pb.ReviewPullRequestRequest{
RepoId: 1,
PullRequestId: 42,
PrTitle: "Add user authentication",
PrDescription: "Implements JWT-based authentication for the API",
BaseBranch: "main",
HeadBranch: "feature/auth",
Files: []*pb.FileDiff{
{
Path: "src/auth/jwt.go",
Status: "added",
Language: "go",
Patch: `
+package auth
+
+import "github.com/golang-jwt/jwt/v5"
+
+func GenerateToken(userID string) (string, error) {
+ token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
+ "user_id": userID,
+ })
+ return token.SignedString([]byte("secret"))
+}
`,
},
},
Options: &pb.ReviewOptions{
CheckSecurity: true,
CheckPerformance: true,
SuggestImprovements: true,
},
})
if err != nil {
log.Printf("Review failed: %v", err)
} else {
fmt.Printf("Verdict: %v\n", reviewResp.Verdict)
summary := reviewResp.Summary
if len(summary) > 500 {
summary = summary[:500] + "..."
}
fmt.Printf("Summary: %s\n\n", summary)
}
// Example 2: Generate Commit Message
fmt.Println("Example 2: Generating Commit Message")
fmt.Println("-------------------------------------")
commitResp, err := client.GenerateCommitMessage(ctx, &pb.GenerateCommitMessageRequest{
RepoId: 1,
Style: "conventional",
Files: []*pb.FileDiff{
{
Path: "src/api/handlers.go",
Status: "modified",
Patch: `
@@ -45,6 +45,15 @@ func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
+func (h *Handler) DeleteUser(w http.ResponseWriter, r *http.Request) {
+ id := chi.URLParam(r, "id")
+ if err := h.userService.Delete(r.Context(), id); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ w.WriteHeader(http.StatusNoContent)
+}
`,
},
},
})
if err != nil {
log.Printf("Commit message generation failed: %v", err)
} else {
fmt.Printf("Suggested message: %s\n", commitResp.Message)
if len(commitResp.Alternatives) > 0 {
fmt.Println("Alternatives:")
for _, alt := range commitResp.Alternatives {
fmt.Printf(" - %s\n", alt)
}
}
fmt.Println()
}
// Example 3: Start a Workflow
fmt.Println("Example 3: Starting a Code Review Workflow")
fmt.Println("-------------------------------------------")
err = client.StartWorkflow(ctx, &pb.StartWorkflowRequest{
RepoId: 1,
WorkflowType: "code_review",
Goal: "Review all changes in the auth module for security issues",
Parameters: map[string]string{
"path": "src/auth/",
},
}, func(event *pb.WorkflowEvent) error {
fmt.Printf("[%s] %s: %s\n", event.Type, event.AgentId, event.Message)
return nil
})
if err != nil {
log.Printf("Workflow failed: %v", err)
}
fmt.Println()
fmt.Println("Done!")
}

View File

@@ -0,0 +1,161 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
// Package gitcaddyai provides a Go client for the GitCaddy AI Service.
package gitcaddyai
import (
"context"
"io"
pb "git.marketally.com/gitcaddy/gitcaddy-ai/go/gitcaddy-ai-client/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// Client provides access to GitCaddy AI Service.
type Client struct {
conn *grpc.ClientConn
client pb.GitCaddyAIClient
}
// NewClient creates a new GitCaddy AI client.
func NewClient(address string, opts ...grpc.DialOption) (*Client, error) {
if len(opts) == 0 {
opts = []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
}
conn, err := grpc.NewClient(address, opts...)
if err != nil {
return nil, err
}
return &Client{
conn: conn,
client: pb.NewGitCaddyAIClient(conn),
}, nil
}
// Close closes the client connection.
func (c *Client) Close() error {
return c.conn.Close()
}
// ReviewPullRequest reviews a pull request and returns AI feedback.
func (c *Client) ReviewPullRequest(ctx context.Context, req *pb.ReviewPullRequestRequest) (*pb.ReviewPullRequestResponse, error) {
return c.client.ReviewPullRequest(ctx, req)
}
// ReviewCommit reviews a single commit.
func (c *Client) ReviewCommit(ctx context.Context, req *pb.ReviewCommitRequest) (*pb.ReviewCommitResponse, error) {
return c.client.ReviewCommit(ctx, req)
}
// SummarizeChanges summarizes code changes.
func (c *Client) SummarizeChanges(ctx context.Context, req *pb.SummarizeChangesRequest) (*pb.SummarizeChangesResponse, error) {
return c.client.SummarizeChanges(ctx, req)
}
// ExplainCode explains a piece of code.
func (c *Client) ExplainCode(ctx context.Context, req *pb.ExplainCodeRequest) (*pb.ExplainCodeResponse, error) {
return c.client.ExplainCode(ctx, req)
}
// SuggestFix suggests a fix for code with an error.
func (c *Client) SuggestFix(ctx context.Context, req *pb.SuggestFixRequest) (*pb.SuggestFixResponse, error) {
return c.client.SuggestFix(ctx, req)
}
// TriageIssue triages an issue with priority and category.
func (c *Client) TriageIssue(ctx context.Context, req *pb.TriageIssueRequest) (*pb.TriageIssueResponse, error) {
return c.client.TriageIssue(ctx, req)
}
// SuggestLabels suggests labels for an issue.
func (c *Client) SuggestLabels(ctx context.Context, req *pb.SuggestLabelsRequest) (*pb.SuggestLabelsResponse, error) {
return c.client.SuggestLabels(ctx, req)
}
// GenerateIssueResponse generates a response to an issue.
func (c *Client) GenerateIssueResponse(ctx context.Context, req *pb.GenerateIssueResponseRequest) (*pb.GenerateIssueResponseResponse, error) {
return c.client.GenerateIssueResponse(ctx, req)
}
// GenerateDocumentation generates documentation for code.
func (c *Client) GenerateDocumentation(ctx context.Context, req *pb.GenerateDocumentationRequest) (*pb.GenerateDocumentationResponse, error) {
return c.client.GenerateDocumentation(ctx, req)
}
// GenerateCommitMessage generates a commit message from changes.
func (c *Client) GenerateCommitMessage(ctx context.Context, req *pb.GenerateCommitMessageRequest) (*pb.GenerateCommitMessageResponse, error) {
return c.client.GenerateCommitMessage(ctx, req)
}
// ExecuteTask executes a single task.
func (c *Client) ExecuteTask(ctx context.Context, req *pb.ExecuteTaskRequest) (*pb.ExecuteTaskResponse, error) {
return c.client.ExecuteTask(ctx, req)
}
// CheckHealth checks service health.
func (c *Client) CheckHealth(ctx context.Context) (*pb.HealthCheckResponse, error) {
return c.client.CheckHealth(ctx, &pb.HealthCheckRequest{})
}
// ValidateLicense validates a license key.
func (c *Client) ValidateLicense(ctx context.Context, licenseKey string) (*pb.ValidateLicenseResponse, error) {
return c.client.ValidateLicense(ctx, &pb.ValidateLicenseRequest{LicenseKey: licenseKey})
}
// WorkflowEventHandler handles workflow events from StartWorkflow.
type WorkflowEventHandler func(event *pb.WorkflowEvent) error
// StartWorkflow starts a workflow and calls the handler for each event.
func (c *Client) StartWorkflow(ctx context.Context, req *pb.StartWorkflowRequest, handler WorkflowEventHandler) error {
stream, err := c.client.StartWorkflow(ctx, req)
if err != nil {
return err
}
for {
event, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
if err := handler(event); err != nil {
return err
}
}
}
// ChatSession represents an active chat session.
type ChatSession struct {
stream pb.GitCaddyAI_ChatClient
}
// StartChat starts a new chat session.
func (c *Client) StartChat(ctx context.Context) (*ChatSession, error) {
stream, err := c.client.Chat(ctx)
if err != nil {
return nil, err
}
return &ChatSession{stream: stream}, nil
}
// Send sends a message in the chat session.
func (s *ChatSession) Send(req *pb.ChatRequest) error {
return s.stream.Send(req)
}
// Recv receives a response from the chat session.
func (s *ChatSession) Recv() (*pb.ChatResponse, error) {
return s.stream.Recv()
}
// Close closes the chat session.
func (s *ChatSession) Close() error {
return s.stream.CloseSend()
}

View File

@@ -0,0 +1,6 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
package gitcaddyai
//go:generate protoc --go_out=./proto --go_opt=paths=source_relative --go-grpc_out=./proto --go-grpc_opt=paths=source_relative -I../../protos ../../protos/gitcaddy_ai.proto

View File

@@ -0,0 +1,15 @@
module git.marketally.com/gitcaddy/gitcaddy-ai/go/gitcaddy-ai-client
go 1.23
require (
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.36.3
)
require (
golang.org/x/net v0.34.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
)

View File

@@ -0,0 +1,190 @@
// Copyright 2026 MarketAlly. All rights reserved.
// SPDX-License-Identifier: BSL-1.1
using GitCaddy.AI.Proto;
using Grpc.Core;
using Grpc.Net.Client;
namespace GitCaddy.AI.Client;
/// <summary>
/// Client for connecting to GitCaddy AI Service.
/// </summary>
public class GitCaddyAIClient : IDisposable
{
private readonly GrpcChannel _channel;
private readonly GitCaddyAI.GitCaddyAIClient _client;
private bool _disposed;
public GitCaddyAIClient(string address)
{
_channel = GrpcChannel.ForAddress(address);
_client = new GitCaddyAI.GitCaddyAIClient(_channel);
}
public GitCaddyAIClient(GrpcChannel channel)
{
_channel = channel;
_client = new GitCaddyAI.GitCaddyAIClient(channel);
}
/// <summary>
/// Reviews a pull request and returns AI-generated feedback.
/// </summary>
public async Task<ReviewPullRequestResponse> ReviewPullRequestAsync(
ReviewPullRequestRequest request,
CancellationToken cancellationToken = default)
{
return await _client.ReviewPullRequestAsync(request, cancellationToken: cancellationToken);
}
/// <summary>
/// Reviews a single commit.
/// </summary>
public async Task<ReviewCommitResponse> ReviewCommitAsync(
ReviewCommitRequest request,
CancellationToken cancellationToken = default)
{
return await _client.ReviewCommitAsync(request, cancellationToken: cancellationToken);
}
/// <summary>
/// Summarizes code changes.
/// </summary>
public async Task<SummarizeChangesResponse> SummarizeChangesAsync(
SummarizeChangesRequest request,
CancellationToken cancellationToken = default)
{
return await _client.SummarizeChangesAsync(request, cancellationToken: cancellationToken);
}
/// <summary>
/// Explains a piece of code.
/// </summary>
public async Task<ExplainCodeResponse> ExplainCodeAsync(
ExplainCodeRequest request,
CancellationToken cancellationToken = default)
{
return await _client.ExplainCodeAsync(request, cancellationToken: cancellationToken);
}
/// <summary>
/// Suggests a fix for code with an error.
/// </summary>
public async Task<SuggestFixResponse> SuggestFixAsync(
SuggestFixRequest request,
CancellationToken cancellationToken = default)
{
return await _client.SuggestFixAsync(request, cancellationToken: cancellationToken);
}
/// <summary>
/// Triages an issue with priority and category.
/// </summary>
public async Task<TriageIssueResponse> TriageIssueAsync(
TriageIssueRequest request,
CancellationToken cancellationToken = default)
{
return await _client.TriageIssueAsync(request, cancellationToken: cancellationToken);
}
/// <summary>
/// Suggests labels for an issue.
/// </summary>
public async Task<SuggestLabelsResponse> SuggestLabelsAsync(
SuggestLabelsRequest request,
CancellationToken cancellationToken = default)
{
return await _client.SuggestLabelsAsync(request, cancellationToken: cancellationToken);
}
/// <summary>
/// Generates a response to an issue.
/// </summary>
public async Task<GenerateIssueResponseResponse> GenerateIssueResponseAsync(
GenerateIssueResponseRequest request,
CancellationToken cancellationToken = default)
{
return await _client.GenerateIssueResponseAsync(request, cancellationToken: cancellationToken);
}
/// <summary>
/// Generates documentation for code.
/// </summary>
public async Task<GenerateDocumentationResponse> GenerateDocumentationAsync(
GenerateDocumentationRequest request,
CancellationToken cancellationToken = default)
{
return await _client.GenerateDocumentationAsync(request, cancellationToken: cancellationToken);
}
/// <summary>
/// Generates a commit message from changes.
/// </summary>
public async Task<GenerateCommitMessageResponse> GenerateCommitMessageAsync(
GenerateCommitMessageRequest request,
CancellationToken cancellationToken = default)
{
return await _client.GenerateCommitMessageAsync(request, cancellationToken: cancellationToken);
}
/// <summary>
/// Starts a workflow and streams events.
/// </summary>
public AsyncServerStreamingCall<WorkflowEvent> StartWorkflow(
StartWorkflowRequest request,
CancellationToken cancellationToken = default)
{
return _client.StartWorkflow(request, cancellationToken: cancellationToken);
}
/// <summary>
/// Executes a single task.
/// </summary>
public async Task<ExecuteTaskResponse> ExecuteTaskAsync(
ExecuteTaskRequest request,
CancellationToken cancellationToken = default)
{
return await _client.ExecuteTaskAsync(request, cancellationToken: cancellationToken);
}
/// <summary>
/// Starts a bidirectional chat session.
/// </summary>
public AsyncDuplexStreamingCall<ChatRequest, ChatResponse> Chat(
CancellationToken cancellationToken = default)
{
return _client.Chat(cancellationToken: cancellationToken);
}
/// <summary>
/// Checks service health.
/// </summary>
public async Task<HealthCheckResponse> CheckHealthAsync(
CancellationToken cancellationToken = default)
{
return await _client.CheckHealthAsync(new HealthCheckRequest(), cancellationToken: cancellationToken);
}
/// <summary>
/// Validates a license key.
/// </summary>
public async Task<ValidateLicenseResponse> ValidateLicenseAsync(
string licenseKey,
CancellationToken cancellationToken = default)
{
return await _client.ValidateLicenseAsync(
new ValidateLicenseRequest { LicenseKey = licenseKey },
cancellationToken: cancellationToken);
}
public void Dispose()
{
if (!_disposed)
{
_channel.Dispose();
_disposed = true;
}
GC.SuppressFinalize(this);
}
}

View File

@@ -26,18 +26,18 @@ public class AIProviderFactory : IAIProviderFactory
_providerOptions = providerOptions.Value;
}
public IAIConversationBuilder CreateConversation()
public AIConversationBuilder CreateConversation()
{
return CreateConversation(_serviceOptions.DefaultProvider, _serviceOptions.DefaultModel);
}
public IAIConversationBuilder CreateConversation(string provider)
public AIConversationBuilder CreateConversation(string provider)
{
var model = GetDefaultModelForProvider(provider);
return CreateConversation(provider, model);
}
public IAIConversationBuilder CreateConversation(string provider, string model)
public AIConversationBuilder CreateConversation(string provider, string model)
{
var aiProvider = ParseProvider(provider);
var config = GetProviderConfig(provider);
@@ -55,11 +55,6 @@ public class AIProviderFactory : IAIProviderFactory
.WithMaxTokens(_serviceOptions.MaxTokens)
.WithTemperature(_serviceOptions.Temperature);
if (!string.IsNullOrEmpty(config.BaseUrl))
{
builder.WithBaseUrl(config.BaseUrl);
}
return builder;
}

View File

@@ -13,15 +13,15 @@ public interface IAIProviderFactory
/// <summary>
/// Creates a new conversation builder with default settings.
/// </summary>
IAIConversationBuilder CreateConversation();
AIConversationBuilder CreateConversation();
/// <summary>
/// Creates a new conversation builder with a specific provider.
/// </summary>
IAIConversationBuilder CreateConversation(string provider);
AIConversationBuilder CreateConversation(string provider);
/// <summary>
/// Creates a new conversation builder with a specific provider and model.
/// </summary>
IAIConversationBuilder CreateConversation(string provider, string model);
AIConversationBuilder CreateConversation(string provider, string model);
}

View File

@@ -21,7 +21,7 @@
<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="Microsoft.Extensions.Logging.Abstractions" Version="9.0.10" />
<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" />

View File

@@ -42,14 +42,7 @@ builder.Services.AddGrpc(options =>
});
// 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");
});
builder.Services.AddHealthChecks();
var app = builder.Build();
@@ -68,7 +61,7 @@ else
// Map gRPC services
app.MapGrpcService<GitCaddyAIServiceImpl>();
app.MapGrpcHealthChecksService();
app.MapHealthChecks("/healthz");
// HTTP endpoint for basic health check
app.MapGet("/", () => "GitCaddy AI Service is running. Use gRPC to connect.");

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"GitCaddy.AI.Service": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5050;http://localhost:5051",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Docker": {
"commandName": "Docker",
"launchBrowser": false,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"publishAllPorts": true,
"useSSL": false
}
}
}

View File

@@ -5,7 +5,6 @@ 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;
@@ -20,7 +19,7 @@ public class ChatService : IChatService
private readonly AIServiceOptions _options;
// In-memory conversation cache (replace with Redis for production scaling)
private readonly ConcurrentDictionary<string, IAIConversation> _conversations = new();
private readonly ConcurrentDictionary<string, MarketAlly.AIPlugin.Conversation.AIConversation> _conversations = new();
public ChatService(
ILogger<ChatService> logger,
@@ -62,14 +61,17 @@ public class ChatService : IChatService
var message = BuildChatMessage(request);
// Stream the response
await foreach (var chunk in conversation.SendMessageStreamAsync(message, cancellationToken))
await foreach (var chunk in conversation.SendStreamingAsync(message, cancellationToken))
{
await responseStream.WriteAsync(new ChatResponse
if (!string.IsNullOrEmpty(chunk.TextDelta))
{
ConversationId = conversationId,
Message = chunk,
IsComplete = false
}, cancellationToken);
await responseStream.WriteAsync(new ChatResponse
{
ConversationId = conversationId,
Message = chunk.TextDelta,
IsComplete = false
}, cancellationToken);
}
}
// Send completion marker

View File

@@ -40,9 +40,9 @@ public class CodeIntelligenceService : ICodeIntelligenceService
.Build();
var prompt = BuildSummarizePrompt(request);
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
var response = await conversation.SendAsync(prompt, cancellationToken);
return ParseSummarizeResponse(response);
return ParseSummarizeResponse(response.FinalMessage ?? "");
}
public async Task<ExplainCodeResponse> ExplainCodeAsync(
@@ -72,11 +72,11 @@ public class CodeIntelligenceService : ICodeIntelligenceService
{(string.IsNullOrEmpty(request.Question) ? "" : $"Specific question: {request.Question}")}
""";
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
var response = await conversation.SendAsync(prompt, cancellationToken);
return new ExplainCodeResponse
{
Explanation = response
Explanation = response.FinalMessage ?? ""
};
}
@@ -111,11 +111,11 @@ public class CodeIntelligenceService : ICodeIntelligenceService
Please suggest a fix.
""";
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
var response = await conversation.SendAsync(prompt, cancellationToken);
return new SuggestFixResponse
{
Explanation = response
Explanation = response.FinalMessage ?? ""
};
}

View File

@@ -39,10 +39,10 @@ public class CodeReviewService : ICodeReviewService
var prompt = BuildPullRequestReviewPrompt(request);
// Get AI response
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
var response = await conversation.SendAsync(prompt, cancellationToken);
// Parse and return structured response
return ParsePullRequestReviewResponse(response);
return ParsePullRequestReviewResponse(response.FinalMessage ?? "");
}
public async Task<ReviewCommitResponse> ReviewCommitAsync(
@@ -53,9 +53,9 @@ public class CodeReviewService : ICodeReviewService
.Build();
var prompt = BuildCommitReviewPrompt(request);
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
var response = await conversation.SendAsync(prompt, cancellationToken);
return ParseCommitReviewResponse(response);
return ParseCommitReviewResponse(response.FinalMessage ?? "");
}
private static string GetCodeReviewSystemPrompt(ReviewOptions? options)

View File

@@ -71,11 +71,11 @@ public class DocumentationService : IDocumentationService
```
""";
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
var response = await conversation.SendAsync(prompt, cancellationToken);
return new GenerateDocumentationResponse
{
Documentation = response
Documentation = response.FinalMessage ?? ""
};
}
@@ -128,13 +128,14 @@ public class DocumentationService : IDocumentationService
sb.AppendLine();
}
var response = await conversation.SendMessageAsync(sb.ToString(), cancellationToken);
var response = await conversation.SendAsync(sb.ToString(), cancellationToken);
var responseContent = response.FinalMessage ?? "";
// Parse response - first line is primary, rest are alternatives
var lines = response.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var lines = responseContent.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var result = new GenerateCommitMessageResponse
{
Message = lines.Length > 0 ? lines[0].Trim() : response.Trim()
Message = lines.Length > 0 ? lines[0].Trim() : responseContent.Trim()
};
// Extract alternatives if present

View File

@@ -32,22 +32,22 @@ public class IssueService : IIssueService
var availableLabels = string.Join(", ", request.AvailableLabels);
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt($"""
.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}
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();
@@ -61,30 +61,30 @@ public class IssueService : IIssueService
Existing labels: {string.Join(", ", request.ExistingLabels)}
""";
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
var response = await conversation.SendAsync(prompt, cancellationToken);
// Parse JSON response
return ParseTriageResponse(response);
return ParseTriageResponse(response.FinalMessage ?? "");
}
public async Task<SuggestLabelsResponse> SuggestLabelsAsync(
SuggestLabelsRequest request, CancellationToken cancellationToken)
{
var conversation = _providerFactory.CreateConversation()
.WithSystemPrompt($"""
.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)}
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": "..."}}
{"label": "...", "confidence": 0.9, "reason": "..."}
]
}}
}
""")
.Build();
@@ -95,9 +95,9 @@ public class IssueService : IIssueService
{request.Body}
""";
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
var response = await conversation.SendAsync(prompt, cancellationToken);
return ParseSuggestLabelsResponse(response, request.AvailableLabels);
return ParseSuggestLabelsResponse(response.FinalMessage ?? "", request.AvailableLabels);
}
public async Task<GenerateIssueResponseResponse> GenerateResponseAsync(
@@ -140,11 +140,11 @@ public class IssueService : IIssueService
sb.AppendLine();
sb.AppendLine("Please generate an appropriate response.");
var response = await conversation.SendMessageAsync(sb.ToString(), cancellationToken);
var response = await conversation.SendAsync(sb.ToString(), cancellationToken);
return new GenerateIssueResponseResponse
{
Response = response
Response = response.FinalMessage ?? ""
};
}

View File

@@ -68,9 +68,10 @@ public class WorkflowService : IWorkflowService
}, cancellationToken);
// Execute the workflow
var response = await conversation.SendMessageAsync(
var aiResponse = await conversation.SendAsync(
$"Goal: {request.Goal}\n\nParameters: {string.Join(", ", request.Parameters.Select(p => $"{p.Key}={p.Value}"))}",
cancellationToken);
var response = aiResponse.FinalMessage ?? "";
// Send step completed event
await responseStream.WriteAsync(new WorkflowEvent
@@ -137,12 +138,12 @@ public class WorkflowService : IWorkflowService
Please complete this task.
""";
var response = await conversation.SendMessageAsync(prompt, cancellationToken);
var aiResponse = await conversation.SendAsync(prompt, cancellationToken);
return new ExecuteTaskResponse
{
Success = true,
Result = response
Result = aiResponse.FinalMessage ?? ""
};
}
catch (Exception ex)