-
-
Notifications
You must be signed in to change notification settings - Fork 45
feat: implement AI provider architecture with Gemini and Deepseek support #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| OPENAI_API_KEY=your_openai_api_key_here # Optional | ||
| ANTHROPIC_API_KEY=your_claude_api_key_here # Optional | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a comment explaining the purpose of the |
||
|
|
||
| # Provider Configuration | ||
| ENABLED_AI_PROVIDERS=gemini,deepseek | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| AI_PROVIDER_FALLBACK_CHAIN=gemini,deepseek | ||
|
|
||
| # Gemini Settings | ||
| GEMINI_MODEL=gemini-2.0-flash-001 | ||
| GEMINI_TEMPERATURE=0.8 | ||
| GEMINI_TOP_P=0.95 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a comment to |
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the API keys, consider removing the placeholder value for |
||
| CLAUDE_MAX_TOKENS=4000 | ||
|
|
||
| # GitHub Configuration | ||
| GITHUB_TOKEN=your_github_token_here # Required for GitHub API access | ||
| 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" | ||
| } | ||
| } |
| 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" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| 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 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider validating the |
||
| AI_PROVIDER: | ||
| description: 'AI provider to use (gemini, deepseek)' | ||
| required: false | ||
| default: 'gemini' | ||
| GEMINI_API_KEY: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider setting a default value for |
||
| description: 'Google Gemini API key' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The file name has changed from |
||
| required: true | ||
| description: 'Google Gemini API key (required if using Gemini)' | ||
| required: false | ||
| GEMINI_MODEL: | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a validation step within the action to ensure that either For example, if |
||
| description: 'The Gemini model to use for code review' | ||
| required: false | ||
| default: 'gemini-1.5-flash-002' | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The default Gemini model is updated from |
||
| 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: | ||
|
|
@@ -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 | ||
| 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 |
| 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 = { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a default value for |
||
| "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:] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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., |
||
| 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 [ | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code removes |
||
| 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 [] | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
| 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() | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider adding a try-except block around |
||
| 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 | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| @classmethod | ||
| def get_available_providers(cls) -> list[str]: | ||
| """Get list of available provider names.""" | ||
| return list(cls._providers.keys()) | ||
| 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('```'): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The code removes ' |
||
| 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"] | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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" | ||
There was a problem hiding this comment.
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.envand 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.