commitai 1.0.5__tar.gz → 2.2.2__tar.gz
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.
- {commitai-1.0.5 → commitai-2.2.2}/.github/workflows/main.yml +1 -1
- {commitai-1.0.5 → commitai-2.2.2}/.pre-commit-config.yaml +10 -11
- {commitai-1.0.5 → commitai-2.2.2}/PKG-INFO +22 -19
- {commitai-1.0.5 → commitai-2.2.2}/README.md +11 -5
- {commitai-1.0.5 → commitai-2.2.2}/commitai/__init__.py +1 -1
- commitai-2.2.2/commitai/agent.py +252 -0
- commitai-2.2.2/commitai/chains.py +135 -0
- {commitai-1.0.5 → commitai-2.2.2}/commitai/cli.py +147 -85
- {commitai-1.0.5 → commitai-2.2.2}/commitai/template.py +21 -0
- {commitai-1.0.5 → commitai-2.2.2}/pyproject.toml +14 -18
- commitai-2.2.2/tests/test_agent.py +27 -0
- commitai-2.2.2/tests/test_agent_chains.py +88 -0
- {commitai-1.0.5 → commitai-2.2.2}/tests/test_cli.py +111 -207
- {commitai-1.0.5 → commitai-2.2.2}/.gitignore +0 -0
- {commitai-1.0.5 → commitai-2.2.2}/LICENSE +0 -0
- {commitai-1.0.5 → commitai-2.2.2}/assets/commitaai.gif +0 -0
- {commitai-1.0.5 → commitai-2.2.2}/bitmap.png +0 -0
- {commitai-1.0.5 → commitai-2.2.2}/commitai/git.py +0 -0
- {commitai-1.0.5 → commitai-2.2.2}/tests/test_git.py +0 -0
- {commitai-1.0.5 → commitai-2.2.2}/tests/test_template.py +0 -0
|
@@ -12,7 +12,7 @@ repos:
|
|
|
12
12
|
|
|
13
13
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
14
14
|
# Ruff version. Must be aligned with the version in pyproject.toml
|
|
15
|
-
rev: v0.
|
|
15
|
+
rev: v0.15.0 # Match local version
|
|
16
16
|
hooks:
|
|
17
17
|
# Run the linter. Applies fixes including import sorting, etc.
|
|
18
18
|
- id: ruff
|
|
@@ -25,16 +25,15 @@ repos:
|
|
|
25
25
|
hooks:
|
|
26
26
|
- id: mypy
|
|
27
27
|
# Ensure mypy runs with the necessary dependencies installed
|
|
28
|
-
additional_dependencies:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
]
|
|
28
|
+
additional_dependencies:
|
|
29
|
+
- "click>=8.0,<9.0"
|
|
30
|
+
- "langchain>=1.0"
|
|
31
|
+
- "langchain-core>=1.0"
|
|
32
|
+
- "langchain-community>=0.4.0"
|
|
33
|
+
- "langchain-google-genai>=1.0"
|
|
34
|
+
- "langgraph>=1.0.0"
|
|
35
|
+
- "pydantic>=2.0,<3.0"
|
|
36
|
+
- "types-setuptools"
|
|
38
37
|
args: [--config-file=pyproject.toml] # Point mypy to the config
|
|
39
38
|
# You might need to adjust entry if your structure changes
|
|
40
39
|
# entry: mypy commitai commitai/tests
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: commitai
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: Commitai helps you generate git commit messages using AI
|
|
5
5
|
Project-URL: Bug Tracker, https://github.com/lguibr/commitai/issues
|
|
6
6
|
Project-URL: Documentation, https://github.com/lguibr/commitai/blob/main/README.md
|
|
@@ -34,28 +34,25 @@ Classifier: Intended Audience :: Developers
|
|
|
34
34
|
Classifier: License :: OSI Approved :: MIT License
|
|
35
35
|
Classifier: Operating System :: OS Independent
|
|
36
36
|
Classifier: Programming Language :: Python :: 3
|
|
37
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
38
37
|
Classifier: Programming Language :: Python :: 3.10
|
|
39
38
|
Classifier: Programming Language :: Python :: 3.11
|
|
40
39
|
Classifier: Programming Language :: Python :: 3.12
|
|
41
40
|
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
42
41
|
Classifier: Topic :: Utilities
|
|
43
|
-
Requires-Python: >=3.
|
|
44
|
-
Requires-Dist: click
|
|
45
|
-
Requires-Dist: langchain-
|
|
46
|
-
Requires-Dist: langchain-
|
|
47
|
-
Requires-Dist: langchain-
|
|
48
|
-
Requires-Dist: langchain
|
|
49
|
-
Requires-Dist:
|
|
50
|
-
Requires-Dist:
|
|
51
|
-
Requires-Dist: langchain<=0.3.25,>=0.1.0
|
|
52
|
-
Requires-Dist: pydantic<3.0,>=2.0
|
|
42
|
+
Requires-Python: >=3.10
|
|
43
|
+
Requires-Dist: click>=8.1
|
|
44
|
+
Requires-Dist: langchain-community>=0.4.0
|
|
45
|
+
Requires-Dist: langchain-core>=1.0
|
|
46
|
+
Requires-Dist: langchain-google-genai>=1.0
|
|
47
|
+
Requires-Dist: langchain>=1.0
|
|
48
|
+
Requires-Dist: langgraph>=1.0.0
|
|
49
|
+
Requires-Dist: pydantic>=2.0
|
|
53
50
|
Provides-Extra: test
|
|
54
|
-
Requires-Dist: langchain-google-genai
|
|
51
|
+
Requires-Dist: langchain-google-genai>=2.0.0; extra == 'test'
|
|
55
52
|
Requires-Dist: mypy>=1.9.0; extra == 'test'
|
|
56
53
|
Requires-Dist: pytest-cov>=3.0; extra == 'test'
|
|
57
54
|
Requires-Dist: pytest>=7.0; extra == 'test'
|
|
58
|
-
Requires-Dist: ruff
|
|
55
|
+
Requires-Dist: ruff>=0.4.4; extra == 'test'
|
|
59
56
|
Requires-Dist: types-setuptools; extra == 'test'
|
|
60
57
|
Description-Content-Type: text/markdown
|
|
61
58
|
|
|
@@ -102,7 +99,7 @@ Simply stage your files and run `commitai`. It analyzes the diff, optionally tak
|
|
|
102
99
|
|
|
103
100
|
## Features
|
|
104
101
|
|
|
105
|
-
* 🧠 **Intelligent Commit Generation**: Analyzes staged code differences (`git diff --staged`) using state-of-the-art AI models (GPT, Claude
|
|
102
|
+
* 🧠 **Intelligent Commit Generation**: Analyzes staged code differences (`git diff --staged`) using state-of-the-art AI models (Gemini, GPT, Claude) to create meaningful commit messages.
|
|
106
103
|
* 📄 **Conventional Commits**: Automatically formats messages according to the Conventional Commits specification (e.g., `feat(auth): add JWT authentication`). This improves readability and enables automated changelog generation.
|
|
107
104
|
* 📝 **Optional Explanations**: Provide a high-level description of your changes as input to guide the AI, or let it infer the context solely from the code diff.
|
|
108
105
|
* ✅ **Pre-commit Hook Integration**: Automatically runs your existing native Git pre-commit hook (`.git/hooks/pre-commit`) before generating the message, ensuring code quality and style checks pass.
|
|
@@ -157,7 +154,7 @@ CommitAi requires API keys for the AI provider you intend to use. Set these as e
|
|
|
157
154
|
export GOOGLE_API_KEY="your_google_api_key_here"
|
|
158
155
|
```
|
|
159
156
|
|
|
160
|
-
You only need to set the key for the provider corresponding to the model you select (or the default, Gemini).
|
|
157
|
+
You only need to set the key for the provider corresponding to the model you select (or the default, Gemini 3 Flash with Google).
|
|
161
158
|
|
|
162
159
|
### Ollama
|
|
163
160
|
|
|
@@ -224,14 +221,20 @@ The `commitai` command (which is an alias for `commitai generate`) accepts the f
|
|
|
224
221
|
* Example: `commitai -c "Fix typo in documentation"` (for minor changes)
|
|
225
222
|
* Can be combined with `-a`: `commitai -a -c "Quick fix and commit all"`
|
|
226
223
|
|
|
224
|
+
* `--review` / `--no-review`:
|
|
225
|
+
* Toggle a preliminary AI review of the staged diff before generating the commit message. Default is `--review` (enabled).
|
|
226
|
+
* When enabled, CommitAi prints a brief review and asks if you want to proceed.
|
|
227
|
+
* Example: `commitai --no-review` to skip the review step.
|
|
228
|
+
|
|
227
229
|
* `-m <model_name>`, `--model <model_name>`:
|
|
228
230
|
* Specifies which AI model to use.
|
|
229
|
-
*
|
|
231
|
+
* Specifies which AI model to use.
|
|
232
|
+
* Defaults to `gemini-3-flash-preview`.
|
|
230
233
|
* Ensure the corresponding API key environment variable is set.
|
|
231
234
|
* Examples:
|
|
235
|
+
* `commitai -m gemini-3-pro-preview "Use Google's Gemini 3 Pro"`
|
|
232
236
|
* `commitai -m gpt-4 "Use OpenAI's GPT-4"`
|
|
233
|
-
* `commitai -m claude-3-opus
|
|
234
|
-
* `commitai -m gemini-2.5-flash-preview-04-17 "Use Google's Gemini 1.5 Flash"`
|
|
237
|
+
* `commitai -m claude-3-opus "Use Anthropic's Claude 3 Opus"`
|
|
235
238
|
|
|
236
239
|
### Creating Repository Templates
|
|
237
240
|
|
|
@@ -41,7 +41,7 @@ Simply stage your files and run `commitai`. It analyzes the diff, optionally tak
|
|
|
41
41
|
|
|
42
42
|
## Features
|
|
43
43
|
|
|
44
|
-
* 🧠 **Intelligent Commit Generation**: Analyzes staged code differences (`git diff --staged`) using state-of-the-art AI models (GPT, Claude
|
|
44
|
+
* 🧠 **Intelligent Commit Generation**: Analyzes staged code differences (`git diff --staged`) using state-of-the-art AI models (Gemini, GPT, Claude) to create meaningful commit messages.
|
|
45
45
|
* 📄 **Conventional Commits**: Automatically formats messages according to the Conventional Commits specification (e.g., `feat(auth): add JWT authentication`). This improves readability and enables automated changelog generation.
|
|
46
46
|
* 📝 **Optional Explanations**: Provide a high-level description of your changes as input to guide the AI, or let it infer the context solely from the code diff.
|
|
47
47
|
* ✅ **Pre-commit Hook Integration**: Automatically runs your existing native Git pre-commit hook (`.git/hooks/pre-commit`) before generating the message, ensuring code quality and style checks pass.
|
|
@@ -96,7 +96,7 @@ CommitAi requires API keys for the AI provider you intend to use. Set these as e
|
|
|
96
96
|
export GOOGLE_API_KEY="your_google_api_key_here"
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
You only need to set the key for the provider corresponding to the model you select (or the default, Gemini).
|
|
99
|
+
You only need to set the key for the provider corresponding to the model you select (or the default, Gemini 3 Flash with Google).
|
|
100
100
|
|
|
101
101
|
### Ollama
|
|
102
102
|
|
|
@@ -163,14 +163,20 @@ The `commitai` command (which is an alias for `commitai generate`) accepts the f
|
|
|
163
163
|
* Example: `commitai -c "Fix typo in documentation"` (for minor changes)
|
|
164
164
|
* Can be combined with `-a`: `commitai -a -c "Quick fix and commit all"`
|
|
165
165
|
|
|
166
|
+
* `--review` / `--no-review`:
|
|
167
|
+
* Toggle a preliminary AI review of the staged diff before generating the commit message. Default is `--review` (enabled).
|
|
168
|
+
* When enabled, CommitAi prints a brief review and asks if you want to proceed.
|
|
169
|
+
* Example: `commitai --no-review` to skip the review step.
|
|
170
|
+
|
|
166
171
|
* `-m <model_name>`, `--model <model_name>`:
|
|
167
172
|
* Specifies which AI model to use.
|
|
168
|
-
*
|
|
173
|
+
* Specifies which AI model to use.
|
|
174
|
+
* Defaults to `gemini-3-flash-preview`.
|
|
169
175
|
* Ensure the corresponding API key environment variable is set.
|
|
170
176
|
* Examples:
|
|
177
|
+
* `commitai -m gemini-3-pro-preview "Use Google's Gemini 3 Pro"`
|
|
171
178
|
* `commitai -m gpt-4 "Use OpenAI's GPT-4"`
|
|
172
|
-
* `commitai -m claude-3-opus
|
|
173
|
-
* `commitai -m gemini-2.5-flash-preview-04-17 "Use Google's Gemini 1.5 Flash"`
|
|
179
|
+
* `commitai -m claude-3-opus "Use Anthropic's Claude 3 Opus"`
|
|
174
180
|
|
|
175
181
|
### Creating Repository Templates
|
|
176
182
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# This __version__ string is read by hatchling during the build process
|
|
5
5
|
# Make sure to update it for new releases.
|
|
6
|
-
__version__ = "
|
|
6
|
+
__version__ = "2.2.2"
|
|
7
7
|
|
|
8
8
|
# The importlib.metadata approach is generally for reading the version
|
|
9
9
|
# of an *already installed* package at runtime. We don't need it here
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import Any, Dict, Type
|
|
5
|
+
|
|
6
|
+
# from langchain.agents import AgentExecutor, create_tool_calling_agent # Removed
|
|
7
|
+
from langchain_core.language_models import BaseChatModel
|
|
8
|
+
from langchain_core.runnables import Runnable
|
|
9
|
+
from langchain_core.tools import BaseTool
|
|
10
|
+
from langgraph.prebuilt import create_react_agent
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
# --- TOOLS ---
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ShellInput(BaseModel):
|
|
17
|
+
command: str = Field(
|
|
18
|
+
description=(
|
|
19
|
+
"The git command to execute (e.g., 'git status', 'git log'). "
|
|
20
|
+
"Must start with 'git'."
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ReadOnlyShellTool(BaseTool):
|
|
26
|
+
name: str = "git_shell"
|
|
27
|
+
description: str = (
|
|
28
|
+
"Run read-only git commands to inspect the repository state. "
|
|
29
|
+
"Only 'git' commands are allowed. Write operations are blocked."
|
|
30
|
+
)
|
|
31
|
+
args_schema: Type[BaseModel] = ShellInput
|
|
32
|
+
|
|
33
|
+
def _run(self, command: str) -> str:
|
|
34
|
+
command = command.strip()
|
|
35
|
+
if not command.startswith("git"):
|
|
36
|
+
return "Error: Only 'git' commands are allowed."
|
|
37
|
+
|
|
38
|
+
# Simple blocklist for write operations
|
|
39
|
+
forbidden = [
|
|
40
|
+
"push",
|
|
41
|
+
"pull",
|
|
42
|
+
"commit",
|
|
43
|
+
"merge",
|
|
44
|
+
"rebase",
|
|
45
|
+
"cherry-pick",
|
|
46
|
+
"stash",
|
|
47
|
+
"clean",
|
|
48
|
+
"reset",
|
|
49
|
+
"checkout",
|
|
50
|
+
"switch",
|
|
51
|
+
"branch",
|
|
52
|
+
]
|
|
53
|
+
if any(w in command.split() for w in forbidden):
|
|
54
|
+
return f"Error: Command '{command}' contains forbidden write operations."
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
# shell=True is dangerous in general, but we heavily restricted input above
|
|
58
|
+
result = subprocess.run(
|
|
59
|
+
command, shell=True, capture_output=True, text=True, cwd=os.getcwd()
|
|
60
|
+
)
|
|
61
|
+
if result.returncode != 0:
|
|
62
|
+
return f"Error ({result.returncode}): {result.stderr}"
|
|
63
|
+
return result.stdout
|
|
64
|
+
except Exception as e:
|
|
65
|
+
return f"Execution Error: {str(e)}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class FileSearchInput(BaseModel):
|
|
69
|
+
pattern: str = Field(
|
|
70
|
+
description="The glob pattern to search for files (e.g., 'src/**/*.py')."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class FileSearchTool(BaseTool):
|
|
75
|
+
name: str = "file_search"
|
|
76
|
+
description: str = (
|
|
77
|
+
"Search for file paths in the project using glob patterns. "
|
|
78
|
+
"Useful to find files to inspect."
|
|
79
|
+
)
|
|
80
|
+
args_schema: Type[BaseModel] = FileSearchInput
|
|
81
|
+
|
|
82
|
+
def _run(self, pattern: str) -> str:
|
|
83
|
+
try:
|
|
84
|
+
# Security: prevent breaking out of repo?
|
|
85
|
+
# For simplicity, just run glob.
|
|
86
|
+
if ".." in pattern:
|
|
87
|
+
return "Error: '..' not allowed in patterns."
|
|
88
|
+
|
|
89
|
+
files = glob.glob(pattern, recursive=True)
|
|
90
|
+
if not files:
|
|
91
|
+
return "No files found."
|
|
92
|
+
return "\n".join(files[:20]) # Limit output
|
|
93
|
+
except Exception as e:
|
|
94
|
+
return f"Error: {str(e)}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class FileReadInput(BaseModel):
|
|
98
|
+
file_path: str = Field(description="The path of the file to read.")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class FileReadTool(BaseTool):
|
|
102
|
+
name: str = "file_read"
|
|
103
|
+
description: str = "Read the contents of a specific file."
|
|
104
|
+
args_schema: Type[BaseModel] = FileReadInput
|
|
105
|
+
|
|
106
|
+
def _run(self, file_path: str) -> str:
|
|
107
|
+
if ".." in file_path:
|
|
108
|
+
return "Error: Traversing up directories is not allowed."
|
|
109
|
+
if not os.path.exists(file_path):
|
|
110
|
+
return "Error: File does not exist."
|
|
111
|
+
try:
|
|
112
|
+
with open(file_path, "r") as f:
|
|
113
|
+
content = f.read()
|
|
114
|
+
return content[:2000] # Truncate large files
|
|
115
|
+
except Exception as e:
|
|
116
|
+
return f"Error reading file: {str(e)}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# --- MIDDLEWARE (Simulated for Agent) ---
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class SummarizationMiddleware:
|
|
123
|
+
"""Uses LLM to summarize diff before agent sees it."""
|
|
124
|
+
|
|
125
|
+
def __init__(self, llm: BaseChatModel):
|
|
126
|
+
self.llm = llm
|
|
127
|
+
|
|
128
|
+
def process(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
129
|
+
diff = inputs.get("diff", "")
|
|
130
|
+
if not diff:
|
|
131
|
+
return inputs
|
|
132
|
+
|
|
133
|
+
# Simple summarization chain (inline invocation)
|
|
134
|
+
# Truncate for summary
|
|
135
|
+
msg = f"Summarize these changes in 2 sentences:\n\n{diff[:5000]}"
|
|
136
|
+
resp = self.llm.invoke(msg)
|
|
137
|
+
inputs["summary"] = resp.content
|
|
138
|
+
return inputs
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TodoMiddleware:
|
|
142
|
+
"""Scans diff for TODOs and adds to inputs."""
|
|
143
|
+
|
|
144
|
+
def process(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
145
|
+
diff = inputs.get("diff", "")
|
|
146
|
+
todos = []
|
|
147
|
+
for line in diff.splitlines():
|
|
148
|
+
if line.startswith("+") and any(
|
|
149
|
+
x in line.lower() for x in ["todo", "fixme"]
|
|
150
|
+
):
|
|
151
|
+
todos.append(line[1:].strip())
|
|
152
|
+
inputs["todos"] = todos
|
|
153
|
+
inputs["todo_str"] = "\n".join(f"- {t}" for t in todos) if todos else "None"
|
|
154
|
+
return inputs
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# --- AGENT ---
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# --- AGENT ---
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def create_commit_agent(llm: BaseChatModel) -> Runnable:
|
|
164
|
+
# 1. Init Tools
|
|
165
|
+
tools = [ReadOnlyShellTool(), FileSearchTool(), FileReadTool()]
|
|
166
|
+
|
|
167
|
+
# 2. Middlewares
|
|
168
|
+
summ_mw = SummarizationMiddleware(llm)
|
|
169
|
+
todo_mw = TodoMiddleware()
|
|
170
|
+
|
|
171
|
+
# 3. Prompt
|
|
172
|
+
system_prompt = """You are an expert software engineer acting as a Commit Assistant.
|
|
173
|
+
Your goal is to generate a conventional commit message.
|
|
174
|
+
|
|
175
|
+
Context:
|
|
176
|
+
- User Explanation: {explanation}
|
|
177
|
+
- Detected TODOs: {todo_str}
|
|
178
|
+
- Auto-Summary: {summary}
|
|
179
|
+
- Staged Diff: {diff}
|
|
180
|
+
|
|
181
|
+
You have access to tools to explore the codebase if the diff + explanation is ambiguous.
|
|
182
|
+
- Use `git_shell` to check status or logs.
|
|
183
|
+
- Use `file_search` and `file_read` to understand context of modified files.
|
|
184
|
+
|
|
185
|
+
Protocol:
|
|
186
|
+
1. Analyze the input.
|
|
187
|
+
2. If detecting POTENTIAL SENSITIVE DATA (API keys, secrets) in the diff, you MUST stop
|
|
188
|
+
and ask the user (simulated by returning a warning message).
|
|
189
|
+
3. If clarification is needed, explore files.
|
|
190
|
+
4. Final Answer MUST be ONLY the commit message.
|
|
191
|
+
"""
|
|
192
|
+
# Note: create_react_agent handles the prompt internally or via state_modifier.
|
|
193
|
+
# We can pass a system string or a function. Since our prompt depends on dynamic
|
|
194
|
+
# variables (diff, explanation, etc.), we need to inject them. LangGraph's
|
|
195
|
+
# prebuilt agent usually takes a static system message. However, we can use the
|
|
196
|
+
# 'messages' state. But to keep it simple and compatible with existing 'invoke'
|
|
197
|
+
# interface: We will format the system prompt in the wrapper and pass it as the
|
|
198
|
+
# first message.
|
|
199
|
+
|
|
200
|
+
# Actually, create_react_agent supports 'state_modifier'.
|
|
201
|
+
# If we pass a formatted string, it works as system prompt.
|
|
202
|
+
|
|
203
|
+
# 4. Construct Graph
|
|
204
|
+
# We don't construct the graph with ALL variables pre-bound if they change per run.
|
|
205
|
+
# Instead, we'll format the prompt in the pipeline and pass it to the agent.
|
|
206
|
+
|
|
207
|
+
agent_graph = create_react_agent(llm, tools)
|
|
208
|
+
|
|
209
|
+
# 5. Pipeline with Middleware
|
|
210
|
+
def run_pipeline(inputs: Dict[str, Any]) -> str:
|
|
211
|
+
# Run Middleware
|
|
212
|
+
state = inputs.copy()
|
|
213
|
+
state = todo_mw.process(state)
|
|
214
|
+
state = summ_mw.process(state)
|
|
215
|
+
|
|
216
|
+
# Inject formatted fields if missing
|
|
217
|
+
state.setdefault("explanation", "None")
|
|
218
|
+
state.setdefault("summary", "None")
|
|
219
|
+
state.setdefault("todo_str", "None")
|
|
220
|
+
|
|
221
|
+
# Format System Prompt
|
|
222
|
+
formatted_system_prompt = system_prompt.format(
|
|
223
|
+
explanation=state["explanation"],
|
|
224
|
+
todo_str=state["todo_str"],
|
|
225
|
+
summary=state["summary"],
|
|
226
|
+
diff=state.get("diff", ""),
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# Run Agent
|
|
230
|
+
# LangGraph inputs: {"messages": [{"role": "user", "content": ...}]}
|
|
231
|
+
# We inject the system prompt as a SystemMessage or just update the state.
|
|
232
|
+
# create_react_agent primarily looks at 'messages'.
|
|
233
|
+
|
|
234
|
+
from langchain_core.messages import HumanMessage, SystemMessage
|
|
235
|
+
|
|
236
|
+
messages = [
|
|
237
|
+
SystemMessage(content=formatted_system_prompt),
|
|
238
|
+
HumanMessage(content="Generate the commit message."),
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
# Invoke graph
|
|
242
|
+
# result is a dict with 'messages'
|
|
243
|
+
result = agent_graph.invoke({"messages": messages})
|
|
244
|
+
|
|
245
|
+
# Extract last message content
|
|
246
|
+
last_message = result["messages"][-1]
|
|
247
|
+
return str(last_message.content)
|
|
248
|
+
|
|
249
|
+
# Wrap in RunnableLambda to expose 'invoke'
|
|
250
|
+
from langchain_core.runnables import RunnableLambda
|
|
251
|
+
|
|
252
|
+
return RunnableLambda(run_pipeline)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional, TypedDict
|
|
2
|
+
|
|
3
|
+
from langchain_core.language_models import BaseChatModel
|
|
4
|
+
from langchain_core.output_parsers import StrOutputParser
|
|
5
|
+
from langchain_core.prompts import ChatPromptTemplate
|
|
6
|
+
from langchain_core.runnables import Runnable, RunnableLambda, RunnablePassthrough
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class CommitState(TypedDict):
|
|
10
|
+
diff: str
|
|
11
|
+
explanation: Optional[str]
|
|
12
|
+
summary: Optional[str]
|
|
13
|
+
todos: Optional[List[str]]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SummarizationMiddleware:
|
|
17
|
+
"""Middleware to summarize the diff before generating the commit message."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, llm: BaseChatModel):
|
|
20
|
+
self.llm = llm
|
|
21
|
+
self.prompt = ChatPromptTemplate.from_template(
|
|
22
|
+
"Summarize the following code changes concisely in 1-2 sentences:\n\n{diff}"
|
|
23
|
+
)
|
|
24
|
+
self.chain = self.prompt | self.llm | StrOutputParser()
|
|
25
|
+
|
|
26
|
+
def __call__(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
27
|
+
"""Run the summarizer and add 'summary' to the state."""
|
|
28
|
+
diff = inputs.get("diff", "")
|
|
29
|
+
if not diff:
|
|
30
|
+
return {**inputs, "summary": ""}
|
|
31
|
+
|
|
32
|
+
# We invoke the chain synchronously here
|
|
33
|
+
summary = self.chain.invoke({"diff": diff})
|
|
34
|
+
return {**inputs, "summary": summary}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TodoMiddleware:
|
|
38
|
+
"""Middleware to scan the diff for TODO/FIXME/HACK comments."""
|
|
39
|
+
|
|
40
|
+
def __call__(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
41
|
+
diff = inputs.get("diff", "")
|
|
42
|
+
todos = []
|
|
43
|
+
for line in diff.splitlines():
|
|
44
|
+
if line.startswith("+"):
|
|
45
|
+
lower_line = line.lower()
|
|
46
|
+
if (
|
|
47
|
+
"todo" in lower_line
|
|
48
|
+
or "fixme" in lower_line
|
|
49
|
+
or "hack" in lower_line
|
|
50
|
+
):
|
|
51
|
+
# Strip the + and whitespace
|
|
52
|
+
clean_line = line[1:].strip()
|
|
53
|
+
todos.append(clean_line)
|
|
54
|
+
|
|
55
|
+
return {**inputs, "todos": todos}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def create_commit_chain(llm: BaseChatModel) -> Runnable:
|
|
59
|
+
"""Creates the full commit generation pipeline with middleware."""
|
|
60
|
+
|
|
61
|
+
# 1. Initialize Middlewares
|
|
62
|
+
summarizer = SummarizationMiddleware(llm)
|
|
63
|
+
todo_scanner = TodoMiddleware()
|
|
64
|
+
|
|
65
|
+
# 2. Define the Prompt
|
|
66
|
+
# We include placeholders for summary and todos if they exist
|
|
67
|
+
system_template = (
|
|
68
|
+
"You are an expert software engineer and git commit message generator.\n"
|
|
69
|
+
"Your task is to generate a clean, concise commit message following the "
|
|
70
|
+
"Conventional Commits specification.\n\n"
|
|
71
|
+
"Values from middleware:\n"
|
|
72
|
+
"{summary_section}\n"
|
|
73
|
+
"{todo_section}\n\n"
|
|
74
|
+
"Input context:\n"
|
|
75
|
+
"{explanation_section}\n\n"
|
|
76
|
+
"Existing Code Changes (Diff):\n"
|
|
77
|
+
"{diff}\n\n"
|
|
78
|
+
"Instructions:\n"
|
|
79
|
+
"1. Use the format: <type>(<scope>): <subject>\n"
|
|
80
|
+
"2. Keep the subject line under 50 characters if possible.\n"
|
|
81
|
+
"3. If there are multiple changes, provide a bulleted body.\n"
|
|
82
|
+
"4. If TODOs were detected, mention them in the footer or body as "
|
|
83
|
+
"appropriate.\n"
|
|
84
|
+
"5. If an explanation is provided, prioritize it.\n"
|
|
85
|
+
)
|
|
86
|
+
prompt = ChatPromptTemplate.from_template(system_template)
|
|
87
|
+
|
|
88
|
+
# 3. Helper to format the prompt inputs from state
|
|
89
|
+
def format_inputs(state: CommitState) -> Dict[str, Any]:
|
|
90
|
+
summary = state.get("summary")
|
|
91
|
+
todos = state.get("todos")
|
|
92
|
+
explanation = state.get("explanation")
|
|
93
|
+
|
|
94
|
+
summary_section = f"Summary of changes:\n{summary}\n" if summary else ""
|
|
95
|
+
|
|
96
|
+
todo_section = ""
|
|
97
|
+
if todos:
|
|
98
|
+
todo_section = (
|
|
99
|
+
"Detected TODOs in this diff:\n"
|
|
100
|
+
+ "\n".join(f"- {t}" for t in todos)
|
|
101
|
+
+ "\n"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
explanation_section = ""
|
|
105
|
+
if explanation:
|
|
106
|
+
explanation_section = f"User Explanation:\n{explanation}\n"
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
"diff": state["diff"],
|
|
110
|
+
"summary_section": summary_section,
|
|
111
|
+
"todo_section": todo_section,
|
|
112
|
+
"explanation_section": explanation_section,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
# 4. Construct the Pipeline
|
|
116
|
+
# Parallel step to run middlewares
|
|
117
|
+
# (conceptually, though here we chain them or use RunnablePassthrough)
|
|
118
|
+
# Since middlewares modify state, we can chain them:
|
|
119
|
+
|
|
120
|
+
middleware_chain: Runnable = (
|
|
121
|
+
RunnablePassthrough()
|
|
122
|
+
| RunnableLambda(todo_scanner)
|
|
123
|
+
| RunnableLambda(summarizer)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Final generation chain
|
|
127
|
+
generation_chain = (
|
|
128
|
+
middleware_chain
|
|
129
|
+
| RunnableLambda(format_inputs)
|
|
130
|
+
| prompt
|
|
131
|
+
| llm
|
|
132
|
+
| StrOutputParser()
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
return generation_chain
|