From dd342ec86a6287ba32063616a78f0660b211a6ea Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Wed, 4 Jun 2025 12:10:41 +0545 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9A=20docs(changelog):=20updated=20cha?= =?UTF-8?q?ngelog=20format=20and=20added=20ChangelogGenerator=20class=20do?= =?UTF-8?q?cumentation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 53 +++-- README.md | 68 ++++-- devtools/shared/ai.py | 496 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 535 insertions(+), 82 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 401f25a..1c74642 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,30 +1,53 @@ ## 1.0.0 -📝 Documentation +Version 1.0.0 Changelog +------------------------- -- 📚 docs(changelog): updated changelog format for clarity and consistency -- 📚 docs(changelog): added documentation for ChangelogGenerator class +### 🔧 Maintenance -🚀 Performance +* 📝 docs(changelog): updated changelog format for clarity and consistency -- No performance changes in this version +### 📝 Documentation -🐛 Fixed +* 📚 docs(changelog): update changelog format and add ChangelogGenerator class documentation -- No fixes in this version +### 🔄 Changed -🔧 Maintenance +* ♻️ refactor(git.py): remove unused variable in _run_git_command calls +* ♻️ refactor(devtools/shared/git.py): update GitService to return exit code from _run_git_command and improve get_commit_history method signature and docstring. -- Updated LICENSE file for legal compliance +### 📝 Documentation -🗑️ Removed +* Update LICENSE -- No features or components removed in this version +### 🗑️ Removed -🔒 Security +* Initial commit -- No security improvements in this version +## 1.0.0 -✨ Added +### Version 1.0.0 + +#### 🔄 Changed + +- refactor(git.py): Removed unused variable in `_run_git_command` calls +- refactor(devtools/shared/git.py): Updated `GitService` to return exit code from `_run_git_command` and improved `get_commit_history` method signature and docstring + +#### 📝 Documentation + +- docs(changelog): Updated changelog format for clarity and consistency +- docs(changelog): Update changelog format and add ChangelogGenerator class documentation + +#### 🔧 Maintenance + +- Update LICENSE +- Initial commit + +#### 🚀 Performance & 🗑️ Removed + +- None + +#### 🔒 Security + +- None -- Initial commit of project files and structure diff --git a/README.md b/README.md index bd804fb..e247c01 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A collection of developer tools to enhance your workflow. This package includes ```bash # Clone the repository $ git clone https://github.com/S4NKALP/DevTools.git -$ cd DevTools +$ cd devtools # Install in editable mode $ pipx install -e . @@ -20,28 +20,42 @@ $ pipx install -e . The tools use various APIs that require API keys. You can set these using the config command: ```bash -# Set OpenRouter API key (for AI features) +# Set AI provider API key (required for AI features) +# For OpenRouter devtools config set OPENROUTER_API_KEY "your-openrouter-api-key" -# Set GitHub API key (optional, for enhanced features) -devtools config set github_api_key "your-github-api-key" +# For OpenAI +devtools config set OPENAI_API_KEY "your-openai-api-key" + +# For Google Gemini +devtools config set GOOGLE_API_KEY "your-google-api-key" + +# For Anthropic Claude +devtools config set ANTHROPIC_API_KEY "your-anthropic-api-key" + +# For Hugging Face +devtools config set HUGGINGFACE_API_KEY "your-huggingface-api-key" ``` Or set them in your environment: ```bash -# OpenRouter API key +# AI Provider API keys export OPENROUTER_API_KEY="your-openrouter-api-key" - -# GitHub API key -export GITHUB_API_KEY="your-github-api-key" +export OPENAI_API_KEY="your-openai-api-key" +export GOOGLE_API_KEY="your-google-api-key" +export ANTHROPIC_API_KEY="your-anthropic-api-key" +export HUGGINGFACE_API_KEY="your-huggingface-api-key" ``` Or create a `.env` file in your project root: ```bash OPENROUTER_API_KEY=your-openrouter-api-key -GITHUB_API_KEY=your-github-api-key +OPENAI_API_KEY=your-openai-api-key +GOOGLE_API_KEY=your-google-api-key +ANTHROPIC_API_KEY=your-anthropic-api-key +HUGGINGFACE_API_KEY=your-huggingface-api-key ``` ### Managing Configuration @@ -69,8 +83,14 @@ Example config.yaml: ```yaml # API Keys OPENROUTER_API_KEY: +OPENAI_API_KEY: +GOOGLE_API_KEY: +ANTHROPIC_API_KEY: +HUGGINGFACE_API_KEY: # AI Settings +provider: openrouter # or "openai", "gemini", "claude", "huggingface" +model: mistralai/mixtral-8x7b-instruct # model name for the selected provider max_tokens: 1024 temperature: 0.7 output_format: text @@ -87,11 +107,20 @@ repositories: [] You can set up all configuration options for DevTools using the CLI. This is the recommended way to configure your `~/.devtools/config.yaml` file: ```bash -# Set OpenRouter API key (required for AI features) -devtools config set openrouter_api_key "your-openrouter-api-key" +# Set AI provider (required for AI features) +devtools config set provider "openrouter" # or "openai", "gemini", "claude", "huggingface" -# Set preferred AI model (optional, default is Mixtral) +# Set preferred AI model (optional, depends on provider) +# For OpenRouter devtools config set model "mistralai/mixtral-8x7b-instruct" +# For OpenAI +devtools config set model "gpt-4-turbo-preview" +# For Gemini +devtools config set model "gemini-pro" +# For Claude +devtools config set model "claude-3-opus-20240229" +# For Hugging Face +devtools config set model "mistralai/Mixtral-8x7B-Instruct-v0.1" # Set temperature for AI completions (optional, default is 0.7) devtools config set temperature 0.7 @@ -248,7 +277,12 @@ The generator uses the gitignore.io API to fetch templates. No API key is requir ## Features - **Commit Message Generator:** - - AI-powered commit message suggestions using Mixtral 8x7B + - AI-powered commit message suggestions using multiple providers: + - OpenRouter (Mixtral 8x7B) + - OpenAI (GPT-4, GPT-3.5) + - Google Gemini + - Anthropic Claude + - Hugging Face - Conventional commit format with emojis - GPG signing and push support - Changelog generation from commit history @@ -263,8 +297,12 @@ The generator uses the gitignore.io API to fetch templates. No API key is requir - Python 3.8+ - Git -- OpenRouter API key (for AI features) -- GitHub API key (optional) +- AI Provider API key (one of): + - OpenRouter API key + - OpenAI API key + - Google API key + - Anthropic API key + - Hugging Face API key ## Development diff --git a/devtools/shared/ai.py b/devtools/shared/ai.py index 8ec354c..d4c8765 100644 --- a/devtools/shared/ai.py +++ b/devtools/shared/ai.py @@ -1,74 +1,100 @@ """ Shared AI service for devtools. """ + import requests import os from typing import List, Dict, Optional, Any +from abc import ABC, abstractmethod from .config import BaseConfig -class AIService: - """Base AI service that can be extended by specific tools.""" - - def __init__(self, config: BaseConfig): - """Initialize AI service with configuration.""" - self.config = config - self._setup_ai() - def _setup_ai(self) -> None: - """Set up the AI model with configuration.""" - self.api_key = self.config.get_env_or_config("OPENROUTER_API_KEY") +class AIProvider(ABC): + """Base class for AI providers.""" + + @abstractmethod + def setup(self, config: BaseConfig) -> None: + """Set up the provider with configuration.""" + pass + + @abstractmethod + def generate_completion( + self, + system_prompt: str, + user_prompt: str, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + top_p: float = 0.95, + ) -> str: + """Generate a completion from the AI model.""" + pass + + +class OpenRouterProvider(AIProvider): + """OpenRouter AI provider implementation.""" + + def setup(self, config: BaseConfig) -> None: + self.config = config + self.api_key = config.get_env_or_config("OPENROUTER_API_KEY") if not self.api_key: - raise ValueError("OpenRouter API key not found. Please set it in config or environment.") - - self.model = self.config.get("model", "mistralai/mixtral-8x7b-instruct") + raise ValueError( + "OpenRouter API key not found. Please set it in config or environment." + ) + + self.model = config.get("model", "mistralai/mixtral-8x7b-instruct") self.api_url = "https://openrouter.ai/api/v1/chat/completions" self.headers = { "Authorization": f"Bearer {self.api_key}", "HTTP-Referer": "https://github.com/S4NKALP/DevTools", "X-Title": "DevTools", - "Content-Type": "application/json" + "Content-Type": "application/json", } - def _create_prompt(self, system_prompt: str, user_prompt: str) -> List[Dict[str, str]]: - """Create a prompt for the AI model.""" + def _create_prompt( + self, system_prompt: str, user_prompt: str + ) -> List[Dict[str, str]]: return [ {"role": "system", "content": system_prompt}, - {"role": "user", "content": user_prompt} + {"role": "user", "content": user_prompt}, ] - def generate_completion(self, - system_prompt: str, - user_prompt: str, - temperature: Optional[float] = None, - max_tokens: Optional[int] = None, - top_p: float = 0.95) -> str: - """Generate a completion from the AI model.""" + def generate_completion( + self, + system_prompt: str, + user_prompt: str, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + top_p: float = 0.95, + ) -> str: messages = self._create_prompt(system_prompt, user_prompt) - + try: - # Convert config values to proper types - temp = temperature if temperature is not None else float(self.config.get("temperature", 0.7)) - tokens = max_tokens if max_tokens is not None else int(self.config.get("max_tokens", 150)) - + temp = ( + temperature + if temperature is not None + else float(self.config.get("temperature", 0.7)) + ) + tokens = ( + max_tokens + if max_tokens is not None + else int(self.config.get("max_tokens", 150)) + ) + payload = { "model": self.model, "messages": messages, "temperature": temp, "max_tokens": tokens, "top_p": float(top_p), - "stream": False + "stream": False, } - - # Remove None values + payload = {k: v for k, v in payload.items() if v is not None} - + response = requests.post( - self.api_url, - headers=self.headers, - json=payload, - timeout=30 + self.api_url, headers=self.headers, json=payload, timeout=30 ) - + if response.status_code != 200: error_msg = response.text try: @@ -78,30 +104,396 @@ class AIService: except: pass raise Exception(f"OpenRouter API error: {error_msg}") - + result = response.json() - + if "choices" in result and len(result["choices"]) > 0: return result["choices"][0]["message"]["content"].strip() - + return "" - + except Exception as e: raise Exception(f"AI generation failed: {str(e)}") - def generate_batch_completions(self, - system_prompt: str, - prompts: List[str], - temperature: Optional[float] = None, - max_tokens: Optional[int] = None) -> List[str]: + +class OpenAIProvider(AIProvider): + """OpenAI provider implementation.""" + + def setup(self, config: BaseConfig) -> None: + self.config = config + self.api_key = config.get_env_or_config("OPENAI_API_KEY") + if not self.api_key: + raise ValueError( + "OpenAI API key not found. Please set it in config or environment." + ) + + self.model = config.get("model", "gpt-4-turbo-preview") + self.api_url = "https://api.openai.com/v1/chat/completions" + self.headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + + def _create_prompt( + self, system_prompt: str, user_prompt: str + ) -> List[Dict[str, str]]: + return [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + + def generate_completion( + self, + system_prompt: str, + user_prompt: str, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + top_p: float = 0.95, + ) -> str: + messages = self._create_prompt(system_prompt, user_prompt) + + try: + temp = ( + temperature + if temperature is not None + else float(self.config.get("temperature", 0.7)) + ) + tokens = ( + max_tokens + if max_tokens is not None + else int(self.config.get("max_tokens", 150)) + ) + + payload = { + "model": self.model, + "messages": messages, + "temperature": temp, + "max_tokens": tokens, + "top_p": float(top_p), + } + + payload = {k: v for k, v in payload.items() if v is not None} + + response = requests.post( + self.api_url, headers=self.headers, json=payload, timeout=30 + ) + + if response.status_code != 200: + error_msg = response.text + try: + error_json = response.json() + if "error" in error_json: + error_msg = error_json["error"] + except: + pass + raise Exception(f"OpenAI API error: {error_msg}") + + result = response.json() + + if "choices" in result and len(result["choices"]) > 0: + return result["choices"][0]["message"]["content"].strip() + + return "" + + except Exception as e: + raise Exception(f"AI generation failed: {str(e)}") + + +class GeminiProvider(AIProvider): + """Google Gemini provider implementation.""" + + def setup(self, config: BaseConfig) -> None: + self.config = config + self.api_key = config.get_env_or_config("GOOGLE_API_KEY") + if not self.api_key: + raise ValueError( + "Google API key not found. Please set it in config or environment." + ) + + self.model = config.get("model", "gemini-pro") + self.api_url = f"https://generativelanguage.googleapis.com/v1beta/models/{ + self.model + }:generateContent" + self.headers = {"Content-Type": "application/json"} + + def generate_completion( + self, + system_prompt: str, + user_prompt: str, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + top_p: float = 0.95, + ) -> str: + try: + temp = ( + temperature + if temperature is not None + else float(self.config.get("temperature", 0.7)) + ) + tokens = ( + max_tokens + if max_tokens is not None + else int(self.config.get("max_tokens", 150)) + ) + + # Combine system and user prompts for Gemini + full_prompt = f"{system_prompt}\n\n{user_prompt}" + + payload = { + "contents": [{"parts": [{"text": full_prompt}]}], + "generationConfig": { + "temperature": temp, + "maxOutputTokens": tokens, + "topP": float(top_p), + }, + } + + response = requests.post( + f"{self.api_url}?key={self.api_key}", + headers=self.headers, + json=payload, + timeout=30, + ) + + if response.status_code != 200: + error_msg = response.text + try: + error_json = response.json() + if "error" in error_json: + error_msg = error_json["error"]["message"] + except: + pass + raise Exception(f"Gemini API error: {error_msg}") + + result = response.json() + + if "candidates" in result and len(result["candidates"]) > 0: + return result["candidates"][0]["content"]["parts"][0]["text"].strip() + + return "" + + except Exception as e: + raise Exception(f"AI generation failed: {str(e)}") + + +class ClaudeProvider(AIProvider): + """Anthropic Claude provider implementation.""" + + def setup(self, config: BaseConfig) -> None: + self.config = config + self.api_key = config.get_env_or_config("ANTHROPIC_API_KEY") + if not self.api_key: + raise ValueError( + "Anthropic API key not found. Please set it in config or environment." + ) + + self.model = config.get("model", "claude-3-opus-20240229") + self.api_url = "https://api.anthropic.com/v1/messages" + self.headers = { + "x-api-key": self.api_key, + "anthropic-version": "2023-06-01", + "content-type": "application/json", + } + + def _create_prompt( + self, system_prompt: str, user_prompt: str + ) -> List[Dict[str, str]]: + return [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": user_prompt}, + ] + + def generate_completion( + self, + system_prompt: str, + user_prompt: str, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + top_p: float = 0.95, + ) -> str: + messages = self._create_prompt(system_prompt, user_prompt) + + try: + temp = ( + temperature + if temperature is not None + else float(self.config.get("temperature", 0.7)) + ) + tokens = ( + max_tokens + if max_tokens is not None + else int(self.config.get("max_tokens", 150)) + ) + + payload = { + "model": self.model, + "messages": messages, + "temperature": temp, + "max_tokens": tokens, + "top_p": float(top_p), + } + + payload = {k: v for k, v in payload.items() if v is not None} + + response = requests.post( + self.api_url, headers=self.headers, json=payload, timeout=30 + ) + + if response.status_code != 200: + error_msg = response.text + try: + error_json = response.json() + if "error" in error_json: + error_msg = error_json["error"]["message"] + except: + pass + raise Exception(f"Claude API error: {error_msg}") + + result = response.json() + + if "content" in result and len(result["content"]) > 0: + return result["content"][0]["text"].strip() + + return "" + + except Exception as e: + raise Exception(f"AI generation failed: {str(e)}") + + +class HuggingFaceProvider(AIProvider): + """Hugging Face provider implementation.""" + + def setup(self, config: BaseConfig) -> None: + self.config = config + self.api_key = config.get_env_or_config("HUGGINGFACE_API_KEY") + if not self.api_key: + raise ValueError( + "Hugging Face API key not found. Please set it in config or environment." + ) + + self.model = config.get("model", "mistralai/Mixtral-8x7B-Instruct-v0.1") + self.api_url = f"https://api-inference.huggingface.co/models/{self.model}" + self.headers = { + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + } + + def _create_prompt(self, system_prompt: str, user_prompt: str) -> str: + # Format prompt according to model's requirements + return f"[INST] {system_prompt}\n\n{user_prompt} [/INST]" + + def generate_completion( + self, + system_prompt: str, + user_prompt: str, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + top_p: float = 0.95, + ) -> str: + try: + temp = ( + temperature + if temperature is not None + else float(self.config.get("temperature", 0.7)) + ) + tokens = ( + max_tokens + if max_tokens is not None + else int(self.config.get("max_tokens", 150)) + ) + + prompt = self._create_prompt(system_prompt, user_prompt) + + payload = { + "inputs": prompt, + "parameters": { + "temperature": temp, + "max_new_tokens": tokens, + "top_p": float(top_p), + "return_full_text": False, + }, + } + + response = requests.post( + self.api_url, headers=self.headers, json=payload, timeout=30 + ) + + if response.status_code != 200: + error_msg = response.text + try: + error_json = response.json() + if "error" in error_json: + error_msg = error_json["error"] + except: + pass + raise Exception(f"Hugging Face API error: {error_msg}") + + result = response.json() + + if isinstance(result, list) and len(result) > 0: + return result[0]["generated_text"].strip() + + return "" + + except Exception as e: + raise Exception(f"AI generation failed: {str(e)}") + + +class AIService: + """Base AI service that can be extended by specific tools.""" + + def __init__(self, config: BaseConfig): + """Initialize AI service with configuration.""" + self.config = config + self.provider = self._get_provider() + self.provider.setup(config) + + def _get_provider(self) -> AIProvider: + """Get the appropriate AI provider based on configuration.""" + provider = self.config.get("provider", "openrouter").lower() + + providers = { + "openrouter": OpenRouterProvider, + "openai": OpenAIProvider, + "gemini": GeminiProvider, + "claude": ClaudeProvider, + "huggingface": HuggingFaceProvider, + } + + if provider not in providers: + raise ValueError( + f"Unsupported AI provider: {provider}. Supported providers: { + ', '.join(providers.keys()) + }" + ) + + return providers[provider]() + + def generate_completion( + self, + system_prompt: str, + user_prompt: str, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + top_p: float = 0.95, + ) -> str: + """Generate a completion from the AI model.""" + return self.provider.generate_completion( + system_prompt, user_prompt, temperature, max_tokens, top_p + ) + + def generate_batch_completions( + self, + system_prompt: str, + prompts: List[str], + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + ) -> List[str]: """Generate completions for multiple prompts.""" completions = [] for prompt in prompts: completion = self.generate_completion( - system_prompt, - prompt, - temperature, - max_tokens + system_prompt, prompt, temperature, max_tokens ) completions.append(completion) - return completions \ No newline at end of file + return completions