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 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 --config without arguments to add a new configuration:{COLORS['reset']}")
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" 7. {COLORS['cyan']}Remove a configuration at a specific index:{COLORS['reset']}")
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" 8. {COLORS['cyan']}List available models for the current configuration:{COLORS['reset']}")
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=not no_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
- print(response)
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 args.config_index < 0 or args.config_index >= len(configs):
618
- print(f"Error: Configuration index {args.config_index} is out of range. Valid range: 0-{len(configs)-1}")
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[args.config_index]
623
- print(f"Configuration to remove (index {args.config_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, args.config_index)
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 if args.config_index == 0 and '--config-index' not in sys.argv else args.config_index
879
+ config_index = None
647
880
 
648
- # Load existing configs to determine the new index if creating a new config
649
- configs = load_configs(str(config_path))
650
- if config_index is None:
651
- print(f"Creating new configuration at index {len(configs)}")
652
- else:
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
- print(f"Active configuration index: {args.config_index}")
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
- active_str = '(Active)' if i == args.config_index else ''
685
- print(f"\n--- Configuration Index {i} {active_str} ---")
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
- active_marker = "*" if i == args.config_index else " "
702
- print(f"[{i}]{active_marker} {cfg.get('provider', 'N/A')} - {cfg.get('model', 'N/A')} ({'[API Key Set]' if cfg.get('api_key') else '[API Key Not Set]'})")
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
- print(f"\nGenerated code:\n{generated_code}")
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=not args.no_stream, web_search=args.web_search,
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
- if args.no_stream and response:
918
- print(response)
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=not args.no_stream, web_search=args.web_search,
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
- if args.no_stream and response:
944
- print(response)
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
- user_input = input(f"Provider [{entry['provider']}]: ")
79
- if user_input:
80
- entry["provider"] = user_input
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.4
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,,
@@ -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