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.
- git_llm_tool/__init__.py +5 -0
- git_llm_tool/__main__.py +6 -0
- git_llm_tool/cli.py +167 -0
- git_llm_tool/commands/__init__.py +1 -0
- git_llm_tool/commands/changelog_cmd.py +189 -0
- git_llm_tool/commands/commit_cmd.py +134 -0
- git_llm_tool/core/__init__.py +1 -0
- git_llm_tool/core/config.py +352 -0
- git_llm_tool/core/diff_optimizer.py +206 -0
- git_llm_tool/core/exceptions.py +26 -0
- git_llm_tool/core/git_helper.py +250 -0
- git_llm_tool/core/jira_helper.py +238 -0
- git_llm_tool/core/rate_limiter.py +136 -0
- git_llm_tool/core/smart_chunker.py +262 -0
- git_llm_tool/core/token_counter.py +169 -0
- git_llm_tool/providers/__init__.py +21 -0
- git_llm_tool/providers/anthropic_langchain.py +42 -0
- git_llm_tool/providers/azure_openai_langchain.py +59 -0
- git_llm_tool/providers/base.py +203 -0
- git_llm_tool/providers/factory.py +85 -0
- git_llm_tool/providers/gemini_langchain.py +57 -0
- git_llm_tool/providers/langchain_base.py +608 -0
- git_llm_tool/providers/ollama_langchain.py +45 -0
- git_llm_tool/providers/openai_langchain.py +42 -0
- git_llm_tool-0.1.12.dist-info/LICENSE +21 -0
- git_llm_tool-0.1.12.dist-info/METADATA +645 -0
- git_llm_tool-0.1.12.dist-info/RECORD +29 -0
- git_llm_tool-0.1.12.dist-info/WHEEL +4 -0
- git_llm_tool-0.1.12.dist-info/entry_points.txt +3 -0
|
@@ -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
|
+
}
|