ngpt 2.3.4__py3-none-any.whl → 2.5.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.
- ngpt/cli.py +370 -39
- ngpt/config.py +80 -7
- {ngpt-2.3.4.dist-info → ngpt-2.5.0.dist-info}/METADATA +27 -1
- ngpt-2.5.0.dist-info/RECORD +9 -0
- ngpt-2.3.4.dist-info/RECORD +0 -9
- {ngpt-2.3.4.dist-info → ngpt-2.5.0.dist-info}/WHEEL +0 -0
- {ngpt-2.3.4.dist-info → ngpt-2.5.0.dist-info}/entry_points.txt +0 -0
- {ngpt-2.3.4.dist-info → ngpt-2.5.0.dist-info}/licenses/LICENSE +0 -0
ngpt/cli.py
CHANGED
@@ -5,6 +5,23 @@ from .client import NGPTClient
|
|
5
5
|
from .config import load_config, get_config_path, load_configs, add_config_entry, remove_config_entry
|
6
6
|
from . import __version__
|
7
7
|
|
8
|
+
# Try to import markdown rendering libraries
|
9
|
+
try:
|
10
|
+
import rich
|
11
|
+
from rich.markdown import Markdown
|
12
|
+
from rich.console import Console
|
13
|
+
HAS_RICH = True
|
14
|
+
except ImportError:
|
15
|
+
HAS_RICH = False
|
16
|
+
|
17
|
+
# Try to import the glow command if available
|
18
|
+
def has_glow_installed():
|
19
|
+
"""Check if glow is installed in the system."""
|
20
|
+
import shutil
|
21
|
+
return shutil.which("glow") is not None
|
22
|
+
|
23
|
+
HAS_GLOW = has_glow_installed()
|
24
|
+
|
8
25
|
# ANSI color codes for terminal output
|
9
26
|
COLORS = {
|
10
27
|
"reset": "\033[0m",
|
@@ -68,6 +85,162 @@ if not HAS_COLOR:
|
|
68
85
|
for key in COLORS:
|
69
86
|
COLORS[key] = ""
|
70
87
|
|
88
|
+
def has_markdown_renderer(renderer='auto'):
|
89
|
+
"""Check if the specified markdown renderer is available.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
renderer (str): Which renderer to check: 'auto', 'rich', or 'glow'
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
bool: True if the renderer is available, False otherwise
|
96
|
+
"""
|
97
|
+
if renderer == 'auto':
|
98
|
+
return HAS_RICH or HAS_GLOW
|
99
|
+
elif renderer == 'rich':
|
100
|
+
return HAS_RICH
|
101
|
+
elif renderer == 'glow':
|
102
|
+
return HAS_GLOW
|
103
|
+
else:
|
104
|
+
return False
|
105
|
+
|
106
|
+
def show_available_renderers():
|
107
|
+
"""Show which markdown renderers are available and their status."""
|
108
|
+
print(f"\n{COLORS['cyan']}{COLORS['bold']}Available Markdown Renderers:{COLORS['reset']}")
|
109
|
+
|
110
|
+
if HAS_GLOW:
|
111
|
+
print(f" {COLORS['green']}✓ Glow{COLORS['reset']} - Terminal-based Markdown renderer")
|
112
|
+
else:
|
113
|
+
print(f" {COLORS['yellow']}✗ Glow{COLORS['reset']} - Not installed (https://github.com/charmbracelet/glow)")
|
114
|
+
|
115
|
+
if HAS_RICH:
|
116
|
+
print(f" {COLORS['green']}✓ Rich{COLORS['reset']} - Python library for terminal formatting (Recommended)")
|
117
|
+
else:
|
118
|
+
print(f" {COLORS['yellow']}✗ Rich{COLORS['reset']} - Not installed (pip install rich)")
|
119
|
+
|
120
|
+
if not HAS_GLOW and not HAS_RICH:
|
121
|
+
print(f"\n{COLORS['yellow']}To enable prettified markdown output, install one of the above renderers.{COLORS['reset']}")
|
122
|
+
else:
|
123
|
+
renderers = []
|
124
|
+
if HAS_RICH:
|
125
|
+
renderers.append("rich")
|
126
|
+
if HAS_GLOW:
|
127
|
+
renderers.append("glow")
|
128
|
+
print(f"\n{COLORS['green']}Usage examples:{COLORS['reset']}")
|
129
|
+
print(f" ngpt --prettify \"Your prompt here\" {COLORS['gray']}# Beautify markdown responses{COLORS['reset']}")
|
130
|
+
print(f" ngpt -c --prettify \"Write a sort function\" {COLORS['gray']}# Syntax highlight generated code{COLORS['reset']}")
|
131
|
+
if renderers:
|
132
|
+
renderer = renderers[0]
|
133
|
+
print(f" ngpt --prettify --renderer={renderer} \"Your prompt\" {COLORS['gray']}# Specify renderer{COLORS['reset']}")
|
134
|
+
|
135
|
+
print("")
|
136
|
+
|
137
|
+
def warn_if_no_markdown_renderer(renderer='auto'):
|
138
|
+
"""Warn the user if the specified markdown renderer is not available.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
renderer (str): Which renderer to check: 'auto', 'rich', or 'glow'
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
bool: True if the renderer is available, False otherwise
|
145
|
+
"""
|
146
|
+
if has_markdown_renderer(renderer):
|
147
|
+
return True
|
148
|
+
|
149
|
+
if renderer == 'auto':
|
150
|
+
print(f"{COLORS['yellow']}Warning: No markdown rendering library available.{COLORS['reset']}")
|
151
|
+
print(f"{COLORS['yellow']}Install 'rich' package with: pip install rich{COLORS['reset']}")
|
152
|
+
print(f"{COLORS['yellow']}Or install 'glow' from https://github.com/charmbracelet/glow{COLORS['reset']}")
|
153
|
+
elif renderer == 'rich':
|
154
|
+
print(f"{COLORS['yellow']}Warning: Rich is not available.{COLORS['reset']}")
|
155
|
+
print(f"{COLORS['yellow']}Install with: pip install rich{COLORS['reset']}")
|
156
|
+
elif renderer == 'glow':
|
157
|
+
print(f"{COLORS['yellow']}Warning: Glow is not available.{COLORS['reset']}")
|
158
|
+
print(f"{COLORS['yellow']}Install from https://github.com/charmbracelet/glow{COLORS['reset']}")
|
159
|
+
else:
|
160
|
+
print(f"{COLORS['yellow']}Error: Invalid renderer '{renderer}'. Use 'auto', 'rich', or 'glow'.{COLORS['reset']}")
|
161
|
+
|
162
|
+
return False
|
163
|
+
|
164
|
+
def prettify_markdown(text, renderer='auto'):
|
165
|
+
"""Render markdown text with beautiful formatting using either Rich or Glow.
|
166
|
+
|
167
|
+
The function handles both general markdown and code blocks with syntax highlighting.
|
168
|
+
For code generation mode, it automatically wraps the code in markdown code blocks.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
text (str): Markdown text to render
|
172
|
+
renderer (str): Which renderer to use: 'auto', 'rich', or 'glow'
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
bool: True if rendering was successful, False otherwise
|
176
|
+
"""
|
177
|
+
# For 'auto', prefer rich if available, otherwise use glow
|
178
|
+
if renderer == 'auto':
|
179
|
+
if HAS_RICH:
|
180
|
+
return prettify_markdown(text, 'rich')
|
181
|
+
elif HAS_GLOW:
|
182
|
+
return prettify_markdown(text, 'glow')
|
183
|
+
else:
|
184
|
+
return False
|
185
|
+
|
186
|
+
# Use glow for rendering
|
187
|
+
elif renderer == 'glow':
|
188
|
+
if not HAS_GLOW:
|
189
|
+
print(f"{COLORS['yellow']}Warning: Glow is not available. Install from https://github.com/charmbracelet/glow{COLORS['reset']}")
|
190
|
+
# Fall back to rich if available
|
191
|
+
if HAS_RICH:
|
192
|
+
print(f"{COLORS['yellow']}Falling back to Rich renderer.{COLORS['reset']}")
|
193
|
+
return prettify_markdown(text, 'rich')
|
194
|
+
return False
|
195
|
+
|
196
|
+
# Use glow
|
197
|
+
import tempfile
|
198
|
+
import subprocess
|
199
|
+
|
200
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.md', delete=False) as temp:
|
201
|
+
temp_filename = temp.name
|
202
|
+
temp.write(text)
|
203
|
+
|
204
|
+
try:
|
205
|
+
# Execute glow on the temporary file
|
206
|
+
subprocess.run(["glow", temp_filename], check=True)
|
207
|
+
os.unlink(temp_filename)
|
208
|
+
return True
|
209
|
+
except Exception as e:
|
210
|
+
print(f"{COLORS['yellow']}Error using glow: {str(e)}{COLORS['reset']}")
|
211
|
+
os.unlink(temp_filename)
|
212
|
+
|
213
|
+
# Fall back to rich if available
|
214
|
+
if HAS_RICH:
|
215
|
+
print(f"{COLORS['yellow']}Falling back to Rich renderer.{COLORS['reset']}")
|
216
|
+
return prettify_markdown(text, 'rich')
|
217
|
+
return False
|
218
|
+
|
219
|
+
# Use rich for rendering
|
220
|
+
elif renderer == 'rich':
|
221
|
+
if not HAS_RICH:
|
222
|
+
print(f"{COLORS['yellow']}Warning: Rich is not available. Install with: pip install rich{COLORS['reset']}")
|
223
|
+
# Fall back to glow if available
|
224
|
+
if HAS_GLOW:
|
225
|
+
print(f"{COLORS['yellow']}Falling back to Glow renderer.{COLORS['reset']}")
|
226
|
+
return prettify_markdown(text, 'glow')
|
227
|
+
return False
|
228
|
+
|
229
|
+
# Use rich
|
230
|
+
try:
|
231
|
+
console = Console()
|
232
|
+
md = Markdown(text)
|
233
|
+
console.print(md)
|
234
|
+
return True
|
235
|
+
except Exception as e:
|
236
|
+
print(f"{COLORS['yellow']}Error using rich for markdown: {str(e)}{COLORS['reset']}")
|
237
|
+
return False
|
238
|
+
|
239
|
+
# Invalid renderer specified
|
240
|
+
else:
|
241
|
+
print(f"{COLORS['yellow']}Error: Invalid renderer '{renderer}'. Use 'auto', 'rich', or 'glow'.{COLORS['reset']}")
|
242
|
+
return False
|
243
|
+
|
71
244
|
# Custom help formatter with color support
|
72
245
|
class ColoredHelpFormatter(argparse.HelpFormatter):
|
73
246
|
"""Help formatter that properly handles ANSI color codes without breaking alignment."""
|
@@ -302,13 +475,20 @@ def show_config_help():
|
|
302
475
|
print(f" 5. {COLORS['cyan']}Use --config-index to specify which configuration to use or edit:{COLORS['reset']}")
|
303
476
|
print(f" {COLORS['yellow']}ngpt --config-index 1 \"Your prompt\"{COLORS['reset']}")
|
304
477
|
|
305
|
-
print(f" 6. {COLORS['cyan']}Use --
|
478
|
+
print(f" 6. {COLORS['cyan']}Use --provider to specify which configuration to use by provider name:{COLORS['reset']}")
|
479
|
+
print(f" {COLORS['yellow']}ngpt --provider Gemini \"Your prompt\"{COLORS['reset']}")
|
480
|
+
|
481
|
+
print(f" 7. {COLORS['cyan']}Use --config without arguments to add a new configuration:{COLORS['reset']}")
|
306
482
|
print(f" {COLORS['yellow']}ngpt --config{COLORS['reset']}")
|
307
|
-
print(f" Or specify an index to edit an existing configuration:")
|
483
|
+
print(f" Or specify an index or provider to edit an existing configuration:")
|
308
484
|
print(f" {COLORS['yellow']}ngpt --config --config-index 1{COLORS['reset']}")
|
309
|
-
print(f"
|
485
|
+
print(f" {COLORS['yellow']}ngpt --config --provider Gemini{COLORS['reset']}")
|
486
|
+
|
487
|
+
print(f" 8. {COLORS['cyan']}Remove a configuration by index or provider:{COLORS['reset']}")
|
310
488
|
print(f" {COLORS['yellow']}ngpt --config --remove --config-index 1{COLORS['reset']}")
|
311
|
-
print(f"
|
489
|
+
print(f" {COLORS['yellow']}ngpt --config --remove --provider Gemini{COLORS['reset']}")
|
490
|
+
|
491
|
+
print(f" 9. {COLORS['cyan']}List available models for the current configuration:{COLORS['reset']}")
|
312
492
|
print(f" {COLORS['yellow']}ngpt --list-models{COLORS['reset']}")
|
313
493
|
|
314
494
|
def check_config(config):
|
@@ -325,7 +505,7 @@ def check_config(config):
|
|
325
505
|
|
326
506
|
return True
|
327
507
|
|
328
|
-
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):
|
508
|
+
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'):
|
329
509
|
"""Run an interactive chat session with conversation history."""
|
330
510
|
# Get terminal width for better formatting
|
331
511
|
try:
|
@@ -467,6 +647,7 @@ def interactive_chat_session(client, web_search=False, no_stream=False, temperat
|
|
467
647
|
|
468
648
|
# Skip empty messages but don't raise an error
|
469
649
|
if not user_input.strip():
|
650
|
+
print(f"{COLORS['yellow']}Empty message skipped. Type 'exit' to quit.{COLORS['reset']}")
|
470
651
|
continue
|
471
652
|
|
472
653
|
# Add user message to conversation
|
@@ -484,11 +665,19 @@ def interactive_chat_session(client, web_search=False, no_stream=False, temperat
|
|
484
665
|
else:
|
485
666
|
print(f"\n{ngpt_header()}: {COLORS['reset']}", flush=True)
|
486
667
|
|
668
|
+
# If prettify is enabled, we need to disable streaming to collect the full response
|
669
|
+
should_stream = not no_stream and not prettify
|
670
|
+
|
671
|
+
# If prettify is enabled with streaming, inform the user
|
672
|
+
if prettify and not no_stream:
|
673
|
+
print(f"\n{COLORS['yellow']}Note: Streaming disabled to enable markdown rendering.{COLORS['reset']}")
|
674
|
+
print(f"\n{ngpt_header()}: {COLORS['reset']}", flush=True)
|
675
|
+
|
487
676
|
# Get AI response with conversation history
|
488
677
|
response = client.chat(
|
489
678
|
prompt=user_input,
|
490
679
|
messages=conversation,
|
491
|
-
stream=
|
680
|
+
stream=should_stream,
|
492
681
|
web_search=web_search,
|
493
682
|
temperature=temperature,
|
494
683
|
top_p=top_p,
|
@@ -500,9 +689,12 @@ def interactive_chat_session(client, web_search=False, no_stream=False, temperat
|
|
500
689
|
assistant_message = {"role": "assistant", "content": response}
|
501
690
|
conversation.append(assistant_message)
|
502
691
|
|
503
|
-
# Print response if not streamed
|
504
|
-
if no_stream:
|
505
|
-
|
692
|
+
# Print response if not streamed (either due to no_stream or prettify)
|
693
|
+
if no_stream or prettify:
|
694
|
+
if prettify:
|
695
|
+
prettify_markdown(response, renderer)
|
696
|
+
else:
|
697
|
+
print(response)
|
506
698
|
|
507
699
|
# Log assistant response if logging is enabled
|
508
700
|
if log_handle:
|
@@ -554,10 +746,12 @@ def main():
|
|
554
746
|
config_group = parser.add_argument_group('Configuration Options')
|
555
747
|
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')
|
556
748
|
config_group.add_argument('--config-index', type=int, default=0, help='Index of the configuration to use or edit (default: 0)')
|
749
|
+
config_group.add_argument('--provider', help='Provider name to identify the configuration to use')
|
557
750
|
config_group.add_argument('--remove', action='store_true', help='Remove the configuration at the specified index (requires --config and --config-index)')
|
558
751
|
config_group.add_argument('--show-config', action='store_true', help='Show the current configuration(s) and exit')
|
559
752
|
config_group.add_argument('--all', action='store_true', help='Show details for all configurations (requires --show-config)')
|
560
753
|
config_group.add_argument('--list-models', action='store_true', help='List all available models for the current configuration and exit')
|
754
|
+
config_group.add_argument('--list-renderers', action='store_true', help='Show available markdown renderers for use with --prettify')
|
561
755
|
|
562
756
|
# Global options
|
563
757
|
global_group = parser.add_argument_group('Global Options')
|
@@ -578,6 +772,10 @@ def main():
|
|
578
772
|
help='Set filepath to log conversation to (For interactive modes)')
|
579
773
|
global_group.add_argument('--preprompt',
|
580
774
|
help='Set custom system prompt to control AI behavior')
|
775
|
+
global_group.add_argument('--prettify', action='store_const', const='auto',
|
776
|
+
help='Render markdown responses and code with syntax highlighting and formatting')
|
777
|
+
global_group.add_argument('--renderer', choices=['auto', 'rich', 'glow'], default='auto',
|
778
|
+
help='Select which markdown renderer to use with --prettify (auto, rich, or glow)')
|
581
779
|
|
582
780
|
# Mode flags (mutually exclusive)
|
583
781
|
mode_group = parser.add_argument_group('Modes (mutually exclusive)')
|
@@ -599,6 +797,15 @@ def main():
|
|
599
797
|
# Validate --all usage
|
600
798
|
if args.all and not args.show_config:
|
601
799
|
parser.error("--all can only be used with --show-config")
|
800
|
+
|
801
|
+
# Handle --renderers flag to show available markdown renderers
|
802
|
+
if args.list_renderers:
|
803
|
+
show_available_renderers()
|
804
|
+
return
|
805
|
+
|
806
|
+
# Check for mutual exclusivity between --config-index and --provider
|
807
|
+
if args.config_index != 0 and args.provider:
|
808
|
+
parser.error("--config-index and --provider cannot be used together")
|
602
809
|
|
603
810
|
# Handle interactive configuration mode
|
604
811
|
if args.config is True: # --config was used without a value
|
@@ -607,20 +814,46 @@ def main():
|
|
607
814
|
# Handle configuration removal if --remove flag is present
|
608
815
|
if args.remove:
|
609
816
|
# Validate that config_index is explicitly provided
|
610
|
-
if '--config-index' not in sys.argv:
|
611
|
-
parser.error("--remove requires explicitly specifying --config-index")
|
817
|
+
if '--config-index' not in sys.argv and not args.provider:
|
818
|
+
parser.error("--remove requires explicitly specifying --config-index or --provider")
|
612
819
|
|
613
820
|
# Show config details before asking for confirmation
|
614
821
|
configs = load_configs(str(config_path))
|
615
822
|
|
823
|
+
# Determine the config index to remove
|
824
|
+
config_index = args.config_index
|
825
|
+
if args.provider:
|
826
|
+
# Find config index by provider name
|
827
|
+
matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == args.provider.lower()]
|
828
|
+
if not matching_configs:
|
829
|
+
print(f"Error: No configuration found for provider '{args.provider}'")
|
830
|
+
return
|
831
|
+
elif len(matching_configs) > 1:
|
832
|
+
print(f"Multiple configurations found for provider '{args.provider}':")
|
833
|
+
for i, idx in enumerate(matching_configs):
|
834
|
+
print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
|
835
|
+
|
836
|
+
try:
|
837
|
+
choice = input("Choose a configuration to remove (or press Enter to cancel): ")
|
838
|
+
if choice and choice.isdigit() and 0 <= int(choice) < len(matching_configs):
|
839
|
+
config_index = matching_configs[int(choice)]
|
840
|
+
else:
|
841
|
+
print("Configuration removal cancelled.")
|
842
|
+
return
|
843
|
+
except (ValueError, IndexError, KeyboardInterrupt):
|
844
|
+
print("\nConfiguration removal cancelled.")
|
845
|
+
return
|
846
|
+
else:
|
847
|
+
config_index = matching_configs[0]
|
848
|
+
|
616
849
|
# Check if index is valid
|
617
|
-
if
|
618
|
-
print(f"Error: Configuration index {
|
850
|
+
if config_index < 0 or config_index >= len(configs):
|
851
|
+
print(f"Error: Configuration index {config_index} is out of range. Valid range: 0-{len(configs)-1}")
|
619
852
|
return
|
620
853
|
|
621
854
|
# Show the configuration that will be removed
|
622
|
-
config = configs[
|
623
|
-
print(f"Configuration to remove (index {
|
855
|
+
config = configs[config_index]
|
856
|
+
print(f"Configuration to remove (index {config_index}):")
|
624
857
|
print(f" Provider: {config.get('provider', 'N/A')}")
|
625
858
|
print(f" Model: {config.get('model', 'N/A')}")
|
626
859
|
print(f" Base URL: {config.get('base_url', 'N/A')}")
|
@@ -631,7 +864,7 @@ def main():
|
|
631
864
|
print("\nAre you sure you want to remove this configuration? [y/N] ", end='')
|
632
865
|
response = input().lower()
|
633
866
|
if response in ('y', 'yes'):
|
634
|
-
remove_config_entry(config_path,
|
867
|
+
remove_config_entry(config_path, config_index)
|
635
868
|
else:
|
636
869
|
print("Configuration removal cancelled.")
|
637
870
|
except KeyboardInterrupt:
|
@@ -643,20 +876,51 @@ def main():
|
|
643
876
|
# If --config-index was not explicitly specified, create a new entry by passing None
|
644
877
|
# This will cause add_config_entry to create a new entry at the end of the list
|
645
878
|
# Otherwise, edit the existing config at the specified index
|
646
|
-
config_index = None
|
879
|
+
config_index = None
|
647
880
|
|
648
|
-
#
|
649
|
-
|
650
|
-
|
651
|
-
|
652
|
-
|
881
|
+
# Determine if we're editing an existing config or creating a new one
|
882
|
+
if args.provider:
|
883
|
+
# Find config by provider name
|
884
|
+
configs = load_configs(str(config_path))
|
885
|
+
matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == args.provider.lower()]
|
886
|
+
|
887
|
+
if not matching_configs:
|
888
|
+
print(f"No configuration found for provider '{args.provider}'. Creating a new configuration.")
|
889
|
+
elif len(matching_configs) > 1:
|
890
|
+
print(f"Multiple configurations found for provider '{args.provider}':")
|
891
|
+
for i, idx in enumerate(matching_configs):
|
892
|
+
print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
|
893
|
+
|
894
|
+
try:
|
895
|
+
choice = input("Choose a configuration to edit (or press Enter for the first one): ")
|
896
|
+
if choice and choice.isdigit() and 0 <= int(choice) < len(matching_configs):
|
897
|
+
config_index = matching_configs[int(choice)]
|
898
|
+
else:
|
899
|
+
config_index = matching_configs[0]
|
900
|
+
except (ValueError, IndexError, KeyboardInterrupt):
|
901
|
+
config_index = matching_configs[0]
|
902
|
+
else:
|
903
|
+
config_index = matching_configs[0]
|
904
|
+
|
653
905
|
print(f"Editing existing configuration at index {config_index}")
|
906
|
+
elif args.config_index != 0 or '--config-index' in sys.argv:
|
907
|
+
# Check if the index is valid
|
908
|
+
configs = load_configs(str(config_path))
|
909
|
+
if args.config_index >= 0 and args.config_index < len(configs):
|
910
|
+
config_index = args.config_index
|
911
|
+
print(f"Editing existing configuration at index {config_index}")
|
912
|
+
else:
|
913
|
+
print(f"Configuration index {args.config_index} is out of range. Creating a new configuration.")
|
914
|
+
else:
|
915
|
+
# Creating a new config
|
916
|
+
configs = load_configs(str(config_path))
|
917
|
+
print(f"Creating new configuration at index {len(configs)}")
|
654
918
|
|
655
919
|
add_config_entry(config_path, config_index)
|
656
920
|
return
|
657
921
|
|
658
|
-
# Load configuration using the specified index (needed for active config display)
|
659
|
-
active_config = load_config(args.config, args.config_index)
|
922
|
+
# Load configuration using the specified index or provider (needed for active config display)
|
923
|
+
active_config = load_config(args.config, args.config_index, args.provider)
|
660
924
|
|
661
925
|
# Command-line arguments override config settings for active config display
|
662
926
|
# This part is kept to ensure the active config display reflects potential overrides,
|
@@ -675,31 +939,57 @@ def main():
|
|
675
939
|
|
676
940
|
print(f"Configuration file: {config_path}")
|
677
941
|
print(f"Total configurations: {len(configs)}")
|
678
|
-
|
942
|
+
|
943
|
+
# Determine active configuration and display identifier
|
944
|
+
active_identifier = f"index {args.config_index}"
|
945
|
+
if args.provider:
|
946
|
+
active_identifier = f"provider '{args.provider}'"
|
947
|
+
print(f"Active configuration: {active_identifier}")
|
679
948
|
|
680
949
|
if args.all:
|
681
950
|
# Show details for all configurations
|
682
951
|
print("\nAll configuration details:")
|
683
952
|
for i, cfg in enumerate(configs):
|
684
|
-
|
685
|
-
|
953
|
+
provider = cfg.get('provider', 'N/A')
|
954
|
+
active_str = '(Active)' if (
|
955
|
+
(args.provider and provider.lower() == args.provider.lower()) or
|
956
|
+
(not args.provider and i == args.config_index)
|
957
|
+
) else ''
|
958
|
+
print(f"\n--- Configuration Index {i} / Provider: {COLORS['green']}{provider}{COLORS['reset']} {active_str} ---")
|
686
959
|
print(f" API Key: {'[Set]' if cfg.get('api_key') else '[Not Set]'}")
|
687
960
|
print(f" Base URL: {cfg.get('base_url', 'N/A')}")
|
688
|
-
print(f" Provider: {cfg.get('provider', 'N/A')}")
|
689
961
|
print(f" Model: {cfg.get('model', 'N/A')}")
|
690
962
|
else:
|
691
963
|
# Show active config details and summary list
|
692
964
|
print("\nActive configuration details:")
|
965
|
+
print(f" Provider: {COLORS['green']}{active_config.get('provider', 'N/A')}{COLORS['reset']}")
|
693
966
|
print(f" API Key: {'[Set]' if active_config.get('api_key') else '[Not Set]'}")
|
694
967
|
print(f" Base URL: {active_config.get('base_url', 'N/A')}")
|
695
|
-
print(f" Provider: {active_config.get('provider', 'N/A')}")
|
696
968
|
print(f" Model: {active_config.get('model', 'N/A')}")
|
697
969
|
|
698
970
|
if len(configs) > 1:
|
699
971
|
print("\nAvailable configurations:")
|
972
|
+
# Check for duplicate provider names for warning
|
973
|
+
provider_counts = {}
|
974
|
+
for cfg in configs:
|
975
|
+
provider = cfg.get('provider', 'N/A').lower()
|
976
|
+
provider_counts[provider] = provider_counts.get(provider, 0) + 1
|
977
|
+
|
700
978
|
for i, cfg in enumerate(configs):
|
701
|
-
|
702
|
-
|
979
|
+
provider = cfg.get('provider', 'N/A')
|
980
|
+
provider_display = provider
|
981
|
+
# Add warning for duplicate providers
|
982
|
+
if provider_counts.get(provider.lower(), 0) > 1:
|
983
|
+
provider_display = f"{provider} {COLORS['yellow']}(duplicate){COLORS['reset']}"
|
984
|
+
|
985
|
+
active_marker = "*" if (
|
986
|
+
(args.provider and provider.lower() == args.provider.lower()) or
|
987
|
+
(not args.provider and i == args.config_index)
|
988
|
+
) else " "
|
989
|
+
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]'})")
|
990
|
+
|
991
|
+
# Show instruction for using --provider
|
992
|
+
print(f"\nTip: Use {COLORS['yellow']}--provider NAME{COLORS['reset']} to select a configuration by provider name.")
|
703
993
|
|
704
994
|
return
|
705
995
|
|
@@ -712,6 +1002,17 @@ def main():
|
|
712
1002
|
if not args.show_config and not args.list_models and not check_config(active_config):
|
713
1003
|
return
|
714
1004
|
|
1005
|
+
# Check if --prettify is used but no markdown renderer is available
|
1006
|
+
# This will warn the user immediately if they request prettify but don't have the tools
|
1007
|
+
has_renderer = True
|
1008
|
+
if args.prettify:
|
1009
|
+
has_renderer = warn_if_no_markdown_renderer(args.renderer)
|
1010
|
+
if not has_renderer:
|
1011
|
+
# Set a flag to disable prettify since we already warned the user
|
1012
|
+
print(f"{COLORS['yellow']}Continuing without markdown rendering.{COLORS['reset']}")
|
1013
|
+
show_available_renderers()
|
1014
|
+
args.prettify = False
|
1015
|
+
|
715
1016
|
# Initialize client using the potentially overridden active_config
|
716
1017
|
client = NGPTClient(**active_config)
|
717
1018
|
|
@@ -738,7 +1039,7 @@ def main():
|
|
738
1039
|
# Interactive chat mode
|
739
1040
|
interactive_chat_session(client, web_search=args.web_search, no_stream=args.no_stream,
|
740
1041
|
temperature=args.temperature, top_p=args.top_p,
|
741
|
-
max_tokens=args.max_tokens, log_file=args.log, preprompt=args.preprompt)
|
1042
|
+
max_tokens=args.max_tokens, log_file=args.log, preprompt=args.preprompt, prettify=args.prettify, renderer=args.renderer)
|
742
1043
|
elif args.shell:
|
743
1044
|
if args.prompt is None:
|
744
1045
|
try:
|
@@ -792,7 +1093,13 @@ def main():
|
|
792
1093
|
temperature=args.temperature, top_p=args.top_p,
|
793
1094
|
max_tokens=args.max_tokens)
|
794
1095
|
if generated_code:
|
795
|
-
|
1096
|
+
if args.prettify:
|
1097
|
+
# Format code as markdown with proper syntax highlighting
|
1098
|
+
markdown_code = f"```{args.language}\n{generated_code}\n```"
|
1099
|
+
print("\nGenerated code:")
|
1100
|
+
prettify_markdown(markdown_code, args.renderer)
|
1101
|
+
else:
|
1102
|
+
print(f"\nGenerated code:\n{generated_code}")
|
796
1103
|
|
797
1104
|
elif args.text:
|
798
1105
|
if args.prompt is not None:
|
@@ -910,12 +1217,24 @@ def main():
|
|
910
1217
|
{"role": "system", "content": args.preprompt},
|
911
1218
|
{"role": "user", "content": prompt}
|
912
1219
|
]
|
1220
|
+
|
1221
|
+
# If prettify is enabled, we need to disable streaming to collect the full response
|
1222
|
+
should_stream = not args.no_stream and not args.prettify
|
1223
|
+
|
1224
|
+
# If prettify is enabled with streaming, inform the user
|
1225
|
+
if args.prettify and not args.no_stream:
|
1226
|
+
print(f"{COLORS['yellow']}Note: Streaming disabled to enable markdown rendering.{COLORS['reset']}")
|
913
1227
|
|
914
|
-
response = client.chat(prompt, stream=
|
1228
|
+
response = client.chat(prompt, stream=should_stream, web_search=args.web_search,
|
915
1229
|
temperature=args.temperature, top_p=args.top_p,
|
916
1230
|
max_tokens=args.max_tokens, messages=messages)
|
917
|
-
|
918
|
-
|
1231
|
+
|
1232
|
+
# Handle non-stream response (either because no_stream was set or prettify forced it)
|
1233
|
+
if (args.no_stream or args.prettify) and response:
|
1234
|
+
if args.prettify:
|
1235
|
+
prettify_markdown(response, args.renderer)
|
1236
|
+
else:
|
1237
|
+
print(response)
|
919
1238
|
|
920
1239
|
else:
|
921
1240
|
# Default to chat mode
|
@@ -936,12 +1255,24 @@ def main():
|
|
936
1255
|
{"role": "system", "content": args.preprompt},
|
937
1256
|
{"role": "user", "content": prompt}
|
938
1257
|
]
|
1258
|
+
|
1259
|
+
# If prettify is enabled, we need to disable streaming to collect the full response
|
1260
|
+
should_stream = not args.no_stream and not args.prettify
|
1261
|
+
|
1262
|
+
# If prettify is enabled with streaming, inform the user
|
1263
|
+
if args.prettify and not args.no_stream:
|
1264
|
+
print(f"{COLORS['yellow']}Note: Streaming disabled to enable markdown rendering.{COLORS['reset']}")
|
939
1265
|
|
940
|
-
response = client.chat(prompt, stream=
|
1266
|
+
response = client.chat(prompt, stream=should_stream, web_search=args.web_search,
|
941
1267
|
temperature=args.temperature, top_p=args.top_p,
|
942
1268
|
max_tokens=args.max_tokens, messages=messages)
|
943
|
-
|
944
|
-
|
1269
|
+
|
1270
|
+
# Handle non-stream response (either because no_stream was set or prettify forced it)
|
1271
|
+
if (args.no_stream or args.prettify) and response:
|
1272
|
+
if args.prettify:
|
1273
|
+
prettify_markdown(response, args.renderer)
|
1274
|
+
else:
|
1275
|
+
print(response)
|
945
1276
|
|
946
1277
|
except KeyboardInterrupt:
|
947
1278
|
print("\nOperation cancelled by user. Exiting gracefully.")
|
ngpt/config.py
CHANGED
@@ -75,9 +75,29 @@ def add_config_entry(config_path: Path, config_index: Optional[int] = None) -> N
|
|
75
75
|
if user_input:
|
76
76
|
entry["base_url"] = user_input
|
77
77
|
|
78
|
-
|
79
|
-
|
80
|
-
|
78
|
+
# For provider, check for uniqueness when creating new config
|
79
|
+
provider_unique = False
|
80
|
+
original_provider = entry['provider']
|
81
|
+
while not provider_unique:
|
82
|
+
user_input = input(f"Provider [{entry['provider']}]: ")
|
83
|
+
if user_input:
|
84
|
+
provider = user_input
|
85
|
+
else:
|
86
|
+
provider = entry['provider']
|
87
|
+
|
88
|
+
# When creating new config or changing provider, check uniqueness
|
89
|
+
if is_existing_config and provider.lower() == original_provider.lower():
|
90
|
+
# No change in provider name, so keep it
|
91
|
+
provider_unique = True
|
92
|
+
elif is_provider_unique(configs, provider, config_index if is_existing_config else None):
|
93
|
+
provider_unique = True
|
94
|
+
else:
|
95
|
+
print(f"Error: Provider '{provider}' already exists. Please choose a unique provider name.")
|
96
|
+
# If it's the existing provider, allow keeping it (for existing configs)
|
97
|
+
if is_existing_config and provider.lower() == original_provider.lower():
|
98
|
+
provider_unique = True
|
99
|
+
|
100
|
+
entry["provider"] = provider
|
81
101
|
|
82
102
|
user_input = input(f"Model [{entry['model']}]: ")
|
83
103
|
if user_input:
|
@@ -127,13 +147,46 @@ def load_configs(custom_path: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
127
147
|
|
128
148
|
return configs
|
129
149
|
|
130
|
-
def load_config(custom_path: Optional[str] = None, config_index: int = 0) -> Dict[str, Any]:
|
150
|
+
def load_config(custom_path: Optional[str] = None, config_index: int = 0, provider: Optional[str] = None) -> Dict[str, Any]:
|
131
151
|
"""
|
132
|
-
Load a specific configuration by index and apply environment variables.
|
152
|
+
Load a specific configuration by index or provider name and apply environment variables.
|
133
153
|
Environment variables take precedence over the config file.
|
154
|
+
|
155
|
+
Args:
|
156
|
+
custom_path: Optional path to a custom config file
|
157
|
+
config_index: Index of the configuration to use (default: 0)
|
158
|
+
provider: Provider name to identify the configuration
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
The selected configuration with environment variables applied
|
134
162
|
"""
|
135
163
|
configs = load_configs(custom_path)
|
136
164
|
|
165
|
+
# If provider is specified, try to find a matching config
|
166
|
+
if provider:
|
167
|
+
matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == provider.lower()]
|
168
|
+
|
169
|
+
if not matching_configs:
|
170
|
+
print(f"Warning: No configuration found for provider '{provider}'. Using default configuration.")
|
171
|
+
config_index = 0
|
172
|
+
elif len(matching_configs) > 1:
|
173
|
+
print(f"Warning: Multiple configurations found for provider '{provider}'.")
|
174
|
+
for i, idx in enumerate(matching_configs):
|
175
|
+
print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
|
176
|
+
|
177
|
+
try:
|
178
|
+
choice = input("Choose a configuration (or press Enter for the first one): ")
|
179
|
+
if choice and choice.isdigit() and 0 <= int(choice) < len(matching_configs):
|
180
|
+
config_index = matching_configs[int(choice)]
|
181
|
+
else:
|
182
|
+
config_index = matching_configs[0]
|
183
|
+
print(f"Using first matching configuration (index {config_index}).")
|
184
|
+
except (ValueError, IndexError, KeyboardInterrupt):
|
185
|
+
config_index = matching_configs[0]
|
186
|
+
print(f"Using first matching configuration (index {config_index}).")
|
187
|
+
else:
|
188
|
+
config_index = matching_configs[0]
|
189
|
+
|
137
190
|
# If config_index is out of range, use the first config
|
138
191
|
if config_index < 0 or config_index >= len(configs):
|
139
192
|
if len(configs) > 0:
|
@@ -157,7 +210,7 @@ def load_config(custom_path: Optional[str] = None, config_index: int = 0) -> Dic
|
|
157
210
|
if env_var in os.environ and os.environ[env_var]:
|
158
211
|
config[config_key] = os.environ[env_var]
|
159
212
|
|
160
|
-
return config
|
213
|
+
return config
|
161
214
|
|
162
215
|
def remove_config_entry(config_path: Path, config_index: int) -> bool:
|
163
216
|
"""
|
@@ -182,4 +235,24 @@ def remove_config_entry(config_path: Path, config_index: int) -> bool:
|
|
182
235
|
return True
|
183
236
|
except Exception as e:
|
184
237
|
print(f"Error saving configuration: {e}")
|
185
|
-
return False
|
238
|
+
return False
|
239
|
+
|
240
|
+
def is_provider_unique(configs: List[Dict[str, Any]], provider: str, exclude_index: Optional[int] = None) -> bool:
|
241
|
+
"""
|
242
|
+
Check if a provider name is unique among configurations.
|
243
|
+
|
244
|
+
Args:
|
245
|
+
configs: List of configuration dictionaries
|
246
|
+
provider: Provider name to check
|
247
|
+
exclude_index: Optional index to exclude from the check (for updating existing config)
|
248
|
+
|
249
|
+
Returns:
|
250
|
+
True if the provider name is unique, False otherwise
|
251
|
+
"""
|
252
|
+
provider = provider.lower()
|
253
|
+
for i, cfg in enumerate(configs):
|
254
|
+
if i == exclude_index:
|
255
|
+
continue
|
256
|
+
if cfg.get('provider', '').lower() == provider:
|
257
|
+
return False
|
258
|
+
return True
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ngpt
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.5.0
|
4
4
|
Summary: A lightweight Python CLI and library for interacting with OpenAI-compatible APIs, supporting both official and self-hosted LLM endpoints.
|
5
5
|
Project-URL: Homepage, https://github.com/nazdridoy/ngpt
|
6
6
|
Project-URL: Repository, https://github.com/nazdridoy/ngpt
|
@@ -30,6 +30,9 @@ Classifier: Topic :: Utilities
|
|
30
30
|
Requires-Python: >=3.8
|
31
31
|
Requires-Dist: prompt-toolkit>=3.0.0
|
32
32
|
Requires-Dist: requests>=2.31.0
|
33
|
+
Requires-Dist: rich>=14.0.0
|
34
|
+
Provides-Extra: prettify
|
35
|
+
Requires-Dist: rich>=10.0.0; extra == 'prettify'
|
33
36
|
Description-Content-Type: text/markdown
|
34
37
|
|
35
38
|
# nGPT
|
@@ -76,9 +79,18 @@ ngpt -n "Tell me about quantum computing"
|
|
76
79
|
# Generate code
|
77
80
|
ngpt --code "function to calculate the Fibonacci sequence"
|
78
81
|
|
82
|
+
# Generate code with syntax highlighting
|
83
|
+
ngpt --code --prettify "function to calculate the Fibonacci sequence"
|
84
|
+
|
79
85
|
# Generate and execute shell commands
|
80
86
|
ngpt --shell "list all files in the current directory"
|
81
87
|
|
88
|
+
# Display markdown responses with beautiful formatting
|
89
|
+
ngpt --prettify "Explain markdown syntax with examples"
|
90
|
+
|
91
|
+
# Use a specific markdown renderer
|
92
|
+
ngpt --prettify --renderer=rich "Create a markdown table"
|
93
|
+
|
82
94
|
# Use multiline editor for complex prompts
|
83
95
|
ngpt --text
|
84
96
|
|
@@ -99,6 +111,7 @@ For more examples and detailed usage, visit the [CLI Usage Guide](https://nazdri
|
|
99
111
|
- 💬 **Interactive Chat**: Continuous conversation with memory in modern UI
|
100
112
|
- 📊 **Streaming Responses**: Real-time output for better user experience
|
101
113
|
- 🔍 **Web Search**: Integrated with compatible API endpoints
|
114
|
+
- 🎨 **Markdown Rendering**: Beautiful formatting of markdown and code with syntax highlighting
|
102
115
|
- ⚙️ **Multiple Configurations**: Cross-platform config system supporting different profiles
|
103
116
|
- 💻 **Shell Command Generation**: OS-aware command execution
|
104
117
|
- 🧩 **Clean Code Generation**: Output code without markdown or explanations
|
@@ -267,8 +280,12 @@ You can configure the client using the following options:
|
|
267
280
|
| `--max_tokens` | Set maximum response length in tokens |
|
268
281
|
| `--preprompt` | Set custom system prompt to control AI behavior |
|
269
282
|
| `--log` | Set filepath to log conversation to (for interactive modes) |
|
283
|
+
| `--prettify` | Render markdown responses and code with syntax highlighting |
|
284
|
+
| `--renderer` | Select which markdown renderer to use with --prettify (auto, rich, or glow) |
|
285
|
+
| `--list-renderers` | Show available markdown renderers for use with --prettify |
|
270
286
|
| `--config` | Path to a custom configuration file or, when used without a value, enters interactive configuration mode |
|
271
287
|
| `--config-index` | Index of the configuration to use (default: 0) |
|
288
|
+
| `--provider` | Provider name to identify the configuration to use (alternative to --config-index) |
|
272
289
|
| `--remove` | Remove the configuration at the specified index (requires --config and --config-index) |
|
273
290
|
| `--show-config` | Show configuration details and exit |
|
274
291
|
| `--all` | Used with `--show-config` to display all configurations |
|
@@ -291,8 +308,17 @@ ngpt --config
|
|
291
308
|
# Edit an existing configuration at index 1
|
292
309
|
ngpt --config --config-index 1
|
293
310
|
|
311
|
+
# Edit an existing configuration by provider name
|
312
|
+
ngpt --config --provider Gemini
|
313
|
+
|
294
314
|
# Remove a configuration at index 2
|
295
315
|
ngpt --config --remove --config-index 2
|
316
|
+
|
317
|
+
# Remove a configuration by provider name
|
318
|
+
ngpt --config --remove --provider Gemini
|
319
|
+
|
320
|
+
# Use a specific configuration by provider name
|
321
|
+
ngpt --provider OpenAI "Tell me about quantum computing"
|
296
322
|
```
|
297
323
|
|
298
324
|
In interactive mode:
|
@@ -0,0 +1,9 @@
|
|
1
|
+
ngpt/__init__.py,sha256=ehInP9w0MZlS1vZ1g6Cm4YE1ftmgF72CnEddQ3Le9n4,368
|
2
|
+
ngpt/cli.py,sha256=VE-Zp7mx-GC6p0o9CcKAnAJCDbLjwiV_UKxwikceDaM,59632
|
3
|
+
ngpt/client.py,sha256=lJfyLONeBU7YqMVJJys6zPay7gcJTq108_rJ4wvOtok,13067
|
4
|
+
ngpt/config.py,sha256=WYOk_b1eiYjo6hpV3pfXr2RjqhOnmKqwZwKid1T41I4,10363
|
5
|
+
ngpt-2.5.0.dist-info/METADATA,sha256=Guk41uYFdF-_hWqi6QgUHhWXXuhgU2CMai0GvIOnkHA,14685
|
6
|
+
ngpt-2.5.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
7
|
+
ngpt-2.5.0.dist-info/entry_points.txt,sha256=1cnAMujyy34DlOahrJg19lePSnb08bLbkUs_kVerqdk,39
|
8
|
+
ngpt-2.5.0.dist-info/licenses/LICENSE,sha256=mQkpWoADxbHqE0HRefYLJdm7OpdrXBr3vNv5bZ8w72M,1065
|
9
|
+
ngpt-2.5.0.dist-info/RECORD,,
|
ngpt-2.3.4.dist-info/RECORD
DELETED
@@ -1,9 +0,0 @@
|
|
1
|
-
ngpt/__init__.py,sha256=ehInP9w0MZlS1vZ1g6Cm4YE1ftmgF72CnEddQ3Le9n4,368
|
2
|
-
ngpt/cli.py,sha256=pmgUK-vrMAAsALKnTxVAcoFSGZ4DM89d43bXKuiLbN0,43532
|
3
|
-
ngpt/client.py,sha256=lJfyLONeBU7YqMVJJys6zPay7gcJTq108_rJ4wvOtok,13067
|
4
|
-
ngpt/config.py,sha256=BF0G3QeiPma8l7EQyc37bR7LWZog7FHJQNe7uj9cr4w,6896
|
5
|
-
ngpt-2.3.4.dist-info/METADATA,sha256=H78mhW758iTL9wnB2lgMGYUzivmHzDQU7IdEXqC4X4Y,13535
|
6
|
-
ngpt-2.3.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
7
|
-
ngpt-2.3.4.dist-info/entry_points.txt,sha256=1cnAMujyy34DlOahrJg19lePSnb08bLbkUs_kVerqdk,39
|
8
|
-
ngpt-2.3.4.dist-info/licenses/LICENSE,sha256=mQkpWoADxbHqE0HRefYLJdm7OpdrXBr3vNv5bZ8w72M,1065
|
9
|
-
ngpt-2.3.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|