Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# AI Provider API Keys (Required)
GEMINI_API_KEY=your_gemini_api_key_here
DEEPSEEK_API_KEY=your_deepseek_api_key_here
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider removing the default API keys. While this file is named .env.example, users might copy it directly to .env and accidentally commit the placeholder keys. It might be better to leave the values blank and add a comment indicating that the user needs to provide their own keys.

OPENAI_API_KEY=your_openai_api_key_here # Optional
ANTHROPIC_API_KEY=your_claude_api_key_here # Optional
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a comment explaining the purpose of the ANTHROPIC_API_KEY to clarify when it is needed. It's mentioned as optional, but the context of its use isn't immediately obvious from this file alone.


# Provider Configuration
ENABLED_AI_PROVIDERS=gemini,deepseek
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ENABLED_AI_PROVIDERS and AI_PROVIDER_FALLBACK_CHAIN variables should be documented more thoroughly (either here or in the main documentation). It's not immediately obvious what the difference is between these and how they interact. Specifically, what happens if a provider is in the fallback chain but not in the enabled providers?

AI_PROVIDER_FALLBACK_CHAIN=gemini,deepseek

# Gemini Settings
GEMINI_MODEL=gemini-2.0-flash-001
GEMINI_TEMPERATURE=0.8
GEMINI_TOP_P=0.95
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a comment to DEEPSEEK_TEMPERATURE indicating the valid range (e.g., 0.0 to 1.0) to prevent unexpected behavior if an invalid value is used.

GEMINI_MAX_TOKENS=8192

# DeepSeek Settings
DEEPSEEK_TEMPERATURE=0.7

# ChatGPT Settings (Optional)
OPENAI_MODEL=gpt-4
OPENAI_TEMPERATURE=0.7
OPENAI_MAX_TOKENS=4000
OPENAI_TOP_P=0.95
OPENAI_FREQ_PENALTY=0
OPENAI_PRES_PENALTY=0

# Claude Settings (Optional)
CLAUDE_MODEL=claude-3-opus-20240229
CLAUDE_TEMPERATURE=0.7
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to the API keys, consider removing the placeholder value for GITHUB_TOKEN. It's a security risk if someone accidentally commits this default value.

CLAUDE_MAX_TOKENS=4000

# GitHub Configuration
GITHUB_TOKEN=your_github_token_here # Required for GitHub API access
11 changes: 11 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"go.goroot": "/Users/hung/.local/share/mise/installs/go/1.23.5",
"debug.javascript.defaultRuntimeExecutable": {
"pwa-node": "/Users/hung/.local/share/mise/shims/node"
},
"python.defaultInterpreterPath": "/Users/hung/.local/share/mise/installs/python/3.13.1/bin/python",
"go.alternateTools": {
"go": "/Users/hung/.local/share/mise/shims/go",
"dlv": "/Users/hung/.local/share/mise/shims/dlv"
}
}
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,28 @@ This GitHub Action uses the Gemini AI API to provide code review feedback. It wo
3. **Providing feedback**: Gemini AI examines the code and generates review comments.
4. **Delivering the review**: The Action adds the comments directly to your pull request on GitHub.

## Using Deepseek AI for Code Review

In addition to Gemini, this action now supports Deepseek AI for code review. To use Deepseek:

1. Get your Deepseek API key from [Deepseek Platform](https://platform.deepseek.com/)
2. Add the Deepseek API key as a GitHub Secret named `DEEPSEEK_API_KEY`
3. Modify your workflow file to use Deepseek:

```yaml
- uses: truongnh1992/gemini-ai-code-reviewer@main
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
AI_PROVIDER: deepseek
DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }}
DEEPSEEK_MODEL: deepseek-coder-33b-instruct # Optional
EXCLUDE: "*.md,*.txt,package-lock.json,*.yml,*.yaml"
```

- The default model is `deepseek-coder-33b-instruct`
- Trigger a Deepseek review by commenting `/deepseek-review` on your PR
- See [CHANGELOG.md](docs/change_logs.md) for more details about the multi-provider support

## License

This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for more information.
30 changes: 23 additions & 7 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,34 @@
name: "Gemini AI Code Reviewer"
description: "This GitHub Action automatically reviews PRs using Google's Gemini AI model."
name: "AI Code Reviewer"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's better to freeze the versions of the packages to avoid unexpected breakages due to updates. Consider creating a requirements.txt file and using pip install -r requirements.txt

description: "This GitHub Action automatically reviews PRs using various AI models (Gemini, Deepseek, etc.)"
author: 'truongnh1992'

inputs:
GITHUB_TOKEN:
description: 'GitHub token to interact with the repository'
required: true
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider validating the AI_PROVIDER input to only accept gemini or deepseek to prevent unexpected behavior or errors if an invalid provider is specified.

AI_PROVIDER:
description: 'AI provider to use (gemini, deepseek)'
required: false
default: 'gemini'
GEMINI_API_KEY:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider setting a default value for DEEPSEEK_MODEL. This would improve usability, as users wouldn't have to specify the model if they want to use the default one. For example, the default could be deepseek-coder-33b-instruct.

description: 'Google Gemini API key'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file name has changed from review_code_gemini.py to review_code.py. Double check that the correct python script is running and that the transition is handled correctly.

required: true
description: 'Google Gemini API key (required if using Gemini)'
required: false
GEMINI_MODEL:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a validation step within the action to ensure that either GEMINI_API_KEY or DEEPSEEK_API_KEY is provided based on the AI_PROVIDER selection. This will prevent the action from running with missing credentials for the selected provider.

For example, if AI_PROVIDER is 'deepseek', validate that DEEPSEEK_API_KEY is provided and vice-versa.

description: 'The Gemini model to use for code review'
required: false
default: 'gemini-1.5-flash-002'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default Gemini model is updated from gemini-1.5-flash-002 to gemini-2.0-flash-001. Ensure the new default model is generally available and has the desired capabilities for code review. It may be useful to document the rationale for this change in the pull request description.

default: 'gemini-2.0-flash-001'
DEEPSEEK_API_KEY:
description: 'Deepseek API key (required if using Deepseek)'
required: false
DEEPSEEK_MODEL:
description: 'The Deepseek model to use for code review'
required: false
default: 'deepseek-coder-33b-instruct'
EXCLUDE:
description: 'Comma-separated list of file patterns to exclude'
required: false
default: ''

runs:
using: 'composite'
steps:
Expand All @@ -30,12 +42,16 @@ runs:
shell: bash
run: |
python -m pip install --upgrade pip
pip install google-generativeai PyGithub unidiff google-ai-generativelanguage==0.6.10 github3.py==1.3.0
pip install google-generativeai PyGithub unidiff requests

- name: Run code review
shell: bash
env:
GITHUB_TOKEN: ${{ inputs.GITHUB_TOKEN }}
AI_PROVIDER: ${{ inputs.AI_PROVIDER }}
GEMINI_API_KEY: ${{ inputs.GEMINI_API_KEY }}
GEMINI_MODEL: ${{ inputs.GEMINI_MODEL }}
DEEPSEEK_API_KEY: ${{ inputs.DEEPSEEK_API_KEY }}
DEEPSEEK_MODEL: ${{ inputs.DEEPSEEK_MODEL }}
EXCLUDE: ${{ inputs.EXCLUDE }}
run: python ${{ github.action_path }}/review_code_gemini.py
run: python ${{ github.action_path }}/review_code.py
28 changes: 28 additions & 0 deletions ai_providers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from abc import ABC, abstractmethod
from typing import List, Dict, Any

class AIProvider(ABC):
"""Abstract base class for AI code review providers."""

@abstractmethod
def configure(self) -> None:
"""Configure the AI provider with necessary credentials and settings."""
pass

@abstractmethod
def generate_review(self, prompt: str) -> List[Dict[str, Any]]:
"""Generate code review from the given prompt.

Args:
prompt (str): The code review prompt

Returns:
List[Dict[str, Any]]: List of review comments in the format:
[{"lineNumber": int, "reviewComment": str}]
"""
pass

@abstractmethod
def get_name(self) -> str:
"""Get the name of the AI provider."""
pass
74 changes: 74 additions & 0 deletions ai_providers/deepseek_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import os
import json
from typing import List, Dict, Any
import requests

from ai_providers import AIProvider

class DeepseekProvider(AIProvider):
"""Deepseek AI provider implementation."""

def __init__(self):
self.api_url = "https://api.deepseek.com/v1/chat/completions" # Example URL, adjust as needed
self.api_key = None
self.model = None
self.config = {
"temperature": 0.8,
"max_tokens": 8192
}

def configure(self) -> None:
"""Configure Deepseek with API key and model."""
self.api_key = os.environ.get('DEEPSEEK_API_KEY')
if not self.api_key:
raise ValueError("DEEPSEEK_API_KEY environment variable is required")
self.model = os.environ.get('DEEPSEEK_MODEL', 'deepseek-coder-33b-instruct')

def generate_review(self, prompt: str) -> List[Dict[str, Any]]:
"""Generate code review using Deepseek AI."""
try:
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {self.api_key}"
}

data = {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a default value for DEEPSEEK_MODEL directly in the os.environ.get call. This makes the code more readable and maintainable.

"model": self.model,
"messages": [
{"role": "user", "content": prompt}
],
"temperature": self.config["temperature"],
"max_tokens": self.config["max_tokens"]
}

response = requests.post(self.api_url, headers=headers, json=data)
response.raise_for_status()

response_data = response.json()
response_text = response_data['choices'][0]['message']['content'].strip()

if response_text.startswith('```json'):
response_text = response_text[7:]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good that you're handling potential errors during the API call and JSON decoding. However, consider adding more specific error handling and logging. For example, you could log the specific error message and traceback for debugging purposes. Also, consider using a more descriptive log level (e.g., logging.error) instead of print.

if response_text.endswith('```'):
response_text = response_text[:-3]
response_text = response_text.strip()

try:
data = json.loads(response_text)
if "reviews" in data and isinstance(data["reviews"], list):
return [
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code removes json and from the response. Consider adding a check to ensure that these prefixes/suffixes are removed correctly and completely. Edge cases like nested backticks or incomplete prefixes/suffixes should be handled gracefully to prevent unexpected behavior during JSON parsing.

review for review in data["reviews"]
if "lineNumber" in review and "reviewComment" in review
]
except json.JSONDecodeError as e:
print(f"Error decoding JSON response: {e}")
return []
except Exception as e:
print(f"Error during Deepseek API call: {e}")
return []

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of returning an empty list in the except blocks, consider raising a custom exception that encapsulates the original exception and provides more context about the error. This could provide more information to the calling function.

return []

def get_name(self) -> str:
"""Get the provider name."""
return "Deepseek AI"
49 changes: 49 additions & 0 deletions ai_providers/factory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from typing import Dict, Type
from ai_providers import AIProvider
from ai_providers.gemini_provider import GeminiProvider
from ai_providers.deepseek_provider import DeepseekProvider

class AIProviderFactory:
"""Factory class for creating and managing AI providers."""

_providers: Dict[str, Type[AIProvider]] = {
'gemini': GeminiProvider,
'deepseek': DeepseekProvider
}

@classmethod
def get_provider(cls, provider_name: str) -> AIProvider:
"""Get an instance of the specified AI provider.
Args:
provider_name (str): Name of the provider to use ('gemini', 'deepseek', etc.)
Returns:
AIProvider: Configured instance of the specified provider
Raises:
ValueError: If the specified provider is not supported
"""
provider_class = cls._providers.get(provider_name.lower())
if not provider_class:
supported = list(cls._providers.keys())
raise ValueError(f"Unsupported AI provider: {provider_name}. Supported providers: {supported}")

provider = provider_class()
provider.configure()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a try-except block around provider.configure() to catch potential configuration errors (e.g., missing API key) and raise a more informative exception. This will help with debugging and provide better error messages to the user.

return provider

@classmethod
def register_provider(cls, name: str, provider_class: Type[AIProvider]) -> None:
"""Register a new AI provider.
Args:
name (str): Name to register the provider under
provider_class (Type[AIProvider]): The provider class to register
"""
cls._providers[name.lower()] = provider_class

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a check to prevent overwriting existing providers when registering a new one. This could be a simple if name.lower() in cls._providers: check, possibly raising a ValueError if an attempt is made to overwrite.

@classmethod
def get_available_providers(cls) -> list[str]:
"""Get list of available provider names."""
return list(cls._providers.keys())
55 changes: 55 additions & 0 deletions ai_providers/gemini_provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import os
import json
from typing import List, Dict, Any
import google.generativeai as Client

from ai_providers import AIProvider

class GeminiProvider(AIProvider):
"""Gemini AI provider implementation."""

def __init__(self):
self.model = None
self.generation_config = {
"max_output_tokens": 8192,
"temperature": 0.8,
"top_p": 0.95,
}

def configure(self) -> None:
"""Configure Gemini with API key and model."""
Client.configure(api_key=os.environ.get('GEMINI_API_KEY'))
model_name = os.environ.get('GEMINI_MODEL', 'gemini-2.0-flash-001')
self.model = Client.GenerativeModel(model_name)

def generate_review(self, prompt: str) -> List[Dict[str, Any]]:
"""Generate code review using Gemini AI."""
try:
response = self.model.generate_content(prompt, generation_config=self.generation_config)

response_text = response.text.strip()
if response_text.startswith('```json'):
response_text = response_text[7:]
if response_text.endswith('```'):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code removes 'json' and '' from the response text. It would be more robust to use regular expressions to handle variations in whitespace or casing (e.g., 'JSON', ' json '). This will ensure that the parsing works correctly even if the AI's output format isn't perfectly consistent.

response_text = response_text[:-3]
response_text = response_text.strip()

try:
data = json.loads(response_text)
if "reviews" in data and isinstance(data["reviews"], list):
return [
review for review in data["reviews"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding logging with more context when a JSONDecodeError occurs. Include the full response_text in the log (if it's not too large) to help diagnose the issue. Also, consider adding a metric to track the rate of JSON decoding errors.

if "lineNumber" in review and "reviewComment" in review
]
except json.JSONDecodeError as e:
print(f"Error decoding JSON response: {e}")
return []
except Exception as e:
print(f"Error during Gemini API call: {e}")
return []

return []

def get_name(self) -> str:
"""Get the provider name."""
return "Gemini AI"
Loading