git-llm-tool 0.1.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,203 @@
1
+ """Base LLM provider interface."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Dict, Any, Optional
5
+
6
+ from git_llm_tool.core.config import AppConfig
7
+
8
+
9
+ class PromptTemplates:
10
+ """Centralized prompt templates for better code readability."""
11
+
12
+ # Base prompt with conventional commit types
13
+ BASE_COMMIT_PROMPT = """Based on the following git diff, generate a concise commit message in {language}.
14
+
15
+ **Conventional Commit types**:
16
+ - feat: new feature
17
+ - fix: bug fix
18
+ - docs: documentation changes
19
+ - style: formatting, missing semicolons, etc
20
+ - refactor: code restructuring without changing functionality
21
+ - test: adding or modifying tests
22
+ - chore: maintenance tasks
23
+
24
+ Git diff:
25
+ ```
26
+ {diff}
27
+ ```"""
28
+
29
+ # Format for commits without Jira tickets
30
+ NO_JIRA_FORMAT = """
31
+ Generate the commit message in this **exact format**:
32
+ <summary>
33
+ - feat: description of new features
34
+ - fix: description of bug fixes
35
+ - docs: description of documentation changes
36
+ (include only the types that apply to your changes)
37
+
38
+ Example format:
39
+ Implement user authentication system
40
+ - feat: add user authentication endpoints
41
+ - fix: resolve login validation issue"""
42
+
43
+ # Format for commits with Jira tickets
44
+ JIRA_FORMAT = """
45
+ **Jira ticket found**: {jira_ticket}
46
+
47
+ Generate the commit message in this **exact format**:
48
+ {jira_ticket} <summary> #time <time_spent>
49
+ - feat: detailed description of new features
50
+ - fix: detailed description of bug fixes
51
+ - docs: detailed description of documentation changes
52
+ (include only the types that apply to your changes)
53
+
54
+ Where:
55
+ - First line: {jira_ticket} <brief_summary> #time <time_spent>
56
+ - Following lines: List each change type with "- type: description" format
57
+ - Only include the conventional commit types that actually apply to the changes"""
58
+
59
+ # Time tracking instructions
60
+ EXACT_TIME_INSTRUCTION = """
61
+ **Use exact time**: #time {work_hours}
62
+
63
+ Example format:
64
+ {jira_ticket} Implement user authentication system #time {work_hours}
65
+ - feat: add login and registration endpoints
66
+ - feat: implement JWT token validation
67
+ - docs: update API documentation"""
68
+
69
+ ESTIMATE_TIME_INSTRUCTION = """
70
+
71
+ Example format:
72
+ {jira_ticket} Implement user authentication system
73
+ - feat: add login and registration endpoints
74
+ - feat: implement JWT token validation
75
+ - docs: update API documentation"""
76
+
77
+ # Final instruction
78
+ FINAL_INSTRUCTION = "\n\nGenerate ONLY the commit message in the specified format, no additional text or explanation."
79
+
80
+
81
+ class LlmProvider(ABC):
82
+ """Abstract base class for LLM providers."""
83
+
84
+ def __init__(self, config: AppConfig):
85
+ """Initialize the provider with configuration."""
86
+ self.config = config
87
+
88
+ @abstractmethod
89
+ def generate_commit_message(
90
+ self,
91
+ diff: str,
92
+ jira_ticket: Optional[str] = None,
93
+ work_hours: Optional[str] = None,
94
+ **kwargs,
95
+ ) -> str:
96
+ """Generate commit message from git diff and optional Jira information.
97
+
98
+ Args:
99
+ diff: Git diff output
100
+ jira_ticket: Jira ticket number (optional)
101
+ work_hours: Work hours spent (optional)
102
+ **kwargs: Additional provider-specific arguments
103
+
104
+ Returns:
105
+ Generated commit message
106
+
107
+ Raises:
108
+ ApiError: If API call fails
109
+ """
110
+ pass
111
+
112
+ @abstractmethod
113
+ def generate_changelog(self, commit_messages: list[str], **kwargs) -> str:
114
+ """Generate changelog from commit messages.
115
+
116
+ Args:
117
+ commit_messages: List of commit messages
118
+ **kwargs: Additional provider-specific arguments
119
+
120
+ Returns:
121
+ Generated changelog in markdown format
122
+
123
+ Raises:
124
+ ApiError: If API call fails
125
+ """
126
+ pass
127
+
128
+ def _build_commit_prompt(
129
+ self,
130
+ diff: str,
131
+ jira_ticket: Optional[str] = None,
132
+ work_hours: Optional[str] = None,
133
+ ) -> str:
134
+ """Build prompt for commit message generation."""
135
+
136
+ # Prepare all template variables
137
+ template_vars = {
138
+ "language": self.config.llm.language,
139
+ "diff": diff,
140
+ "jira_ticket": jira_ticket or "",
141
+ "work_hours": work_hours or "",
142
+ }
143
+
144
+ # Build prompt components
145
+ prompt_parts = [PromptTemplates.BASE_COMMIT_PROMPT]
146
+
147
+ # Add format-specific instructions
148
+ if jira_ticket:
149
+ prompt_parts.append(PromptTemplates.JIRA_FORMAT)
150
+
151
+ # Add time tracking instructions
152
+ if work_hours:
153
+ prompt_parts.append(PromptTemplates.EXACT_TIME_INSTRUCTION)
154
+ else:
155
+ prompt_parts.append(PromptTemplates.ESTIMATE_TIME_INSTRUCTION)
156
+ else:
157
+ prompt_parts.append(PromptTemplates.NO_JIRA_FORMAT)
158
+
159
+ # Add final instruction
160
+ prompt_parts.append(PromptTemplates.FINAL_INSTRUCTION)
161
+
162
+ # Combine and format all parts at once
163
+ full_template = "".join(prompt_parts)
164
+ return full_template.format(**template_vars)
165
+
166
+ def _build_changelog_prompt(self, commit_messages: list[str]) -> str:
167
+ """Build prompt for changelog generation."""
168
+ commits_text = "\n".join([f"- {msg}" for msg in commit_messages])
169
+
170
+ return f"""Generate a structured changelog in {self.config.llm.language} from the following commit messages.
171
+
172
+ Organize by categories:
173
+ ✨ **Features**
174
+ 🐛 **Bug Fixes**
175
+ 📚 **Documentation**
176
+ 🎨 **Style**
177
+ ♻️ **Refactoring**
178
+ 🧪 **Tests**
179
+ 🔧 **Chores**
180
+ 💥 **Breaking Changes**
181
+
182
+ Only include categories that have items. Use markdown format.
183
+
184
+ Commit messages:
185
+ {commits_text}
186
+
187
+ Generate the changelog:"""
188
+
189
+ @abstractmethod
190
+ def _make_api_call(self, prompt: str, **kwargs) -> str:
191
+ """Make API call to the LLM provider.
192
+
193
+ Args:
194
+ prompt: The prompt to send
195
+ **kwargs: Provider-specific arguments
196
+
197
+ Returns:
198
+ Generated text response
199
+
200
+ Raises:
201
+ ApiError: If API call fails
202
+ """
203
+ pass
@@ -0,0 +1,85 @@
1
+ """LLM Provider factory for automatic provider selection."""
2
+
3
+ from git_llm_tool.core.config import AppConfig
4
+ from git_llm_tool.core.exceptions import ApiError
5
+ from git_llm_tool.providers.base import LlmProvider
6
+
7
+ # LangChain providers (now the only supported providers)
8
+ from git_llm_tool.providers.openai_langchain import OpenAiLangChainProvider
9
+ from git_llm_tool.providers.anthropic_langchain import AnthropicLangChainProvider
10
+ from git_llm_tool.providers.azure_openai_langchain import AzureOpenAiLangChainProvider
11
+ from git_llm_tool.providers.ollama_langchain import OllamaLangChainProvider
12
+ from git_llm_tool.providers.gemini_langchain import GeminiLangChainProvider
13
+
14
+
15
+ def get_provider(config: AppConfig) -> LlmProvider:
16
+ """Get appropriate LLM provider based on model name in config.
17
+
18
+ Args:
19
+ config: Application configuration
20
+
21
+ Returns:
22
+ Initialized LLM provider (LangChain-based only)
23
+
24
+ Raises:
25
+ ApiError: If no suitable provider is found or API key is missing
26
+ """
27
+ model = config.llm.default_model.lower()
28
+
29
+ # Check if Azure OpenAI is configured (highest priority for OpenAI-compatible models)
30
+ if config.llm.azure_openai and config.llm.azure_openai.get("endpoint"):
31
+ if model.startswith(("gpt-", "o1-")) or "azure" in model:
32
+ if "azure_openai" not in config.llm.api_keys:
33
+ raise ApiError("Azure OpenAI API key required for Azure OpenAI models")
34
+ return AzureOpenAiLangChainProvider(config)
35
+
36
+ # OpenAI models (regular OpenAI API)
37
+ if model.startswith(("gpt-", "o1-")):
38
+ if "openai" not in config.llm.api_keys:
39
+ raise ApiError("OpenAI API key required for GPT models")
40
+ return OpenAiLangChainProvider(config)
41
+
42
+ # Anthropic models
43
+ elif model.startswith("claude-"):
44
+ if "anthropic" not in config.llm.api_keys:
45
+ raise ApiError("Anthropic API key required for Claude models")
46
+ return AnthropicLangChainProvider(config)
47
+
48
+ # Google Gemini models
49
+ elif model.startswith("gemini-"):
50
+ if "google" not in config.llm.api_keys:
51
+ raise ApiError("Google API key required for Gemini models")
52
+ return GeminiLangChainProvider(config)
53
+
54
+ # Ollama models (local)
55
+ elif model.startswith(("llama", "codellama", "mistral", "qwen", "phi")):
56
+ return OllamaLangChainProvider(config)
57
+
58
+ # Fallback logic - try providers in order of preference
59
+ else:
60
+ # Try Azure OpenAI first if configured
61
+ if config.llm.azure_openai and config.llm.azure_openai.get("endpoint") and "azure_openai" in config.llm.api_keys:
62
+ return AzureOpenAiLangChainProvider(config)
63
+
64
+ # Try OpenAI second (most common)
65
+ elif "openai" in config.llm.api_keys:
66
+ return OpenAiLangChainProvider(config)
67
+
68
+ # Try Anthropic third
69
+ elif "anthropic" in config.llm.api_keys:
70
+ return AnthropicLangChainProvider(config)
71
+
72
+ # Try Google fourth
73
+ elif "google" in config.llm.api_keys:
74
+ return GeminiLangChainProvider(config)
75
+
76
+ # No API keys available
77
+ else:
78
+ raise ApiError(
79
+ "No API keys configured. Please set at least one API key:\n"
80
+ " git-llm config set llm.api_keys.openai sk-your-key\n"
81
+ " git-llm config set llm.api_keys.azure_openai your-azure-key\n"
82
+ " git-llm config set llm.api_keys.anthropic sk-ant-your-key\n"
83
+ " git-llm config set llm.api_keys.google your-google-key\n"
84
+ "Or use Ollama for local processing (no API key needed)"
85
+ )
@@ -0,0 +1,57 @@
1
+ """Gemini LangChain provider implementation."""
2
+
3
+ from typing import Optional
4
+ from langchain_google_genai import ChatGoogleGenerativeAI
5
+
6
+ from git_llm_tool.core.config import AppConfig
7
+ from git_llm_tool.core.exceptions import ApiError
8
+ from git_llm_tool.providers.langchain_base import LangChainProvider
9
+
10
+
11
+ class GeminiLangChainProvider(LangChainProvider):
12
+ """Google Gemini provider using LangChain."""
13
+
14
+ def __init__(self, config: AppConfig):
15
+ """Initialize Gemini provider."""
16
+ try:
17
+ # Get API key
18
+ api_key = config.llm.api_keys.get("google")
19
+ if not api_key:
20
+ raise ApiError("Google API key not found in configuration")
21
+
22
+ # Determine model
23
+ model = config.llm.default_model
24
+ if not model.startswith("gemini-"):
25
+ # Fallback to Gemini 1.5 Pro if model doesn't look like Gemini model
26
+ model = "gemini-1.5-pro"
27
+
28
+ # Initialize the Gemini LLM
29
+ llm = ChatGoogleGenerativeAI(
30
+ model=model,
31
+ google_api_key=api_key,
32
+ temperature=0.1, # Keep it deterministic for commit messages
33
+ convert_system_message_to_human=True # Required for Gemini
34
+ )
35
+
36
+ super().__init__(config, llm)
37
+
38
+ except Exception as e:
39
+ raise ApiError(f"Failed to initialize Gemini provider: {e}")
40
+
41
+ def validate_config(self) -> bool:
42
+ """Validate Gemini configuration."""
43
+ try:
44
+ # Test with a simple request
45
+ response = self.llm.invoke("test")
46
+ return True
47
+ except Exception as e:
48
+ raise ApiError(f"Gemini validation failed: {e}")
49
+
50
+ def get_model_info(self) -> dict:
51
+ """Get information about the current model."""
52
+ return {
53
+ "provider": "google",
54
+ "model": self.config.llm.default_model,
55
+ "service": "Google Generative AI",
56
+ "local": False
57
+ }