ngpt 2.9.0__py3-none-any.whl → 2.9.2__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.
- ngpt/cli/__init__.py +3 -0
- ngpt/cli/config_manager.py +71 -0
- ngpt/cli/formatters.py +239 -0
- ngpt/cli/interactive.py +275 -0
- ngpt/cli/main.py +593 -0
- ngpt/cli/modes/__init__.py +6 -0
- ngpt/cli/modes/chat.py +72 -0
- ngpt/cli/modes/code.py +97 -0
- ngpt/cli/modes/shell.py +46 -0
- ngpt/cli/modes/text.py +72 -0
- ngpt/cli/renderers.py +258 -0
- ngpt/cli/ui.py +155 -0
- ngpt/cli.py +1 -1745
- ngpt/cli_config.py +27 -8
- ngpt/utils/__init__.py +1 -0
- {ngpt-2.9.0.dist-info → ngpt-2.9.2.dist-info}/METADATA +4 -2
- ngpt-2.9.2.dist-info/RECORD +23 -0
- ngpt-2.9.0.dist-info/RECORD +0 -10
- {ngpt-2.9.0.dist-info → ngpt-2.9.2.dist-info}/WHEEL +0 -0
- {ngpt-2.9.0.dist-info → ngpt-2.9.2.dist-info}/entry_points.txt +0 -0
- {ngpt-2.9.0.dist-info → ngpt-2.9.2.dist-info}/licenses/LICENSE +0 -0
ngpt/cli.py
CHANGED
@@ -1,1748 +1,4 @@
|
|
1
|
-
import
|
2
|
-
import sys
|
3
|
-
import os
|
4
|
-
from .client import NGPTClient
|
5
|
-
from .config import load_config, get_config_path, load_configs, add_config_entry, remove_config_entry
|
6
|
-
from .cli_config import (
|
7
|
-
set_cli_config_option,
|
8
|
-
get_cli_config_option,
|
9
|
-
unset_cli_config_option,
|
10
|
-
apply_cli_config,
|
11
|
-
list_cli_config_options,
|
12
|
-
CLI_CONFIG_OPTIONS
|
13
|
-
)
|
14
|
-
from . import __version__
|
15
|
-
|
16
|
-
# Try to import markdown rendering libraries
|
17
|
-
try:
|
18
|
-
import rich
|
19
|
-
from rich.markdown import Markdown
|
20
|
-
from rich.console import Console
|
21
|
-
HAS_RICH = True
|
22
|
-
except ImportError:
|
23
|
-
HAS_RICH = False
|
24
|
-
|
25
|
-
# Try to import the glow command if available
|
26
|
-
def has_glow_installed():
|
27
|
-
"""Check if glow is installed in the system."""
|
28
|
-
import shutil
|
29
|
-
return shutil.which("glow") is not None
|
30
|
-
|
31
|
-
HAS_GLOW = has_glow_installed()
|
32
|
-
|
33
|
-
# ANSI color codes for terminal output
|
34
|
-
COLORS = {
|
35
|
-
"reset": "\033[0m",
|
36
|
-
"bold": "\033[1m",
|
37
|
-
"cyan": "\033[36m",
|
38
|
-
"green": "\033[32m",
|
39
|
-
"yellow": "\033[33m",
|
40
|
-
"blue": "\033[34m",
|
41
|
-
"magenta": "\033[35m",
|
42
|
-
"gray": "\033[90m",
|
43
|
-
"bg_blue": "\033[44m",
|
44
|
-
"bg_cyan": "\033[46m"
|
45
|
-
}
|
46
|
-
|
47
|
-
# Check if ANSI colors are supported
|
48
|
-
def supports_ansi_colors():
|
49
|
-
"""Check if the current terminal supports ANSI colors."""
|
50
|
-
import os
|
51
|
-
import sys
|
52
|
-
|
53
|
-
# If not a TTY, probably redirected, so no color
|
54
|
-
if not sys.stdout.isatty():
|
55
|
-
return False
|
56
|
-
|
57
|
-
# Windows specific checks
|
58
|
-
if sys.platform == "win32":
|
59
|
-
try:
|
60
|
-
# Windows 10+ supports ANSI colors in cmd/PowerShell
|
61
|
-
import ctypes
|
62
|
-
kernel32 = ctypes.windll.kernel32
|
63
|
-
|
64
|
-
# Try to enable ANSI color support
|
65
|
-
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
66
|
-
|
67
|
-
# Check if TERM_PROGRAM is set (WSL/ConEmu/etc.)
|
68
|
-
if os.environ.get('TERM_PROGRAM') or os.environ.get('WT_SESSION'):
|
69
|
-
return True
|
70
|
-
|
71
|
-
# Check Windows version - 10+ supports ANSI natively
|
72
|
-
winver = sys.getwindowsversion()
|
73
|
-
if winver.major >= 10:
|
74
|
-
return True
|
75
|
-
|
76
|
-
return False
|
77
|
-
except Exception:
|
78
|
-
return False
|
79
|
-
|
80
|
-
# Most UNIX systems support ANSI colors
|
81
|
-
return True
|
82
|
-
|
83
|
-
# Initialize color support
|
84
|
-
HAS_COLOR = supports_ansi_colors()
|
85
|
-
|
86
|
-
# If we're on Windows, use brighter colors that work better in PowerShell
|
87
|
-
if sys.platform == "win32" and HAS_COLOR:
|
88
|
-
COLORS["magenta"] = "\033[95m" # Bright magenta for metavars
|
89
|
-
COLORS["cyan"] = "\033[96m" # Bright cyan for options
|
90
|
-
|
91
|
-
# If no color support, use empty color codes
|
92
|
-
if not HAS_COLOR:
|
93
|
-
for key in COLORS:
|
94
|
-
COLORS[key] = ""
|
95
|
-
|
96
|
-
def has_markdown_renderer(renderer='auto'):
|
97
|
-
"""Check if the specified markdown renderer is available.
|
98
|
-
|
99
|
-
Args:
|
100
|
-
renderer (str): Which renderer to check: 'auto', 'rich', or 'glow'
|
101
|
-
|
102
|
-
Returns:
|
103
|
-
bool: True if the renderer is available, False otherwise
|
104
|
-
"""
|
105
|
-
if renderer == 'auto':
|
106
|
-
return HAS_RICH or HAS_GLOW
|
107
|
-
elif renderer == 'rich':
|
108
|
-
return HAS_RICH
|
109
|
-
elif renderer == 'glow':
|
110
|
-
return HAS_GLOW
|
111
|
-
else:
|
112
|
-
return False
|
113
|
-
|
114
|
-
def show_available_renderers():
|
115
|
-
"""Show which markdown renderers are available and their status."""
|
116
|
-
print(f"\n{COLORS['cyan']}{COLORS['bold']}Available Markdown Renderers:{COLORS['reset']}")
|
117
|
-
|
118
|
-
if HAS_GLOW:
|
119
|
-
print(f" {COLORS['green']}✓ Glow{COLORS['reset']} - Terminal-based Markdown renderer")
|
120
|
-
else:
|
121
|
-
print(f" {COLORS['yellow']}✗ Glow{COLORS['reset']} - Not installed (https://github.com/charmbracelet/glow)")
|
122
|
-
|
123
|
-
if HAS_RICH:
|
124
|
-
print(f" {COLORS['green']}✓ Rich{COLORS['reset']} - Python library for terminal formatting (Recommended)")
|
125
|
-
else:
|
126
|
-
print(f" {COLORS['yellow']}✗ Rich{COLORS['reset']} - Not installed (pip install \"ngpt[full]\" or pip install rich)")
|
127
|
-
|
128
|
-
if not HAS_GLOW and not HAS_RICH:
|
129
|
-
print(f"\n{COLORS['yellow']}To enable prettified markdown output, install one of the above renderers.{COLORS['reset']}")
|
130
|
-
print(f"{COLORS['yellow']}For Rich: pip install \"ngpt[full]\" or pip install rich{COLORS['reset']}")
|
131
|
-
else:
|
132
|
-
renderers = []
|
133
|
-
if HAS_RICH:
|
134
|
-
renderers.append("rich")
|
135
|
-
if HAS_GLOW:
|
136
|
-
renderers.append("glow")
|
137
|
-
print(f"\n{COLORS['green']}Usage examples:{COLORS['reset']}")
|
138
|
-
print(f" ngpt --prettify \"Your prompt here\" {COLORS['gray']}# Beautify markdown responses{COLORS['reset']}")
|
139
|
-
print(f" ngpt -c --prettify \"Write a sort function\" {COLORS['gray']}# Syntax highlight generated code{COLORS['reset']}")
|
140
|
-
if renderers:
|
141
|
-
renderer = renderers[0]
|
142
|
-
print(f" ngpt --prettify --renderer={renderer} \"Your prompt\" {COLORS['gray']}# Specify renderer{COLORS['reset']}")
|
143
|
-
|
144
|
-
print("")
|
145
|
-
|
146
|
-
def warn_if_no_markdown_renderer(renderer='auto'):
|
147
|
-
"""Warn the user if the specified markdown renderer is not available.
|
148
|
-
|
149
|
-
Args:
|
150
|
-
renderer (str): Which renderer to check: 'auto', 'rich', or 'glow'
|
151
|
-
|
152
|
-
Returns:
|
153
|
-
bool: True if the renderer is available, False otherwise
|
154
|
-
"""
|
155
|
-
if has_markdown_renderer(renderer):
|
156
|
-
return True
|
157
|
-
|
158
|
-
if renderer == 'auto':
|
159
|
-
print(f"{COLORS['yellow']}Warning: No markdown rendering library available.{COLORS['reset']}")
|
160
|
-
print(f"{COLORS['yellow']}Install with: pip install \"ngpt[full]\"{COLORS['reset']}")
|
161
|
-
print(f"{COLORS['yellow']}Or install 'glow' from https://github.com/charmbracelet/glow{COLORS['reset']}")
|
162
|
-
elif renderer == 'rich':
|
163
|
-
print(f"{COLORS['yellow']}Warning: Rich is not available.{COLORS['reset']}")
|
164
|
-
print(f"{COLORS['yellow']}Install with: pip install \"ngpt[full]\" or pip install rich{COLORS['reset']}")
|
165
|
-
elif renderer == 'glow':
|
166
|
-
print(f"{COLORS['yellow']}Warning: Glow is not available.{COLORS['reset']}")
|
167
|
-
print(f"{COLORS['yellow']}Install from https://github.com/charmbracelet/glow{COLORS['reset']}")
|
168
|
-
else:
|
169
|
-
print(f"{COLORS['yellow']}Error: Invalid renderer '{renderer}'. Use 'auto', 'rich', or 'glow'.{COLORS['reset']}")
|
170
|
-
|
171
|
-
return False
|
172
|
-
|
173
|
-
def prettify_markdown(text, renderer='auto'):
|
174
|
-
"""Render markdown text with beautiful formatting using either Rich or Glow.
|
175
|
-
|
176
|
-
The function handles both general markdown and code blocks with syntax highlighting.
|
177
|
-
For code generation mode, it automatically wraps the code in markdown code blocks.
|
178
|
-
|
179
|
-
Args:
|
180
|
-
text (str): Markdown text to render
|
181
|
-
renderer (str): Which renderer to use: 'auto', 'rich', or 'glow'
|
182
|
-
|
183
|
-
Returns:
|
184
|
-
bool: True if rendering was successful, False otherwise
|
185
|
-
"""
|
186
|
-
# For 'auto', prefer rich if available, otherwise use glow
|
187
|
-
if renderer == 'auto':
|
188
|
-
if HAS_RICH:
|
189
|
-
return prettify_markdown(text, 'rich')
|
190
|
-
elif HAS_GLOW:
|
191
|
-
return prettify_markdown(text, 'glow')
|
192
|
-
else:
|
193
|
-
return False
|
194
|
-
|
195
|
-
# Use glow for rendering
|
196
|
-
elif renderer == 'glow':
|
197
|
-
if not HAS_GLOW:
|
198
|
-
print(f"{COLORS['yellow']}Warning: Glow is not available. Install from https://github.com/charmbracelet/glow{COLORS['reset']}")
|
199
|
-
# Fall back to rich if available
|
200
|
-
if HAS_RICH:
|
201
|
-
print(f"{COLORS['yellow']}Falling back to Rich renderer.{COLORS['reset']}")
|
202
|
-
return prettify_markdown(text, 'rich')
|
203
|
-
return False
|
204
|
-
|
205
|
-
# Use glow
|
206
|
-
import tempfile
|
207
|
-
import subprocess
|
208
|
-
|
209
|
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as temp:
|
210
|
-
temp_filename = temp.name
|
211
|
-
temp.write(text)
|
212
|
-
|
213
|
-
try:
|
214
|
-
# Execute glow on the temporary file
|
215
|
-
subprocess.run(["glow", temp_filename], check=True)
|
216
|
-
os.unlink(temp_filename)
|
217
|
-
return True
|
218
|
-
except Exception as e:
|
219
|
-
print(f"{COLORS['yellow']}Error using glow: {str(e)}{COLORS['reset']}")
|
220
|
-
os.unlink(temp_filename)
|
221
|
-
|
222
|
-
# Fall back to rich if available
|
223
|
-
if HAS_RICH:
|
224
|
-
print(f"{COLORS['yellow']}Falling back to Rich renderer.{COLORS['reset']}")
|
225
|
-
return prettify_markdown(text, 'rich')
|
226
|
-
return False
|
227
|
-
|
228
|
-
# Use rich for rendering
|
229
|
-
elif renderer == 'rich':
|
230
|
-
if not HAS_RICH:
|
231
|
-
print(f"{COLORS['yellow']}Warning: Rich is not available.{COLORS['reset']}")
|
232
|
-
print(f"{COLORS['yellow']}Install with: pip install \"ngpt[full]\" or pip install rich{COLORS['reset']}")
|
233
|
-
# Fall back to glow if available
|
234
|
-
if HAS_GLOW:
|
235
|
-
print(f"{COLORS['yellow']}Falling back to Glow renderer.{COLORS['reset']}")
|
236
|
-
return prettify_markdown(text, 'glow')
|
237
|
-
return False
|
238
|
-
|
239
|
-
# Use rich
|
240
|
-
try:
|
241
|
-
console = Console()
|
242
|
-
md = Markdown(text)
|
243
|
-
console.print(md)
|
244
|
-
return True
|
245
|
-
except Exception as e:
|
246
|
-
print(f"{COLORS['yellow']}Error using rich for markdown: {str(e)}{COLORS['reset']}")
|
247
|
-
return False
|
248
|
-
|
249
|
-
# Invalid renderer specified
|
250
|
-
else:
|
251
|
-
print(f"{COLORS['yellow']}Error: Invalid renderer '{renderer}'. Use 'auto', 'rich', or 'glow'.{COLORS['reset']}")
|
252
|
-
return False
|
253
|
-
|
254
|
-
# Custom help formatter with color support
|
255
|
-
class ColoredHelpFormatter(argparse.HelpFormatter):
|
256
|
-
"""Help formatter that properly handles ANSI color codes without breaking alignment."""
|
257
|
-
|
258
|
-
def __init__(self, prog):
|
259
|
-
# Import modules needed for terminal size detection
|
260
|
-
import re
|
261
|
-
import textwrap
|
262
|
-
import shutil
|
263
|
-
|
264
|
-
# Get terminal size for dynamic width adjustment
|
265
|
-
try:
|
266
|
-
self.term_width = shutil.get_terminal_size().columns
|
267
|
-
except:
|
268
|
-
self.term_width = 80 # Default if we can't detect terminal width
|
269
|
-
|
270
|
-
# Calculate dynamic layout values based on terminal width
|
271
|
-
self.formatter_width = self.term_width - 2 # Leave some margin
|
272
|
-
|
273
|
-
# For very wide terminals, limit the width to maintain readability
|
274
|
-
if self.formatter_width > 120:
|
275
|
-
self.formatter_width = 120
|
276
|
-
|
277
|
-
# Calculate help position based on terminal width (roughly 1/3 of width)
|
278
|
-
self.help_position = min(max(20, int(self.term_width * 0.33)), 36)
|
279
|
-
|
280
|
-
# Initialize the parent class with dynamic values
|
281
|
-
super().__init__(prog, max_help_position=self.help_position, width=self.formatter_width)
|
282
|
-
|
283
|
-
# Calculate wrap width based on remaining space after help position
|
284
|
-
self.wrap_width = self.formatter_width - self.help_position - 5
|
285
|
-
|
286
|
-
# Set up the text wrapper for help text
|
287
|
-
self.ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
288
|
-
self.wrapper = textwrap.TextWrapper(width=self.wrap_width)
|
289
|
-
|
290
|
-
def _strip_ansi(self, s):
|
291
|
-
"""Strip ANSI escape sequences for width calculations"""
|
292
|
-
return self.ansi_escape.sub('', s)
|
293
|
-
|
294
|
-
def _colorize(self, text, color, bold=False):
|
295
|
-
"""Helper to consistently apply color with optional bold"""
|
296
|
-
if bold:
|
297
|
-
return f"{COLORS['bold']}{COLORS[color]}{text}{COLORS['reset']}"
|
298
|
-
return f"{COLORS[color]}{text}{COLORS['reset']}"
|
299
|
-
|
300
|
-
def _format_action_invocation(self, action):
|
301
|
-
if not action.option_strings:
|
302
|
-
# For positional arguments
|
303
|
-
metavar = self._format_args(action, action.dest.upper())
|
304
|
-
return self._colorize(metavar, 'cyan', bold=True)
|
305
|
-
else:
|
306
|
-
# For optional arguments with different color for metavar
|
307
|
-
if action.nargs != argparse.SUPPRESS:
|
308
|
-
default = self._get_default_metavar_for_optional(action)
|
309
|
-
args_string = self._format_args(action, default)
|
310
|
-
|
311
|
-
# Color option name and metavar differently
|
312
|
-
option_part = ', '.join(action.option_strings)
|
313
|
-
colored_option = self._colorize(option_part, 'cyan', bold=True)
|
314
|
-
|
315
|
-
if args_string:
|
316
|
-
# Make metavars more visible with brackets and color
|
317
|
-
# If HAS_COLOR is False, brackets will help in PowerShell
|
318
|
-
if not HAS_COLOR:
|
319
|
-
# Add brackets to make metavars stand out even without color
|
320
|
-
formatted_args = f"<{args_string}>"
|
321
|
-
else:
|
322
|
-
# Use color for metavar
|
323
|
-
formatted_args = self._colorize(args_string, 'magenta')
|
324
|
-
|
325
|
-
return f"{colored_option} {formatted_args}"
|
326
|
-
else:
|
327
|
-
return colored_option
|
328
|
-
else:
|
329
|
-
return self._colorize(', '.join(action.option_strings), 'cyan', bold=True)
|
330
|
-
|
331
|
-
def _format_usage(self, usage, actions, groups, prefix):
|
332
|
-
usage_text = super()._format_usage(usage, actions, groups, prefix)
|
333
|
-
|
334
|
-
# Replace "usage:" with colored version
|
335
|
-
colored_usage = self._colorize("usage:", 'green', bold=True)
|
336
|
-
usage_text = usage_text.replace("usage:", colored_usage)
|
337
|
-
|
338
|
-
# We won't color metavars in usage text as it breaks the formatting
|
339
|
-
# Just return with the colored usage prefix
|
340
|
-
return usage_text
|
341
|
-
|
342
|
-
def _join_parts(self, part_strings):
|
343
|
-
"""Override to fix any potential formatting issues with section joins"""
|
344
|
-
return '\n'.join([part for part in part_strings if part])
|
345
|
-
|
346
|
-
def start_section(self, heading):
|
347
|
-
# Remove the colon as we'll add it with color
|
348
|
-
if heading.endswith(':'):
|
349
|
-
heading = heading[:-1]
|
350
|
-
heading_text = f"{self._colorize(heading, 'yellow', bold=True)}:"
|
351
|
-
super().start_section(heading_text)
|
352
|
-
|
353
|
-
def _get_help_string(self, action):
|
354
|
-
# Add color to help strings
|
355
|
-
help_text = action.help
|
356
|
-
if help_text:
|
357
|
-
return help_text.replace('(default:', f"{COLORS['gray']}(default:") + COLORS['reset']
|
358
|
-
return help_text
|
359
|
-
|
360
|
-
def _wrap_help_text(self, text, initial_indent="", subsequent_indent=" "):
|
361
|
-
"""Wrap long help text to prevent overflow"""
|
362
|
-
if not text:
|
363
|
-
return text
|
364
|
-
|
365
|
-
# Strip ANSI codes for width calculation
|
366
|
-
clean_text = self._strip_ansi(text)
|
367
|
-
|
368
|
-
# If the text is already short enough, return it as is
|
369
|
-
if len(clean_text) <= self.wrap_width:
|
370
|
-
return text
|
371
|
-
|
372
|
-
# Handle any existing ANSI codes
|
373
|
-
has_ansi = text != clean_text
|
374
|
-
wrap_text = clean_text
|
375
|
-
|
376
|
-
# Wrap the text
|
377
|
-
lines = self.wrapper.wrap(wrap_text)
|
378
|
-
|
379
|
-
# Add indentation to all but the first line
|
380
|
-
wrapped = lines[0]
|
381
|
-
for line in lines[1:]:
|
382
|
-
wrapped += f"\n{subsequent_indent}{line}"
|
383
|
-
|
384
|
-
# Re-add the ANSI codes if they were present
|
385
|
-
if has_ansi and text.endswith(COLORS['reset']):
|
386
|
-
wrapped += COLORS['reset']
|
387
|
-
|
388
|
-
return wrapped
|
389
|
-
|
390
|
-
def _format_action(self, action):
|
391
|
-
# For subparsers, just return the regular formatting
|
392
|
-
if isinstance(action, argparse._SubParsersAction):
|
393
|
-
return super()._format_action(action)
|
394
|
-
|
395
|
-
# Get the action header with colored parts (both option names and metavars)
|
396
|
-
# The coloring is now done in _format_action_invocation
|
397
|
-
action_header = self._format_action_invocation(action)
|
398
|
-
|
399
|
-
# Format help text
|
400
|
-
help_text = self._expand_help(action)
|
401
|
-
|
402
|
-
# Get the raw lengths without ANSI codes for formatting
|
403
|
-
raw_header_len = len(self._strip_ansi(action_header))
|
404
|
-
|
405
|
-
# Calculate the indent for the help text
|
406
|
-
help_position = min(self._action_max_length + 2, self._max_help_position)
|
407
|
-
help_indent = ' ' * help_position
|
408
|
-
|
409
|
-
# If the action header is too long, put help on the next line
|
410
|
-
if raw_header_len > help_position:
|
411
|
-
# An action header that's too long gets a line break
|
412
|
-
# Wrap the help text with proper indentation
|
413
|
-
wrapped_help = self._wrap_help_text(help_text, subsequent_indent=help_indent)
|
414
|
-
line = f"{action_header}\n{help_indent}{wrapped_help}"
|
415
|
-
else:
|
416
|
-
# Standard formatting with proper spacing
|
417
|
-
padding = ' ' * (help_position - raw_header_len)
|
418
|
-
# Wrap the help text with proper indentation
|
419
|
-
wrapped_help = self._wrap_help_text(help_text, subsequent_indent=help_indent)
|
420
|
-
line = f"{action_header}{padding}{wrapped_help}"
|
421
|
-
|
422
|
-
# Handle subactions
|
423
|
-
if action.help is argparse.SUPPRESS:
|
424
|
-
return line
|
425
|
-
|
426
|
-
if not action.help:
|
427
|
-
return line
|
428
|
-
|
429
|
-
return line
|
430
|
-
|
431
|
-
# Optional imports for enhanced UI
|
432
|
-
try:
|
433
|
-
from prompt_toolkit import prompt as pt_prompt
|
434
|
-
from prompt_toolkit.styles import Style
|
435
|
-
from prompt_toolkit.key_binding import KeyBindings
|
436
|
-
from prompt_toolkit.formatted_text import HTML
|
437
|
-
from prompt_toolkit.layout.containers import HSplit, Window
|
438
|
-
from prompt_toolkit.layout.layout import Layout
|
439
|
-
from prompt_toolkit.layout.controls import FormattedTextControl
|
440
|
-
from prompt_toolkit.application import Application
|
441
|
-
from prompt_toolkit.widgets import TextArea
|
442
|
-
from prompt_toolkit.layout.margins import ScrollbarMargin
|
443
|
-
from prompt_toolkit.filters import to_filter
|
444
|
-
from prompt_toolkit.history import InMemoryHistory
|
445
|
-
import shutil
|
446
|
-
HAS_PROMPT_TOOLKIT = True
|
447
|
-
except ImportError:
|
448
|
-
HAS_PROMPT_TOOLKIT = False
|
449
|
-
|
450
|
-
def show_config_help():
|
451
|
-
"""Display help information about configuration."""
|
452
|
-
print(f"\n{COLORS['green']}{COLORS['bold']}Configuration Help:{COLORS['reset']}")
|
453
|
-
print(f" 1. {COLORS['cyan']}Create a config file at one of these locations:{COLORS['reset']}")
|
454
|
-
if sys.platform == "win32":
|
455
|
-
print(f" - {COLORS['yellow']}%APPDATA%\\ngpt\\ngpt.conf{COLORS['reset']}")
|
456
|
-
elif sys.platform == "darwin":
|
457
|
-
print(f" - {COLORS['yellow']}~/Library/Application Support/ngpt/ngpt.conf{COLORS['reset']}")
|
458
|
-
else:
|
459
|
-
print(f" - {COLORS['yellow']}~/.config/ngpt/ngpt.conf{COLORS['reset']}")
|
460
|
-
|
461
|
-
print(f" 2. {COLORS['cyan']}Format your config file as JSON:{COLORS['reset']}")
|
462
|
-
print(f"""{COLORS['yellow']} [
|
463
|
-
{{
|
464
|
-
"api_key": "your-api-key-here",
|
465
|
-
"base_url": "https://api.openai.com/v1/",
|
466
|
-
"provider": "OpenAI",
|
467
|
-
"model": "gpt-3.5-turbo"
|
468
|
-
}},
|
469
|
-
{{
|
470
|
-
"api_key": "your-second-api-key",
|
471
|
-
"base_url": "http://localhost:1337/v1/",
|
472
|
-
"provider": "Another Provider",
|
473
|
-
"model": "different-model"
|
474
|
-
}}
|
475
|
-
]{COLORS['reset']}""")
|
476
|
-
|
477
|
-
print(f" 3. {COLORS['cyan']}Or set environment variables:{COLORS['reset']}")
|
478
|
-
print(f" - {COLORS['yellow']}OPENAI_API_KEY{COLORS['reset']}")
|
479
|
-
print(f" - {COLORS['yellow']}OPENAI_BASE_URL{COLORS['reset']}")
|
480
|
-
print(f" - {COLORS['yellow']}OPENAI_MODEL{COLORS['reset']}")
|
481
|
-
|
482
|
-
print(f" 4. {COLORS['cyan']}Or provide command line arguments:{COLORS['reset']}")
|
483
|
-
print(f" {COLORS['yellow']}ngpt --api-key your-key --base-url https://api.example.com --model your-model \"Your prompt\"{COLORS['reset']}")
|
484
|
-
|
485
|
-
print(f" 5. {COLORS['cyan']}Use --config-index to specify which configuration to use or edit:{COLORS['reset']}")
|
486
|
-
print(f" {COLORS['yellow']}ngpt --config-index 1 \"Your prompt\"{COLORS['reset']}")
|
487
|
-
|
488
|
-
print(f" 6. {COLORS['cyan']}Use --provider to specify which configuration to use by provider name:{COLORS['reset']}")
|
489
|
-
print(f" {COLORS['yellow']}ngpt --provider Gemini \"Your prompt\"{COLORS['reset']}")
|
490
|
-
|
491
|
-
print(f" 7. {COLORS['cyan']}Use --config without arguments to add a new configuration:{COLORS['reset']}")
|
492
|
-
print(f" {COLORS['yellow']}ngpt --config{COLORS['reset']}")
|
493
|
-
print(f" Or specify an index or provider to edit an existing configuration:")
|
494
|
-
print(f" {COLORS['yellow']}ngpt --config --config-index 1{COLORS['reset']}")
|
495
|
-
print(f" {COLORS['yellow']}ngpt --config --provider Gemini{COLORS['reset']}")
|
496
|
-
|
497
|
-
print(f" 8. {COLORS['cyan']}Remove a configuration by index or provider:{COLORS['reset']}")
|
498
|
-
print(f" {COLORS['yellow']}ngpt --config --remove --config-index 1{COLORS['reset']}")
|
499
|
-
print(f" {COLORS['yellow']}ngpt --config --remove --provider Gemini{COLORS['reset']}")
|
500
|
-
|
501
|
-
print(f" 9. {COLORS['cyan']}List available models for the current configuration:{COLORS['reset']}")
|
502
|
-
print(f" {COLORS['yellow']}ngpt --list-models{COLORS['reset']}")
|
503
|
-
|
504
|
-
def check_config(config):
|
505
|
-
"""Check config for common issues and provide guidance."""
|
506
|
-
if not config.get("api_key"):
|
507
|
-
print(f"{COLORS['yellow']}{COLORS['bold']}Error: API key is not set.{COLORS['reset']}")
|
508
|
-
show_config_help()
|
509
|
-
return False
|
510
|
-
|
511
|
-
# Check for common URL mistakes
|
512
|
-
base_url = config.get("base_url", "")
|
513
|
-
if base_url and not (base_url.startswith("http://") or base_url.startswith("https://")):
|
514
|
-
print(f"{COLORS['yellow']}Warning: Base URL '{base_url}' doesn't start with http:// or https://{COLORS['reset']}")
|
515
|
-
|
516
|
-
return True
|
517
|
-
|
518
|
-
def interactive_chat_session(client, web_search=False, no_stream=False, temperature=0.7, top_p=1.0, max_tokens=None, log_file=None, preprompt=None, prettify=False, renderer='auto', stream_prettify=False):
|
519
|
-
"""Start an interactive chat session with the AI.
|
520
|
-
|
521
|
-
Args:
|
522
|
-
client: The NGPTClient instance
|
523
|
-
web_search: Whether to enable web search capability
|
524
|
-
no_stream: Whether to disable streaming
|
525
|
-
temperature: Controls randomness in the response
|
526
|
-
top_p: Controls diversity via nucleus sampling
|
527
|
-
max_tokens: Maximum number of tokens to generate in each response
|
528
|
-
log_file: Optional filepath to log conversation to
|
529
|
-
preprompt: Custom system prompt to control AI behavior
|
530
|
-
prettify: Whether to enable markdown rendering
|
531
|
-
renderer: Which markdown renderer to use
|
532
|
-
stream_prettify: Whether to enable streaming with prettify
|
533
|
-
"""
|
534
|
-
# Get terminal width for better formatting
|
535
|
-
try:
|
536
|
-
term_width = shutil.get_terminal_size().columns
|
537
|
-
except:
|
538
|
-
term_width = 80 # Default fallback
|
539
|
-
|
540
|
-
# Improved visual header with better layout
|
541
|
-
header = f"{COLORS['cyan']}{COLORS['bold']}🤖 nGPT Interactive Chat Session 🤖{COLORS['reset']}"
|
542
|
-
print(f"\n{header}")
|
543
|
-
|
544
|
-
# Create a separator line - use a consistent separator length for all lines
|
545
|
-
separator_length = min(40, term_width - 10)
|
546
|
-
separator = f"{COLORS['gray']}{'─' * separator_length}{COLORS['reset']}"
|
547
|
-
print(separator)
|
548
|
-
|
549
|
-
# Group commands into categories with better formatting
|
550
|
-
print(f"\n{COLORS['cyan']}Navigation:{COLORS['reset']}")
|
551
|
-
print(f" {COLORS['yellow']}↑/↓{COLORS['reset']} : Browse input history")
|
552
|
-
|
553
|
-
print(f"\n{COLORS['cyan']}Session Commands:{COLORS['reset']}")
|
554
|
-
print(f" {COLORS['yellow']}history{COLORS['reset']} : Show conversation history")
|
555
|
-
print(f" {COLORS['yellow']}clear{COLORS['reset']} : Reset conversation")
|
556
|
-
print(f" {COLORS['yellow']}exit{COLORS['reset']} : End session")
|
557
|
-
|
558
|
-
print(f"\n{separator}\n")
|
559
|
-
|
560
|
-
# Initialize log file if provided
|
561
|
-
log_handle = None
|
562
|
-
if log_file:
|
563
|
-
try:
|
564
|
-
import datetime
|
565
|
-
timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
566
|
-
log_handle = open(log_file, 'a', encoding='utf-8')
|
567
|
-
log_handle.write(f"\n--- nGPT Session Log: {sys.argv} ---\n")
|
568
|
-
log_handle.write(f"Started at: {timestamp}\n\n")
|
569
|
-
print(f"{COLORS['green']}Logging conversation to: {log_file}{COLORS['reset']}")
|
570
|
-
except Exception as e:
|
571
|
-
print(f"{COLORS['yellow']}Warning: Could not open log file: {str(e)}{COLORS['reset']}")
|
572
|
-
log_handle = None
|
573
|
-
|
574
|
-
# Custom separator - use the same length for consistency
|
575
|
-
def print_separator():
|
576
|
-
print(f"\n{separator}\n")
|
577
|
-
|
578
|
-
# Initialize conversation history
|
579
|
-
system_prompt = preprompt if preprompt else "You are a helpful assistant."
|
580
|
-
|
581
|
-
# Add markdown formatting instruction to system prompt if prettify is enabled
|
582
|
-
if prettify:
|
583
|
-
if system_prompt:
|
584
|
-
system_prompt += " You can use markdown formatting in your responses where appropriate."
|
585
|
-
else:
|
586
|
-
system_prompt = "You are a helpful assistant. You can use markdown formatting in your responses where appropriate."
|
587
|
-
|
588
|
-
conversation = []
|
589
|
-
system_message = {"role": "system", "content": system_prompt}
|
590
|
-
conversation.append(system_message)
|
591
|
-
|
592
|
-
# Log system prompt if logging is enabled
|
593
|
-
if log_handle and preprompt:
|
594
|
-
log_handle.write(f"System: {system_prompt}\n\n")
|
595
|
-
log_handle.flush()
|
596
|
-
|
597
|
-
# Initialize prompt_toolkit history
|
598
|
-
prompt_history = InMemoryHistory() if HAS_PROMPT_TOOLKIT else None
|
599
|
-
|
600
|
-
# Decorative chat headers with rounded corners
|
601
|
-
def user_header():
|
602
|
-
return f"{COLORS['cyan']}{COLORS['bold']}╭─ 👤 You {COLORS['reset']}"
|
603
|
-
|
604
|
-
def ngpt_header():
|
605
|
-
return f"{COLORS['green']}{COLORS['bold']}╭─ 🤖 nGPT {COLORS['reset']}"
|
606
|
-
|
607
|
-
# Function to display conversation history
|
608
|
-
def display_history():
|
609
|
-
if len(conversation) <= 1: # Only system message
|
610
|
-
print(f"\n{COLORS['yellow']}No conversation history yet.{COLORS['reset']}")
|
611
|
-
return
|
612
|
-
|
613
|
-
print(f"\n{COLORS['cyan']}{COLORS['bold']}Conversation History:{COLORS['reset']}")
|
614
|
-
print(separator)
|
615
|
-
|
616
|
-
# Skip system message
|
617
|
-
message_count = 0
|
618
|
-
for i, msg in enumerate(conversation):
|
619
|
-
if msg["role"] == "system":
|
620
|
-
continue
|
621
|
-
|
622
|
-
if msg["role"] == "user":
|
623
|
-
message_count += 1
|
624
|
-
print(f"\n{user_header()}")
|
625
|
-
print(f"{COLORS['cyan']}│ [{message_count}] {COLORS['reset']}{msg['content']}")
|
626
|
-
elif msg["role"] == "assistant":
|
627
|
-
print(f"\n{ngpt_header()}")
|
628
|
-
print(f"{COLORS['green']}│ {COLORS['reset']}{msg['content']}")
|
629
|
-
|
630
|
-
print(f"\n{separator}") # Consistent separator at the end
|
631
|
-
|
632
|
-
# Function to clear conversation history
|
633
|
-
def clear_history():
|
634
|
-
nonlocal conversation
|
635
|
-
conversation = [{"role": "system", "content": system_prompt}]
|
636
|
-
print(f"\n{COLORS['yellow']}Conversation history cleared.{COLORS['reset']}")
|
637
|
-
print(separator) # Add separator for consistency
|
638
|
-
|
639
|
-
try:
|
640
|
-
while True:
|
641
|
-
# Get user input
|
642
|
-
if HAS_PROMPT_TOOLKIT:
|
643
|
-
# Custom styling for prompt_toolkit
|
644
|
-
style = Style.from_dict({
|
645
|
-
'prompt': 'ansicyan bold',
|
646
|
-
'input': 'ansiwhite',
|
647
|
-
})
|
648
|
-
|
649
|
-
# Create key bindings for Ctrl+C handling
|
650
|
-
kb = KeyBindings()
|
651
|
-
@kb.add('c-c')
|
652
|
-
def _(event):
|
653
|
-
event.app.exit(result=None)
|
654
|
-
raise KeyboardInterrupt()
|
655
|
-
|
656
|
-
# Get user input with styled prompt - using proper HTML formatting
|
657
|
-
user_input = pt_prompt(
|
658
|
-
HTML("<ansicyan><b>╭─ 👤 You:</b></ansicyan> "),
|
659
|
-
style=style,
|
660
|
-
key_bindings=kb,
|
661
|
-
history=prompt_history
|
662
|
-
)
|
663
|
-
else:
|
664
|
-
user_input = input(f"{user_header()}: {COLORS['reset']}")
|
665
|
-
|
666
|
-
# Check for exit commands
|
667
|
-
if user_input.lower() in ('exit', 'quit', 'bye'):
|
668
|
-
print(f"\n{COLORS['green']}Ending chat session. Goodbye!{COLORS['reset']}")
|
669
|
-
break
|
670
|
-
|
671
|
-
# Check for special commands
|
672
|
-
if user_input.lower() == 'history':
|
673
|
-
display_history()
|
674
|
-
continue
|
675
|
-
|
676
|
-
if user_input.lower() == 'clear':
|
677
|
-
clear_history()
|
678
|
-
continue
|
679
|
-
|
680
|
-
# Skip empty messages but don't raise an error
|
681
|
-
if not user_input.strip():
|
682
|
-
print(f"{COLORS['yellow']}Empty message skipped. Type 'exit' to quit.{COLORS['reset']}")
|
683
|
-
continue
|
684
|
-
|
685
|
-
# Add user message to conversation
|
686
|
-
user_message = {"role": "user", "content": user_input}
|
687
|
-
conversation.append(user_message)
|
688
|
-
|
689
|
-
# Log user message if logging is enabled
|
690
|
-
if log_handle:
|
691
|
-
log_handle.write(f"User: {user_input}\n")
|
692
|
-
log_handle.flush()
|
693
|
-
|
694
|
-
# Print assistant indicator with formatting
|
695
|
-
if not no_stream and not stream_prettify:
|
696
|
-
print(f"\n{ngpt_header()}: {COLORS['reset']}", end="", flush=True)
|
697
|
-
elif not stream_prettify:
|
698
|
-
print(f"\n{ngpt_header()}: {COLORS['reset']}", flush=True)
|
699
|
-
|
700
|
-
# If prettify is enabled with regular streaming
|
701
|
-
if prettify and not no_stream and not stream_prettify:
|
702
|
-
print(f"\n{COLORS['yellow']}Note: Streaming disabled to enable markdown rendering.{COLORS['reset']}")
|
703
|
-
print(f"\n{ngpt_header()}: {COLORS['reset']}", flush=True)
|
704
|
-
should_stream = False
|
705
|
-
else:
|
706
|
-
# Regular behavior with stream-prettify taking precedence
|
707
|
-
should_stream = not no_stream
|
708
|
-
|
709
|
-
# Setup for stream-prettify
|
710
|
-
stream_callback = None
|
711
|
-
live_display = None
|
712
|
-
|
713
|
-
if stream_prettify and should_stream:
|
714
|
-
# Get the correct header for interactive mode
|
715
|
-
header = ngpt_header()
|
716
|
-
live_display, stream_callback = prettify_streaming_markdown(renderer, is_interactive=True, header_text=header)
|
717
|
-
if not live_display:
|
718
|
-
# Fallback to normal prettify if live display setup failed
|
719
|
-
prettify = True
|
720
|
-
stream_prettify = False
|
721
|
-
should_stream = False
|
722
|
-
print(f"{COLORS['yellow']}Falling back to regular prettify mode.{COLORS['reset']}")
|
723
|
-
|
724
|
-
# Start live display if using stream-prettify
|
725
|
-
if stream_prettify and live_display:
|
726
|
-
live_display.start()
|
727
|
-
|
728
|
-
# Get AI response with conversation history
|
729
|
-
response = client.chat(
|
730
|
-
prompt=user_input,
|
731
|
-
messages=conversation,
|
732
|
-
stream=should_stream,
|
733
|
-
web_search=web_search,
|
734
|
-
temperature=temperature,
|
735
|
-
top_p=top_p,
|
736
|
-
max_tokens=max_tokens,
|
737
|
-
markdown_format=prettify or stream_prettify,
|
738
|
-
stream_callback=stream_callback
|
739
|
-
)
|
740
|
-
|
741
|
-
# Stop live display if using stream-prettify
|
742
|
-
if stream_prettify and live_display:
|
743
|
-
live_display.stop()
|
744
|
-
|
745
|
-
# Add AI response to conversation history
|
746
|
-
if response:
|
747
|
-
assistant_message = {"role": "assistant", "content": response}
|
748
|
-
conversation.append(assistant_message)
|
749
|
-
|
750
|
-
# Print response if not streamed (either due to no_stream or prettify)
|
751
|
-
if no_stream or prettify:
|
752
|
-
if prettify:
|
753
|
-
prettify_markdown(response, renderer)
|
754
|
-
else:
|
755
|
-
print(response)
|
756
|
-
|
757
|
-
# Log assistant response if logging is enabled
|
758
|
-
if log_handle:
|
759
|
-
log_handle.write(f"Assistant: {response}\n\n")
|
760
|
-
log_handle.flush()
|
761
|
-
|
762
|
-
# Print separator between exchanges
|
763
|
-
print_separator()
|
764
|
-
|
765
|
-
except KeyboardInterrupt:
|
766
|
-
print(f"\n\n{COLORS['green']}Chat session ended by user. Goodbye!{COLORS['reset']}")
|
767
|
-
except Exception as e:
|
768
|
-
print(f"\n{COLORS['yellow']}Error during chat session: {str(e)}{COLORS['reset']}")
|
769
|
-
# Print traceback for debugging if it's a serious error
|
770
|
-
import traceback
|
771
|
-
traceback.print_exc()
|
772
|
-
finally:
|
773
|
-
# Close log file if it was opened
|
774
|
-
if log_handle:
|
775
|
-
log_handle.write(f"\n--- End of Session ---\n")
|
776
|
-
log_handle.close()
|
777
|
-
|
778
|
-
def prettify_streaming_markdown(renderer='rich', is_interactive=False, header_text=None):
|
779
|
-
"""Set up streaming markdown rendering.
|
780
|
-
|
781
|
-
This function creates a live display context for rendering markdown
|
782
|
-
that can be updated in real-time as streaming content arrives.
|
783
|
-
|
784
|
-
Args:
|
785
|
-
renderer (str): Which renderer to use (currently only 'rich' is supported for streaming)
|
786
|
-
is_interactive (bool): Whether this is being used in interactive mode
|
787
|
-
header_text (str): Header text to include at the top (for interactive mode)
|
788
|
-
|
789
|
-
Returns:
|
790
|
-
tuple: (live_display, update_function) if successful, (None, None) otherwise
|
791
|
-
"""
|
792
|
-
# Only warn if explicitly specifying a renderer other than 'rich' or 'auto'
|
793
|
-
if renderer != 'rich' and renderer != 'auto':
|
794
|
-
print(f"{COLORS['yellow']}Warning: Streaming prettify only supports 'rich' renderer currently.{COLORS['reset']}")
|
795
|
-
print(f"{COLORS['yellow']}Falling back to Rich renderer.{COLORS['reset']}")
|
796
|
-
|
797
|
-
# Always use rich for streaming prettify
|
798
|
-
renderer = 'rich'
|
799
|
-
|
800
|
-
if not HAS_RICH:
|
801
|
-
print(f"{COLORS['yellow']}Warning: Rich is not available for streaming prettify.{COLORS['reset']}")
|
802
|
-
print(f"{COLORS['yellow']}Install with: pip install \"ngpt[full]\" or pip install rich{COLORS['reset']}")
|
803
|
-
return None, None
|
804
|
-
|
805
|
-
try:
|
806
|
-
from rich.live import Live
|
807
|
-
from rich.markdown import Markdown
|
808
|
-
from rich.console import Console
|
809
|
-
from rich.text import Text
|
810
|
-
from rich.panel import Panel
|
811
|
-
import rich.box
|
812
|
-
|
813
|
-
console = Console()
|
814
|
-
|
815
|
-
# Create an empty markdown object to start with
|
816
|
-
if is_interactive and header_text:
|
817
|
-
# For interactive mode, include header in a panel
|
818
|
-
# Clean up the header text to avoid duplication - use just "🤖 nGPT" instead of "╭─ 🤖 nGPT"
|
819
|
-
clean_header = "🤖 nGPT"
|
820
|
-
panel_title = Text(clean_header, style="cyan bold")
|
821
|
-
|
822
|
-
# Create a nicer, more compact panel
|
823
|
-
padding = (1, 1) # Less horizontal padding (left, right)
|
824
|
-
md_obj = Panel(
|
825
|
-
Markdown(""),
|
826
|
-
title=panel_title,
|
827
|
-
title_align="left",
|
828
|
-
border_style="cyan",
|
829
|
-
padding=padding,
|
830
|
-
width=console.width - 4, # Make panel slightly narrower than console
|
831
|
-
box=rich.box.ROUNDED
|
832
|
-
)
|
833
|
-
else:
|
834
|
-
md_obj = Markdown("")
|
835
|
-
|
836
|
-
# Initialize the Live display with an empty markdown
|
837
|
-
live = Live(md_obj, console=console, refresh_per_second=10)
|
838
|
-
|
839
|
-
# Define an update function that will be called with new content
|
840
|
-
def update_content(content):
|
841
|
-
nonlocal md_obj
|
842
|
-
if is_interactive and header_text:
|
843
|
-
# Update the panel content
|
844
|
-
md_obj.renderable = Markdown(content)
|
845
|
-
live.update(md_obj)
|
846
|
-
else:
|
847
|
-
md_obj = Markdown(content)
|
848
|
-
live.update(md_obj)
|
849
|
-
|
850
|
-
return live, update_content
|
851
|
-
except Exception as e:
|
852
|
-
print(f"{COLORS['yellow']}Error setting up Rich streaming display: {str(e)}{COLORS['reset']}")
|
853
|
-
return None, None
|
854
|
-
|
855
|
-
def show_cli_config_help():
|
856
|
-
"""Display help information about CLI configuration."""
|
857
|
-
print(f"\n{COLORS['green']}{COLORS['bold']}CLI Configuration Help:{COLORS['reset']}")
|
858
|
-
print(f" {COLORS['cyan']}Command syntax:{COLORS['reset']}")
|
859
|
-
print(f" {COLORS['yellow']}ngpt --cli-config set OPTION VALUE{COLORS['reset']} - Set a default value for OPTION")
|
860
|
-
print(f" {COLORS['yellow']}ngpt --cli-config get OPTION{COLORS['reset']} - Get the current value of OPTION")
|
861
|
-
print(f" {COLORS['yellow']}ngpt --cli-config get{COLORS['reset']} - Show all CLI configuration settings")
|
862
|
-
print(f" {COLORS['yellow']}ngpt --cli-config unset OPTION{COLORS['reset']} - Remove OPTION from configuration")
|
863
|
-
print(f" {COLORS['yellow']}ngpt --cli-config list{COLORS['reset']} - List all available options")
|
864
|
-
|
865
|
-
print(f"\n {COLORS['cyan']}Available options:{COLORS['reset']}")
|
866
|
-
|
867
|
-
# Group options by context
|
868
|
-
context_groups = {
|
869
|
-
"all": [],
|
870
|
-
"code": [],
|
871
|
-
"interactive": [],
|
872
|
-
"text": [],
|
873
|
-
"shell": []
|
874
|
-
}
|
875
|
-
|
876
|
-
for option, meta in CLI_CONFIG_OPTIONS.items():
|
877
|
-
for context in meta["context"]:
|
878
|
-
if context in context_groups:
|
879
|
-
if context == "all":
|
880
|
-
context_groups[context].append(option)
|
881
|
-
break
|
882
|
-
else:
|
883
|
-
context_groups[context].append(option)
|
884
|
-
|
885
|
-
# Print general options (available in all contexts)
|
886
|
-
print(f" {COLORS['yellow']}General options (all modes):{COLORS['reset']}")
|
887
|
-
for option in sorted(context_groups["all"]):
|
888
|
-
meta = CLI_CONFIG_OPTIONS[option]
|
889
|
-
default = f"(default: {meta['default']})" if meta['default'] is not None else ""
|
890
|
-
exclusive = f" [exclusive with: {', '.join(meta['exclusive'])}]" if "exclusive" in meta else ""
|
891
|
-
print(f" {COLORS['green']}{option}{COLORS['reset']} - {meta['type']} {default}{exclusive}")
|
892
|
-
|
893
|
-
# Print mode-specific options
|
894
|
-
for mode, options in [
|
895
|
-
("code", "Code generation mode"),
|
896
|
-
("interactive", "Interactive mode"),
|
897
|
-
("text", "Text mode"),
|
898
|
-
("shell", "Shell mode")
|
899
|
-
]:
|
900
|
-
if context_groups[mode]:
|
901
|
-
print(f"\n {COLORS['yellow']}Options for {options}:{COLORS['reset']}")
|
902
|
-
for option in sorted(context_groups[mode]):
|
903
|
-
# Skip if already listed in general options
|
904
|
-
if option in context_groups["all"]:
|
905
|
-
continue
|
906
|
-
meta = CLI_CONFIG_OPTIONS[option]
|
907
|
-
default = f"(default: {meta['default']})" if meta['default'] is not None else ""
|
908
|
-
exclusive = f" [exclusive with: {', '.join(meta['exclusive'])}]" if "exclusive" in meta else ""
|
909
|
-
print(f" {COLORS['green']}{option}{COLORS['reset']} - {meta['type']} {default}{exclusive}")
|
910
|
-
|
911
|
-
print(f"\n {COLORS['cyan']}Example usage:{COLORS['reset']}")
|
912
|
-
print(f" {COLORS['yellow']}ngpt --cli-config set language java{COLORS['reset']} - Set default language to java for code generation")
|
913
|
-
print(f" {COLORS['yellow']}ngpt --cli-config set temperature 0.9{COLORS['reset']} - Set default temperature to 0.9")
|
914
|
-
print(f" {COLORS['yellow']}ngpt --cli-config set no-stream true{COLORS['reset']} - Disable streaming by default")
|
915
|
-
print(f" {COLORS['yellow']}ngpt --cli-config unset language{COLORS['reset']} - Remove language setting")
|
916
|
-
|
917
|
-
print(f"\n {COLORS['cyan']}Notes:{COLORS['reset']}")
|
918
|
-
print(f" - CLI configuration is stored in {COLORS['yellow']}~/.config/ngpt/ngpt-cli.conf{COLORS['reset']} (or equivalent for your OS)")
|
919
|
-
print(f" - Settings are applied based on context (e.g., language only applies to code generation mode)")
|
920
|
-
print(f" - Command-line arguments always override CLI configuration")
|
921
|
-
print(f" - Some options are mutually exclusive and will not be applied together")
|
922
|
-
|
923
|
-
def handle_cli_config(action, option=None, value=None):
|
924
|
-
"""Handle CLI configuration commands."""
|
925
|
-
if action == "list":
|
926
|
-
# List all available options
|
927
|
-
print(f"{COLORS['green']}{COLORS['bold']}Available CLI configuration options:{COLORS['reset']}")
|
928
|
-
for option in list_cli_config_options():
|
929
|
-
meta = CLI_CONFIG_OPTIONS[option]
|
930
|
-
default = f"(default: {meta['default']})" if meta['default'] is not None else ""
|
931
|
-
contexts = ', '.join(meta['context'])
|
932
|
-
if "all" in meta['context']:
|
933
|
-
contexts = "all modes"
|
934
|
-
print(f" {COLORS['cyan']}{option}{COLORS['reset']} - {meta['type']} {default} - Available in: {contexts}")
|
935
|
-
return
|
936
|
-
|
937
|
-
if action == "get":
|
938
|
-
if option is None:
|
939
|
-
# Get all options
|
940
|
-
success, config = get_cli_config_option()
|
941
|
-
if success and config:
|
942
|
-
print(f"{COLORS['green']}{COLORS['bold']}Current CLI configuration:{COLORS['reset']}")
|
943
|
-
for opt, val in config.items():
|
944
|
-
if opt in CLI_CONFIG_OPTIONS:
|
945
|
-
print(f" {COLORS['cyan']}{opt}{COLORS['reset']} = {val}")
|
946
|
-
else:
|
947
|
-
print(f" {COLORS['yellow']}{opt}{COLORS['reset']} = {val} (unknown option)")
|
948
|
-
else:
|
949
|
-
print(f"{COLORS['yellow']}No CLI configuration set. Use 'ngpt --cli-config set OPTION VALUE' to set options.{COLORS['reset']}")
|
950
|
-
else:
|
951
|
-
# Get specific option
|
952
|
-
success, result = get_cli_config_option(option)
|
953
|
-
if success:
|
954
|
-
if result is None:
|
955
|
-
print(f"{COLORS['cyan']}{option}{COLORS['reset']} is not set (default: {CLI_CONFIG_OPTIONS.get(option, {}).get('default', 'N/A')})")
|
956
|
-
else:
|
957
|
-
print(f"{COLORS['cyan']}{option}{COLORS['reset']} = {result}")
|
958
|
-
else:
|
959
|
-
print(f"{COLORS['yellow']}{result}{COLORS['reset']}")
|
960
|
-
return
|
961
|
-
|
962
|
-
if action == "set":
|
963
|
-
if option is None or value is None:
|
964
|
-
print(f"{COLORS['yellow']}Error: Both OPTION and VALUE are required for 'set' command.{COLORS['reset']}")
|
965
|
-
print(f"Usage: ngpt --cli-config set OPTION VALUE")
|
966
|
-
return
|
967
|
-
|
968
|
-
success, message = set_cli_config_option(option, value)
|
969
|
-
if success:
|
970
|
-
print(f"{COLORS['green']}{message}{COLORS['reset']}")
|
971
|
-
else:
|
972
|
-
print(f"{COLORS['yellow']}{message}{COLORS['reset']}")
|
973
|
-
return
|
974
|
-
|
975
|
-
if action == "unset":
|
976
|
-
if option is None:
|
977
|
-
print(f"{COLORS['yellow']}Error: OPTION is required for 'unset' command.{COLORS['reset']}")
|
978
|
-
print(f"Usage: ngpt --cli-config unset OPTION")
|
979
|
-
return
|
980
|
-
|
981
|
-
success, message = unset_cli_config_option(option)
|
982
|
-
if success:
|
983
|
-
print(f"{COLORS['green']}{message}{COLORS['reset']}")
|
984
|
-
else:
|
985
|
-
print(f"{COLORS['yellow']}{message}{COLORS['reset']}")
|
986
|
-
return
|
987
|
-
|
988
|
-
# If we get here, the action is not recognized
|
989
|
-
print(f"{COLORS['yellow']}Error: Unknown action '{action}'. Use 'set', 'get', 'unset', or 'list'.{COLORS['reset']}")
|
990
|
-
show_cli_config_help()
|
991
|
-
|
992
|
-
def main():
|
993
|
-
# Colorize description - use a shorter description to avoid line wrapping issues
|
994
|
-
description = f"{COLORS['cyan']}{COLORS['bold']}nGPT{COLORS['reset']} - Interact with AI language models via OpenAI-compatible APIs"
|
995
|
-
|
996
|
-
# Minimalist, clean epilog design
|
997
|
-
epilog = f"\n{COLORS['yellow']}nGPT {COLORS['bold']}v{__version__}{COLORS['reset']} • {COLORS['green']}Docs: {COLORS['bold']}https://nazdridoy.github.io/ngpt/usage/cli_usage.html{COLORS['reset']}"
|
998
|
-
|
999
|
-
parser = argparse.ArgumentParser(description=description, formatter_class=ColoredHelpFormatter, epilog=epilog)
|
1000
|
-
|
1001
|
-
# Add custom error method with color
|
1002
|
-
original_error = parser.error
|
1003
|
-
def error_with_color(message):
|
1004
|
-
parser.print_usage(sys.stderr)
|
1005
|
-
parser.exit(2, f"{COLORS['bold']}{COLORS['yellow']}error: {COLORS['reset']}{message}\n")
|
1006
|
-
parser.error = error_with_color
|
1007
|
-
|
1008
|
-
# Custom version action with color
|
1009
|
-
class ColoredVersionAction(argparse.Action):
|
1010
|
-
def __call__(self, parser, namespace, values, option_string=None):
|
1011
|
-
print(f"{COLORS['green']}{COLORS['bold']}nGPT{COLORS['reset']} version {COLORS['yellow']}{__version__}{COLORS['reset']}")
|
1012
|
-
parser.exit()
|
1013
|
-
|
1014
|
-
# Version flag
|
1015
|
-
parser.add_argument('-v', '--version', action=ColoredVersionAction, nargs=0, help='Show version information and exit')
|
1016
|
-
|
1017
|
-
# Config options
|
1018
|
-
config_group = parser.add_argument_group('Configuration Options')
|
1019
|
-
config_group.add_argument('--config', nargs='?', const=True, help='Path to a custom config file or, if no value provided, enter interactive configuration mode to create a new config')
|
1020
|
-
config_group.add_argument('--config-index', type=int, default=0, help='Index of the configuration to use or edit (default: 0)')
|
1021
|
-
config_group.add_argument('--provider', help='Provider name to identify the configuration to use')
|
1022
|
-
config_group.add_argument('--remove', action='store_true', help='Remove the configuration at the specified index (requires --config and --config-index)')
|
1023
|
-
config_group.add_argument('--show-config', action='store_true', help='Show the current configuration(s) and exit')
|
1024
|
-
config_group.add_argument('--all', action='store_true', help='Show details for all configurations (requires --show-config)')
|
1025
|
-
config_group.add_argument('--list-models', action='store_true', help='List all available models for the current configuration and exit')
|
1026
|
-
config_group.add_argument('--list-renderers', action='store_true', help='Show available markdown renderers for use with --prettify')
|
1027
|
-
|
1028
|
-
# Global options
|
1029
|
-
global_group = parser.add_argument_group('Global Options')
|
1030
|
-
global_group.add_argument('--api-key', help='API key for the service')
|
1031
|
-
global_group.add_argument('--base-url', help='Base URL for the API')
|
1032
|
-
global_group.add_argument('--model', help='Model to use')
|
1033
|
-
global_group.add_argument('--web-search', action='store_true',
|
1034
|
-
help='Enable web search capability (Note: Your API endpoint must support this feature)')
|
1035
|
-
global_group.add_argument('-n', '--no-stream', action='store_true',
|
1036
|
-
help='Return the whole response without streaming')
|
1037
|
-
global_group.add_argument('--temperature', type=float, default=0.7,
|
1038
|
-
help='Set temperature (controls randomness, default: 0.7)')
|
1039
|
-
global_group.add_argument('--top_p', type=float, default=1.0,
|
1040
|
-
help='Set top_p (controls diversity, default: 1.0)')
|
1041
|
-
global_group.add_argument('--max_tokens', type=int,
|
1042
|
-
help='Set max response length in tokens')
|
1043
|
-
global_group.add_argument('--log', metavar='FILE',
|
1044
|
-
help='Set filepath to log conversation to (For interactive modes)')
|
1045
|
-
global_group.add_argument('--preprompt',
|
1046
|
-
help='Set custom system prompt to control AI behavior')
|
1047
|
-
global_group.add_argument('--prettify', action='store_const', const='auto',
|
1048
|
-
help='Render markdown responses and code with syntax highlighting and formatting')
|
1049
|
-
global_group.add_argument('--stream-prettify', action='store_true',
|
1050
|
-
help='Enable streaming with markdown rendering (automatically uses Rich renderer)')
|
1051
|
-
global_group.add_argument('--renderer', choices=['auto', 'rich', 'glow'], default='auto',
|
1052
|
-
help='Select which markdown renderer to use with --prettify (auto, rich, or glow)')
|
1053
|
-
|
1054
|
-
# Mode flags (mutually exclusive)
|
1055
|
-
mode_group = parser.add_argument_group('Modes (mutually exclusive)')
|
1056
|
-
mode_exclusive_group = mode_group.add_mutually_exclusive_group()
|
1057
|
-
mode_exclusive_group.add_argument('-i', '--interactive', action='store_true', help='Start an interactive chat session')
|
1058
|
-
mode_exclusive_group.add_argument('-s', '--shell', action='store_true', help='Generate and execute shell commands')
|
1059
|
-
mode_exclusive_group.add_argument('-c', '--code', action='store_true', help='Generate code')
|
1060
|
-
mode_exclusive_group.add_argument('-t', '--text', action='store_true', help='Enter multi-line text input (submit with Ctrl+D)')
|
1061
|
-
# Note: --show-config is handled separately and implicitly acts as a mode
|
1062
|
-
|
1063
|
-
# Language option for code mode
|
1064
|
-
parser.add_argument('--language', default="python", help='Programming language to generate code in (for code mode)')
|
1065
|
-
|
1066
|
-
# Prompt argument
|
1067
|
-
parser.add_argument('prompt', nargs='?', default=None, help='The prompt to send')
|
1068
|
-
|
1069
|
-
# Add CLI configuration command
|
1070
|
-
config_group.add_argument('--cli-config', nargs='*', metavar='COMMAND',
|
1071
|
-
help='Manage CLI configuration (set, get, unset, list)')
|
1072
|
-
|
1073
|
-
args = parser.parse_args()
|
1074
|
-
|
1075
|
-
# Handle CLI configuration command
|
1076
|
-
if args.cli_config is not None:
|
1077
|
-
# Show help if no arguments or "help" argument
|
1078
|
-
if len(args.cli_config) == 0 or (len(args.cli_config) > 0 and args.cli_config[0].lower() == "help"):
|
1079
|
-
show_cli_config_help()
|
1080
|
-
return
|
1081
|
-
|
1082
|
-
action = args.cli_config[0].lower()
|
1083
|
-
option = args.cli_config[1] if len(args.cli_config) > 1 else None
|
1084
|
-
value = args.cli_config[2] if len(args.cli_config) > 2 else None
|
1085
|
-
|
1086
|
-
if action in ("set", "get", "unset", "list"):
|
1087
|
-
handle_cli_config(action, option, value)
|
1088
|
-
return
|
1089
|
-
else:
|
1090
|
-
show_cli_config_help()
|
1091
|
-
return
|
1092
|
-
|
1093
|
-
# Validate --all usage
|
1094
|
-
if args.all and not args.show_config:
|
1095
|
-
parser.error("--all can only be used with --show-config")
|
1096
|
-
|
1097
|
-
# Handle --renderers flag to show available markdown renderers
|
1098
|
-
if args.list_renderers:
|
1099
|
-
show_available_renderers()
|
1100
|
-
return
|
1101
|
-
|
1102
|
-
# Load CLI configuration early
|
1103
|
-
from .cli_config import load_cli_config
|
1104
|
-
cli_config = load_cli_config()
|
1105
|
-
|
1106
|
-
# Priority order for config selection:
|
1107
|
-
# 1. Command-line arguments (args.provider, args.config_index)
|
1108
|
-
# 2. CLI configuration (cli_config["provider"], cli_config["config-index"])
|
1109
|
-
# 3. Default values (None, 0)
|
1110
|
-
|
1111
|
-
# Get provider/config-index from CLI config if not specified in args
|
1112
|
-
effective_provider = args.provider
|
1113
|
-
effective_config_index = args.config_index
|
1114
|
-
|
1115
|
-
# Only apply CLI config for provider/config-index if not explicitly set on command line
|
1116
|
-
if not effective_provider and 'provider' in cli_config and '--provider' not in sys.argv:
|
1117
|
-
effective_provider = cli_config['provider']
|
1118
|
-
|
1119
|
-
if effective_config_index == 0 and 'config-index' in cli_config and '--config-index' not in sys.argv:
|
1120
|
-
effective_config_index = cli_config['config-index']
|
1121
|
-
|
1122
|
-
# Check for mutual exclusivity between provider and config-index
|
1123
|
-
if effective_config_index != 0 and effective_provider:
|
1124
|
-
parser.error("--config-index and --provider cannot be used together")
|
1125
|
-
|
1126
|
-
# Handle interactive configuration mode
|
1127
|
-
if args.config is True: # --config was used without a value
|
1128
|
-
config_path = get_config_path()
|
1129
|
-
|
1130
|
-
# Handle configuration removal if --remove flag is present
|
1131
|
-
if args.remove:
|
1132
|
-
# Validate that config_index is explicitly provided
|
1133
|
-
if '--config-index' not in sys.argv and not effective_provider:
|
1134
|
-
parser.error("--remove requires explicitly specifying --config-index or --provider")
|
1135
|
-
|
1136
|
-
# Show config details before asking for confirmation
|
1137
|
-
configs = load_configs(str(config_path))
|
1138
|
-
|
1139
|
-
# Determine the config index to remove
|
1140
|
-
config_index = effective_config_index
|
1141
|
-
if effective_provider:
|
1142
|
-
# Find config index by provider name
|
1143
|
-
matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == effective_provider.lower()]
|
1144
|
-
if not matching_configs:
|
1145
|
-
print(f"Error: No configuration found for provider '{effective_provider}'")
|
1146
|
-
return
|
1147
|
-
elif len(matching_configs) > 1:
|
1148
|
-
print(f"Multiple configurations found for provider '{effective_provider}':")
|
1149
|
-
for i, idx in enumerate(matching_configs):
|
1150
|
-
print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
|
1151
|
-
|
1152
|
-
try:
|
1153
|
-
choice = input("Choose a configuration to remove (or press Enter to cancel): ")
|
1154
|
-
if choice and choice.isdigit() and 0 <= int(choice) < len(matching_configs):
|
1155
|
-
config_index = matching_configs[int(choice)]
|
1156
|
-
else:
|
1157
|
-
print("Configuration removal cancelled.")
|
1158
|
-
return
|
1159
|
-
except (ValueError, IndexError, KeyboardInterrupt):
|
1160
|
-
print("\nConfiguration removal cancelled.")
|
1161
|
-
return
|
1162
|
-
else:
|
1163
|
-
config_index = matching_configs[0]
|
1164
|
-
|
1165
|
-
# Check if index is valid
|
1166
|
-
if config_index < 0 or config_index >= len(configs):
|
1167
|
-
print(f"Error: Configuration index {config_index} is out of range. Valid range: 0-{len(configs)-1}")
|
1168
|
-
return
|
1169
|
-
|
1170
|
-
# Show the configuration that will be removed
|
1171
|
-
config = configs[config_index]
|
1172
|
-
print(f"Configuration to remove (index {config_index}):")
|
1173
|
-
print(f" Provider: {config.get('provider', 'N/A')}")
|
1174
|
-
print(f" Model: {config.get('model', 'N/A')}")
|
1175
|
-
print(f" Base URL: {config.get('base_url', 'N/A')}")
|
1176
|
-
print(f" API Key: {'[Set]' if config.get('api_key') else '[Not Set]'}")
|
1177
|
-
|
1178
|
-
# Ask for confirmation
|
1179
|
-
try:
|
1180
|
-
print("\nAre you sure you want to remove this configuration? [y/N] ", end='')
|
1181
|
-
response = input().lower()
|
1182
|
-
if response in ('y', 'yes'):
|
1183
|
-
remove_config_entry(config_path, config_index)
|
1184
|
-
else:
|
1185
|
-
print("Configuration removal cancelled.")
|
1186
|
-
except KeyboardInterrupt:
|
1187
|
-
print("\nConfiguration removal cancelled by user.")
|
1188
|
-
|
1189
|
-
return
|
1190
|
-
|
1191
|
-
# Regular config addition/editing (existing code)
|
1192
|
-
# If --config-index was not explicitly specified, create a new entry by passing None
|
1193
|
-
# This will cause add_config_entry to create a new entry at the end of the list
|
1194
|
-
# Otherwise, edit the existing config at the specified index
|
1195
|
-
config_index = None
|
1196
|
-
|
1197
|
-
# Determine if we're editing an existing config or creating a new one
|
1198
|
-
if effective_provider:
|
1199
|
-
# Find config by provider name
|
1200
|
-
configs = load_configs(str(config_path))
|
1201
|
-
matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == effective_provider.lower()]
|
1202
|
-
|
1203
|
-
if not matching_configs:
|
1204
|
-
print(f"No configuration found for provider '{effective_provider}'. Creating a new configuration.")
|
1205
|
-
elif len(matching_configs) > 1:
|
1206
|
-
print(f"Multiple configurations found for provider '{effective_provider}':")
|
1207
|
-
for i, idx in enumerate(matching_configs):
|
1208
|
-
print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
|
1209
|
-
|
1210
|
-
try:
|
1211
|
-
choice = input("Choose a configuration to edit (or press Enter for the first one): ")
|
1212
|
-
if choice and choice.isdigit() and 0 <= int(choice) < len(matching_configs):
|
1213
|
-
config_index = matching_configs[int(choice)]
|
1214
|
-
else:
|
1215
|
-
config_index = matching_configs[0]
|
1216
|
-
except (ValueError, IndexError, KeyboardInterrupt):
|
1217
|
-
config_index = matching_configs[0]
|
1218
|
-
else:
|
1219
|
-
config_index = matching_configs[0]
|
1220
|
-
|
1221
|
-
print(f"Editing existing configuration at index {config_index}")
|
1222
|
-
elif effective_config_index != 0 or '--config-index' in sys.argv:
|
1223
|
-
# Check if the index is valid
|
1224
|
-
configs = load_configs(str(config_path))
|
1225
|
-
if effective_config_index >= 0 and effective_config_index < len(configs):
|
1226
|
-
config_index = effective_config_index
|
1227
|
-
print(f"Editing existing configuration at index {config_index}")
|
1228
|
-
else:
|
1229
|
-
print(f"Configuration index {effective_config_index} is out of range. Creating a new configuration.")
|
1230
|
-
else:
|
1231
|
-
# Creating a new config
|
1232
|
-
configs = load_configs(str(config_path))
|
1233
|
-
print(f"Creating new configuration at index {len(configs)}")
|
1234
|
-
|
1235
|
-
add_config_entry(config_path, config_index)
|
1236
|
-
return
|
1237
|
-
|
1238
|
-
# Load configuration using the effective provider/config-index
|
1239
|
-
active_config = load_config(args.config, effective_config_index, effective_provider)
|
1240
|
-
|
1241
|
-
# Command-line arguments override config settings for active config display
|
1242
|
-
if args.api_key:
|
1243
|
-
active_config["api_key"] = args.api_key
|
1244
|
-
if args.base_url:
|
1245
|
-
active_config["base_url"] = args.base_url
|
1246
|
-
if args.model:
|
1247
|
-
active_config["model"] = args.model
|
1248
|
-
|
1249
|
-
# Show config if requested
|
1250
|
-
if args.show_config:
|
1251
|
-
config_path = get_config_path(args.config)
|
1252
|
-
configs = load_configs(args.config)
|
1253
|
-
|
1254
|
-
print(f"Configuration file: {config_path}")
|
1255
|
-
print(f"Total configurations: {len(configs)}")
|
1256
|
-
|
1257
|
-
# Determine active configuration and display identifier
|
1258
|
-
active_identifier = f"index {effective_config_index}"
|
1259
|
-
if effective_provider:
|
1260
|
-
active_identifier = f"provider '{effective_provider}'"
|
1261
|
-
print(f"Active configuration: {active_identifier}")
|
1262
|
-
|
1263
|
-
if args.all:
|
1264
|
-
# Show details for all configurations
|
1265
|
-
print("\nAll configuration details:")
|
1266
|
-
for i, cfg in enumerate(configs):
|
1267
|
-
provider = cfg.get('provider', 'N/A')
|
1268
|
-
active_str = '(Active)' if (
|
1269
|
-
(effective_provider and provider.lower() == effective_provider.lower()) or
|
1270
|
-
(not effective_provider and i == effective_config_index)
|
1271
|
-
) else ''
|
1272
|
-
print(f"\n--- Configuration Index {i} / Provider: {COLORS['green']}{provider}{COLORS['reset']} {active_str} ---")
|
1273
|
-
print(f" API Key: {'[Set]' if cfg.get('api_key') else '[Not Set]'}")
|
1274
|
-
print(f" Base URL: {cfg.get('base_url', 'N/A')}")
|
1275
|
-
print(f" Model: {cfg.get('model', 'N/A')}")
|
1276
|
-
else:
|
1277
|
-
# Show active config details and summary list
|
1278
|
-
print("\nActive configuration details:")
|
1279
|
-
print(f" Provider: {COLORS['green']}{active_config.get('provider', 'N/A')}{COLORS['reset']}")
|
1280
|
-
print(f" API Key: {'[Set]' if active_config.get('api_key') else '[Not Set]'}")
|
1281
|
-
print(f" Base URL: {active_config.get('base_url', 'N/A')}")
|
1282
|
-
print(f" Model: {active_config.get('model', 'N/A')}")
|
1283
|
-
|
1284
|
-
if len(configs) > 1:
|
1285
|
-
print("\nAvailable configurations:")
|
1286
|
-
# Check for duplicate provider names for warning
|
1287
|
-
provider_counts = {}
|
1288
|
-
for cfg in configs:
|
1289
|
-
provider = cfg.get('provider', 'N/A').lower()
|
1290
|
-
provider_counts[provider] = provider_counts.get(provider, 0) + 1
|
1291
|
-
|
1292
|
-
for i, cfg in enumerate(configs):
|
1293
|
-
provider = cfg.get('provider', 'N/A')
|
1294
|
-
provider_display = provider
|
1295
|
-
# Add warning for duplicate providers
|
1296
|
-
if provider_counts.get(provider.lower(), 0) > 1:
|
1297
|
-
provider_display = f"{provider} {COLORS['yellow']}(duplicate){COLORS['reset']}"
|
1298
|
-
|
1299
|
-
active_marker = "*" if (
|
1300
|
-
(effective_provider and provider.lower() == effective_provider.lower()) or
|
1301
|
-
(not effective_provider and i == effective_config_index)
|
1302
|
-
) else " "
|
1303
|
-
print(f"[{i}]{active_marker} {COLORS['green']}{provider_display}{COLORS['reset']} - {cfg.get('model', 'N/A')} ({'[API Key Set]' if cfg.get('api_key') else '[API Key Not Set]'})")
|
1304
|
-
|
1305
|
-
# Show instruction for using --provider
|
1306
|
-
print(f"\nTip: Use {COLORS['yellow']}--provider NAME{COLORS['reset']} to select a configuration by provider name.")
|
1307
|
-
|
1308
|
-
return
|
1309
|
-
|
1310
|
-
# For interactive mode, we'll allow continuing without a specific prompt
|
1311
|
-
if not args.prompt and not (args.shell or args.code or args.text or args.interactive or args.show_config or args.list_models):
|
1312
|
-
parser.print_help()
|
1313
|
-
return
|
1314
|
-
|
1315
|
-
# Check configuration (using the potentially overridden active_config)
|
1316
|
-
if not args.show_config and not args.list_models and not check_config(active_config):
|
1317
|
-
return
|
1318
|
-
|
1319
|
-
# Check if --prettify is used but no markdown renderer is available
|
1320
|
-
# This will warn the user immediately if they request prettify but don't have the tools
|
1321
|
-
has_renderer = True
|
1322
|
-
if args.prettify:
|
1323
|
-
has_renderer = warn_if_no_markdown_renderer(args.renderer)
|
1324
|
-
if not has_renderer:
|
1325
|
-
# Set a flag to disable prettify since we already warned the user
|
1326
|
-
print(f"{COLORS['yellow']}Continuing without markdown rendering.{COLORS['reset']}")
|
1327
|
-
show_available_renderers()
|
1328
|
-
args.prettify = False
|
1329
|
-
|
1330
|
-
# Check if --prettify is used with --stream-prettify (conflict)
|
1331
|
-
if args.prettify and args.stream_prettify:
|
1332
|
-
parser.error("--prettify and --stream-prettify cannot be used together. Choose one option.")
|
1333
|
-
|
1334
|
-
# Check if --stream-prettify is used but Rich is not available
|
1335
|
-
if args.stream_prettify and not has_markdown_renderer('rich'):
|
1336
|
-
parser.error("--stream-prettify requires Rich to be installed. Install with: pip install \"ngpt[full]\" or pip install rich")
|
1337
|
-
|
1338
|
-
# Initialize client using the potentially overridden active_config
|
1339
|
-
client = NGPTClient(**active_config)
|
1340
|
-
|
1341
|
-
try:
|
1342
|
-
# Handle listing models
|
1343
|
-
if args.list_models:
|
1344
|
-
print("Retrieving available models...")
|
1345
|
-
models = client.list_models()
|
1346
|
-
if models:
|
1347
|
-
print(f"\nAvailable models for {active_config.get('provider', 'API')}:")
|
1348
|
-
print("-" * 50)
|
1349
|
-
for model in models:
|
1350
|
-
if "id" in model:
|
1351
|
-
owned_by = f" ({model.get('owned_by', 'Unknown')})" if "owned_by" in model else ""
|
1352
|
-
current = " [active]" if model["id"] == active_config["model"] else ""
|
1353
|
-
print(f"- {model['id']}{owned_by}{current}")
|
1354
|
-
print("\nUse --model MODEL_NAME to select a specific model")
|
1355
|
-
else:
|
1356
|
-
print("No models available or could not retrieve models.")
|
1357
|
-
return
|
1358
|
-
|
1359
|
-
# Handle modes
|
1360
|
-
if args.interactive:
|
1361
|
-
# Apply CLI config for interactive mode
|
1362
|
-
args = apply_cli_config(args, "interactive")
|
1363
|
-
|
1364
|
-
# Interactive chat mode
|
1365
|
-
interactive_chat_session(
|
1366
|
-
client,
|
1367
|
-
web_search=args.web_search,
|
1368
|
-
no_stream=args.no_stream,
|
1369
|
-
temperature=args.temperature,
|
1370
|
-
top_p=args.top_p,
|
1371
|
-
max_tokens=args.max_tokens,
|
1372
|
-
log_file=args.log,
|
1373
|
-
preprompt=args.preprompt,
|
1374
|
-
prettify=args.prettify,
|
1375
|
-
renderer=args.renderer,
|
1376
|
-
stream_prettify=args.stream_prettify
|
1377
|
-
)
|
1378
|
-
elif args.shell:
|
1379
|
-
# Apply CLI config for shell mode
|
1380
|
-
args = apply_cli_config(args, "shell")
|
1381
|
-
|
1382
|
-
if args.prompt is None:
|
1383
|
-
try:
|
1384
|
-
print("Enter shell command description: ", end='')
|
1385
|
-
prompt = input()
|
1386
|
-
except KeyboardInterrupt:
|
1387
|
-
print("\nInput cancelled by user. Exiting gracefully.")
|
1388
|
-
sys.exit(130)
|
1389
|
-
else:
|
1390
|
-
prompt = args.prompt
|
1391
|
-
|
1392
|
-
command = client.generate_shell_command(prompt, web_search=args.web_search,
|
1393
|
-
temperature=args.temperature, top_p=args.top_p,
|
1394
|
-
max_tokens=args.max_tokens)
|
1395
|
-
if not command:
|
1396
|
-
return # Error already printed by client
|
1397
|
-
|
1398
|
-
print(f"\nGenerated command: {command}")
|
1399
|
-
|
1400
|
-
try:
|
1401
|
-
print("Do you want to execute this command? [y/N] ", end='')
|
1402
|
-
response = input().lower()
|
1403
|
-
except KeyboardInterrupt:
|
1404
|
-
print("\nCommand execution cancelled by user.")
|
1405
|
-
return
|
1406
|
-
|
1407
|
-
if response == 'y' or response == 'yes':
|
1408
|
-
import subprocess
|
1409
|
-
try:
|
1410
|
-
try:
|
1411
|
-
print("\nExecuting command... (Press Ctrl+C to cancel)")
|
1412
|
-
result = subprocess.run(command, shell=True, check=True, capture_output=True, text=True)
|
1413
|
-
print(f"\nOutput:\n{result.stdout}")
|
1414
|
-
except KeyboardInterrupt:
|
1415
|
-
print("\nCommand execution cancelled by user.")
|
1416
|
-
except subprocess.CalledProcessError as e:
|
1417
|
-
print(f"\nError:\n{e.stderr}")
|
1418
|
-
|
1419
|
-
elif args.code:
|
1420
|
-
# Apply CLI config for code mode
|
1421
|
-
args = apply_cli_config(args, "code")
|
1422
|
-
|
1423
|
-
if args.prompt is None:
|
1424
|
-
try:
|
1425
|
-
print("Enter code description: ", end='')
|
1426
|
-
prompt = input()
|
1427
|
-
except KeyboardInterrupt:
|
1428
|
-
print("\nInput cancelled by user. Exiting gracefully.")
|
1429
|
-
sys.exit(130)
|
1430
|
-
else:
|
1431
|
-
prompt = args.prompt
|
1432
|
-
|
1433
|
-
# Setup for streaming and prettify logic
|
1434
|
-
stream_callback = None
|
1435
|
-
live_display = None
|
1436
|
-
should_stream = True # Default to streaming
|
1437
|
-
use_stream_prettify = False
|
1438
|
-
use_regular_prettify = False
|
1439
|
-
|
1440
|
-
# Determine final behavior based on flag priority
|
1441
|
-
if args.stream_prettify:
|
1442
|
-
# Highest priority: stream-prettify
|
1443
|
-
if has_markdown_renderer('rich'):
|
1444
|
-
should_stream = True
|
1445
|
-
use_stream_prettify = True
|
1446
|
-
live_display, stream_callback = prettify_streaming_markdown(args.renderer)
|
1447
|
-
if not live_display:
|
1448
|
-
# Fallback if live display fails
|
1449
|
-
use_stream_prettify = False
|
1450
|
-
use_regular_prettify = True
|
1451
|
-
should_stream = False
|
1452
|
-
print(f"{COLORS['yellow']}Live display setup failed. Falling back to regular prettify mode.{COLORS['reset']}")
|
1453
|
-
else:
|
1454
|
-
# Rich not available for stream-prettify
|
1455
|
-
print(f"{COLORS['yellow']}Warning: Rich is not available for --stream-prettify. Install with: pip install \"ngpt[full]\".{COLORS['reset']}")
|
1456
|
-
print(f"{COLORS['yellow']}Falling back to default streaming without prettify.{COLORS['reset']}")
|
1457
|
-
should_stream = True
|
1458
|
-
use_stream_prettify = False
|
1459
|
-
elif args.no_stream:
|
1460
|
-
# Second priority: no-stream
|
1461
|
-
should_stream = False
|
1462
|
-
use_regular_prettify = False # No prettify if no streaming
|
1463
|
-
elif args.prettify:
|
1464
|
-
# Third priority: prettify (requires disabling stream)
|
1465
|
-
if has_markdown_renderer(args.renderer):
|
1466
|
-
should_stream = False
|
1467
|
-
use_regular_prettify = True
|
1468
|
-
print(f"{COLORS['yellow']}Note: Streaming disabled to enable regular markdown rendering (--prettify).{COLORS['reset']}")
|
1469
|
-
else:
|
1470
|
-
# Renderer not available for prettify
|
1471
|
-
print(f"{COLORS['yellow']}Warning: Renderer '{args.renderer}' not available for --prettify.{COLORS['reset']}")
|
1472
|
-
show_available_renderers()
|
1473
|
-
print(f"{COLORS['yellow']}Falling back to default streaming without prettify.{COLORS['reset']}")
|
1474
|
-
should_stream = True
|
1475
|
-
use_regular_prettify = False
|
1476
|
-
# else: Default is should_stream = True
|
1477
|
-
|
1478
|
-
print("\nGenerating code...")
|
1479
|
-
|
1480
|
-
# Start live display if using stream-prettify
|
1481
|
-
if use_stream_prettify and live_display:
|
1482
|
-
live_display.start()
|
1483
|
-
|
1484
|
-
generated_code = client.generate_code(
|
1485
|
-
prompt=prompt,
|
1486
|
-
language=args.language,
|
1487
|
-
web_search=args.web_search,
|
1488
|
-
temperature=args.temperature,
|
1489
|
-
top_p=args.top_p,
|
1490
|
-
max_tokens=args.max_tokens,
|
1491
|
-
# Request markdown from API if any prettify option is active
|
1492
|
-
markdown_format=use_regular_prettify or use_stream_prettify,
|
1493
|
-
stream=should_stream,
|
1494
|
-
stream_callback=stream_callback
|
1495
|
-
)
|
1496
|
-
|
1497
|
-
# Stop live display if using stream-prettify
|
1498
|
-
if use_stream_prettify and live_display:
|
1499
|
-
live_display.stop()
|
1500
|
-
|
1501
|
-
# Print non-streamed output if needed
|
1502
|
-
if generated_code and not should_stream:
|
1503
|
-
if use_regular_prettify:
|
1504
|
-
print("\nGenerated code:")
|
1505
|
-
prettify_markdown(generated_code, args.renderer)
|
1506
|
-
else:
|
1507
|
-
# Should only happen if --no-stream was used without prettify
|
1508
|
-
print(f"\nGenerated code:\n{generated_code}")
|
1509
|
-
|
1510
|
-
elif args.text:
|
1511
|
-
# Apply CLI config for text mode
|
1512
|
-
args = apply_cli_config(args, "text")
|
1513
|
-
|
1514
|
-
if args.prompt is not None:
|
1515
|
-
prompt = args.prompt
|
1516
|
-
else:
|
1517
|
-
try:
|
1518
|
-
if HAS_PROMPT_TOOLKIT:
|
1519
|
-
print("\033[94m\033[1m" + "Multi-line Input Mode" + "\033[0m")
|
1520
|
-
print("Press Ctrl+D to submit, Ctrl+C to exit")
|
1521
|
-
print("Use arrow keys to navigate, Enter for new line")
|
1522
|
-
|
1523
|
-
# Create key bindings
|
1524
|
-
kb = KeyBindings()
|
1525
|
-
|
1526
|
-
# Explicitly bind Ctrl+D to exit
|
1527
|
-
@kb.add('c-d')
|
1528
|
-
def _(event):
|
1529
|
-
event.app.exit(result=event.app.current_buffer.text)
|
1530
|
-
|
1531
|
-
# Explicitly bind Ctrl+C to exit
|
1532
|
-
@kb.add('c-c')
|
1533
|
-
def _(event):
|
1534
|
-
event.app.exit(result=None)
|
1535
|
-
print("\nInput cancelled by user. Exiting gracefully.")
|
1536
|
-
sys.exit(130)
|
1537
|
-
|
1538
|
-
# Get terminal dimensions
|
1539
|
-
term_width, term_height = shutil.get_terminal_size()
|
1540
|
-
|
1541
|
-
# Create a styled TextArea
|
1542
|
-
text_area = TextArea(
|
1543
|
-
style="class:input-area",
|
1544
|
-
multiline=True,
|
1545
|
-
wrap_lines=True,
|
1546
|
-
width=term_width - 10,
|
1547
|
-
height=min(15, term_height - 10),
|
1548
|
-
prompt=HTML("<ansicyan><b>> </b></ansicyan>"),
|
1549
|
-
scrollbar=True,
|
1550
|
-
focus_on_click=True,
|
1551
|
-
lexer=None,
|
1552
|
-
)
|
1553
|
-
text_area.window.right_margins = [ScrollbarMargin(display_arrows=True)]
|
1554
|
-
|
1555
|
-
# Create a title bar
|
1556
|
-
title_bar = FormattedTextControl(
|
1557
|
-
HTML("<ansicyan><b> nGPT Multi-line Editor </b></ansicyan>")
|
1558
|
-
)
|
1559
|
-
|
1560
|
-
# Create a status bar with key bindings info
|
1561
|
-
status_bar = FormattedTextControl(
|
1562
|
-
HTML("<ansiblue><b>Ctrl+D</b></ansiblue>: Submit | <ansiblue><b>Ctrl+C</b></ansiblue>: Cancel | <ansiblue><b>↑↓←→</b></ansiblue>: Navigate")
|
1563
|
-
)
|
1564
|
-
|
1565
|
-
# Create the layout
|
1566
|
-
layout = Layout(
|
1567
|
-
HSplit([
|
1568
|
-
Window(title_bar, height=1),
|
1569
|
-
Window(height=1, char="─", style="class:separator"),
|
1570
|
-
text_area,
|
1571
|
-
Window(height=1, char="─", style="class:separator"),
|
1572
|
-
Window(status_bar, height=1),
|
1573
|
-
])
|
1574
|
-
)
|
1575
|
-
|
1576
|
-
# Create a style
|
1577
|
-
style = Style.from_dict({
|
1578
|
-
"separator": "ansicyan",
|
1579
|
-
"input-area": "fg:ansiwhite",
|
1580
|
-
"cursor": "bg:ansiwhite fg:ansiblack",
|
1581
|
-
})
|
1582
|
-
|
1583
|
-
# Create and run the application
|
1584
|
-
app = Application(
|
1585
|
-
layout=layout,
|
1586
|
-
full_screen=False,
|
1587
|
-
key_bindings=kb,
|
1588
|
-
style=style,
|
1589
|
-
mouse_support=True,
|
1590
|
-
)
|
1591
|
-
|
1592
|
-
prompt = app.run()
|
1593
|
-
|
1594
|
-
if not prompt or not prompt.strip():
|
1595
|
-
print("Empty prompt. Exiting.")
|
1596
|
-
return
|
1597
|
-
else:
|
1598
|
-
# Fallback to standard input with a better implementation
|
1599
|
-
print("Enter your multi-line prompt (press Ctrl+D to submit):")
|
1600
|
-
print("Note: Install 'prompt_toolkit' package for an enhanced input experience")
|
1601
|
-
|
1602
|
-
# Use a more robust approach for multiline input without prompt_toolkit
|
1603
|
-
lines = []
|
1604
|
-
while True:
|
1605
|
-
try:
|
1606
|
-
line = input()
|
1607
|
-
lines.append(line)
|
1608
|
-
except EOFError: # Ctrl+D was pressed
|
1609
|
-
break
|
1610
|
-
|
1611
|
-
prompt = "\n".join(lines)
|
1612
|
-
if not prompt.strip():
|
1613
|
-
print("Empty prompt. Exiting.")
|
1614
|
-
return
|
1615
|
-
|
1616
|
-
except KeyboardInterrupt:
|
1617
|
-
print("\nInput cancelled by user. Exiting gracefully.")
|
1618
|
-
sys.exit(130)
|
1619
|
-
|
1620
|
-
print("\nSubmission successful. Waiting for response...")
|
1621
|
-
|
1622
|
-
# Create messages array with preprompt if available
|
1623
|
-
messages = None
|
1624
|
-
if args.preprompt:
|
1625
|
-
messages = [
|
1626
|
-
{"role": "system", "content": args.preprompt},
|
1627
|
-
{"role": "user", "content": prompt}
|
1628
|
-
]
|
1629
|
-
|
1630
|
-
# Set default streaming behavior based on --no-stream and --prettify arguments
|
1631
|
-
should_stream = not args.no_stream and not args.prettify
|
1632
|
-
|
1633
|
-
# If stream-prettify is enabled
|
1634
|
-
stream_callback = None
|
1635
|
-
live_display = None
|
1636
|
-
|
1637
|
-
if args.stream_prettify:
|
1638
|
-
should_stream = True # Enable streaming
|
1639
|
-
# This is the standard mode, not interactive
|
1640
|
-
live_display, stream_callback = prettify_streaming_markdown(args.renderer)
|
1641
|
-
if not live_display:
|
1642
|
-
# Fallback to normal prettify if live display setup failed
|
1643
|
-
args.prettify = True
|
1644
|
-
args.stream_prettify = False
|
1645
|
-
should_stream = False
|
1646
|
-
print(f"{COLORS['yellow']}Falling back to regular prettify mode.{COLORS['reset']}")
|
1647
|
-
|
1648
|
-
# If regular prettify is enabled with streaming, inform the user
|
1649
|
-
if args.prettify and not args.no_stream:
|
1650
|
-
print(f"{COLORS['yellow']}Note: Streaming disabled to enable markdown rendering.{COLORS['reset']}")
|
1651
|
-
|
1652
|
-
# Start live display if using stream-prettify
|
1653
|
-
if args.stream_prettify and live_display:
|
1654
|
-
live_display.start()
|
1655
|
-
|
1656
|
-
response = client.chat(prompt, stream=should_stream, web_search=args.web_search,
|
1657
|
-
temperature=args.temperature, top_p=args.top_p,
|
1658
|
-
max_tokens=args.max_tokens, messages=messages,
|
1659
|
-
markdown_format=args.prettify or args.stream_prettify,
|
1660
|
-
stream_callback=stream_callback)
|
1661
|
-
|
1662
|
-
# Stop live display if using stream-prettify
|
1663
|
-
if args.stream_prettify and live_display:
|
1664
|
-
live_display.stop()
|
1665
|
-
|
1666
|
-
# Handle non-stream response or regular prettify
|
1667
|
-
if (args.no_stream or args.prettify) and response:
|
1668
|
-
if args.prettify:
|
1669
|
-
prettify_markdown(response, args.renderer)
|
1670
|
-
else:
|
1671
|
-
print(response)
|
1672
|
-
|
1673
|
-
else:
|
1674
|
-
# Default to chat mode
|
1675
|
-
# Apply CLI config for default chat mode
|
1676
|
-
args = apply_cli_config(args, "all")
|
1677
|
-
|
1678
|
-
if args.prompt is None:
|
1679
|
-
try:
|
1680
|
-
print("Enter your prompt: ", end='')
|
1681
|
-
prompt = input()
|
1682
|
-
except KeyboardInterrupt:
|
1683
|
-
print("\nInput cancelled by user. Exiting gracefully.")
|
1684
|
-
sys.exit(130)
|
1685
|
-
else:
|
1686
|
-
prompt = args.prompt
|
1687
|
-
|
1688
|
-
# Create messages array with preprompt if available
|
1689
|
-
messages = None
|
1690
|
-
if args.preprompt:
|
1691
|
-
messages = [
|
1692
|
-
{"role": "system", "content": args.preprompt},
|
1693
|
-
{"role": "user", "content": prompt}
|
1694
|
-
]
|
1695
|
-
|
1696
|
-
# Set default streaming behavior based on --no-stream and --prettify arguments
|
1697
|
-
should_stream = not args.no_stream and not args.prettify
|
1698
|
-
|
1699
|
-
# If stream-prettify is enabled
|
1700
|
-
stream_callback = None
|
1701
|
-
live_display = None
|
1702
|
-
|
1703
|
-
if args.stream_prettify:
|
1704
|
-
should_stream = True # Enable streaming
|
1705
|
-
# This is the standard mode, not interactive
|
1706
|
-
live_display, stream_callback = prettify_streaming_markdown(args.renderer)
|
1707
|
-
if not live_display:
|
1708
|
-
# Fallback to normal prettify if live display setup failed
|
1709
|
-
args.prettify = True
|
1710
|
-
args.stream_prettify = False
|
1711
|
-
should_stream = False
|
1712
|
-
print(f"{COLORS['yellow']}Falling back to regular prettify mode.{COLORS['reset']}")
|
1713
|
-
|
1714
|
-
# If regular prettify is enabled with streaming, inform the user
|
1715
|
-
if args.prettify and not args.no_stream:
|
1716
|
-
print(f"{COLORS['yellow']}Note: Streaming disabled to enable markdown rendering.{COLORS['reset']}")
|
1717
|
-
|
1718
|
-
# Start live display if using stream-prettify
|
1719
|
-
if args.stream_prettify and live_display:
|
1720
|
-
live_display.start()
|
1721
|
-
|
1722
|
-
response = client.chat(prompt, stream=should_stream, web_search=args.web_search,
|
1723
|
-
temperature=args.temperature, top_p=args.top_p,
|
1724
|
-
max_tokens=args.max_tokens, messages=messages,
|
1725
|
-
markdown_format=args.prettify or args.stream_prettify,
|
1726
|
-
stream_callback=stream_callback)
|
1727
|
-
|
1728
|
-
# Stop live display if using stream-prettify
|
1729
|
-
if args.stream_prettify and live_display:
|
1730
|
-
live_display.stop()
|
1731
|
-
|
1732
|
-
# Handle non-stream response or regular prettify
|
1733
|
-
if (args.no_stream or args.prettify) and response:
|
1734
|
-
if args.prettify:
|
1735
|
-
prettify_markdown(response, args.renderer)
|
1736
|
-
else:
|
1737
|
-
print(response)
|
1738
|
-
|
1739
|
-
except KeyboardInterrupt:
|
1740
|
-
print("\nOperation cancelled by user. Exiting gracefully.")
|
1741
|
-
# Make sure we exit with a non-zero status code to indicate the operation was cancelled
|
1742
|
-
sys.exit(130) # 130 is the standard exit code for SIGINT (Ctrl+C)
|
1743
|
-
except Exception as e:
|
1744
|
-
print(f"Error: {e}")
|
1745
|
-
sys.exit(1) # Exit with error code
|
1
|
+
from .cli.main import main
|
1746
2
|
|
1747
3
|
if __name__ == "__main__":
|
1748
4
|
main()
|