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/main.py
ADDED
@@ -0,0 +1,593 @@
|
|
1
|
+
import argparse
|
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
|
+
load_cli_config
|
14
|
+
)
|
15
|
+
from .. import __version__
|
16
|
+
|
17
|
+
from .formatters import COLORS, ColoredHelpFormatter
|
18
|
+
from .renderers import has_markdown_renderer, warn_if_no_markdown_renderer, show_available_renderers
|
19
|
+
from .config_manager import show_config_help, check_config
|
20
|
+
from .interactive import interactive_chat_session
|
21
|
+
from .modes.chat import chat_mode
|
22
|
+
from .modes.code import code_mode
|
23
|
+
from .modes.shell import shell_mode
|
24
|
+
from .modes.text import text_mode
|
25
|
+
|
26
|
+
def show_cli_config_help():
|
27
|
+
"""Display help information about CLI configuration."""
|
28
|
+
print(f"\n{COLORS['green']}{COLORS['bold']}CLI Configuration Help:{COLORS['reset']}")
|
29
|
+
print(f" {COLORS['cyan']}Command syntax:{COLORS['reset']}")
|
30
|
+
print(f" {COLORS['yellow']}ngpt --cli-config help{COLORS['reset']} - Show this help message")
|
31
|
+
print(f" {COLORS['yellow']}ngpt --cli-config set OPTION VALUE{COLORS['reset']} - Set a default value for OPTION")
|
32
|
+
print(f" {COLORS['yellow']}ngpt --cli-config get OPTION{COLORS['reset']} - Get the current value of OPTION")
|
33
|
+
print(f" {COLORS['yellow']}ngpt --cli-config get{COLORS['reset']} - Show all CLI configuration settings")
|
34
|
+
print(f" {COLORS['yellow']}ngpt --cli-config unset OPTION{COLORS['reset']} - Remove OPTION from configuration")
|
35
|
+
print(f" {COLORS['yellow']}ngpt --cli-config list{COLORS['reset']} - List all available options")
|
36
|
+
|
37
|
+
print(f"\n {COLORS['cyan']}Available options:{COLORS['reset']}")
|
38
|
+
|
39
|
+
# Group options by context
|
40
|
+
context_groups = {
|
41
|
+
"all": [],
|
42
|
+
"code": [],
|
43
|
+
"interactive": [],
|
44
|
+
"text": [],
|
45
|
+
"shell": []
|
46
|
+
}
|
47
|
+
|
48
|
+
for option, meta in CLI_CONFIG_OPTIONS.items():
|
49
|
+
for context in meta["context"]:
|
50
|
+
if context in context_groups:
|
51
|
+
if context == "all":
|
52
|
+
context_groups[context].append(option)
|
53
|
+
break
|
54
|
+
else:
|
55
|
+
context_groups[context].append(option)
|
56
|
+
|
57
|
+
# Print general options (available in all contexts)
|
58
|
+
print(f" {COLORS['yellow']}General options (all modes):{COLORS['reset']}")
|
59
|
+
for option in sorted(context_groups["all"]):
|
60
|
+
meta = CLI_CONFIG_OPTIONS[option]
|
61
|
+
default = f"(default: {meta['default']})" if meta['default'] is not None else ""
|
62
|
+
exclusive = f" [exclusive with: {', '.join(meta['exclusive'])}]" if "exclusive" in meta else ""
|
63
|
+
print(f" {COLORS['green']}{option}{COLORS['reset']} - {meta['type']} {default}{exclusive}")
|
64
|
+
|
65
|
+
# Print mode-specific options
|
66
|
+
for mode, options in [
|
67
|
+
("code", "Code generation mode"),
|
68
|
+
("interactive", "Interactive mode"),
|
69
|
+
("text", "Text mode"),
|
70
|
+
("shell", "Shell mode")
|
71
|
+
]:
|
72
|
+
if context_groups[mode]:
|
73
|
+
print(f"\n {COLORS['yellow']}Options for {options}:{COLORS['reset']}")
|
74
|
+
for option in sorted(context_groups[mode]):
|
75
|
+
# Skip if already listed in general options
|
76
|
+
if option in context_groups["all"]:
|
77
|
+
continue
|
78
|
+
meta = CLI_CONFIG_OPTIONS[option]
|
79
|
+
default = f"(default: {meta['default']})" if meta['default'] is not None else ""
|
80
|
+
exclusive = f" [exclusive with: {', '.join(meta['exclusive'])}]" if "exclusive" in meta else ""
|
81
|
+
print(f" {COLORS['green']}{option}{COLORS['reset']} - {meta['type']} {default}{exclusive}")
|
82
|
+
|
83
|
+
print(f"\n {COLORS['cyan']}Example usage:{COLORS['reset']}")
|
84
|
+
print(f" {COLORS['yellow']}ngpt --cli-config set language java{COLORS['reset']} - Set default language to java for code generation")
|
85
|
+
print(f" {COLORS['yellow']}ngpt --cli-config set temperature 0.9{COLORS['reset']} - Set default temperature to 0.9")
|
86
|
+
print(f" {COLORS['yellow']}ngpt --cli-config set no-stream true{COLORS['reset']} - Disable streaming by default")
|
87
|
+
print(f" {COLORS['yellow']}ngpt --cli-config get temperature{COLORS['reset']} - Check the current temperature setting")
|
88
|
+
print(f" {COLORS['yellow']}ngpt --cli-config get{COLORS['reset']} - Show all current CLI settings")
|
89
|
+
print(f" {COLORS['yellow']}ngpt --cli-config unset language{COLORS['reset']} - Remove language setting")
|
90
|
+
|
91
|
+
print(f"\n {COLORS['cyan']}Notes:{COLORS['reset']}")
|
92
|
+
print(f" - CLI configuration is stored in:")
|
93
|
+
print(f" • Linux: {COLORS['yellow']}~/.config/ngpt/ngpt-cli.conf{COLORS['reset']}")
|
94
|
+
print(f" • macOS: {COLORS['yellow']}~/Library/Application Support/ngpt/ngpt-cli.conf{COLORS['reset']}")
|
95
|
+
print(f" • Windows: {COLORS['yellow']}%APPDATA%\\ngpt\\ngpt-cli.conf{COLORS['reset']}")
|
96
|
+
print(f" - Settings are applied based on context (e.g., language only applies to code generation mode)")
|
97
|
+
print(f" - Command-line arguments always override CLI configuration")
|
98
|
+
print(f" - Some options are mutually exclusive and will not be applied together")
|
99
|
+
|
100
|
+
def handle_cli_config(action, option=None, value=None):
|
101
|
+
"""Handle CLI configuration commands."""
|
102
|
+
if action == "help":
|
103
|
+
show_cli_config_help()
|
104
|
+
return
|
105
|
+
|
106
|
+
if action == "list":
|
107
|
+
# List all available options
|
108
|
+
print(f"{COLORS['green']}{COLORS['bold']}Available CLI configuration options:{COLORS['reset']}")
|
109
|
+
for option in list_cli_config_options():
|
110
|
+
meta = CLI_CONFIG_OPTIONS[option]
|
111
|
+
default = f"(default: {meta['default']})" if meta['default'] is not None else ""
|
112
|
+
contexts = ', '.join(meta['context'])
|
113
|
+
if "all" in meta['context']:
|
114
|
+
contexts = "all modes"
|
115
|
+
print(f" {COLORS['cyan']}{option}{COLORS['reset']} - {meta['type']} {default} - Available in: {contexts}")
|
116
|
+
return
|
117
|
+
|
118
|
+
if action == "get":
|
119
|
+
if option is None:
|
120
|
+
# Get all options
|
121
|
+
success, config = get_cli_config_option()
|
122
|
+
if success and config:
|
123
|
+
print(f"{COLORS['green']}{COLORS['bold']}Current CLI configuration:{COLORS['reset']}")
|
124
|
+
for opt, val in config.items():
|
125
|
+
if opt in CLI_CONFIG_OPTIONS:
|
126
|
+
print(f" {COLORS['cyan']}{opt}{COLORS['reset']} = {val}")
|
127
|
+
else:
|
128
|
+
print(f" {COLORS['yellow']}{opt}{COLORS['reset']} = {val} (unknown option)")
|
129
|
+
else:
|
130
|
+
print(f"{COLORS['yellow']}No CLI configuration set. Use 'ngpt --cli-config set OPTION VALUE' to set options.{COLORS['reset']}")
|
131
|
+
else:
|
132
|
+
# Get specific option
|
133
|
+
success, result = get_cli_config_option(option)
|
134
|
+
if success:
|
135
|
+
if result is None:
|
136
|
+
print(f"{COLORS['cyan']}{option}{COLORS['reset']} is not set (default: {CLI_CONFIG_OPTIONS.get(option, {}).get('default', 'N/A')})")
|
137
|
+
else:
|
138
|
+
print(f"{COLORS['cyan']}{option}{COLORS['reset']} = {result}")
|
139
|
+
else:
|
140
|
+
print(f"{COLORS['yellow']}{result}{COLORS['reset']}")
|
141
|
+
return
|
142
|
+
|
143
|
+
if action == "set":
|
144
|
+
if option is None or value is None:
|
145
|
+
print(f"{COLORS['yellow']}Error: Both OPTION and VALUE are required for 'set' command.{COLORS['reset']}")
|
146
|
+
print(f"Usage: ngpt --cli-config set OPTION VALUE")
|
147
|
+
return
|
148
|
+
|
149
|
+
success, message = set_cli_config_option(option, value)
|
150
|
+
if success:
|
151
|
+
print(f"{COLORS['green']}{message}{COLORS['reset']}")
|
152
|
+
else:
|
153
|
+
print(f"{COLORS['yellow']}{message}{COLORS['reset']}")
|
154
|
+
return
|
155
|
+
|
156
|
+
if action == "unset":
|
157
|
+
if option is None:
|
158
|
+
print(f"{COLORS['yellow']}Error: OPTION is required for 'unset' command.{COLORS['reset']}")
|
159
|
+
print(f"Usage: ngpt --cli-config unset OPTION")
|
160
|
+
return
|
161
|
+
|
162
|
+
success, message = unset_cli_config_option(option)
|
163
|
+
if success:
|
164
|
+
print(f"{COLORS['green']}{message}{COLORS['reset']}")
|
165
|
+
else:
|
166
|
+
print(f"{COLORS['yellow']}{message}{COLORS['reset']}")
|
167
|
+
return
|
168
|
+
|
169
|
+
# If we get here, the action is not recognized
|
170
|
+
print(f"{COLORS['yellow']}Error: Unknown action '{action}'. Use 'set', 'get', 'unset', or 'list'.{COLORS['reset']}")
|
171
|
+
show_cli_config_help()
|
172
|
+
|
173
|
+
def main():
|
174
|
+
# Colorize description - use a shorter description to avoid line wrapping issues
|
175
|
+
description = f"{COLORS['cyan']}{COLORS['bold']}nGPT{COLORS['reset']} - Interact with AI language models via OpenAI-compatible APIs"
|
176
|
+
|
177
|
+
# Minimalist, clean epilog design
|
178
|
+
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']}"
|
179
|
+
|
180
|
+
parser = argparse.ArgumentParser(description=description, formatter_class=ColoredHelpFormatter, epilog=epilog)
|
181
|
+
|
182
|
+
# Add custom error method with color
|
183
|
+
original_error = parser.error
|
184
|
+
def error_with_color(message):
|
185
|
+
parser.print_usage(sys.stderr)
|
186
|
+
parser.exit(2, f"{COLORS['bold']}{COLORS['yellow']}error: {COLORS['reset']}{message}\n")
|
187
|
+
parser.error = error_with_color
|
188
|
+
|
189
|
+
# Custom version action with color
|
190
|
+
class ColoredVersionAction(argparse.Action):
|
191
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
192
|
+
print(f"{COLORS['green']}{COLORS['bold']}nGPT{COLORS['reset']} version {COLORS['yellow']}{__version__}{COLORS['reset']}")
|
193
|
+
parser.exit()
|
194
|
+
|
195
|
+
# Version flag
|
196
|
+
parser.add_argument('-v', '--version', action=ColoredVersionAction, nargs=0, help='Show version information and exit')
|
197
|
+
|
198
|
+
# Config options
|
199
|
+
config_group = parser.add_argument_group('Configuration Options')
|
200
|
+
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')
|
201
|
+
config_group.add_argument('--config-index', type=int, default=0, help='Index of the configuration to use or edit (default: 0)')
|
202
|
+
config_group.add_argument('--provider', help='Provider name to identify the configuration to use')
|
203
|
+
config_group.add_argument('--remove', action='store_true', help='Remove the configuration at the specified index (requires --config and --config-index)')
|
204
|
+
config_group.add_argument('--show-config', action='store_true', help='Show the current configuration(s) and exit')
|
205
|
+
config_group.add_argument('--all', action='store_true', help='Show details for all configurations (requires --show-config)')
|
206
|
+
config_group.add_argument('--list-models', action='store_true', help='List all available models for the current configuration and exit')
|
207
|
+
config_group.add_argument('--list-renderers', action='store_true', help='Show available markdown renderers for use with --prettify')
|
208
|
+
|
209
|
+
# Global options
|
210
|
+
global_group = parser.add_argument_group('Global Options')
|
211
|
+
global_group.add_argument('--api-key', help='API key for the service')
|
212
|
+
global_group.add_argument('--base-url', help='Base URL for the API')
|
213
|
+
global_group.add_argument('--model', help='Model to use')
|
214
|
+
global_group.add_argument('--web-search', action='store_true',
|
215
|
+
help='Enable web search capability (Note: Your API endpoint must support this feature)')
|
216
|
+
global_group.add_argument('-n', '--no-stream', action='store_true',
|
217
|
+
help='Return the whole response without streaming')
|
218
|
+
global_group.add_argument('--temperature', type=float, default=0.7,
|
219
|
+
help='Set temperature (controls randomness, default: 0.7)')
|
220
|
+
global_group.add_argument('--top_p', type=float, default=1.0,
|
221
|
+
help='Set top_p (controls diversity, default: 1.0)')
|
222
|
+
global_group.add_argument('--max_tokens', type=int,
|
223
|
+
help='Set max response length in tokens')
|
224
|
+
global_group.add_argument('--log', metavar='FILE',
|
225
|
+
help='Set filepath to log conversation to (For interactive modes)')
|
226
|
+
global_group.add_argument('--preprompt',
|
227
|
+
help='Set custom system prompt to control AI behavior')
|
228
|
+
global_group.add_argument('--prettify', action='store_const', const='auto',
|
229
|
+
help='Render markdown responses and code with syntax highlighting and formatting')
|
230
|
+
global_group.add_argument('--stream-prettify', action='store_true',
|
231
|
+
help='Enable streaming with markdown rendering (automatically uses Rich renderer)')
|
232
|
+
global_group.add_argument('--renderer', choices=['auto', 'rich', 'glow'], default='auto',
|
233
|
+
help='Select which markdown renderer to use with --prettify (auto, rich, or glow)')
|
234
|
+
|
235
|
+
# Mode flags (mutually exclusive)
|
236
|
+
mode_group = parser.add_argument_group('Modes (mutually exclusive)')
|
237
|
+
mode_exclusive_group = mode_group.add_mutually_exclusive_group()
|
238
|
+
mode_exclusive_group.add_argument('-i', '--interactive', action='store_true', help='Start an interactive chat session')
|
239
|
+
mode_exclusive_group.add_argument('-s', '--shell', action='store_true', help='Generate and execute shell commands')
|
240
|
+
mode_exclusive_group.add_argument('-c', '--code', action='store_true', help='Generate code')
|
241
|
+
mode_exclusive_group.add_argument('-t', '--text', action='store_true', help='Enter multi-line text input (submit with Ctrl+D)')
|
242
|
+
# Note: --show-config is handled separately and implicitly acts as a mode
|
243
|
+
|
244
|
+
# Language option for code mode
|
245
|
+
parser.add_argument('--language', default="python", help='Programming language to generate code in (for code mode)')
|
246
|
+
|
247
|
+
# Prompt argument
|
248
|
+
parser.add_argument('prompt', nargs='?', default=None, help='The prompt to send')
|
249
|
+
|
250
|
+
# Add CLI configuration command
|
251
|
+
config_group.add_argument('--cli-config', nargs='*', metavar='COMMAND',
|
252
|
+
help='Manage CLI configuration (set, get, unset, list)')
|
253
|
+
|
254
|
+
args = parser.parse_args()
|
255
|
+
|
256
|
+
# Handle CLI configuration command
|
257
|
+
if args.cli_config is not None:
|
258
|
+
# Show help if no arguments or "help" argument
|
259
|
+
if len(args.cli_config) == 0 or (len(args.cli_config) > 0 and args.cli_config[0].lower() == "help"):
|
260
|
+
show_cli_config_help()
|
261
|
+
return
|
262
|
+
|
263
|
+
action = args.cli_config[0].lower()
|
264
|
+
option = args.cli_config[1] if len(args.cli_config) > 1 else None
|
265
|
+
value = args.cli_config[2] if len(args.cli_config) > 2 else None
|
266
|
+
|
267
|
+
if action in ("set", "get", "unset", "list", "help"):
|
268
|
+
handle_cli_config(action, option, value)
|
269
|
+
return
|
270
|
+
else:
|
271
|
+
show_cli_config_help()
|
272
|
+
return
|
273
|
+
|
274
|
+
# Validate --all usage
|
275
|
+
if args.all and not args.show_config:
|
276
|
+
parser.error("--all can only be used with --show-config")
|
277
|
+
|
278
|
+
# Handle --renderers flag to show available markdown renderers
|
279
|
+
if args.list_renderers:
|
280
|
+
show_available_renderers()
|
281
|
+
return
|
282
|
+
|
283
|
+
# Load CLI configuration early
|
284
|
+
cli_config = load_cli_config()
|
285
|
+
|
286
|
+
# Priority order for config selection:
|
287
|
+
# 1. Command-line arguments (args.provider, args.config_index)
|
288
|
+
# 2. CLI configuration (cli_config["provider"], cli_config["config-index"])
|
289
|
+
# 3. Default values (None, 0)
|
290
|
+
|
291
|
+
# Get provider/config-index from CLI config if not specified in args
|
292
|
+
effective_provider = args.provider
|
293
|
+
effective_config_index = args.config_index
|
294
|
+
|
295
|
+
# Only apply CLI config for provider/config-index if not explicitly set on command line
|
296
|
+
if not effective_provider and 'provider' in cli_config and '--provider' not in sys.argv:
|
297
|
+
effective_provider = cli_config['provider']
|
298
|
+
|
299
|
+
if effective_config_index == 0 and 'config-index' in cli_config and '--config-index' not in sys.argv:
|
300
|
+
effective_config_index = cli_config['config-index']
|
301
|
+
|
302
|
+
# Check for mutual exclusivity between provider and config-index
|
303
|
+
if effective_config_index != 0 and effective_provider:
|
304
|
+
parser.error("--config-index and --provider cannot be used together")
|
305
|
+
|
306
|
+
# Handle interactive configuration mode
|
307
|
+
if args.config is True: # --config was used without a value
|
308
|
+
config_path = get_config_path()
|
309
|
+
|
310
|
+
# Handle configuration removal if --remove flag is present
|
311
|
+
if args.remove:
|
312
|
+
# Validate that config_index is explicitly provided
|
313
|
+
if '--config-index' not in sys.argv and not effective_provider:
|
314
|
+
parser.error("--remove requires explicitly specifying --config-index or --provider")
|
315
|
+
|
316
|
+
# Show config details before asking for confirmation
|
317
|
+
configs = load_configs(str(config_path))
|
318
|
+
|
319
|
+
# Determine the config index to remove
|
320
|
+
config_index = effective_config_index
|
321
|
+
if effective_provider:
|
322
|
+
# Find config index by provider name
|
323
|
+
matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == effective_provider.lower()]
|
324
|
+
if not matching_configs:
|
325
|
+
print(f"Error: No configuration found for provider '{effective_provider}'")
|
326
|
+
return
|
327
|
+
elif len(matching_configs) > 1:
|
328
|
+
print(f"Multiple configurations found for provider '{effective_provider}':")
|
329
|
+
for i, idx in enumerate(matching_configs):
|
330
|
+
print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
|
331
|
+
|
332
|
+
try:
|
333
|
+
choice = input("Choose a configuration to remove (or press Enter to cancel): ")
|
334
|
+
if choice and choice.isdigit() and 0 <= int(choice) < len(matching_configs):
|
335
|
+
config_index = matching_configs[int(choice)]
|
336
|
+
else:
|
337
|
+
print("Configuration removal cancelled.")
|
338
|
+
return
|
339
|
+
except (ValueError, IndexError, KeyboardInterrupt):
|
340
|
+
print("\nConfiguration removal cancelled.")
|
341
|
+
return
|
342
|
+
else:
|
343
|
+
config_index = matching_configs[0]
|
344
|
+
|
345
|
+
# Check if index is valid
|
346
|
+
if config_index < 0 or config_index >= len(configs):
|
347
|
+
print(f"Error: Configuration index {config_index} is out of range. Valid range: 0-{len(configs)-1}")
|
348
|
+
return
|
349
|
+
|
350
|
+
# Show the configuration that will be removed
|
351
|
+
config = configs[config_index]
|
352
|
+
print(f"Configuration to remove (index {config_index}):")
|
353
|
+
print(f" Provider: {config.get('provider', 'N/A')}")
|
354
|
+
print(f" Model: {config.get('model', 'N/A')}")
|
355
|
+
print(f" Base URL: {config.get('base_url', 'N/A')}")
|
356
|
+
print(f" API Key: {'[Set]' if config.get('api_key') else '[Not Set]'}")
|
357
|
+
|
358
|
+
# Ask for confirmation
|
359
|
+
try:
|
360
|
+
print("\nAre you sure you want to remove this configuration? [y/N] ", end='')
|
361
|
+
response = input().lower()
|
362
|
+
if response in ('y', 'yes'):
|
363
|
+
remove_config_entry(config_path, config_index)
|
364
|
+
else:
|
365
|
+
print("Configuration removal cancelled.")
|
366
|
+
except KeyboardInterrupt:
|
367
|
+
print("\nConfiguration removal cancelled by user.")
|
368
|
+
|
369
|
+
return
|
370
|
+
|
371
|
+
# Regular config addition/editing (existing code)
|
372
|
+
# If --config-index was not explicitly specified, create a new entry by passing None
|
373
|
+
# This will cause add_config_entry to create a new entry at the end of the list
|
374
|
+
# Otherwise, edit the existing config at the specified index
|
375
|
+
config_index = None
|
376
|
+
|
377
|
+
# Determine if we're editing an existing config or creating a new one
|
378
|
+
if effective_provider:
|
379
|
+
# Find config by provider name
|
380
|
+
configs = load_configs(str(config_path))
|
381
|
+
matching_configs = [i for i, cfg in enumerate(configs) if cfg.get('provider', '').lower() == effective_provider.lower()]
|
382
|
+
|
383
|
+
if not matching_configs:
|
384
|
+
print(f"No configuration found for provider '{effective_provider}'. Creating a new configuration.")
|
385
|
+
elif len(matching_configs) > 1:
|
386
|
+
print(f"Multiple configurations found for provider '{effective_provider}':")
|
387
|
+
for i, idx in enumerate(matching_configs):
|
388
|
+
print(f" [{i}] Index {idx}: {configs[idx].get('model', 'Unknown model')}")
|
389
|
+
|
390
|
+
try:
|
391
|
+
choice = input("Choose a configuration to edit (or press Enter for the first one): ")
|
392
|
+
if choice and choice.isdigit() and 0 <= int(choice) < len(matching_configs):
|
393
|
+
config_index = matching_configs[int(choice)]
|
394
|
+
else:
|
395
|
+
config_index = matching_configs[0]
|
396
|
+
except (ValueError, IndexError, KeyboardInterrupt):
|
397
|
+
config_index = matching_configs[0]
|
398
|
+
else:
|
399
|
+
config_index = matching_configs[0]
|
400
|
+
|
401
|
+
print(f"Editing existing configuration at index {config_index}")
|
402
|
+
elif effective_config_index != 0 or '--config-index' in sys.argv:
|
403
|
+
# Check if the index is valid
|
404
|
+
configs = load_configs(str(config_path))
|
405
|
+
if effective_config_index >= 0 and effective_config_index < len(configs):
|
406
|
+
config_index = effective_config_index
|
407
|
+
print(f"Editing existing configuration at index {config_index}")
|
408
|
+
else:
|
409
|
+
print(f"Configuration index {effective_config_index} is out of range. Creating a new configuration.")
|
410
|
+
else:
|
411
|
+
# Creating a new config
|
412
|
+
configs = load_configs(str(config_path))
|
413
|
+
print(f"Creating new configuration at index {len(configs)}")
|
414
|
+
|
415
|
+
add_config_entry(config_path, config_index)
|
416
|
+
return
|
417
|
+
|
418
|
+
# Load configuration using the effective provider/config-index
|
419
|
+
active_config = load_config(args.config, effective_config_index, effective_provider)
|
420
|
+
|
421
|
+
# Command-line arguments override config settings for active config display
|
422
|
+
if args.api_key:
|
423
|
+
active_config["api_key"] = args.api_key
|
424
|
+
if args.base_url:
|
425
|
+
active_config["base_url"] = args.base_url
|
426
|
+
if args.model:
|
427
|
+
active_config["model"] = args.model
|
428
|
+
|
429
|
+
# Show config if requested
|
430
|
+
if args.show_config:
|
431
|
+
config_path = get_config_path(args.config)
|
432
|
+
configs = load_configs(args.config)
|
433
|
+
|
434
|
+
print(f"Configuration file: {config_path}")
|
435
|
+
print(f"Total configurations: {len(configs)}")
|
436
|
+
|
437
|
+
# Determine active configuration and display identifier
|
438
|
+
active_identifier = f"index {effective_config_index}"
|
439
|
+
if effective_provider:
|
440
|
+
active_identifier = f"provider '{effective_provider}'"
|
441
|
+
print(f"Active configuration: {active_identifier}")
|
442
|
+
|
443
|
+
if args.all:
|
444
|
+
# Show details for all configurations
|
445
|
+
print("\nAll configuration details:")
|
446
|
+
for i, cfg in enumerate(configs):
|
447
|
+
provider = cfg.get('provider', 'N/A')
|
448
|
+
active_str = '(Active)' if (
|
449
|
+
(effective_provider and provider.lower() == effective_provider.lower()) or
|
450
|
+
(not effective_provider and i == effective_config_index)
|
451
|
+
) else ''
|
452
|
+
print(f"\n--- Configuration Index {i} / Provider: {COLORS['green']}{provider}{COLORS['reset']} {active_str} ---")
|
453
|
+
print(f" API Key: {'[Set]' if cfg.get('api_key') else '[Not Set]'}")
|
454
|
+
print(f" Base URL: {cfg.get('base_url', 'N/A')}")
|
455
|
+
print(f" Model: {cfg.get('model', 'N/A')}")
|
456
|
+
else:
|
457
|
+
# Show active config details and summary list
|
458
|
+
print("\nActive configuration details:")
|
459
|
+
print(f" Provider: {COLORS['green']}{active_config.get('provider', 'N/A')}{COLORS['reset']}")
|
460
|
+
print(f" API Key: {'[Set]' if active_config.get('api_key') else '[Not Set]'}")
|
461
|
+
print(f" Base URL: {active_config.get('base_url', 'N/A')}")
|
462
|
+
print(f" Model: {active_config.get('model', 'N/A')}")
|
463
|
+
|
464
|
+
if len(configs) > 1:
|
465
|
+
print("\nAvailable configurations:")
|
466
|
+
# Check for duplicate provider names for warning
|
467
|
+
provider_counts = {}
|
468
|
+
for cfg in configs:
|
469
|
+
provider = cfg.get('provider', 'N/A').lower()
|
470
|
+
provider_counts[provider] = provider_counts.get(provider, 0) + 1
|
471
|
+
|
472
|
+
for i, cfg in enumerate(configs):
|
473
|
+
provider = cfg.get('provider', 'N/A')
|
474
|
+
provider_display = provider
|
475
|
+
# Add warning for duplicate providers
|
476
|
+
if provider_counts.get(provider.lower(), 0) > 1:
|
477
|
+
provider_display = f"{provider} {COLORS['yellow']}(duplicate){COLORS['reset']}"
|
478
|
+
|
479
|
+
active_marker = "*" if (
|
480
|
+
(effective_provider and provider.lower() == effective_provider.lower()) or
|
481
|
+
(not effective_provider and i == effective_config_index)
|
482
|
+
) else " "
|
483
|
+
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]'})")
|
484
|
+
|
485
|
+
# Show instruction for using --provider
|
486
|
+
print(f"\nTip: Use {COLORS['yellow']}--provider NAME{COLORS['reset']} to select a configuration by provider name.")
|
487
|
+
|
488
|
+
return
|
489
|
+
|
490
|
+
# For interactive mode, we'll allow continuing without a specific prompt
|
491
|
+
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):
|
492
|
+
parser.print_help()
|
493
|
+
return
|
494
|
+
|
495
|
+
# Check configuration (using the potentially overridden active_config)
|
496
|
+
if not args.show_config and not args.list_models and not check_config(active_config):
|
497
|
+
return
|
498
|
+
|
499
|
+
# Check if --prettify is used but no markdown renderer is available
|
500
|
+
# This will warn the user immediately if they request prettify but don't have the tools
|
501
|
+
has_renderer = True
|
502
|
+
if args.prettify:
|
503
|
+
has_renderer = warn_if_no_markdown_renderer(args.renderer)
|
504
|
+
if not has_renderer:
|
505
|
+
# Set a flag to disable prettify since we already warned the user
|
506
|
+
print(f"{COLORS['yellow']}Continuing without markdown rendering.{COLORS['reset']}")
|
507
|
+
show_available_renderers()
|
508
|
+
args.prettify = False
|
509
|
+
|
510
|
+
# Check if --prettify is used with --stream-prettify (conflict)
|
511
|
+
if args.prettify and args.stream_prettify:
|
512
|
+
parser.error("--prettify and --stream-prettify cannot be used together. Choose one option.")
|
513
|
+
|
514
|
+
# Check if --stream-prettify is used but Rich is not available
|
515
|
+
if args.stream_prettify and not has_markdown_renderer('rich'):
|
516
|
+
parser.error("--stream-prettify requires Rich to be installed. Install with: pip install \"ngpt[full]\" or pip install rich")
|
517
|
+
|
518
|
+
# Initialize client using the potentially overridden active_config
|
519
|
+
client = NGPTClient(**active_config)
|
520
|
+
|
521
|
+
try:
|
522
|
+
# Handle listing models
|
523
|
+
if args.list_models:
|
524
|
+
print("Retrieving available models...")
|
525
|
+
models = client.list_models()
|
526
|
+
if models:
|
527
|
+
print(f"\nAvailable models for {active_config.get('provider', 'API')}:")
|
528
|
+
print("-" * 50)
|
529
|
+
for model in models:
|
530
|
+
if "id" in model:
|
531
|
+
owned_by = f" ({model.get('owned_by', 'Unknown')})" if "owned_by" in model else ""
|
532
|
+
current = " [active]" if model["id"] == active_config["model"] else ""
|
533
|
+
print(f"- {model['id']}{owned_by}{current}")
|
534
|
+
print("\nUse --model MODEL_NAME to select a specific model")
|
535
|
+
else:
|
536
|
+
print("No models available or could not retrieve models.")
|
537
|
+
return
|
538
|
+
|
539
|
+
# Handle modes
|
540
|
+
if args.interactive:
|
541
|
+
# Apply CLI config for interactive mode
|
542
|
+
args = apply_cli_config(args, "interactive")
|
543
|
+
|
544
|
+
# Interactive chat mode
|
545
|
+
interactive_chat_session(
|
546
|
+
client,
|
547
|
+
web_search=args.web_search,
|
548
|
+
no_stream=args.no_stream,
|
549
|
+
temperature=args.temperature,
|
550
|
+
top_p=args.top_p,
|
551
|
+
max_tokens=args.max_tokens,
|
552
|
+
log_file=args.log,
|
553
|
+
preprompt=args.preprompt,
|
554
|
+
prettify=args.prettify,
|
555
|
+
renderer=args.renderer,
|
556
|
+
stream_prettify=args.stream_prettify
|
557
|
+
)
|
558
|
+
elif args.shell:
|
559
|
+
# Apply CLI config for shell mode
|
560
|
+
args = apply_cli_config(args, "shell")
|
561
|
+
|
562
|
+
# Shell command generation mode
|
563
|
+
shell_mode(client, args)
|
564
|
+
|
565
|
+
elif args.code:
|
566
|
+
# Apply CLI config for code mode
|
567
|
+
args = apply_cli_config(args, "code")
|
568
|
+
|
569
|
+
# Code generation mode
|
570
|
+
code_mode(client, args)
|
571
|
+
|
572
|
+
elif args.text:
|
573
|
+
# Apply CLI config for text mode
|
574
|
+
args = apply_cli_config(args, "text")
|
575
|
+
|
576
|
+
# Text mode (multiline input)
|
577
|
+
text_mode(client, args)
|
578
|
+
|
579
|
+
else:
|
580
|
+
# Default to chat mode
|
581
|
+
# Apply CLI config for default chat mode
|
582
|
+
args = apply_cli_config(args, "all")
|
583
|
+
|
584
|
+
# Standard chat mode
|
585
|
+
chat_mode(client, args)
|
586
|
+
|
587
|
+
except KeyboardInterrupt:
|
588
|
+
print("\nOperation cancelled by user. Exiting gracefully.")
|
589
|
+
# Make sure we exit with a non-zero status code to indicate the operation was cancelled
|
590
|
+
sys.exit(130) # 130 is the standard exit code for SIGINT (Ctrl+C)
|
591
|
+
except Exception as e:
|
592
|
+
print(f"Error: {e}")
|
593
|
+
sys.exit(1) # Exit with error code
|
ngpt/cli/modes/chat.py
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
from ..formatters import COLORS
|
2
|
+
from ..renderers import prettify_markdown, prettify_streaming_markdown
|
3
|
+
import sys
|
4
|
+
|
5
|
+
def chat_mode(client, args):
|
6
|
+
"""Handle the standard chat mode with a single prompt.
|
7
|
+
|
8
|
+
Args:
|
9
|
+
client: The NGPTClient instance
|
10
|
+
args: The parsed command-line arguments
|
11
|
+
"""
|
12
|
+
# Get the prompt
|
13
|
+
if args.prompt is None:
|
14
|
+
try:
|
15
|
+
print("Enter your prompt: ", end='')
|
16
|
+
prompt = input()
|
17
|
+
except KeyboardInterrupt:
|
18
|
+
print("\nInput cancelled by user. Exiting gracefully.")
|
19
|
+
sys.exit(130)
|
20
|
+
else:
|
21
|
+
prompt = args.prompt
|
22
|
+
|
23
|
+
# Create messages array with preprompt if available
|
24
|
+
messages = None
|
25
|
+
if args.preprompt:
|
26
|
+
messages = [
|
27
|
+
{"role": "system", "content": args.preprompt},
|
28
|
+
{"role": "user", "content": prompt}
|
29
|
+
]
|
30
|
+
|
31
|
+
# Set default streaming behavior based on --no-stream and --prettify arguments
|
32
|
+
should_stream = not args.no_stream and not args.prettify
|
33
|
+
|
34
|
+
# If stream-prettify is enabled
|
35
|
+
stream_callback = None
|
36
|
+
live_display = None
|
37
|
+
|
38
|
+
if args.stream_prettify:
|
39
|
+
should_stream = True # Enable streaming
|
40
|
+
# This is the standard mode, not interactive
|
41
|
+
live_display, stream_callback = prettify_streaming_markdown(args.renderer)
|
42
|
+
if not live_display:
|
43
|
+
# Fallback to normal prettify if live display setup failed
|
44
|
+
args.prettify = True
|
45
|
+
args.stream_prettify = False
|
46
|
+
should_stream = False
|
47
|
+
print(f"{COLORS['yellow']}Falling back to regular prettify mode.{COLORS['reset']}")
|
48
|
+
|
49
|
+
# If regular prettify is enabled with streaming, inform the user
|
50
|
+
if args.prettify and not args.no_stream:
|
51
|
+
print(f"{COLORS['yellow']}Note: Streaming disabled to enable markdown rendering.{COLORS['reset']}")
|
52
|
+
|
53
|
+
# Start live display if using stream-prettify
|
54
|
+
if args.stream_prettify and live_display:
|
55
|
+
live_display.start()
|
56
|
+
|
57
|
+
response = client.chat(prompt, stream=should_stream, web_search=args.web_search,
|
58
|
+
temperature=args.temperature, top_p=args.top_p,
|
59
|
+
max_tokens=args.max_tokens, messages=messages,
|
60
|
+
markdown_format=args.prettify or args.stream_prettify,
|
61
|
+
stream_callback=stream_callback)
|
62
|
+
|
63
|
+
# Stop live display if using stream-prettify
|
64
|
+
if args.stream_prettify and live_display:
|
65
|
+
live_display.stop()
|
66
|
+
|
67
|
+
# Handle non-stream response or regular prettify
|
68
|
+
if (args.no_stream or args.prettify) and response:
|
69
|
+
if args.prettify:
|
70
|
+
prettify_markdown(response, args.renderer)
|
71
|
+
else:
|
72
|
+
print(response)
|