comit-cli 1.0.0__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.
comit/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
comit/ai/__init__.py ADDED
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ from comit.ai.base import (
4
+ ComitAIError,
5
+ APIKeyMissingError,
6
+ AIAuthenticationError,
7
+ AIServiceError,
8
+ AIResponseError,
9
+ UnsupportedProviderError,
10
+ AIProvider,
11
+ )
12
+ from comit.ai.groq_provider import GroqProvider
13
+ from comit.ai.gemini_provider import GeminiProvider
14
+ from comit.ai.openai_provider import OpenAIProvider
15
+ from comit.ai.ollama_provider import OllamaProvider
16
+ from comit.ai.factory import (
17
+ SUPPORTED_PROVIDERS,
18
+ get_provider,
19
+ get_default_provider,
20
+ generate_commit_message,
21
+ )
22
+
23
+ __all__ = [
24
+ "ComitAIError",
25
+ "APIKeyMissingError",
26
+ "AIAuthenticationError",
27
+ "AIServiceError",
28
+ "AIResponseError",
29
+ "UnsupportedProviderError",
30
+ "AIProvider",
31
+ "GroqProvider",
32
+ "GeminiProvider",
33
+ "OpenAIProvider",
34
+ "OllamaProvider",
35
+ "SUPPORTED_PROVIDERS",
36
+ "get_provider",
37
+ "get_default_provider",
38
+ "generate_commit_message",
39
+ ]
comit/ai/base.py ADDED
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import List, Optional, Union
5
+
6
+ from comit.commit.context import CommitContext
7
+
8
+
9
+ class ComitAIError(Exception):
10
+ pass
11
+
12
+
13
+ class APIKeyMissingError(ComitAIError):
14
+ pass
15
+
16
+
17
+ class AIAuthenticationError(ComitAIError):
18
+ pass
19
+
20
+
21
+ class AIServiceError(ComitAIError):
22
+ pass
23
+
24
+
25
+ class AIResponseError(ComitAIError):
26
+ pass
27
+
28
+
29
+ class UnsupportedProviderError(ComitAIError):
30
+ pass
31
+
32
+
33
+ class AIProvider(ABC):
34
+ @abstractmethod
35
+ def generate_commit_message(
36
+ self,
37
+ context: Union[CommitContext, str, None] = None,
38
+ recent_commits: Optional[List[str]] = None,
39
+ avoid_messages: Optional[List[str]] = None,
40
+ diff: Optional[str] = None,
41
+ ) -> str:
42
+ pass
comit/ai/factory.py ADDED
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional, Union
4
+
5
+ from comit.ai.base import (
6
+ AIProvider,
7
+ UnsupportedProviderError,
8
+ )
9
+ from comit.ai.groq_provider import GroqProvider
10
+ from comit.ai.gemini_provider import GeminiProvider
11
+ from comit.ai.openai_provider import OpenAIProvider
12
+ from comit.ai.ollama_provider import OllamaProvider
13
+ from comit.commit.context import CommitContext
14
+ from comit.config import get_provider as config_get_provider
15
+
16
+ SUPPORTED_PROVIDERS = ("groq", "gemini", "openai", "ollama")
17
+
18
+
19
+ def get_provider(
20
+ provider_name: Optional[str] = None,
21
+ model: Optional[str] = None,
22
+ api_key: Optional[str] = None,
23
+ ) -> AIProvider:
24
+ name = (provider_name or config_get_provider()).strip().lower()
25
+
26
+ if name == "groq":
27
+ return GroqProvider(api_key=api_key, model=model)
28
+ elif name == "gemini":
29
+ return GeminiProvider(api_key=api_key, model=model)
30
+ elif name == "openai":
31
+ return OpenAIProvider(api_key=api_key, model=model)
32
+ elif name == "ollama":
33
+ return OllamaProvider(model=model)
34
+ else:
35
+ supported_str = ", ".join(SUPPORTED_PROVIDERS)
36
+ raise UnsupportedProviderError(
37
+ f"Unsupported AI provider '{name}'. Supported providers: {supported_str}"
38
+ )
39
+
40
+
41
+ def get_default_provider() -> AIProvider:
42
+ return get_provider()
43
+
44
+
45
+ def generate_commit_message(
46
+ context: Union[CommitContext, str, None] = None,
47
+ recent_commits: Optional[List[str]] = None,
48
+ avoid_messages: Optional[List[str]] = None,
49
+ diff: Optional[str] = None,
50
+ provider: Optional[AIProvider] = None,
51
+ ) -> str:
52
+ if provider is None:
53
+ provider = get_default_provider()
54
+ target = context if context is not None else (diff or "")
55
+ return provider.generate_commit_message(
56
+ context=target,
57
+ recent_commits=recent_commits,
58
+ avoid_messages=avoid_messages,
59
+ )
@@ -0,0 +1,125 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional, Union
4
+
5
+ from comit.ai.base import (
6
+ AIProvider,
7
+ APIKeyMissingError,
8
+ AIAuthenticationError,
9
+ AIServiceError,
10
+ AIResponseError,
11
+ )
12
+ from comit.commit.context import CommitContext
13
+ from comit.config import get_api_key, get_model
14
+ from comit.prompts import SYSTEM_PROMPT, build_commit_prompt, sanitize_commit_message
15
+
16
+
17
+ class GeminiProvider(AIProvider):
18
+ def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
19
+ self.api_key = get_api_key(api_key, provider="gemini")
20
+ if not self.api_key:
21
+ raise APIKeyMissingError(
22
+ "GEMINI_API_KEY is not configured.\n"
23
+ "Run 'git ai settings set-key --provider gemini' or export GEMINI_API_KEY in your shell."
24
+ )
25
+
26
+ self.model = get_model(model, provider="gemini")
27
+ self._client = None
28
+
29
+ def _get_client(self):
30
+ if self._client is None:
31
+ try:
32
+ from google import genai
33
+ self._client = genai.Client(api_key=self.api_key)
34
+ except ImportError as exc:
35
+ raise AIServiceError(
36
+ "The 'google-genai' package is not installed. Please run: pip install google-genai"
37
+ ) from exc
38
+ except Exception as exc:
39
+ raise AIServiceError(f"Failed to initialize Gemini client: {exc}") from exc
40
+ return self._client
41
+
42
+ def generate_commit_message(
43
+ self,
44
+ context: Union[CommitContext, str, None] = None,
45
+ recent_commits: Optional[List[str]] = None,
46
+ avoid_messages: Optional[List[str]] = None,
47
+ diff: Optional[str] = None,
48
+ ) -> str:
49
+ target = context if context is not None else (diff or "")
50
+ client = self._get_client()
51
+ user_prompt = build_commit_prompt(target, recent_commits=recent_commits, avoid_messages=avoid_messages)
52
+ temperature = 0.7 if avoid_messages else 0.2
53
+
54
+ try:
55
+ from google.genai import types
56
+ config = types.GenerateContentConfig(
57
+ system_instruction=SYSTEM_PROMPT,
58
+ temperature=temperature,
59
+ max_output_tokens=250,
60
+ )
61
+ except ImportError:
62
+ config = {
63
+ "system_instruction": SYSTEM_PROMPT,
64
+ "temperature": temperature,
65
+ "max_output_tokens": 250,
66
+ }
67
+
68
+ try:
69
+ response = client.models.generate_content(
70
+ model=self.model,
71
+ contents=user_prompt,
72
+ config=config,
73
+ )
74
+ except Exception as exc:
75
+ err_msg = str(exc).lower()
76
+ if "api_key_invalid" in err_msg or "unauthenticated" in err_msg or "401" in err_msg or "403" in err_msg or "permission denied" in err_msg:
77
+ raise AIAuthenticationError(
78
+ "Gemini authentication failed. Please verify that your GEMINI_API_KEY is valid."
79
+ ) from exc
80
+ if "resource_exhausted" in err_msg or "429" in err_msg or "quota" in err_msg:
81
+ raise AIServiceError(
82
+ "Gemini rate limit reached. Please wait a moment and try again."
83
+ ) from exc
84
+ raise AIServiceError(f"Gemini API returned an error: {exc}") from exc
85
+
86
+ if not response or not hasattr(response, "text") or not response.text:
87
+ raise AIResponseError("Gemini API returned an empty response. Please try again.")
88
+
89
+ cleaned = sanitize_commit_message(response.text)
90
+
91
+ if avoid_messages and cleaned in avoid_messages:
92
+ try:
93
+ stronger_prompt = (
94
+ f"{user_prompt}\n\n"
95
+ "CRITICAL: The previous message was already generated. You MUST produce a distinct alternative phrasing."
96
+ )
97
+ try:
98
+ from google.genai import types
99
+ retry_config = types.GenerateContentConfig(
100
+ system_instruction=SYSTEM_PROMPT,
101
+ temperature=0.9,
102
+ max_output_tokens=250,
103
+ )
104
+ except ImportError:
105
+ retry_config = {
106
+ "system_instruction": SYSTEM_PROMPT,
107
+ "temperature": 0.9,
108
+ "max_output_tokens": 250,
109
+ }
110
+ retry_response = client.models.generate_content(
111
+ model=self.model,
112
+ contents=stronger_prompt,
113
+ config=retry_config,
114
+ )
115
+ if retry_response and hasattr(retry_response, "text") and retry_response.text:
116
+ retry_cleaned = sanitize_commit_message(retry_response.text)
117
+ if retry_cleaned:
118
+ return retry_cleaned
119
+ except Exception:
120
+ pass
121
+
122
+ if not cleaned:
123
+ raise AIResponseError("Failed to extract a valid commit message from the AI response.")
124
+
125
+ return cleaned
@@ -0,0 +1,121 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional, Union
4
+
5
+ from comit.ai.base import (
6
+ AIProvider,
7
+ APIKeyMissingError,
8
+ AIAuthenticationError,
9
+ AIServiceError,
10
+ AIResponseError,
11
+ )
12
+ from comit.commit.context import CommitContext
13
+ from comit.config import get_api_key, get_model
14
+ from comit.prompts import SYSTEM_PROMPT, build_commit_prompt, sanitize_commit_message
15
+
16
+
17
+ class GroqProvider(AIProvider):
18
+ def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
19
+ self.api_key = get_api_key(api_key, provider="groq")
20
+ if not self.api_key:
21
+ raise APIKeyMissingError(
22
+ "GROQ_API_KEY is not configured.\n"
23
+ "Run 'git ai settings set-key' or export GROQ_API_KEY in your shell."
24
+ )
25
+
26
+ self.model = get_model(model, provider="groq")
27
+ self._client = None
28
+
29
+ def _get_client(self):
30
+ if self._client is None:
31
+ try:
32
+ from groq import Groq
33
+ self._client = Groq(api_key=self.api_key)
34
+ except ImportError as exc:
35
+ raise AIServiceError(
36
+ "The 'groq' package is not installed. Please run: pip install groq"
37
+ ) from exc
38
+ except Exception as exc:
39
+ raise AIServiceError(f"Failed to initialize Groq client: {exc}") from exc
40
+ return self._client
41
+
42
+ def generate_commit_message(
43
+ self,
44
+ context: Union[CommitContext, str, None] = None,
45
+ recent_commits: Optional[List[str]] = None,
46
+ avoid_messages: Optional[List[str]] = None,
47
+ diff: Optional[str] = None,
48
+ ) -> str:
49
+ try:
50
+ import groq
51
+ except ImportError as exc:
52
+ raise AIServiceError(
53
+ "The 'groq' package is not installed. Please run: pip install groq"
54
+ ) from exc
55
+
56
+ target = context if context is not None else (diff or "")
57
+ client = self._get_client()
58
+ user_prompt = build_commit_prompt(target, recent_commits=recent_commits, avoid_messages=avoid_messages)
59
+ temperature = 0.7 if avoid_messages else 0.2
60
+
61
+ try:
62
+ response = client.chat.completions.create(
63
+ model=self.model,
64
+ messages=[
65
+ {"role": "system", "content": SYSTEM_PROMPT},
66
+ {"role": "user", "content": user_prompt},
67
+ ],
68
+ temperature=temperature,
69
+ max_tokens=250,
70
+ )
71
+ except groq.AuthenticationError as exc:
72
+ raise AIAuthenticationError(
73
+ "Groq authentication failed. Please verify that your GROQ_API_KEY is valid."
74
+ ) from exc
75
+ except groq.RateLimitError as exc:
76
+ raise AIServiceError(
77
+ "Groq rate limit reached. Please wait a moment and try again."
78
+ ) from exc
79
+ except groq.APIConnectionError as exc:
80
+ raise AIServiceError(
81
+ "Could not connect to the Groq API. Please check your internet connection."
82
+ ) from exc
83
+ except groq.APIStatusError as exc:
84
+ raise AIServiceError(
85
+ f"Groq API returned an error (status {exc.status_code}): {exc.message}"
86
+ ) from exc
87
+ except Exception as exc:
88
+ raise AIServiceError(f"Unexpected error while calling Groq API: {exc}") from exc
89
+
90
+ if not response.choices or not response.choices[0].message.content:
91
+ raise AIResponseError("Groq API returned an empty response. Please try again.")
92
+
93
+ raw_message = response.choices[0].message.content
94
+ cleaned = sanitize_commit_message(raw_message)
95
+
96
+ if avoid_messages and cleaned in avoid_messages:
97
+ try:
98
+ stronger_prompt = (
99
+ f"{user_prompt}\n\n"
100
+ "CRITICAL: The previous message was already generated. You MUST produce a distinct alternative phrasing."
101
+ )
102
+ retry_response = client.chat.completions.create(
103
+ model=self.model,
104
+ messages=[
105
+ {"role": "system", "content": SYSTEM_PROMPT},
106
+ {"role": "user", "content": stronger_prompt},
107
+ ],
108
+ temperature=0.9,
109
+ max_tokens=250,
110
+ )
111
+ if retry_response.choices and retry_response.choices[0].message.content:
112
+ retry_cleaned = sanitize_commit_message(retry_response.choices[0].message.content)
113
+ if retry_cleaned:
114
+ return retry_cleaned
115
+ except Exception:
116
+ pass
117
+
118
+ if not cleaned:
119
+ raise AIResponseError("Failed to extract a valid commit message from the AI response.")
120
+
121
+ return cleaned
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional, Union
4
+
5
+ from comit.ai.base import (
6
+ AIProvider,
7
+ AIServiceError,
8
+ AIResponseError,
9
+ )
10
+ from comit.commit.context import CommitContext
11
+ from comit.config import get_model, get_ollama_host
12
+ from comit.prompts import SYSTEM_PROMPT, build_commit_prompt, sanitize_commit_message
13
+
14
+
15
+ class OllamaProvider(AIProvider):
16
+ def __init__(self, model: Optional[str] = None, host: Optional[str] = None):
17
+ self.model = get_model(model, provider="ollama")
18
+ self.host = get_ollama_host(host)
19
+ self._client = None
20
+
21
+ def _get_client(self):
22
+ if self._client is None:
23
+ try:
24
+ import ollama
25
+ self._client = ollama.Client(host=self.host)
26
+ except ImportError as exc:
27
+ raise AIServiceError(
28
+ "The 'ollama' package is not installed. Please run: pip install ollama"
29
+ ) from exc
30
+ except Exception as exc:
31
+ raise AIServiceError(f"Failed to initialize Ollama client: {exc}") from exc
32
+ return self._client
33
+
34
+ def generate_commit_message(
35
+ self,
36
+ context: Union[CommitContext, str, None] = None,
37
+ recent_commits: Optional[List[str]] = None,
38
+ avoid_messages: Optional[List[str]] = None,
39
+ diff: Optional[str] = None,
40
+ ) -> str:
41
+ target = context if context is not None else (diff or "")
42
+ client = self._get_client()
43
+ user_prompt = build_commit_prompt(target, recent_commits=recent_commits, avoid_messages=avoid_messages)
44
+ temperature = 0.7 if avoid_messages else 0.2
45
+
46
+ try:
47
+ response = client.chat(
48
+ model=self.model,
49
+ messages=[
50
+ {"role": "system", "content": SYSTEM_PROMPT},
51
+ {"role": "user", "content": user_prompt},
52
+ ],
53
+ options={"temperature": temperature, "num_predict": 250},
54
+ )
55
+ except Exception as exc:
56
+ err_msg = str(exc)
57
+ if "ConnectError" in err_msg or "ConnectionRefused" in err_msg or "connection refused" in err_msg.lower() or "Failed to connect" in err_msg:
58
+ raise AIServiceError(
59
+ f"Could not connect to Ollama server at {self.host}. Please ensure the Ollama service is running."
60
+ ) from exc
61
+ if "not found" in err_msg.lower() and "model" in err_msg.lower():
62
+ raise AIServiceError(
63
+ f"Ollama model '{self.model}' was not found. Please run 'ollama pull {self.model}' first."
64
+ ) from exc
65
+ raise AIServiceError(f"Ollama error: {exc}") from exc
66
+
67
+ content = ""
68
+ if isinstance(response, dict):
69
+ content = response.get("message", {}).get("content", "")
70
+ elif hasattr(response, "message") and hasattr(response.message, "content"):
71
+ content = response.message.content
72
+
73
+ if not content:
74
+ raise AIResponseError("Ollama returned an empty response. Please try again.")
75
+
76
+ cleaned = sanitize_commit_message(content)
77
+
78
+ if avoid_messages and cleaned in avoid_messages:
79
+ try:
80
+ stronger_prompt = (
81
+ f"{user_prompt}\n\n"
82
+ "CRITICAL: The previous message was already generated. You MUST produce a distinct alternative phrasing."
83
+ )
84
+ retry_response = client.chat(
85
+ model=self.model,
86
+ messages=[
87
+ {"role": "system", "content": SYSTEM_PROMPT},
88
+ {"role": "user", "content": stronger_prompt},
89
+ ],
90
+ options={"temperature": 0.9, "num_predict": 250},
91
+ )
92
+ retry_content = ""
93
+ if isinstance(retry_response, dict):
94
+ retry_content = retry_response.get("message", {}).get("content", "")
95
+ elif hasattr(retry_response, "message") and hasattr(retry_response.message, "content"):
96
+ retry_content = retry_response.message.content
97
+ if retry_content:
98
+ retry_cleaned = sanitize_commit_message(retry_content)
99
+ if retry_cleaned:
100
+ return retry_cleaned
101
+ except Exception:
102
+ pass
103
+
104
+ if not cleaned:
105
+ raise AIResponseError("Failed to extract a valid commit message from the AI response.")
106
+
107
+ return cleaned
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import List, Optional, Union
4
+
5
+ from comit.ai.base import (
6
+ AIProvider,
7
+ APIKeyMissingError,
8
+ AIAuthenticationError,
9
+ AIServiceError,
10
+ AIResponseError,
11
+ )
12
+ from comit.commit.context import CommitContext
13
+ from comit.config import get_api_key, get_model
14
+ from comit.prompts import SYSTEM_PROMPT, build_commit_prompt, sanitize_commit_message
15
+
16
+
17
+ class OpenAIProvider(AIProvider):
18
+ def __init__(self, api_key: Optional[str] = None, model: Optional[str] = None):
19
+ self.api_key = get_api_key(api_key, provider="openai")
20
+ if not self.api_key:
21
+ raise APIKeyMissingError(
22
+ "OPENAI_API_KEY is not configured.\n"
23
+ "Run 'git ai settings set-key --provider openai' or export OPENAI_API_KEY in your shell."
24
+ )
25
+
26
+ self.model = get_model(model, provider="openai")
27
+ self._client = None
28
+
29
+ def _get_client(self):
30
+ if self._client is None:
31
+ try:
32
+ from openai import OpenAI
33
+ self._client = OpenAI(api_key=self.api_key)
34
+ except ImportError as exc:
35
+ raise AIServiceError(
36
+ "The 'openai' package is not installed. Please run: pip install openai"
37
+ ) from exc
38
+ except Exception as exc:
39
+ raise AIServiceError(f"Failed to initialize OpenAI client: {exc}") from exc
40
+ return self._client
41
+
42
+ def generate_commit_message(
43
+ self,
44
+ context: Union[CommitContext, str, None] = None,
45
+ recent_commits: Optional[List[str]] = None,
46
+ avoid_messages: Optional[List[str]] = None,
47
+ diff: Optional[str] = None,
48
+ ) -> str:
49
+ target = context if context is not None else (diff or "")
50
+ client = self._get_client()
51
+ user_prompt = build_commit_prompt(target, recent_commits=recent_commits, avoid_messages=avoid_messages)
52
+ temperature = 0.7 if avoid_messages else 0.2
53
+
54
+ try:
55
+ response = client.chat.completions.create(
56
+ model=self.model,
57
+ messages=[
58
+ {"role": "system", "content": SYSTEM_PROMPT},
59
+ {"role": "user", "content": user_prompt},
60
+ ],
61
+ temperature=temperature,
62
+ max_tokens=250,
63
+ )
64
+ except Exception as exc:
65
+ err_msg = str(exc).lower()
66
+ if "authentication" in err_msg or "incorrect api key" in err_msg or "401" in err_msg or "invalid_api_key" in err_msg:
67
+ raise AIAuthenticationError(
68
+ "OpenAI authentication failed. Please verify that your OPENAI_API_KEY is valid."
69
+ ) from exc
70
+ if "rate limit" in err_msg or "429" in err_msg or "quota" in err_msg:
71
+ raise AIServiceError(
72
+ "OpenAI rate limit reached. Please wait a moment and try again."
73
+ ) from exc
74
+ raise AIServiceError(f"OpenAI API returned an error: {exc}") from exc
75
+
76
+ if not response or not hasattr(response, "choices") or not response.choices or not response.choices[0].message.content:
77
+ raise AIResponseError("OpenAI API returned an empty response. Please try again.")
78
+
79
+ raw_message = response.choices[0].message.content
80
+ cleaned = sanitize_commit_message(raw_message)
81
+
82
+ if avoid_messages and cleaned in avoid_messages:
83
+ try:
84
+ stronger_prompt = (
85
+ f"{user_prompt}\n\n"
86
+ "CRITICAL: The previous message was already generated. You MUST produce a distinct alternative phrasing."
87
+ )
88
+ retry_response = client.chat.completions.create(
89
+ model=self.model,
90
+ messages=[
91
+ {"role": "system", "content": SYSTEM_PROMPT},
92
+ {"role": "user", "content": stronger_prompt},
93
+ ],
94
+ temperature=0.9,
95
+ max_tokens=250,
96
+ )
97
+ if retry_response and retry_response.choices and retry_response.choices[0].message.content:
98
+ retry_cleaned = sanitize_commit_message(retry_response.choices[0].message.content)
99
+ if retry_cleaned:
100
+ return retry_cleaned
101
+ except Exception:
102
+ pass
103
+
104
+ if not cleaned:
105
+ raise AIResponseError("Failed to extract a valid commit message from the AI response.")
106
+
107
+ return cleaned