gitpr-cli 0.0.25__py3-none-any.whl → 0.0.27__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitpr-cli
3
- Version: 0.0.25
3
+ Version: 0.0.27
4
4
  Summary: Automação de PRs, Commits e Code Review com IA (Gemini e DeepSeek)
5
5
  Author-email: Natan Fiuza <contato@natanfiuza.dev.br>
6
6
  Requires-Python: >=3.10
@@ -16,8 +16,13 @@ Requires-Dist: textual
16
16
  Requires-Dist: requests
17
17
  Dynamic: license-file
18
18
 
19
+
19
20
  # **GitPR CLI 🚀**
20
21
 
22
+ <p align="center">
23
+ <img src="docs/logo.png" alt="GitPR Logo" width="200">
24
+ </p>
25
+
21
26
  GitPR CLI is a command-line automation tool that uses **Google Gemini** and **DeepSeek** artificial intelligence to analyze your code changes (git diff) or entire files. The tool automatically generates commit messages in the *Conventional Commits* standard, detailed Pull Request descriptions, and deep Code Reviews aimed at reducing technical debt.
22
27
 
23
28
  ## **🛠️ Technologies and Libraries Used**
@@ -126,7 +131,9 @@ You can pass the following *flags* for specific actions:
126
131
  * `-r` or `--review`: Performs a detailed **Code Review** of local changes.
127
132
  * `-f` or `--fullreview`: Performs a **Full Code Review** analyzing all changes since the remote branch.
128
133
  * `-i <file>` or `--input <file>`: **Full File Audit.** Must be used together with `-r` or `-f`; it ignores git history and does a Code Review of the entire file. Excellent for acting as a consultant on legacy code refactoring.
129
- * `--provider <gemini|deepseek>`: Forces the use of a specific AI only for this execution, ignoring your default saved in `.env`.
134
+ * `--provider <gemini|deepseek|ollama>`: Forces the use of a specific AI only for this execution, ignoring your default saved in `.env`.
135
+ * `--lang <code>`: Forces the interface language for this execution (e.g.: `en_us`, `pt_br`). Overrides `GITPR_LANG` in `.env` without persisting the change.
136
+ * `-ch` or `--chat`: Opens the **Interactive Pair Programming Chat** — a TUI terminal where the AI sees your current diff and maintains a contextual conversation. Features memory per branch, slash commands (`/explain`, `/tests`, `/optimize`, `/clear`), auto-patching (F5), diff refresh (F2), and session export (F6).
130
137
  * `-l` or `--linter`: Runs **only the local static linter** (no AI calls). Ideal for use in CI/CD pipelines to block non-compliant code.
131
138
  * `-ih` or `--installhooks`: Automatically installs **local Git Hooks** (`pre-commit` and `prepare-commit-msg`) in your repository.
132
139
  * `-s` or `--skill`: Creates the AI context template files (`.gitpr.commit.md`, `.gitpr.pr.md`, `.gitpr.review.md`, `.gitpr.filereview.md`, `.gitpr.issue.md`, `.gitpr.blame.md`) and the Linter (`.gitpr.linter.yml`) at the project root.
@@ -138,6 +145,14 @@ You can pass the following *flags* for specific actions:
138
145
  * `-u` or `--update`: Checks and installs the latest version of GitPR (Auto-Updater).
139
146
 
140
147
  > **⚙️ Technical Note (--hook):** GitPR has a hidden flag `--hook <file>` that is triggered exclusively by the Git Hooks system in the background. It allows the AI to inject the suggested message directly into Git's temporary file, without cluttering your terminal.
148
+ >
149
+ > **⚙️ Technical Note (--pre-save):** GitPR has a hidden debug flag `--pre-save` that can be combined with any AI command (e.g.: `gitpr -c --pre-save`). Before each AI call, it saves the full payload that will be sent to the model (system instruction + prompt + character counters) to a `_{action}-{datetime}.json` file in the current folder, and then proceeds normally. Useful for inspecting very large prompts. Note: when the response comes from the local cache, no call is made and no file is generated.
150
+
151
+ ### 📦 Huge Diffs (Map-Reduce)
152
+
153
+ When your diff is too large for a single AI call (over ~90k estimated tokens), GitPR automatically splits it into batches by file, asks the AI for a technical summary of each part (Map), and unifies everything into the final commit message, review, or PR description (Reduce). No flags needed — it activates on demand and shows the progress in the console.
154
+
155
+ 📚 Full documentation: [docs/map-reduce-diff.md](docs/map-reduce-diff.md)
141
156
 
142
157
  ## 🛡️ Local Linter (Static Analysis)
143
158
 
@@ -165,6 +180,7 @@ The Linter analyzes only the **added lines** in your `git diff`, ensuring a focu
165
180
  GitPR is not tied to a single Artificial Intelligence. During initial setup, the user can choose their default engine. We currently support:
166
181
  * **Google Gemini** (Default: `gemini-2.5-flash`)
167
182
  * **DeepSeek** (Default: `deepseek-chat`)
183
+ * **Ollama** (Local) — run models locally without internet, fully compatible with the OpenAI API format
168
184
 
169
185
  You can dynamically switch models by configuring the `GEMINI_API_MODEL` or `DEEPSEEK_API_MODEL` variables in your `~/.gitpr/.env` file, or switch in real-time using the `--provider` flag.
170
186
 
@@ -197,20 +213,35 @@ To force a specific language, set `GITPR_LANG=pt_br` or `GITPR_LANG=en` in `~/.g
197
213
 
198
214
  To keep this README concise, we detail the most advanced **DevOps** and **Continuous Integration** focused implementations in separate documents.
199
215
 
200
- If you want to implement GitPR as an automated quality barrier in your team, check out the guides below:
201
-
202
- * [**Local Git Hooks (Shift-Left)**](docs/git-hooks-locais.md): How to use `gitpr --installhooks` to create guardrails on the developer's machine and use AI to automatically write commit messages.
203
- * [**Customizable Static Linter**](docs/linter-regras-customizadas.md): How to create validation rules in `.gitpr.linter.yml` for CI/CD and pre-commit hooks.
204
- * [**Issue Generation and TUI Interface**](docs/issue-tui-help.md): How to use the terminal graphical interface (TUI) and the 3 context engines to manage structured Issues.
205
- * [**AI Code Review**](docs/code-review-ia.md): Guide to review modes (`--review`, `--fullreview`) and file auditing (`--input`).
206
- * [**AI Commit Messages**](docs/commit-message-ia.md): How to generate messages in the Conventional Commits standard and integrate with Git Hooks.
207
- * [**Code Archaeologist (Git Blame)**](docs/blame-arqueologo.md): How to trace the origin of business rules with `git blame` and AI.
208
- * [**Skills and Templates System**](docs/skill-template.md): How to customize AI behavior with `.gitpr.*.md` files.
209
- * [**Auto-Updater**](docs/auto-update.md): How GitPR's automatic update (hot-swap) works.
210
- * [**AI Providers**](docs/providers-ia.md): Configuration and selection between Google Gemini and DeepSeek.
211
- * [**Pull Request (Default Mode)**](docs/pr-descricao-padrao.md): Complete flow for generating PR descriptions without flags.
212
- * [**CI/CD Integration (GitHub Actions)**](docs/github-ci-linter.md): How to run GitPR in the pipeline to block "Merge" of PRs with violations.
213
- * [**GitHub Token (PAT) Integration and Security**](docs/github-pat-integration.md): Understand how GitPR creates issues directly in the repository with authentication.
216
+ If you want to implement GitPR as an automated quality barrier in your team, check out the guides below.
217
+
218
+ > 🌐 Each guide is available in **5 languages** add `.pt_br`, `.pt_pt`, `.fr_fr`, or `.es_es` before the `.md` extension for translated versions (e.g., `docs/understanding_chat_functionality.pt_br.md`). English is the default with no suffix.
219
+
220
+ ### Chat & Interactive Features
221
+
222
+ * [**🧠 Interactive Chat (Pair Programming)**](https://github.com/natanfiuza/gitpr/blob/main/docs/understanding_chat_functionality.md) How to use the AI chat with memory, slash commands, auto-patch, and session export.
223
+
224
+ ### DevOps & CI/CD
225
+
226
+ * [**Local Git Hooks (Shift-Left)**](https://github.com/natanfiuza/gitpr/blob/main/docs/git-hooks-locais.md) How to use `gitpr --installhooks` to create guardrails on the developer's machine and use AI to automatically write commit messages.
227
+ * [**Customizable Static Linter**](https://github.com/natanfiuza/gitpr/blob/main/docs/linter-regras-customizadas.md) How to create validation rules in `.gitpr.linter.yml` for CI/CD and pre-commit hooks.
228
+ * [**CI/CD Integration (GitHub Actions)**](https://github.com/natanfiuza/gitpr/blob/main/docs/github-ci-linter.md) How to run GitPR in the pipeline to block "Merge" of PRs with violations.
229
+
230
+ ### Core Features
231
+
232
+ * [**Pull Request (Default Mode)**](https://github.com/natanfiuza/gitpr/blob/main/docs/pr-descricao-padrao.md) — Complete flow for generating PR descriptions without flags.
233
+ * [**AI Code Review**](https://github.com/natanfiuza/gitpr/blob/main/docs/code-review-ia.md) — Guide to review modes (`--review`, `--fullreview`) and file auditing (`--input`).
234
+ * [**AI Commit Messages**](https://github.com/natanfiuza/gitpr/blob/main/docs/commit-message-ia.md) — How to generate messages in the Conventional Commits standard and integrate with Git Hooks.
235
+ * [**Issue Generation and TUI Interface**](https://github.com/natanfiuza/gitpr/blob/main/docs/issue-tui-help.md) — How to use the terminal graphical interface (TUI) and the 3 context engines to manage structured Issues.
236
+ * [**Code Archaeologist (Git Blame)**](https://github.com/natanfiuza/gitpr/blob/main/docs/blame-arqueologo.md) — How to trace the origin of business rules with `git blame` and AI.
237
+ * [**Skills and Templates System**](https://github.com/natanfiuza/gitpr/blob/main/docs/skill-template.md) — How to customize AI behavior with `.gitpr.*.md` files.
238
+
239
+ ### Configuration & Infrastructure
240
+
241
+ * [**AI Providers**](https://github.com/natanfiuza/gitpr/blob/main/docs/providers-ia.md) — Configuration and selection between Google Gemini, DeepSeek, and Ollama.
242
+ * [**Auto-Updater**](https://github.com/natanfiuza/gitpr/blob/main/docs/auto-update.md) — How GitPR's automatic update (hot-swap) works.
243
+ * [**GitHub Token (PAT) Integration and Security**](https://github.com/natanfiuza/gitpr/blob/main/docs/github-pat-integration.md) — Understand how GitPR creates issues directly in the repository with authentication.
244
+ * [**Internationalization (i18n)**](https://github.com/natanfiuza/gitpr/blob/main/docs/i18n_explanation.md) — Architecture, usage patterns, and how to add new languages.
214
245
 
215
246
  ## ⚡ Local Cache System (Quota Savings)
216
247
 
@@ -0,0 +1,25 @@
1
+ gitpr_cli-0.0.27.dist-info/licenses/LICENSE,sha256=UQzLC6mwqb-ZNDkBBAqOxC-SMQcEID0gaM6oFcu6u48,27030
2
+ src/__init__.py,sha256=U6ksarernNcryHO20aj4rNG7w0thT7xdMs5KzBJyPvE,28
3
+ src/ai_providers.py,sha256=cxPuytZ9LfvhQbWFPBJ4dud3lHLpAQpa3cbY8bKESk8,10954
4
+ src/blame_engine.py,sha256=z-uh8mHOF--JxY_nnbp5r_9y_a_IO4Zn42P0AeLBpto,10436
5
+ src/cache.py,sha256=XggMTy00GlZfeF1VoLEYCdxL5h0eTg11TJfPIRPx7fs,3201
6
+ src/chat_memory.py,sha256=vdYuElIQb6-flFm4SfzapnQZGRIwgbLECzIcsAc0Oo8,8724
7
+ src/config.py,sha256=VJ9ekDlTVG5ETWDuO1kZWlgrncXbrvduxa5Zg7wN_ZY,9064
8
+ src/core.py,sha256=M7dPzenRa7HtOVPvFXltwg3OR2ca2EsJFJY60ajUijg,24297
9
+ src/i18n.py,sha256=QED7hTSdWCC1lSNJ-l6VuOba18OrWgFTMQcZ7cQmXbY,3599
10
+ src/issue_engine.py,sha256=XJ7WNLoeg3wct079wS7OQqU62f_avn3A8LmMuPWchzE,3642
11
+ src/linter_engine.py,sha256=D04wvOgRa-vZEYknm-z-Cg7V_3gnoQz_64PMorWnISo,4414
12
+ src/main.py,sha256=s-7JO9MudWZ5mqurDtzTtcj8GoMNirlptgbTQ6y6-vU,31742
13
+ src/security.py,sha256=Li0zM8ZURTK2nKRHIt49TWLO4DIddBEHzXPrVycv0Rs,1343
14
+ src/spinner.py,sha256=16ngB2oT1t7O-c3D2DWCJbFb9nMyD-HWv6KcvhKTelQ,7651
15
+ src/tui_issue.py,sha256=gcAJCIfMcdeMjrFJUWQQvdgBv7fnOBtxQxwdBk2Sk40,1789
16
+ src/updater.py,sha256=QPvYVEBpORF37rfD2oB_vVKnfNIw5amlaJk3NDSckuA,5886
17
+ src/ui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
+ src/ui/chat_app.py,sha256=y-py5UpisQbPLqqDON8AIkyA7X1IM5LAn1GbeYr2p_U,25598
19
+ src/ui/help_screen.py,sha256=tjbokXhwyzLnkOC0S56NbpX-78hcFQnvsQMpNU1wt0c,1862
20
+ src/ui/issue_app.py,sha256=8MdBcOVdCHPFx6EUhs80CiZ2G-29KulskkCz9qMZ_Lg,4737
21
+ gitpr_cli-0.0.27.dist-info/METADATA,sha256=RaWiAINchrmj4I8CpZxeaObnRVH4IwpEgneVOFCxkG4,18530
22
+ gitpr_cli-0.0.27.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
23
+ gitpr_cli-0.0.27.dist-info/entry_points.txt,sha256=mh_4F_idM4cj0XhfRGBDQb5Nnku93YJ-dyKh5SufwlE,39
24
+ gitpr_cli-0.0.27.dist-info/top_level.txt,sha256=74rtVfumQlgAPzR5_2CgYN24MB0XARCg0t-gzk6gTrM,4
25
+ gitpr_cli-0.0.27.dist-info/RECORD,,
src/ai_providers.py CHANGED
@@ -1,18 +1,75 @@
1
1
  import json
2
2
  import time
3
+ from datetime import datetime
3
4
  import click
4
5
  from google import genai
5
6
  from openai import OpenAI
7
+ import urllib.request
8
+ from pathlib import Path
9
+
6
10
  from src.spinner import Spinner
7
- from src.i18n import __
11
+ from src.i18n import __,CURRENT_LANG
12
+
13
+
14
+ # Hidden --pre-save debug flag: when enabled, every AI payload is dumped
15
+ # to a JSON file in the current directory before being sent to the model.
16
+ PRE_SAVE_ENABLED = False
17
+
18
+
19
+ def set_pre_save(enabled):
20
+ """Enable/disable the pre-save payload dump (set once from the CLI)."""
21
+ global PRE_SAVE_ENABLED
22
+ PRE_SAVE_ENABLED = enabled
8
23
 
9
- def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quiet=False):
24
+
25
+ def _save_pre_save_payload(action, provider, api_model, system_instruction, prompt=None, chat_history=None, new_message=None):
26
+ """
27
+ Dump the full AI payload to a _{action}-{datetime}.json file in the current directory.
28
+ Returns the filename on success or None on failure (a debug dump must never break the flow).
29
+ """
30
+ filename = f"_{action}-{datetime.now().strftime('%Y%m%d%H%M%S%f')}.json"
31
+
32
+ payload = {
33
+ "datetime": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
34
+ "action": action,
35
+ "provider": provider,
36
+ "model": api_model,
37
+ "system_instruction": system_instruction,
38
+ "system_instruction_chars": len(system_instruction) if system_instruction else 0,
39
+ }
40
+
41
+ if chat_history is not None:
42
+ payload["chat_history"] = chat_history
43
+ payload["new_message"] = new_message
44
+ payload["chat_history_chars"] = len(json.dumps(chat_history, ensure_ascii=False)) if chat_history else 0
45
+ payload["new_message_chars"] = len(new_message) if new_message else 0
46
+ payload["total_chars"] = payload["system_instruction_chars"] + payload["chat_history_chars"] + payload["new_message_chars"]
47
+ else:
48
+ payload["prompt"] = prompt
49
+ payload["prompt_chars"] = len(prompt) if prompt else 0
50
+ payload["total_chars"] = payload["system_instruction_chars"] + payload["prompt_chars"]
51
+
52
+ try:
53
+ with open(filename, "w", encoding="utf-8") as f:
54
+ json.dump(payload, f, ensure_ascii=False, indent=2)
55
+ return filename
56
+ except Exception:
57
+ return None
58
+
59
+
60
+ def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quiet=False, action="ai_call"):
10
61
  """
11
62
  Unified engine for AI calls.
12
63
  Supports 'gemini' and 'deepseek'.
13
64
  """
14
65
  max_retries = 3
15
66
  retry_delay = 2
67
+
68
+ if PRE_SAVE_ENABLED:
69
+ saved_file = _save_pre_save_payload(action, provider, api_model, system_instruction, prompt=prompt)
70
+ if saved_file and not quiet:
71
+ click.secho(__("📝 Pre-save: AI payload saved to {filename}", filename=saved_file), fg="yellow", dim=True)
72
+
16
73
  spinner = Spinner(quiet=quiet)
17
74
  spinner.start()
18
75
 
@@ -77,4 +134,122 @@ def call_ai_model(provider, api_key, api_model, prompt, system_instruction, quie
77
134
  click.secho(__("\r❌ Critical error contacting {provider} API after {max_retries} attempts: {error}", provider=provider.capitalize(), max_retries=max_retries, error=str(e)), fg="red", bold=True)
78
135
  return None
79
136
  finally:
80
- spinner.stop()
137
+ spinner.stop()
138
+
139
+ def load_chat_commands():
140
+ """Download and cache the translated chat commands."""
141
+ lang_suffix = "" if CURRENT_LANG.startswith("en") else f".{CURRENT_LANG}"
142
+ url = f"https://raw.githubusercontent.com/natanfiuza/gitpr/main/templates/chat_commands{lang_suffix}.json"
143
+
144
+ cache_dir = Path.home() / ".gitpr" / "cache"
145
+ cache_dir.mkdir(parents=True, exist_ok=True)
146
+ cache_file = cache_dir / f"chat_commands{lang_suffix}.json"
147
+
148
+ # Try loading from the local cache first to avoid slowing down the terminal
149
+ if cache_file.exists():
150
+ try:
151
+ with open(cache_file, "r", encoding="utf-8") as f:
152
+ return json.load(f)
153
+ except Exception:
154
+ pass
155
+
156
+ # If it is not in the cache, download it from the remote repository
157
+ try:
158
+ req = urllib.request.Request(url, headers={'User-Agent': 'GitPR-Chat'})
159
+ with urllib.request.urlopen(req, timeout=5) as response:
160
+ data = json.loads(response.read().decode())
161
+ with open(cache_file, "w", encoding="utf-8") as f:
162
+ json.dump(data, f, ensure_ascii=False, indent=2)
163
+ return data
164
+ except Exception:
165
+ # Safety fallback if the user is offline
166
+ return {
167
+ "/explain": "Explains the diff line by line.",
168
+ "/tests": "Generates unit tests for the changed functions.",
169
+ "/optimize": "Analyzes cyclomatic complexity and performance.",
170
+ "/clear": "Clears conversation and creates a new chat session for the current diff."
171
+ }
172
+
173
+ def process_chat_command(message):
174
+ """
175
+ Check whether the message is a command (e.g., /explain).
176
+ Returns a tuple: (is_command, is_clear_command, processed_message)
177
+ """
178
+ msg_trimmed = message.strip().lower()
179
+ if not msg_trimmed.startswith("/"):
180
+ return False, False, message
181
+
182
+ commands = load_chat_commands()
183
+
184
+ for cmd, prompt in commands.items():
185
+ if msg_trimmed == cmd.lower():
186
+ # Special flag so the UI knows whether to reset the session without calling the AI
187
+ is_clear = (cmd.lower() in ["/clear", "/limpar", "/limpiar", "/effacer"])
188
+ return True, is_clear, prompt
189
+
190
+ return False, False, message
191
+
192
+ def call_ai_chat(provider, api_key, api_model, system_instruction, chat_history, new_message, quiet=False):
193
+ """
194
+ Dedicated engine for the Interactive Chat.
195
+ Keeps the historical context and returns free Markdown (does not force JSON).
196
+ """
197
+ if PRE_SAVE_ENABLED:
198
+ saved_file = _save_pre_save_payload("chat", provider, api_model, system_instruction, chat_history=chat_history, new_message=new_message)
199
+ if saved_file and not quiet:
200
+ click.secho(__("📝 Pre-save: AI payload saved to {filename}", filename=saved_file), fg="yellow", dim=True)
201
+
202
+ spinner = Spinner(quiet=quiet)
203
+ spinner.start()
204
+
205
+ try:
206
+ if provider == "gemini":
207
+ client = genai.Client(api_key=api_key)
208
+
209
+ # Format the history into the Gemini SDK format
210
+ formatted_contents = []
211
+ for msg in chat_history:
212
+ role = "model" if msg["role"] == "assistant" else "user"
213
+ formatted_contents.append({"role": role, "parts": [{"text": msg["content"]}]})
214
+
215
+ formatted_contents.append({"role": "user", "parts": [{"text": new_message}]})
216
+
217
+ response = client.models.generate_content(
218
+ model=api_model,
219
+ contents=formatted_contents,
220
+ config={
221
+ "system_instruction": system_instruction,
222
+ "temperature": 0.3 # Slightly higher so the chat feels more natural
223
+ }
224
+ )
225
+ result_text = response.text
226
+
227
+ elif provider in ["deepseek", "ollama"]:
228
+ base_url = "https://api.deepseek.com" if provider == "deepseek" else "http://localhost:11434/v1"
229
+ client = OpenAI(api_key=api_key, base_url=base_url)
230
+
231
+ messages = [{"role": "system", "content": system_instruction}]
232
+ for msg in chat_history:
233
+ messages.append({"role": msg["role"], "content": msg["content"]})
234
+ messages.append({"role": "user", "content": new_message})
235
+
236
+ response = client.chat.completions.create(
237
+ model=api_model,
238
+ messages=messages,
239
+ temperature=0.3
240
+ )
241
+ result_text = response.choices[0].message.content
242
+ else:
243
+ spinner.stop()
244
+ click.secho(__("❌ Unknown AI provider: {provider}", provider=provider), fg="red")
245
+ return None
246
+
247
+ spinner.stop()
248
+ return result_text
249
+
250
+ except Exception as e:
251
+ spinner.stop()
252
+ click.secho(__("\r❌ Critical error in Chat API ({provider}): {error}", provider=provider.capitalize(), error=str(e)), fg="red", bold=True)
253
+ return None
254
+ finally:
255
+ spinner.stop()
src/blame_engine.py CHANGED
@@ -83,7 +83,7 @@ def analyze_commit_with_ai(commit_hash, file_path):
83
83
 
84
84
  click.secho(__(" 🤖 Consulting AI ({api_model}) about commit {commit_hash}...", api_model=api_model, commit_hash=commit_hash[:8]), fg="cyan", dim=True)
85
85
 
86
- result_json = call_ai_model(provider, api_key, api_model, prompt, sys_inst)
86
+ result_json = call_ai_model(provider, api_key, api_model, prompt, sys_inst, action="blame")
87
87
 
88
88
  if result_json and "status" in result_json:
89
89
  return result_json
@@ -210,7 +210,7 @@ def run_blame_analysis(file_path, start_line, end_line, return_data=False):
210
210
  sys_inst = __('You are a Software Architect. Generate ONLY a JSON object in the format {"resumo": "summary text"}.')
211
211
 
212
212
  click.secho(__(" 🤖 Consulting AI ({api_model}) for the Executive Summary...", api_model=api_model), fg="cyan", dim=True)
213
- summary_json = call_ai_model(provider, api_key, api_model, summary_prompt, sys_inst)
213
+ summary_json = call_ai_model(provider, api_key, api_model, summary_prompt, sys_inst, action="blame_summary")
214
214
 
215
215
  resumo_texto = summary_json.get("resumo", __("Summary not available.")) if summary_json else __("Summary not available.")
216
216
 
src/chat_memory.py ADDED
@@ -0,0 +1,218 @@
1
+ import os
2
+ import json
3
+ import hashlib
4
+ import random
5
+ import string
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ def gerar_uuid_base_15():
10
+ """
11
+ Generate a UUID with a base of 15 characters (group 4-5-4)
12
+
13
+ Returns:
14
+ str: UUID in the format XXXX-XXXXX-XXXX
15
+ """
16
+
17
+ def gerar_grupo(tamanho):
18
+ """
19
+ Helper function to generate a group of characters with at least one number
20
+
21
+ Args:
22
+ tamanho (int): Size of the group to be generated
23
+
24
+ Returns:
25
+ str: Group of characters with at least one number
26
+ """
27
+ letras = string.ascii_letters # 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
28
+ numeros = string.digits # '0123456789'
29
+ todos_caracteres = letras + numeros
30
+
31
+ # Ensures that at least one number will be present in the group
32
+ grupo = []
33
+ numero_inserido = False
34
+
35
+ for i in range(tamanho):
36
+ if i == tamanho - 1 and not numero_inserido:
37
+ # If it is the last character and we have not inserted a number yet
38
+ grupo.append(random.choice(numeros))
39
+ else:
40
+ caractere = random.choice(todos_caracteres)
41
+ if caractere.isdigit():
42
+ numero_inserido = True
43
+ grupo.append(caractere)
44
+
45
+ return ''.join(grupo)
46
+
47
+ # Generate the three groups of characters
48
+ grupo1 = gerar_grupo(4)
49
+ grupo2 = gerar_grupo(5)
50
+ grupo3 = gerar_grupo(4)
51
+
52
+ # Combine the groups into the desired format
53
+ return f"{grupo1}-{grupo2}-{grupo3}"
54
+
55
+ class ChatMemoryManager:
56
+ """
57
+ State and Memory Manager for GitPR's Hybrid Chat sessions.
58
+ Keeps the message history and tracks code changes (diff) during the session.
59
+ """
60
+ def __init__(self, repo_name, branch_name, current_diff, git_user, git_email):
61
+ self.repo_name = repo_name
62
+ self.branch_name = branch_name
63
+ self.git_user = git_user
64
+ self.git_email = git_email
65
+
66
+ self.base_dir = Path.home() / ".gitpr" / "cache" / "chat"
67
+ self.base_dir.mkdir(parents=True, exist_ok=True)
68
+
69
+ self.session_uuid = None
70
+ self.session_dir = None
71
+ self.config_file = None
72
+ self.conversation_file = None
73
+
74
+ self._initialize_session(current_diff)
75
+
76
+ def _generate_md5(self, text):
77
+ return hashlib.md5(text.encode('utf-8')).hexdigest()
78
+
79
+ def _initialize_session(self, current_diff):
80
+ """Look for an open session for the current branch or create a new one."""
81
+ diff_md5 = self._generate_md5(current_diff)
82
+ latest_session = None
83
+ latest_time = None
84
+
85
+ # Look for the most recent session for this repository and branch
86
+ for session_folder in self.base_dir.iterdir():
87
+ if session_folder.is_dir():
88
+ uuid_str = session_folder.name
89
+ cfg_path = session_folder / f"chat-config_{uuid_str}.json"
90
+
91
+ if cfg_path.exists():
92
+ try:
93
+ with open(cfg_path, "r", encoding="utf-8") as f:
94
+ cfg = json.load(f)
95
+
96
+ if cfg.get("repo") == self.repo_name and cfg.get("branch") == self.branch_name:
97
+ # Compare the modification time to pick the most recent chat for this branch
98
+ mtime = cfg_path.stat().st_mtime
99
+ if latest_time is None or mtime > latest_time:
100
+ latest_time = mtime
101
+ latest_session = (uuid_str, session_folder, cfg_path, cfg)
102
+ except Exception:
103
+ continue
104
+
105
+ if latest_session:
106
+ # Reopen the existing session
107
+ self.session_uuid, self.session_dir, self.config_file, cfg_data = latest_session
108
+ self.conversation_file = self.session_dir / f"conversation_{self.session_uuid}.json"
109
+
110
+ # Check whether the code (diff) has changed since last time
111
+ diff_history = cfg_data.get("diff_history", [])
112
+ last_diff_md5 = diff_history[-1]["md5"] if diff_history else None
113
+
114
+ if last_diff_md5 != diff_md5:
115
+ self._append_diff_to_history(cfg_data, current_diff, diff_md5)
116
+ else:
117
+ # No session found for this branch, create a new one from scratch
118
+ self._create_new_session(current_diff, diff_md5)
119
+
120
+ def _append_diff_to_history(self, cfg_data, diff_text, diff_md5):
121
+ """Store the new diff in the configuration file."""
122
+ new_entry = {
123
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
124
+ "md5": diff_md5,
125
+ "diff": diff_text
126
+ }
127
+ cfg_data.setdefault("diff_history", []).append(new_entry)
128
+
129
+ with open(self.config_file, "w", encoding="utf-8") as f:
130
+ json.dump(cfg_data, f, indent=2, ensure_ascii=False)
131
+
132
+ def _create_new_session(self, current_diff, diff_md5):
133
+ """Set up a new base-15 UUID folder with the initial JSON files."""
134
+ self.session_uuid = gerar_uuid_base_15()
135
+ self.session_dir = self.base_dir / self.session_uuid
136
+ self.session_dir.mkdir(parents=True, exist_ok=True)
137
+
138
+ self.config_file = self.session_dir / f"chat-config_{self.session_uuid}.json"
139
+ self.conversation_file = self.session_dir / f"conversation_{self.session_uuid}.json"
140
+
141
+ initial_config = {
142
+ "session_uuid": self.session_uuid,
143
+ "folder_name": self.session_uuid,
144
+ "repo": self.repo_name,
145
+ "branch": self.branch_name,
146
+ "git_user": self.git_user,
147
+ "git_email": self.git_email,
148
+ "created_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
149
+ "diff_history": [
150
+ {
151
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
152
+ "md5": diff_md5,
153
+ "diff": current_diff
154
+ }
155
+ ]
156
+ }
157
+
158
+ # Write the metadata
159
+ with open(self.config_file, "w", encoding="utf-8") as f:
160
+ json.dump(initial_config, f, indent=2, ensure_ascii=False)
161
+
162
+ # Create the empty chat memory
163
+ with open(self.conversation_file, "w", encoding="utf-8") as f:
164
+ json.dump([], f, indent=2, ensure_ascii=False)
165
+
166
+ def get_history(self):
167
+ """Return the conversation history from the JSON file."""
168
+ if self.conversation_file.exists():
169
+ try:
170
+ with open(self.conversation_file, "r", encoding="utf-8") as f:
171
+ return json.load(f)
172
+ except Exception:
173
+ return []
174
+ return []
175
+
176
+ def save_message(self, role, content):
177
+ """
178
+ Save a new message to the conversation.
179
+ 'role' must be 'user', 'assistant' or 'system'.
180
+ """
181
+ history = self.get_history()
182
+ history.append({
183
+ "role": role,
184
+ "content": content,
185
+ "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
186
+ })
187
+ with open(self.conversation_file, "w", encoding="utf-8") as f:
188
+ json.dump(history, f, indent=2, ensure_ascii=False)
189
+
190
+ def get_latest_diff(self):
191
+ """Retrieve the most recent current diff from the session configuration."""
192
+ try:
193
+ with open(self.config_file, "r", encoding="utf-8") as f:
194
+ cfg = json.load(f)
195
+ return cfg.get("diff_history", [])[-1]["diff"]
196
+ except Exception:
197
+ return ""
198
+
199
+ def update_diff_if_changed(self, new_diff):
200
+ """
201
+ Silently check whether the code has changed.
202
+ Used by the F2 key to update the chat context in real time.
203
+ Returns True if the diff was updated, False otherwise.
204
+ """
205
+ new_md5 = self._generate_md5(new_diff)
206
+ try:
207
+ with open(self.config_file, "r", encoding="utf-8") as f:
208
+ cfg = json.load(f)
209
+
210
+ diff_history = cfg.get("diff_history", [])
211
+ last_diff_md5 = diff_history[-1]["md5"] if diff_history else None
212
+
213
+ if last_diff_md5 != new_md5:
214
+ self._append_diff_to_history(cfg, new_diff, new_md5)
215
+ return True
216
+ return False
217
+ except Exception:
218
+ return False