ngpt 2.12.0__py3-none-any.whl → 2.14.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.
@@ -0,0 +1,209 @@
1
+ import sys
2
+ import platform
3
+ from ..formatters import COLORS
4
+ from ..renderers import prettify_markdown, prettify_streaming_markdown
5
+ from ..ui import get_multiline_input
6
+
7
+ # System prompt for rewriting text
8
+ REWRITE_SYSTEM_PROMPT = """You are an expert text editor and rewriter. Your task is to rewrite the user's text to improve readability and flow while carefully preserving the original meaning, tone, and style.
9
+
10
+ PRIMARY GOAL:
11
+ Improve the quality and clarity of writing without changing the author's voice or intent.
12
+
13
+ PRESERVATION RULES (HIGHEST PRIORITY):
14
+ 1. Preserve the exact meaning and information content
15
+ 2. Maintain the original tone (formal/casual/technical/friendly/serious/rude)
16
+ 3. Keep the author's perspective and point of view
17
+ 4. Respect the style of expression when intentional
18
+ 5. Retain technical terminology, jargon, and domain-specific language
19
+ 6. Keep all facts, data points, quotes, and references exactly as provided
20
+
21
+ IMPROVEMENT FOCUS:
22
+ 1. Fix grammar, spelling, and punctuation errors
23
+ 2. Improve sentence structure and flow
24
+ 3. Enhance clarity and readability
25
+ 4. Make language more concise and precise
26
+ 5. Replace awkward phrasings with more natural alternatives
27
+ 6. Break up sentences longer than 25 words
28
+ 7. Convert passive voice to active when appropriate
29
+ 8. Remove redundancies, filler words, and unnecessary repetition
30
+
31
+ FORMAT PRESERVATION:
32
+ 1. Maintain all paragraph breaks and section structures
33
+ 2. Preserve formatting of lists, bullet points, and numbering
34
+ 3. Keep code blocks (```) exactly as they appear with no changes to code
35
+ 4. Respect all markdown formatting (bold, italic, headers, etc.)
36
+ 5. Preserve URLs, email addresses, file paths, and variables exactly
37
+ 6. Maintain the structure of tables and other special formats
38
+
39
+ CONTENT-SPECIFIC GUIDANCE:
40
+ - For technical content: Prioritize precision and clarity over stylistic changes
41
+ - For casual text: Maintain conversational flow and personality
42
+ - For formal writing: Preserve professionalism while improving structure
43
+ - For emotional content: Carefully maintain the emotional resonance and intensity
44
+
45
+ STRICTLY AVOID:
46
+ 1. Adding new information not present in the original
47
+ 2. Removing key points or substantive content
48
+ 3. Significantly changing the formality level
49
+ 4. Inserting your own opinions or commentary
50
+ 5. Explaining what you changed (just provide the improved text)
51
+ 6. Altering the meaning of any sentence, even slightly
52
+ 7. Changing domain-specific terminology or jargon to general terms
53
+
54
+ OUTPUT INSTRUCTION:
55
+ Provide ONLY the rewritten text with no explanations, comments, or meta-text.
56
+
57
+ EXAMPLES:
58
+
59
+ ORIGINAL: "The implementation of the feature, which was delayed due to unforeseen technical complications, is now scheduled for next week's release."
60
+ BETTER: "We delayed the feature implementation due to unforeseen technical complications. It's now scheduled for next week's release."
61
+
62
+ ORIGINAL: "We was hoping you could help with this issue what we are having with the server."
63
+ BETTER: "We were hoping you could help with this issue we're having with the server."
64
+
65
+ ORIGINAL: "The user interface, which is built using React, Redux, and various other frontend technologies, needs to be redesigned to accommodate the new features that we want to add to the application."
66
+ BETTER: "The React/Redux user interface needs redesigning to accommodate our planned new features."
67
+ """
68
+
69
+ def get_terminal_input():
70
+ """Get input from terminal in a cross-platform way, even when stdin is redirected."""
71
+ if platform.system() == 'Windows':
72
+ # Windows-specific solution
73
+ try:
74
+ import msvcrt
75
+ print("Press Y/N...", end="")
76
+ sys.stdout.flush()
77
+ # Wait for a keypress
78
+ char = msvcrt.getch().decode('utf-8').lower()
79
+ print(char) # Echo the character
80
+ return char
81
+ except ImportError:
82
+ # Fallback if msvcrt is not available
83
+ return None
84
+ else:
85
+ # Unix-like systems (Linux, macOS)
86
+ try:
87
+ with open('/dev/tty', 'r') as tty:
88
+ return tty.readline().strip().lower()
89
+ except (IOError, OSError):
90
+ return None
91
+
92
+ def rewrite_mode(client, args, logger=None):
93
+ """Handle the text rewriting mode.
94
+
95
+ Args:
96
+ client: The NGPTClient instance
97
+ args: The parsed command-line arguments
98
+ logger: Optional logger instance
99
+ """
100
+ # Determine the input source (stdin pipe, command-line argument, or multiline input)
101
+ if not sys.stdin.isatty():
102
+ # Read from stdin if data is piped
103
+ input_text = sys.stdin.read().strip()
104
+
105
+ # If stdin is empty but prompt is provided, use the prompt
106
+ if not input_text and args.prompt:
107
+ input_text = args.prompt
108
+ elif args.prompt:
109
+ # Use the command-line argument if provided
110
+ input_text = args.prompt
111
+ else:
112
+ # No pipe or prompt - use multiline input
113
+ print("Enter or paste text to rewrite (Ctrl+D or Ctrl+Z to submit):")
114
+ input_text = get_multiline_input()
115
+ if input_text is None:
116
+ # Input was cancelled or empty
117
+ print("Exiting.")
118
+ return
119
+
120
+ # Check if input is empty
121
+ if not input_text:
122
+ print(f"{COLORS['yellow']}Error: Empty input. Please provide text to rewrite.{COLORS['reset']}")
123
+ return
124
+
125
+ # Set up messages array with system prompt and user content
126
+ messages = [
127
+ {"role": "system", "content": REWRITE_SYSTEM_PROMPT},
128
+ {"role": "user", "content": input_text}
129
+ ]
130
+
131
+ print("\nSubmission successful. Waiting for response...")
132
+
133
+ # Log the messages if logging is enabled
134
+ if logger:
135
+ logger.log("system", REWRITE_SYSTEM_PROMPT)
136
+ logger.log("user", input_text)
137
+
138
+ # Set default streaming behavior based on --no-stream and --prettify arguments
139
+ should_stream = not args.no_stream and not args.prettify
140
+
141
+ # If stream-prettify is enabled
142
+ stream_callback = None
143
+ live_display = None
144
+
145
+ if args.stream_prettify:
146
+ should_stream = True # Enable streaming
147
+ live_display, stream_callback = prettify_streaming_markdown(args.renderer)
148
+ if not live_display:
149
+ # Fallback to normal prettify if live display setup failed
150
+ args.prettify = True
151
+ args.stream_prettify = False
152
+ should_stream = False
153
+ print(f"{COLORS['yellow']}Falling back to regular prettify mode.{COLORS['reset']}")
154
+
155
+ # If regular prettify is enabled with streaming, inform the user
156
+ if args.prettify and not args.no_stream:
157
+ print(f"{COLORS['yellow']}Note: Streaming disabled to enable markdown rendering.{COLORS['reset']}")
158
+
159
+ # Start live display if using stream-prettify
160
+ if args.stream_prettify and live_display:
161
+ live_display.start()
162
+
163
+ response = client.chat(
164
+ prompt=None, # Not used when messages are provided
165
+ stream=should_stream,
166
+ web_search=args.web_search,
167
+ temperature=args.temperature,
168
+ top_p=args.top_p,
169
+ max_tokens=args.max_tokens,
170
+ markdown_format=args.prettify or args.stream_prettify,
171
+ stream_callback=stream_callback,
172
+ messages=messages # Use messages array instead of prompt
173
+ )
174
+
175
+ # Stop live display if using stream-prettify
176
+ if args.stream_prettify and live_display:
177
+ live_display.stop()
178
+
179
+ # Log the AI response if logging is enabled
180
+ if logger and response:
181
+ logger.log("assistant", response)
182
+
183
+ # Handle non-stream response or regular prettify
184
+ if (args.no_stream or args.prettify) and response:
185
+ if args.prettify:
186
+ prettify_markdown(response, args.renderer)
187
+ else:
188
+ print(response)
189
+
190
+ # Offer to copy to clipboard if not in a redirected output
191
+ if not args.no_stream and sys.stdout.isatty():
192
+ try:
193
+ # Make sure to flush output before asking for input
194
+ print("\nCopy to clipboard? (y/n) ", end="")
195
+ sys.stdout.flush()
196
+
197
+ # Cross-platform terminal input
198
+ answer = get_terminal_input()
199
+
200
+ if answer == 'y':
201
+ try:
202
+ import pyperclip
203
+ pyperclip.copy(response)
204
+ print("Copied to clipboard.")
205
+ except ImportError:
206
+ print(f"{COLORS['yellow']}pyperclip not installed. Try: pip install \"ngpt[clipboard]\" {COLORS['reset']}")
207
+
208
+ except (KeyboardInterrupt, EOFError):
209
+ pass
ngpt/utils/cli_config.py CHANGED
@@ -19,6 +19,12 @@ CLI_CONFIG_OPTIONS = {
19
19
  "renderer": {"type": "str", "default": "auto", "context": ["all"]},
20
20
  "config-index": {"type": "int", "default": 0, "context": ["all"], "exclusive": ["provider"]},
21
21
  "web-search": {"type": "bool", "default": False, "context": ["all"]},
22
+ # GitCommit message options
23
+ "message-context": {"type": "str", "default": None, "context": ["gitcommsg"]},
24
+ "recursive-chunk": {"type": "bool", "default": False, "context": ["gitcommsg"]},
25
+ "diff": {"type": "str", "default": None, "context": ["gitcommsg"]},
26
+ "chunk-size": {"type": "int", "default": 200, "context": ["gitcommsg"]},
27
+ "max-depth": {"type": "int", "default": 3, "context": ["gitcommsg"]},
22
28
  }
23
29
 
24
30
  def get_cli_config_dir() -> Path:
ngpt/utils/log.py CHANGED
@@ -1,14 +1,24 @@
1
1
  import os
2
2
  import sys
3
3
  import datetime
4
+ import logging
5
+ import tempfile
4
6
  from pathlib import Path
5
- from typing import Optional, TextIO, Dict, Any
7
+ from typing import Optional, TextIO, Dict, Any, Union
6
8
 
7
- # Simple color definitions for fallback message
9
+ # Define colors locally to avoid circular imports
8
10
  COLORS = {
9
- "green": "\033[32m",
11
+ "reset": "\033[0m",
12
+ "bold": "\033[1m",
13
+ "cyan": "\033[36m",
14
+ "green": "\033[32m",
10
15
  "yellow": "\033[33m",
11
- "reset": "\033[0m"
16
+ "red": "\033[31m",
17
+ "blue": "\033[34m",
18
+ "magenta": "\033[35m",
19
+ "gray": "\033[90m",
20
+ "bg_blue": "\033[44m",
21
+ "bg_cyan": "\033[46m"
12
22
  }
13
23
 
14
24
  class Logger:
@@ -165,6 +175,281 @@ class Logger:
165
175
  """
166
176
  return self.is_temp
167
177
 
178
+ # Add standard logging methods to be compatible with gitcommsg mode
179
+ def info(self, message: str):
180
+ """Log an info message."""
181
+ self.log("INFO", message)
182
+
183
+ def debug(self, message: str):
184
+ """Log a debug message."""
185
+ self.log("DEBUG", message)
186
+
187
+ def warning(self, message: str):
188
+ """Log a warning message."""
189
+ self.log("WARNING", message)
190
+
191
+ def error(self, message: str, exc_info=False):
192
+ """Log an error message."""
193
+ if exc_info:
194
+ import traceback
195
+ message += "\n" + traceback.format_exc()
196
+ self.log("ERROR", message)
197
+
198
+
199
+ class GitCommsgLogger:
200
+ """Specialized logger for gitcommsg mode with standard logging methods."""
201
+
202
+ def __init__(self, log_path: Optional[str] = None):
203
+ """
204
+ Initialize the gitcommsg logger.
205
+
206
+ Args:
207
+ log_path: Optional path to the log file. If None, a temporary file will be created.
208
+ """
209
+ self.log_path = log_path
210
+ self.logger = None
211
+ self.is_temp = False
212
+ self.command_args = sys.argv
213
+
214
+ # Create a temporary log file if no path provided
215
+ if self.log_path is True or self.log_path is None:
216
+ timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
217
+ if sys.platform == "win32":
218
+ temp_dir = os.environ.get("TEMP", "")
219
+ self.log_path = os.path.join(temp_dir, f"ngpt_gitcommsg_{timestamp}.log")
220
+ else:
221
+ self.log_path = f"/tmp/ngpt_gitcommsg_{timestamp}.log"
222
+ self.is_temp = True
223
+
224
+ def setup(self):
225
+ """Set up the logger."""
226
+ # Set up Python's standard logging module
227
+ self.logger = logging.getLogger("gitcommsg")
228
+ self.logger.setLevel(logging.DEBUG)
229
+
230
+ # Clear any existing handlers
231
+ if self.logger.handlers:
232
+ for handler in self.logger.handlers:
233
+ self.logger.removeHandler(handler)
234
+
235
+ # Create file handler
236
+ try:
237
+ # Ensure the directory exists
238
+ log_dir = os.path.dirname(self.log_path)
239
+ if log_dir and not os.path.exists(log_dir):
240
+ os.makedirs(log_dir, exist_ok=True)
241
+
242
+ file_handler = logging.FileHandler(self.log_path)
243
+ file_handler.setLevel(logging.DEBUG)
244
+
245
+ # Create formatter
246
+ formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
247
+ file_handler.setFormatter(formatter)
248
+
249
+ # Add handler to logger
250
+ self.logger.addHandler(file_handler)
251
+
252
+ print(f"{COLORS['green']}Logging enabled. Log file: {self.log_path}{COLORS['reset']}")
253
+ self.logger.info("GitCommitMsg mode started")
254
+ self.logger.info(f"Command: {' '.join(self.command_args)}")
255
+
256
+ return True
257
+ except Exception as e:
258
+ print(f"{COLORS['yellow']}Error setting up logger: {str(e)}{COLORS['reset']}")
259
+ return False
260
+
261
+ def info(self, message: str):
262
+ """Log an info message."""
263
+ if self.logger:
264
+ self.logger.info(message)
265
+
266
+ def debug(self, message: str):
267
+ """Log a debug message."""
268
+ if self.logger:
269
+ self.logger.debug(message)
270
+
271
+ def warning(self, message: str):
272
+ """Log a warning message."""
273
+ if self.logger:
274
+ self.logger.warning(message)
275
+
276
+ def error(self, message: str, exc_info=False):
277
+ """Log an error message."""
278
+ if self.logger:
279
+ self.logger.error(message, exc_info=exc_info)
280
+
281
+ def get_log_path(self) -> str:
282
+ """
283
+ Get the path to the log file.
284
+
285
+ Returns:
286
+ str: Path to the log file
287
+ """
288
+ return self.log_path
289
+
290
+ def is_temporary(self) -> bool:
291
+ """
292
+ Check if the log file is temporary.
293
+
294
+ Returns:
295
+ bool: True if the log file is temporary
296
+ """
297
+ return self.is_temp
298
+
299
+ def log_file_contents(self, level: str, description: str, filepath: str):
300
+ """
301
+ Log the contents of a file.
302
+
303
+ Args:
304
+ level: Log level (DEBUG, INFO, etc.)
305
+ description: Description of the file contents
306
+ filepath: Path to the file
307
+ """
308
+ if not self.logger or not os.path.isfile(filepath):
309
+ return
310
+
311
+ try:
312
+ # Start marker with description
313
+ self.logger.log(
314
+ getattr(logging, level.upper()),
315
+ f"===== BEGIN {description}: {filepath} ====="
316
+ )
317
+
318
+ # Read and log file contents
319
+ with open(filepath, 'r', encoding='utf-8', errors='replace') as f:
320
+ content = f.read()
321
+ for line in content.split('\n'):
322
+ self.logger.log(
323
+ getattr(logging, level.upper()),
324
+ line
325
+ )
326
+
327
+ # End marker
328
+ self.logger.log(
329
+ getattr(logging, level.upper()),
330
+ f"===== END {description}: {filepath} ====="
331
+ )
332
+ except Exception as e:
333
+ self.logger.error(f"Error logging file contents: {str(e)}")
334
+
335
+ def log_content(self, level: str, description: str, content: str):
336
+ """
337
+ Log the provided content.
338
+
339
+ Args:
340
+ level: Log level (DEBUG, INFO, etc.)
341
+ description: Description of the content
342
+ content: The content to log
343
+ """
344
+ if not self.logger:
345
+ return
346
+
347
+ try:
348
+ # Create a temporary file for the content
349
+ fd, temp_path = tempfile.mkstemp(prefix="ngpt_log_", suffix=".txt")
350
+ with os.fdopen(fd, 'w', encoding='utf-8') as f:
351
+ f.write(content)
352
+
353
+ # Log the content from the file
354
+ self.log_file_contents(level, description, temp_path)
355
+
356
+ # Clean up the temporary file
357
+ try:
358
+ os.unlink(temp_path)
359
+ except Exception:
360
+ pass
361
+ except Exception as e:
362
+ if self.logger:
363
+ self.logger.error(f"Error logging content: {str(e)}")
364
+
365
+ def log_prompt(self, level: str, system_prompt: Optional[str], user_prompt: str):
366
+ """
367
+ Log AI prompt information with detailed content.
368
+
369
+ Args:
370
+ level: Log level (DEBUG, INFO, etc.)
371
+ system_prompt: Optional system prompt
372
+ user_prompt: User prompt
373
+ """
374
+ if not self.logger:
375
+ return
376
+
377
+ # Log summary
378
+ self.logger.log(
379
+ getattr(logging, level.upper()),
380
+ "AI Request:"
381
+ )
382
+
383
+ # Log system prompt if provided
384
+ if system_prompt:
385
+ self.log_content(level, "SYSTEM_PROMPT", system_prompt)
386
+
387
+ # Log user prompt
388
+ self.log_content(level, "USER_PROMPT", user_prompt)
389
+
390
+ def log_response(self, level: str, response: str):
391
+ """
392
+ Log AI response with full content.
393
+
394
+ Args:
395
+ level: Log level (DEBUG, INFO, etc.)
396
+ response: The AI response
397
+ """
398
+ if not self.logger:
399
+ return
400
+
401
+ # Log response
402
+ self.log_content(level, "AI_RESPONSE", response)
403
+
404
+ def log_diff(self, level: str, diff_content: str):
405
+ """
406
+ Log git diff content.
407
+
408
+ Args:
409
+ level: Log level (DEBUG, INFO, etc.)
410
+ diff_content: The git diff content
411
+ """
412
+ if not self.logger:
413
+ return
414
+
415
+ # Log diff content
416
+ self.log_content(level, "GIT_DIFF", diff_content)
417
+
418
+ def log_chunks(self, level: str, chunk_number: int, total_chunks: int, chunk_content: str):
419
+ """
420
+ Log chunk content for processing.
421
+
422
+ Args:
423
+ level: Log level (DEBUG, INFO, etc.)
424
+ chunk_number: Current chunk number
425
+ total_chunks: Total number of chunks
426
+ chunk_content: Content of the chunk
427
+ """
428
+ if not self.logger:
429
+ return
430
+
431
+ # Log chunk content
432
+ self.log_content(
433
+ level,
434
+ f"CHUNK_{chunk_number}_OF_{total_chunks}",
435
+ chunk_content
436
+ )
437
+
438
+ def log_template(self, level: str, template_type: str, template: str):
439
+ """
440
+ Log prompt template.
441
+
442
+ Args:
443
+ level: Log level (DEBUG, INFO, etc.)
444
+ template_type: Type of template (INITIAL, CHUNK, COMBINE, etc.)
445
+ template: Template content
446
+ """
447
+ if not self.logger:
448
+ return
449
+
450
+ # Log template
451
+ self.log_content(level, f"{template_type}_TEMPLATE", template)
452
+
168
453
 
169
454
  def create_logger(log_path: Optional[str] = None) -> Logger:
170
455
  """
@@ -176,4 +461,19 @@ def create_logger(log_path: Optional[str] = None) -> Logger:
176
461
  Returns:
177
462
  Logger: Logger instance
178
463
  """
179
- return Logger(log_path)
464
+ return Logger(log_path)
465
+
466
+
467
+ def create_gitcommsg_logger(log_path: Optional[str] = None) -> GitCommsgLogger:
468
+ """
469
+ Create a gitcommsg logger instance.
470
+
471
+ Args:
472
+ log_path: Optional path to the log file
473
+
474
+ Returns:
475
+ GitCommsgLogger: GitCommsgLogger instance
476
+ """
477
+ logger = GitCommsgLogger(log_path)
478
+ logger.setup()
479
+ return logger