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